rm-extensions 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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