rm-extensions 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,36 +1,246 @@
1
- # RMExtensions
1
+ RMExtensions
2
+ -----------------
2
3
 
3
- Extensions and helpers for dealing with various areas of rubymotion:
4
+ #### Extensions and helpers for dealing with various areas of rubymotion.
4
5
 
5
- - memory management
6
- - block/scope/local variable issues
7
- - GCD blocks
8
- - retaining objects through async procedures
9
- - weak attr_accessors
6
+ ## Observation
10
7
 
11
- Currently depends on bubblewrap.
8
+ #### Make observations without needing to clean up/unobserve, and avoid retain-cycles
12
9
 
13
- AssociatedObject objc runtime taken from BlocksKit, modified to work with rubymotion.
10
+ Call from anywhere on anything without prior inclusion of BW::KVO:
14
11
 
15
- ## Installation
12
+ ```ruby
13
+ class MyViewController < UIViewController
14
+ def viewDidLoad
15
+ super.tap do
16
+ rmext_observe(@model, "name") do |val|
17
+ p "name is #{val}"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ ```
16
23
 
17
- Add this line to your application's Gemfile:
24
+ Under the hood this piggy-backs on Bubblewrap's KVO implementation.
18
25
 
19
- gem 'rm-extensions'
26
+ Differences:
27
+
28
+ - No need to include BW::KVO anywhere
29
+ - The default is to observe and immediately fire the supplied callback
30
+ - The callback only takes one argument, the new value
31
+ - the object observing is not retained, and when it is deallocated, the observation
32
+ will be removed automatically for you. there is typically no need to clean up
33
+ and unobserve in viewWillDisappear, or similar.
34
+ - because the observation actually happens on an unretained proxy object, the real
35
+ object shouldnt incur any retain cycles.
36
+
37
+ Similarities:
38
+
39
+ - the object observed is retained
40
+
41
+
42
+ ## Accessors
43
+
44
+ #### weak attr_accessors when you need to avoid retain-cycles:
45
+
46
+ ```ruby
47
+
48
+ class MyView < UIView
49
+ rmext_weak_attr_accessor :delegate
50
+ end
51
+
52
+ class MyViewController < UIViewController
53
+ def viewDidLoad
54
+ super.tap do
55
+ v = MyView.alloc.initWithFrame(CGRectZero)
56
+ view.addSubview(v)
57
+ # if delegate was a normal attr_accessor, this controller could never be deallocated
58
+ v.delegate = self
59
+ end
60
+ end
61
+ end
62
+
63
+ ```
64
+
65
+ ## Deallocation
66
+
67
+ #### watch for an object to deallocate, and execute a callback:
68
+
69
+ ```ruby
70
+ def add_view_controller
71
+ controller = UIViewController.alloc.init
72
+ controller.rmext_on_dealloc(&test_dealloc_proc)
73
+ navigationController.pushViewController(controller, animated: true)
74
+ end
75
+
76
+ def test_dealloc_proc
77
+ proc { |x| p "it deallocated!" }
78
+ end
79
+
80
+ # now you can verify the controller gets deallocated by calling #add_view_controller
81
+ # and then popping it off the navigationController
82
+
83
+ # you should be careful not to create the block inline, since it could easily create a retain cycle
84
+ # depending what other objects are in scope.
85
+ ```
86
+ ## Queues
87
+
88
+ #### Wraps GCD to avoid complier issues with blocks and also ensures the block passed is retained until executed on the queue:
89
+
90
+ ```ruby
91
+ # note +i+ will appear in order, and the thread will never change (main)
92
+ 100.times do |i|
93
+ rmext_on_main_q do
94
+ p "i: #{i} thread: #{NSThread.currentThread}"
95
+ end
96
+ end
97
+
98
+ # note +i+ will appear in order, and the thread will change
99
+ 100.times do |i|
100
+ rmext_on_serial_q("testing") do
101
+ p "i: #{i} thread: #{NSThread.currentThread}"
102
+ end
103
+ end
104
+
105
+ # note +i+ will sometimes appear out of order, and the thread will change
106
+ 100.times do |i|
107
+ rmext_on_concurrent_q("testing") do
108
+ p "i: #{i} thread: #{NSThread.currentThread}"
109
+ end
110
+ end
111
+ ```
112
+
113
+ ## Context
114
+
115
+ #### break through local variable scope bugs, where using instance variables would mean your method is not re-entrant. retain objects through asynchronous operations.
116
+
117
+ ##### rmext_context
118
+
119
+ ```ruby
120
+ # yields an object you can treat like an openstruct. you can get/set any property
121
+ # on it. useful for scope issues where local variables wont work, and where instance
122
+ # variables would clutter the object and not be re-entrant.
123
+
124
+ # Consider this example:
125
+
126
+ button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
127
+ button.when_tapped do
128
+ button.setTitle("Tapped", forState:UIControlStateNormal)
129
+ end
130
+ view.addSubview(button)
20
131
 
21
- Add this line to your application's Rakefile:
132
+ # when button is tapped, you will get this:
133
+ # >> Program received signal EXC_BAD_ACCESS, Could not access memory.
22
134
 
23
- pod 'BlocksKit'
135
+ # Workaround using +rmext_context+:
136
+
137
+ rmext_context do |x|
138
+ x.button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
139
+ x.button.when_tapped do
140
+ x.button.setTitle("Tapped", forState:UIControlStateNormal)
141
+ end
142
+ view.addSubview(x.button)
143
+ end
144
+
145
+ # when button is tapped, it works.
146
+
147
+ # a note about the different use cases for +rmext_context+ and +rmext_retained_context+,
148
+ # because its important to understand when to use which, and what different purposes they
149
+ # are for:
150
+
151
+ # +rmext_context+ is used here instead of +rmext_retained_context+ because:
152
+
153
+ # 1. the button is already going to be retained by the view its added to, so
154
+ # there is no need for us to retain it explicitly.
155
+ # 2. there would be no clear way to eventually "detach" it, since the button
156
+ # could be clicked any number of times.
157
+ ```
158
+
159
+ ##### rmext_retained_context
160
+
161
+ ```ruby
162
+ # like +rmext_context+ but the context is retained (as well as anything set on it) until you
163
+ # explicitly call +detach!+ or +detach_on_death_of+ and that object is deallocated. prevents
164
+ # deallocation of objects until you are done with them, for example through asynchronous
165
+ # operations.
166
+
167
+ # also has a useful shortcut for beginBackgroundTaskWithExpirationHandler/endBackgroundTask
168
+ # via +begin_background!+. when you call +detach!+ the background task will be ended for you
169
+ # as well.
170
+
171
+ # use this over +rmext_context+ when you have a scenario when eventually you know everything
172
+ # is complete, and can call +detach!+. for example, an operation that makes an http request,
173
+ # uses the result to call another operation on a specific queue, and is finally considered
174
+ # "finished" at some point in time in the future. there is a definitive "end", at some point
175
+ # in the future.
176
+
177
+ # example:
178
+
179
+ rmext_retained_context do |x|
180
+ rmext_on_serial_q("my_serial_q") do
181
+ some_async_http_request do |results1|
182
+ x.results1 = results1
183
+ rmext_on_serial_q("my_serial_q") do
184
+ some_other_async_http_request do |results2|
185
+ x.results2 = results2
186
+ rmext_on_main_q do
187
+ p "results1", x.results1
188
+ p "results2", x.results2
189
+ x.detach!
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ ```
197
+
198
+ ## Retention
199
+
200
+ #### A type of retain/release that just uses rubymotion's memory-management rules instead of calling the native retain/release:
201
+
202
+ ```ruby
203
+ class MyViewController < UITableViewController
204
+
205
+ # note here, if the view controller is deallocated during the http request (someone hits Back, etc),
206
+ # and then the http request finishes, and you try to call tableView.reloadData, it will be a
207
+ # EXC_BAD_ACCESS:
208
+ def fetch_unsafe
209
+ remote_http_request do |result|
210
+ @models = []
211
+ tableView.reloadData
212
+ end
213
+ end
214
+
215
+ # ensure self stay around long enough for the block to be called
216
+ def fetch
217
+ rmext_retain!
218
+ remote_http_request do |result|
219
+ @models = []
220
+ tableView.reloadData
221
+ rmext_detach!
222
+ end
223
+ end
224
+
225
+ end
226
+ ```
227
+
228
+ Installation
229
+ -----------------
230
+
231
+ Add this line to your application's Gemfile:
232
+
233
+ gem 'rm-extensions'
24
234
 
25
235
  And then execute:
26
236
 
27
237
  $ bundle
28
238
 
29
- ## Usage
239
+ * Currently depends on bubblewrap (for BW::KVO).
240
+ * AssociatedObject objc runtime taken from BlocksKit, modified to work with rubymotion.
30
241
 
31
- Some code is commented. TODO.
32
-
33
- ## Contributing
242
+ Contributing
243
+ -----------------
34
244
 
35
245
  If you have a better way to accomplish anything this library is doing, please share!
36
246
 
@@ -39,3 +249,14 @@ If you have a better way to accomplish anything this library is doing, please sh
39
249
  3. Commit your changes (`git commit -am 'Add some feature'`)
40
250
  4. Push to the branch (`git push origin my-new-feature`)
41
251
  5. Create new Pull Request
252
+
253
+ License
254
+ -----------------
255
+
256
+ Please see [LICENSE](https://github.com/joenoon/rm-extensions/blob/master/LICENSE.txt) for licensing details.
257
+
258
+
259
+ Author
260
+ -----------------
261
+
262
+ Joe Noon, [joenoon](https://github.com/joenoon)
@@ -0,0 +1,44 @@
1
+ module RMExtensions
2
+
3
+ module ObjectExtensions
4
+
5
+ module Accessors
6
+
7
+ # creates an +attr_accessor+ like behavior, but the objects are stored with
8
+ # a weak reference (OBJC_ASSOCIATION_ASSIGN). useful to avoid retain cycles
9
+ # when you want to have access to an object in a place that isnt responsible
10
+ # for that object's lifecycle.
11
+ # does not conform to KVO like attr_accessor does.
12
+ def rmext_weak_attr_accessor(*attrs)
13
+ attrs.each do |attr|
14
+ define_method(attr) do
15
+ rmext_associatedValueForKey(attr.to_sym)
16
+ end
17
+ define_method("#{attr}=") do |val|
18
+ rmext_weaklyAssociateValue(val, withKey: attr.to_sym)
19
+ val
20
+ end
21
+ end
22
+ end
23
+
24
+ # creates an +attr_accessor+ like behavior, but the objects are stored with
25
+ # OBJC_ASSOCIATION_COPY.
26
+ # does not conform to KVO like attr_accessor does.
27
+ def rmext_copy_attr_accessor(*attrs)
28
+ attrs.each do |attr|
29
+ define_method(attr) do
30
+ rmext_associatedValueForKey(attr.to_sym)
31
+ end
32
+ define_method("#{attr}=") do |val|
33
+ rmext_atomicallyAssociateCopyOfValue(val, withKey: attr.to_sym)
34
+ val
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ Object.send(:include, ::RMExtensions::ObjectExtensions::Accessors)
@@ -0,0 +1,118 @@
1
+ module RMExtensions
2
+
3
+ module ObjectExtensions
4
+
5
+ module Context
6
+
7
+ # yields an object you can treat like an openstruct. you can get/set any property
8
+ # on it. useful for scope issues where local variables wont work, and where instance
9
+ # variables would clutter the object and not be re-entrant.
10
+ def rmext_context(&block)
11
+ ::RMExtensions::Context.create(self, &block)
12
+ end
13
+
14
+ # like +rmext_context+ but the context is retained (as well as anything set on it) until you
15
+ # explicitly call +detach!+ or +detach_on_death_of+ and that object is deallocated. prevents
16
+ # deallocation of objects until you are done with them, for example through asynchronous
17
+ # operations.
18
+ #
19
+ # also has a useful shortcut for beginBackgroundTaskWithExpirationHandler/endBackgroundTask
20
+ # via +begin_background!+. when you call +detach!+ the background task will be ended for you
21
+ # as well.
22
+ #
23
+ # use this over +rmext_context+ when you have a scenario when eventually you know everything
24
+ # is complete, and can call +detach!+. for example, an operation that makes an http request,
25
+ # uses the result to call another operation on a specific queue, and is finally considered
26
+ # "finished" at some point in time in the future. there is a definitive "end", at some point
27
+ # in the future.
28
+ def rmext_retained_context(&block)
29
+ ::RMExtensions::RetainedContext.create(self, &block)
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+
36
+ class Context
37
+
38
+ class << self
39
+ def create(origin, &block)
40
+ x = new
41
+ block.call(x) unless block.nil?
42
+ x
43
+ end
44
+ end
45
+
46
+ attr_accessor :hash
47
+
48
+ def initialize
49
+ self.hash = {}
50
+ end
51
+
52
+ def method_missing(method, *args)
53
+ m = method.to_s
54
+ if m =~ /(.+)?=$/
55
+ hash[$1] = args.first
56
+ else
57
+ hash[m]
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ class RetainedContext < Context
64
+
65
+ class << self
66
+ def create(origin, &block)
67
+ x = new
68
+ # automatically retain the origin and block
69
+ x.hash["retained_origin"] = origin
70
+ x.hash["retained_block"] = block
71
+ x.rmext_retain!
72
+ block.call(x) unless block.nil?
73
+ x
74
+ end
75
+ end
76
+
77
+ # if you provide a block, you are responsible for calling #detach!,
78
+ # otherwise, the expiration handler will just call #detach!
79
+ def begin_background!(&block)
80
+ hash["bgTaskExpirationHandler"] = block
81
+ hash["bgTask"] = UIApplication.sharedApplication.beginBackgroundTaskWithExpirationHandler(-> do
82
+ if hash["bgTaskExpirationHandler"]
83
+ hash["bgTaskExpirationHandler"].call
84
+ else
85
+ detach!
86
+ end
87
+ end)
88
+ end
89
+
90
+ def detach!
91
+ # end the bgTask if one was created
92
+ if hash["bgTask"] && hash["bgTask"] != UIBackgroundTaskInvalid
93
+ UIApplication.sharedApplication.endBackgroundTask(hash["bgTask"])
94
+ end
95
+ self.hash = nil
96
+ rmext_detach!
97
+ end
98
+
99
+ # watch some other object for deallocation, and when it does, +detach!+ self
100
+ def detach_on_death_of(object)
101
+ object.rmext_on_dealloc(&detach_death_proc)
102
+ end
103
+
104
+ def detach_death_proc
105
+ proc { |x| detach! }
106
+ end
107
+
108
+ def method_missing(method, *args)
109
+ unless hash
110
+ raise "You detached this rmext_retained_context and then called: #{method}"
111
+ end
112
+ super
113
+ end
114
+
115
+ end
116
+
117
+ end
118
+ Object.send(:include, ::RMExtensions::ObjectExtensions::Context)
@@ -0,0 +1,56 @@
1
+ module RMExtensions
2
+
3
+ module ObjectExtensions
4
+
5
+ module Deallocation
6
+
7
+ # perform a block before +self+ will dealloc.
8
+ # the block given should have one argument, the object about to be deallocated.
9
+ def rmext_on_dealloc(&block)
10
+ internalObject = ::RMExtensions::OnDeallocInternalObject.create("#{self.class.name}:#{object_id}", self, block)
11
+ @rmext_on_dealloc_blocks ||= {}
12
+ @rmext_on_dealloc_blocks[internalObject] = internalObject
13
+ nil
14
+ end
15
+
16
+ # removes a previously added block from the deallocation callback list
17
+ def rmext_cancel_on_dealloc(block)
18
+ @rmext_on_dealloc_blocks ||= {}
19
+ if internalObject = @rmext_on_dealloc_blocks[block]
20
+ internalObject.block = nil
21
+ @rmext_on_dealloc_blocks.delete(block)
22
+ end
23
+ nil
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+
30
+ # Used internally by +rmext_on_dealloc+. The idea is this object is added to the
31
+ # object we want to watch for deallocation. When the object we want to watch
32
+ # is about to dealloc, this object will dealloc first, so we can execute the block.
33
+ # the object it follows is kept only as a weak reference to not create
34
+ # a retain cycle.
35
+ class OnDeallocInternalObject
36
+ attr_accessor :description, :block
37
+ rmext_weak_attr_accessor :obj
38
+ def self.create(description, obj, block)
39
+ x = new
40
+ x.description = description
41
+ x.obj = obj
42
+ x.block = block
43
+ x
44
+ end
45
+ def dealloc
46
+ # p "dealloc OnDeallocInternalObject #{description}"
47
+ if block
48
+ block.call(obj)
49
+ self.block = nil
50
+ end
51
+ super
52
+ end
53
+ end
54
+
55
+ end
56
+ Object.send(:include, ::RMExtensions::ObjectExtensions::Deallocation)
@@ -0,0 +1,97 @@
1
+ module RMExtensions
2
+
3
+ module ObjectExtensions
4
+
5
+ module Observation
6
+
7
+ # observe an object.key. takes a block that will be called with the
8
+ # new value upon change.
9
+ #
10
+ # rmext_observe_passive(@model, "name") do |val|
11
+ # p "name is #{val}"
12
+ # end
13
+ def rmext_observe_passive(object, key, &block)
14
+ wop = ::RMExtensions::WeakObserverProxy.get(self)
15
+ b = -> (old_value, new_value) do
16
+ block.call(new_value) unless block.nil?
17
+ end
18
+ wop.observe(object, key, &b)
19
+ end
20
+
21
+ # like +rmext_observe_passive+ but additionally fires the callback immediately.
22
+ def rmext_observe(object, key, &block)
23
+ # p "+ rmext_observe", self, object, key
24
+ rmext_observe_passive(object, key, &block)
25
+ block.call(object.send(key)) unless block.nil?
26
+ end
27
+
28
+ # unobserve an existing observation
29
+ def rmext_unobserve(object, key)
30
+ wop = ::RMExtensions::WeakObserverProxy.get(self)
31
+ wop.unobserve(object, key)
32
+ wop.clear_empty_targets!
33
+ end
34
+
35
+ # unobserve all existing observations
36
+ def rmext_unobserve_all
37
+ wop = ::RMExtensions::WeakObserverProxy.get(self)
38
+ wop.unobserve_all
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+
45
+ # Proxy class used to hold the actual observation and watches for the real
46
+ # class intended to hold the observation to be deallocated, so the
47
+ # observation can be cleaned up.
48
+ class WeakObserverProxy
49
+ include BW::KVO
50
+ rmext_weak_attr_accessor :obj
51
+ attr_accessor :strong_object_id, :strong_class_name
52
+ def initialize(strong_object)
53
+ self.obj = strong_object
54
+ self.strong_object_id = strong_object.object_id
55
+ self.strong_class_name = strong_object.class.name
56
+ self.class.weak_observer_map[strong_object_id] = self
57
+ strong_object.rmext_on_dealloc(&kill_observation_proc)
58
+ end
59
+ # isolate this in its own method so it wont create a retain cycle
60
+ def kill_observation_proc
61
+ proc { |x|
62
+ # uncomment to verify deallocation is working. if not, there is probably
63
+ # a retain cycle somewhere in your code.
64
+ # p "kill_observation_proc", self
65
+ self.obj = nil
66
+ unobserve_all
67
+ self.class.weak_observer_map.delete(strong_object_id)
68
+ }
69
+ end
70
+ # get rid of targets that dont contain anything to avoid retain cycles.
71
+ def clear_empty_targets!
72
+ return if @targets.nil?
73
+ @targets.each_pair do |target, key_paths|
74
+ if !key_paths || key_paths.size == 0
75
+ @targets.delete(target)
76
+ end
77
+ end
78
+ nil
79
+ end
80
+ def inspect
81
+ "#{strong_class_name}:#{strong_object_id}"
82
+ end
83
+ def targets
84
+ @targets
85
+ end
86
+ def self.weak_observer_map
87
+ Dispatch.once { @weak_observer_map = {} }
88
+ @weak_observer_map
89
+ end
90
+ def self.get(obj)
91
+ return obj if obj.is_a?(WeakObserverProxy)
92
+ weak_observer_map[obj.object_id] || new(obj)
93
+ end
94
+ end
95
+
96
+ end
97
+ Object.send(:include, ::RMExtensions::ObjectExtensions::Observation)
@@ -0,0 +1,68 @@
1
+ module RMExtensions
2
+
3
+ # A hash used by +rmext_on_serial_q+ storing created serial queues, so they
4
+ # are not instantiated each time they are used.
5
+ def self.serial_qs
6
+ Dispatch.once { @serial_qs = {} }
7
+ @serial_qs
8
+ end
9
+
10
+ module ObjectExtensions
11
+
12
+ # Wrapper methods to work around some bugs with GCD and blocks and how the compiler
13
+ # handles them. See here for more information:
14
+ #
15
+ # https://gist.github.com/mattetti/2951773
16
+ # https://github.com/MacRuby/MacRuby/issues/152
17
+ # blocks within blocks can be a problem with GCD (and maybe RM/MacRuby in general?).
18
+ # these helpers make it easy to use nested blocks with GCD, and also ensures those
19
+ # blocks will not be garbage collected until at least after they have been called.
20
+ #
21
+ # Also has the added benefit of ensuring your block is retained at least until
22
+ # it's been executed on the queue used.
23
+ #
24
+ # These helper methods are all for async mode.
25
+ module Queues
26
+
27
+ # execute a block on the main queue, asynchronously.
28
+ def rmext_on_main_q(&block)
29
+ rmext_retained_context do |x|
30
+ x.block = -> do
31
+ block.call
32
+ x.detach!
33
+ end
34
+ Dispatch::Queue.main.async(&x.block)
35
+ end
36
+ end
37
+
38
+ # execute a block on a serial queue, asynchronously.
39
+ def rmext_on_serial_q(q, &block)
40
+ rmext_retained_context do |x|
41
+ x.block = -> do
42
+ block.call
43
+ x.detach!
44
+ end
45
+ x.key = "#{NSBundle.mainBundle.bundleIdentifier}.serial.#{q}"
46
+ ::RMExtensions.serial_qs[x.key] ||= Dispatch::Queue.new(x.key)
47
+ ::RMExtensions.serial_qs[x.key].async(&x.block)
48
+ end
49
+ end
50
+
51
+ # execute a block on a concurrent queue, asynchronously.
52
+ def rmext_on_concurrent_q(q, &block)
53
+ rmext_retained_context do |x|
54
+ x.block = -> do
55
+ block.call
56
+ x.detach!
57
+ end
58
+ x.key = "#{NSBundle.mainBundle.bundleIdentifier}.concurrent.#{q}"
59
+ Dispatch::Queue.concurrent(x.key).async(&x.block)
60
+ end
61
+ end
62
+
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+ Object.send(:include, ::RMExtensions::ObjectExtensions::Queues)
@@ -0,0 +1,44 @@
1
+ module RMExtensions
2
+
3
+ # A retained array, which will hold other objects we want retained.
4
+ def self.retained_items
5
+ Dispatch.once { @retained_items = [] }
6
+ @retained_items
7
+ end
8
+
9
+ # A serial queue to perform all retain/detach operations on, to ensure we are always modifying
10
+ # +retained_items+ on the same thread.
11
+ def self.retains_queue
12
+ Dispatch.once { @retains_queue = Dispatch::Queue.new("#{NSBundle.mainBundle.bundleIdentifier}.rmext_retains_queue") }
13
+ @retains_queue
14
+ end
15
+
16
+ module ObjectExtensions
17
+
18
+ module Retention
19
+
20
+ # adds +self+ to +retained_items+. this ensures +self+ will be retained at least
21
+ # until +self+ is removed from +retained_items+ by calling +rmext_detach!+
22
+ def rmext_retain!
23
+ ::RMExtensions.retains_queue.sync do
24
+ ::RMExtensions.retained_items.push(self)
25
+ end
26
+ end
27
+
28
+ # removes one instance of +self+ from +retained_items+. if +rmext_retain!+ has been called
29
+ # multiple times on an object, +rmext_detach!+ must be called an equal number of times for
30
+ # it to be completely removed from +retained_items+. even after the object is completely
31
+ # out of +retained_items+, it may still be retained in memory if there are strong references
32
+ # to it anywhere else in your code.
33
+ def rmext_detach!
34
+ ::RMExtensions.retains_queue.async do
35
+ ::RMExtensions.retained_items.delete_at(::RMExtensions.retained_items.index(self) || ::RMExtensions.retained_items.length)
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ Object.send(:include, ::RMExtensions::ObjectExtensions::Retention)
@@ -0,0 +1,18 @@
1
+ module RMExtensions
2
+
3
+ module ObjectExtensions
4
+
5
+ module Util
6
+
7
+ # Raises an exception when called from a thread other than the main thread.
8
+ # Good for development and experimenting.
9
+ def rmext_assert_main_thread!
10
+ raise "This method must be called on the main thread." unless NSThread.currentThread.isMainThread
11
+ end
12
+
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ Object.send(:include, ::RMExtensions::ObjectExtensions::Util)
data/lib/rm-extensions.rb CHANGED
@@ -5,8 +5,16 @@ unless defined?(Motion::Project::Config)
5
5
  end
6
6
 
7
7
  Motion::Project::App.setup do |app|
8
- Dir.glob(File.join(File.dirname(__FILE__), 'motion/**/*.rb')).each do |file|
9
- app.files.unshift(file)
8
+ %w(
9
+ util
10
+ retention
11
+ accessors
12
+ deallocation
13
+ context
14
+ observation
15
+ queues
16
+ ).reverse.each do |x|
17
+ app.files.unshift(File.join(File.dirname(__FILE__), "motion/#{x}.rb"))
10
18
  end
11
19
  app.vendor_project(File.join(File.dirname(__FILE__), '../ext'), :static)
12
20
  end
@@ -1,3 +1,3 @@
1
1
  module RMExtensions
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.6"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rm-extensions
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-02 00:00:00.000000000 Z
12
+ date: 2013-05-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bubble-wrap
@@ -41,7 +41,13 @@ files:
41
41
  - Rakefile
42
42
  - ext/NSObject+RMExtensions.h
43
43
  - ext/NSObject+RMExtensions.m
44
- - lib/motion/rm-extensions.rb
44
+ - lib/motion/accessors.rb
45
+ - lib/motion/context.rb
46
+ - lib/motion/deallocation.rb
47
+ - lib/motion/observation.rb
48
+ - lib/motion/queues.rb
49
+ - lib/motion/retention.rb
50
+ - lib/motion/util.rb
45
51
  - lib/rm-extensions.rb
46
52
  - lib/rm-extensions/version.rb
47
53
  - rm-extensions.gemspec
@@ -1,342 +0,0 @@
1
- module RMExtensions
2
-
3
- # this module is included on Object, so these methods are available from anywhere in your code.
4
- module ObjectExtensions
5
-
6
- def rmext_weak_attr_accessor(*attrs)
7
- attrs.each do |attr|
8
- define_method(attr) do
9
- rmext_associatedValueForKey(attr.to_sym)
10
- end
11
- define_method("#{attr}=") do |val|
12
- rmext_weaklyAssociateValue(val, withKey: attr.to_sym)
13
- val
14
- end
15
- end
16
- end
17
-
18
- def rmext_copy_attr_accessor(*attrs)
19
- attrs.each do |attr|
20
- define_method(attr) do
21
- rmext_associatedValueForKey(attr.to_sym)
22
- end
23
- define_method("#{attr}=") do |val|
24
- rmext_associateCopyOfValue(val, withKey: attr.to_sym)
25
- val
26
- end
27
- end
28
- end
29
-
30
- def rmext_assert_main_thread!
31
- raise "This method must be called on the main thread." unless NSThread.currentThread.isMainThread
32
- end
33
-
34
- # https://gist.github.com/mattetti/2951773
35
- # https://github.com/MacRuby/MacRuby/issues/152
36
- # blocks within blocks can be a problem with GCD (and maybe RM/MacRuby in general?).
37
- # these helpers make it easy to use nested blocks with GCD, and also ensures those
38
- # blocks will not be garbage collected until at least after they have been called.
39
-
40
- def rmext_on_main_q(&block)
41
- rmext_retained_context do |x|
42
- x.block = -> do
43
- block.call
44
- x.detach!
45
- end
46
- Dispatch::Queue.main.async(&x.block)
47
- end
48
- end
49
-
50
- def rmext_on_serial_q(q, &block)
51
- Dispatch.once { $serial_qs = {} }
52
- rmext_retained_context do |x|
53
- x.block = -> do
54
- block.call
55
- x.detach!
56
- end
57
- x.key = "#{NSBundle.mainBundle.bundleIdentifier}.serial.#{q}"
58
- $serial_qs[x.key] ||= Dispatch::Queue.new(x.key)
59
- $serial_qs[x.key].async(&x.block)
60
- end
61
- end
62
-
63
- def rmext_on_concurrent_q(q, &block)
64
- rmext_retained_context do |x|
65
- x.block = -> do
66
- block.call
67
- x.detach!
68
- end
69
- x.key = "#{NSBundle.mainBundle.bundleIdentifier}.concurrent.#{q}"
70
- Dispatch::Queue.concurrent(x.key).async(&x.block)
71
- end
72
- end
73
-
74
- # #rmext_retain! is different than a normal retain. it adds the object(self) to a retained
75
- # array, utilizing RM's underlying GC logic
76
- #
77
- # you most likely want to use #rmext_retained_context and not call this directly
78
- #
79
- def rmext_retain!
80
- ::RMExtensions::RetainedContext.rmext_retains_queue.sync do
81
- ::RMExtensions::RetainedContext.rmext_retains.push(self)
82
- end
83
- end
84
-
85
- # #rmext_detach! is slightly similar to the concept of "release". it removes the object(self)
86
- # from a retained array (only one hit, in case the same object is #rmext_retain!'d multiple times),
87
- # utilizing RM's underlying GC logic. if nothing else has a strong reference to the object after
88
- # it is detached, it will eventually be handled by RM's GC.
89
- #
90
- # you most likely want to use #rmext_retained_context and not call this directly
91
- #
92
- def rmext_detach!
93
- ::RMExtensions::RetainedContext.rmext_retains_queue.async do
94
- ::RMExtensions::RetainedContext.rmext_retains.delete_at(::RMExtensions::RetainedContext.rmext_retains.index(self) || ::RMExtensions::RetainedContext.rmext_retains.length)
95
- end
96
- end
97
-
98
- # #rmext_context yields an object you can treat like an openstruct (the "context")
99
- def rmext_context(&block)
100
- ::RMExtensions::Context.create(self, &block)
101
- end
102
-
103
- # #rmext_retained_context yields an object you can treat like an openstruct. you can get/set any
104
- # property on it. the context is globally retained, until #detach! is called on the context.
105
- # this convention should fill the gap where local variables and scope bugs currently occur in RM,
106
- # and it also solves the re-entrant problem of using instance variables for retaining purposes.
107
- #
108
- # always be sure to #detach! the context at the correct place in time.
109
- #
110
- # example:
111
- #
112
- # rmext_retained_context do |x|
113
- # rmext_on_serial_q("my_serial_q") do
114
- # some_async_http_request do |results1|
115
- # x.results1 = results1
116
- # rmext_on_serial_q("my_serial_q") do
117
- # some_other_async_http_request do |results2|
118
- # x.results2 = results2
119
- # rmext_on_main_q do
120
- # p "results1", x.results1
121
- # p "results2", x.results2
122
- # x.detach!
123
- # end
124
- # end
125
- # end
126
- # end
127
- # end
128
- # end
129
- #
130
- # experimental feature:
131
- #
132
- # you can call #begin_background! on the context, and it will check-out a background task identifier,
133
- # and automatically end the background task when you call #detach! as normal.
134
- def rmext_retained_context(&block)
135
- ::RMExtensions::RetainedContext.create(self, &block)
136
- end
137
-
138
-
139
- def rmext_observe(object, key, &block)
140
- # p "+ rmext_observe", self, object, key
141
- rmext_observe_passive(object, key, &block)
142
- block.call(object.send(key)) unless block.nil?
143
- end
144
-
145
- def rmext_observe_passive(object, key, &block)
146
- wop = ::RMExtensions::WeakObserverProxy.get(self)
147
- b = -> (old_value, new_value) do
148
- block.call(new_value) unless block.nil?
149
- end
150
- wop.observe(object, key, &b)
151
- end
152
-
153
- def rmext_unobserve(object, key)
154
- wop = ::RMExtensions::WeakObserverProxy.get(self)
155
- wop.unobserve(object, key)
156
- wop.clear_empty_targets!
157
- end
158
-
159
- def rmext_unobserve_all
160
- wop = ::RMExtensions::WeakObserverProxy.get(self)
161
- wop.unobserve_all
162
- end
163
-
164
- def rmext_on_dealloc(&block)
165
- internalObject = ::RMExtensions::OnDeallocInternalObject.create("#{self.class.name}:#{object_id}", self, block)
166
- @rmext_on_dealloc_blocks ||= {}
167
- @rmext_on_dealloc_blocks[internalObject] = internalObject
168
- nil
169
- end
170
-
171
- def rmext_cancel_on_dealloc(block)
172
- @rmext_on_dealloc_blocks ||= {}
173
- if internalObject = @rmext_on_dealloc_blocks[block]
174
- internalObject.block = nil
175
- @rmext_on_dealloc_blocks.delete(block)
176
- end
177
- nil
178
- end
179
-
180
- end
181
-
182
- end
183
- Object.send(:include, ::RMExtensions::ObjectExtensions)
184
-
185
- module RMExtensions
186
- # You don't use these classes directly.
187
- class Context
188
-
189
- class << self
190
- def create(origin, &block)
191
- x = new
192
- block.call(x) unless block.nil?
193
- x
194
- end
195
- end
196
-
197
- attr_accessor :hash
198
-
199
- def initialize
200
- self.hash = {}
201
- end
202
-
203
- def method_missing(method, *args)
204
- m = method.to_s
205
- if m =~ /(.+)?=$/
206
- hash[$1] = args.first
207
- else
208
- hash[m]
209
- end
210
- end
211
-
212
- end
213
-
214
- class RetainedContext < Context
215
-
216
- class << self
217
- def rmext_retains
218
- Dispatch.once { @rmext_retains = [] }
219
- @rmext_retains
220
- end
221
-
222
- def rmext_retains_queue
223
- Dispatch.once { @rmext_retains_queue = Dispatch::Queue.new("#{NSBundle.mainBundle.bundleIdentifier}.rmext_retains_queue") }
224
- @rmext_retains_queue
225
- end
226
-
227
- def create(origin, &block)
228
- x = new
229
- x.hash["retained_origin"] = origin
230
- x.hash["retained_block"] = block
231
- x.rmext_retain!
232
- block.call(x) unless block.nil?
233
- x
234
- end
235
- end
236
-
237
- # if you provide a block, you are responsible for calling #detach!,
238
- # otherwise, the expiration handler will just call #detach!
239
- def begin_background!(&block)
240
- hash["bgTaskExpirationHandler"] = block
241
- hash["bgTask"] = UIApplication.sharedApplication.beginBackgroundTaskWithExpirationHandler(-> do
242
- if hash["bgTaskExpirationHandler"]
243
- hash["bgTaskExpirationHandler"].call
244
- else
245
- detach!
246
- end
247
- end)
248
- end
249
-
250
- def detach!
251
- if hash["bgTask"] && hash["bgTask"] != UIBackgroundTaskInvalid
252
- UIApplication.sharedApplication.endBackgroundTask(hash["bgTask"])
253
- end
254
- self.hash = nil
255
- rmext_detach!
256
- end
257
-
258
- def detach_on_death_of(object)
259
- object.rmext_on_dealloc(&detach_death_proc)
260
- end
261
-
262
- def detach_death_proc
263
- proc { |x| detach! }
264
- end
265
-
266
- def method_missing(method, *args)
267
- unless hash
268
- raise "You detached this rmext_retained_context and then called: #{method}"
269
- end
270
- super
271
- end
272
-
273
- end
274
-
275
- class WeakObserverProxy
276
- include BW::KVO
277
- rmext_weak_attr_accessor :obj
278
- attr_accessor :strong_object_id, :strong_class_name
279
- def initialize(strong_object)
280
- self.obj = strong_object
281
- self.strong_object_id = strong_object.object_id
282
- self.strong_class_name = strong_object.class.name
283
- self.class.weak_observer_map[strong_object_id] = self
284
- strong_object.rmext_on_dealloc(&kill_observation_proc)
285
- end
286
- # isolate this in its own method so it wont create a retain cycle
287
- def kill_observation_proc
288
- proc { |x|
289
- # uncomment to verify deallocation is working. if not, there is probably
290
- # a retain cycle somewhere in your code.
291
- # p "kill_observation_proc", self
292
- self.obj = nil
293
- unobserve_all
294
- self.class.weak_observer_map.delete(strong_object_id)
295
- }
296
- end
297
- def clear_empty_targets!
298
- return if @targets.nil?
299
- @targets.each_pair do |target, key_paths|
300
- if !key_paths || key_paths.size == 0
301
- @targets.delete(target)
302
- end
303
- end
304
- nil
305
- end
306
- def inspect
307
- "#{strong_class_name}:#{strong_object_id}"
308
- end
309
- def targets
310
- @targets
311
- end
312
- def self.weak_observer_map
313
- Dispatch.once { $weak_observer_map = {} }
314
- $weak_observer_map
315
- end
316
- def self.get(obj)
317
- return obj if obj.is_a?(WeakObserverProxy)
318
- weak_observer_map[obj.object_id] || new(obj)
319
- end
320
- end
321
-
322
- class OnDeallocInternalObject
323
- attr_accessor :description, :block
324
- rmext_weak_attr_accessor :obj
325
- def self.create(description, obj, block)
326
- x = new
327
- x.description = description
328
- x.obj = obj
329
- x.block = block
330
- x
331
- end
332
- def dealloc
333
- # p "dealloc OnDeallocInternalObject #{description}"
334
- if block
335
- block.call(obj)
336
- self.block = nil
337
- end
338
- super
339
- end
340
- end
341
-
342
- end