motion-prime 0.8.1 → 0.8.2

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.
Files changed (41) hide show
  1. checksums.yaml +8 -8
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile.lock +1 -1
  4. data/ROADMAP.md +3 -0
  5. data/files/Gemfile +1 -1
  6. data/motion-prime/api_client.rb +5 -2
  7. data/motion-prime/core_ext/kernel.rb +36 -0
  8. data/motion-prime/elements/_content_text_mixin.rb +32 -11
  9. data/motion-prime/elements/base_element.rb +33 -16
  10. data/motion-prime/elements/draw.rb +10 -4
  11. data/motion-prime/elements/draw/image.rb +26 -15
  12. data/motion-prime/elements/map.rb +5 -0
  13. data/motion-prime/models/_association_mixin.rb +0 -3
  14. data/motion-prime/models/_base_mixin.rb +24 -3
  15. data/motion-prime/models/_filter_mixin.rb +28 -0
  16. data/motion-prime/models/_sync_mixin.rb +13 -10
  17. data/motion-prime/models/association_collection.rb +41 -16
  18. data/motion-prime/models/errors.rb +46 -24
  19. data/motion-prime/models/model.rb +8 -0
  20. data/motion-prime/screens/_sections_mixin.rb +1 -1
  21. data/motion-prime/screens/extensions/_indicators_mixin.rb +4 -2
  22. data/motion-prime/screens/extensions/_navigation_bar_mixin.rb +1 -1
  23. data/motion-prime/screens/screen.rb +4 -0
  24. data/motion-prime/sections/_async_form_mixin.rb +12 -0
  25. data/motion-prime/sections/_async_table_mixin.rb +193 -0
  26. data/motion-prime/sections/_cell_section_mixin.rb +6 -6
  27. data/motion-prime/sections/_draw_section_mixin.rb +13 -5
  28. data/motion-prime/sections/base_section.rb +62 -36
  29. data/motion-prime/sections/form.rb +19 -35
  30. data/motion-prime/sections/form/base_field_section.rb +42 -34
  31. data/motion-prime/sections/form/static_field_section.rb +9 -0
  32. data/motion-prime/sections/table.rb +143 -201
  33. data/motion-prime/sections/table/table_delegate.rb +15 -15
  34. data/motion-prime/services/table_data_indexes.rb +12 -2
  35. data/motion-prime/styles/base.rb +1 -1
  36. data/motion-prime/styles/form.rb +6 -6
  37. data/motion-prime/version.rb +1 -1
  38. data/motion-prime/views/layout.rb +5 -2
  39. data/motion-prime/views/view_styler.rb +2 -0
  40. data/spec/models/association_collection_spec.rb +28 -6
  41. metadata +6 -2
@@ -53,6 +53,10 @@ module MotionPrime
53
53
  super
54
54
  end
55
55
 
56
+ def strong_references
57
+ self.main_controller
58
+ end
59
+
56
60
  def visible?
57
61
  @visible
58
62
  end
@@ -0,0 +1,12 @@
1
+ module Prime
2
+ module AsyncFormMixin
3
+ def reload_table_data
4
+ return super unless async_data?
5
+ sections = NSMutableIndexSet.new
6
+ number_of_groups.times do |section_id|
7
+ sections.addIndex(section_id)
8
+ end
9
+ table_view.reloadSections sections, withRowAnimation: UITableViewRowAnimationFade
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,193 @@
1
+ module Prime
2
+ module AsyncTableMixin
3
+ extend ::MotionSupport::Concern
4
+
5
+ included do
6
+ class_attribute :async_data_options
7
+ end
8
+
9
+ # Returns true if table section have enabled async data. False by defaul.
10
+ #
11
+ # @return [Boolean] is async data enabled.
12
+ def async_data?
13
+ self.class.async_data_options
14
+ end
15
+
16
+ def table_element_options
17
+ options = super
18
+ if async_data? && self.class.async_data_options.has_key?(:estimated_cell_height)
19
+ options[:estimated_cell_height] = self.class.async_data_options[:estimated_cell_height]
20
+ end
21
+ options
22
+ end
23
+
24
+ # Reset async loaded table data and preloader queue.
25
+ #
26
+ # @return [Boolean] true
27
+ def reset_data
28
+ super # must be before to update fixed_table_data
29
+ @async_loaded_data = async_data? ? fixed_table_data : nil
30
+ Array.wrap(@preloader_queue).each { |queue| queue[:state] = :cancelled }
31
+ @preloader_next_starts_from = nil
32
+ end
33
+
34
+ def height_for_index(table, index)
35
+ section = cell_section_by_index(index)
36
+ preload_section_by_index(index)
37
+ section.container_height
38
+ end
39
+
40
+ def render_cell(index, table)
41
+ preload_sections_after(index)
42
+ super
43
+ end
44
+
45
+ def on_async_data_loaded; end
46
+ def on_queue_preloaded(queue_id, loaded_index); end
47
+ def on_cell_section_preloaded(section, index); end
48
+
49
+ # Preloads sections after rendering cell in current sheduled index or given index.
50
+ # TODO: probably should be in separate class.
51
+ #
52
+ # @param from_index [NSIndexPath] Value of first index to load if current sheduled index not exists.
53
+ # @return [NSIndexPath, Boolean] Index of next sheduled index.
54
+ def preload_sections_after(from_index)
55
+ return unless async_data?
56
+ service = preloader_index_service
57
+ load_limit = self.class.async_data_options.try(:[], :preload_cells_count)
58
+
59
+ if @preloader_next_starts_from
60
+ index_to_start_preloading = service.sum_index(@preloader_next_starts_from, load_limit ? -load_limit/2 : 0)
61
+ # should we start preload based on index of rendered cell
62
+ return false if service.compare_indexes(from_index, index_to_start_preloading) < 0
63
+ end
64
+
65
+ # adjust start/finish points based on current queues
66
+ current_group = from_index.section
67
+ left_to_load_in_group = cell_sections_for_group(current_group).count - from_index.row
68
+ load_count = [left_to_load_in_group, load_limit].compact.min
69
+ to_index = service.sum_index(from_index, load_count - 1)
70
+ @preloader_next_starts_from = to_index
71
+
72
+ Array.wrap(@preloader_queue).each do |queue_info|
73
+ # cancelled and dealloc are left from prev data
74
+ next unless [:in_progress, :completed].include?(queue_info[:state])
75
+ # filter by current group
76
+ next unless queue_info[:from_index].section == current_group
77
+ # reject not started threads
78
+ next if queue_info[:to_index].nil? && queue_info[:state] != :in_progress
79
+
80
+ if from_index.row >= queue_info[:from_index].row
81
+ from_index = NSIndexPath.indexPathForRow([from_index.row, queue_info[:to_index].try(:row).try(:+, 1), (queue_info[:target_index] if queue_info[:state] == :in_progress).try(:row).try(:+, 1)].compact.max, inSection: current_group)
82
+ else
83
+ to_index = NSIndexPath.indexPathForRow([to_index.row, queue_info[:from_index].try(:row).try(:-, 1)].compact.min, inSection: current_group)
84
+ end
85
+ end
86
+
87
+ load_count = to_index.row - from_index.row + 1
88
+ preload_sections_schedule_from(from_index, load_count) if load_count > 0
89
+ end
90
+
91
+ # Schedules preloading sections starting with given index with given limit.
92
+ # TODO: probably should be in separate class.
93
+ #
94
+ # @param index [NSIndexPath] Value of first index to load.
95
+ # @param load_count [Integer] Count of sections to load.
96
+ # @return [Integer] Queue ID
97
+ def preload_sections_schedule_from(index, load_count)
98
+ service = preloader_index_service
99
+
100
+ @preloader_queue ||= []
101
+
102
+ # TODO: we do we need to keep screen ref too?
103
+ queue_id = @preloader_queue.count
104
+
105
+ allocate_strong_references(queue_id)
106
+
107
+ @preloader_queue[queue_id] = {
108
+ state: :in_progress,
109
+ target_index: service.sum_index(index, load_count-1),
110
+ from_index: index
111
+ }
112
+
113
+ BW::Reactor.schedule(queue_id) do |queue_id|
114
+ result = load_count.times do |offset|
115
+ if @preloader_queue[queue_id][:state] == :cancelled
116
+ release_strong_references(queue_id)
117
+ break
118
+ end
119
+ if allocated_references_released?
120
+ @preloader_queue[queue_id][:state] = :dealloc
121
+ release_strong_references(queue_id)
122
+ break
123
+ end
124
+
125
+ if section = preload_section_by_index(index)
126
+ on_cell_section_preloaded(section, index)
127
+ end
128
+
129
+ @preloader_queue[queue_id][:to_index] = index
130
+ unless offset == load_count - 1
131
+ index = service.sum_index(index, 1)
132
+ end
133
+ true
134
+ end
135
+
136
+ if result
137
+ @preloader_queue[queue_id][:state] = :completed
138
+ on_queue_preloaded(queue_id, index)
139
+ end
140
+ release_strong_references(queue_id)
141
+ end
142
+ queue_id
143
+ end
144
+
145
+ def preloader_index_service
146
+ TableDataIndexes.new(@data)
147
+ end
148
+
149
+ private
150
+ def set_table_data
151
+ sections = load_sections_async
152
+ prepare_table_cell_sections(sections)
153
+ @data = sections
154
+ reset_data_stamps
155
+ @data
156
+ end
157
+
158
+ def load_sections_async
159
+ @async_loaded_data || begin
160
+ ref_key = allocate_strong_references
161
+ BW::Reactor.schedule_on_main do
162
+ @async_loaded_data = fixed_table_data
163
+ @data = nil
164
+ reload_table_data
165
+ on_async_data_loaded
166
+ release_strong_references(ref_key)
167
+ end
168
+ []
169
+ end
170
+ end
171
+
172
+ def preload_section_by_index(index)
173
+ section = cell_section_by_index(index)
174
+ if section.create_elements && !section.container_element && async_data? # perform only if just loaded
175
+ section.load_container_with_elements(container: container_element_options_for(index))
176
+ section
177
+ end
178
+ end
179
+
180
+ def create_section_elements; end
181
+
182
+ module ClassMethods
183
+ def inherited(subclass)
184
+ super
185
+ subclass.async_data_options = self.async_data_options.try(:clone)
186
+ end
187
+
188
+ def set_async_data_options(options = {})
189
+ self.async_data_options = options
190
+ end
191
+ end
192
+ end
193
+ end
@@ -9,7 +9,7 @@ module MotionPrime
9
9
  attr_reader :pending_display
10
10
 
11
11
  included do
12
- class_attribute :custom_cell_name
12
+ class_attribute :custom_cell_section_name
13
13
  container_element type: :table_view_cell
14
14
  end
15
15
 
@@ -18,7 +18,7 @@ module MotionPrime
18
18
  end
19
19
 
20
20
  def section_styles
21
- @section_styles ||= table.try(:cell_styles, self) || {}
21
+ @section_styles ||= table.try(:cell_section_styles, self) || {}
22
22
  end
23
23
 
24
24
  def cell_type
@@ -27,8 +27,8 @@ module MotionPrime
27
27
  end
28
28
  end
29
29
 
30
- def cell_name
31
- self.class.custom_cell_name || begin
30
+ def cell_section_name
31
+ self.class.custom_cell_section_name || begin
32
32
  return name unless table
33
33
  table_name = table.name.gsub('_table', '')
34
34
  name.gsub("#{table_name}_", '')
@@ -68,8 +68,8 @@ module MotionPrime
68
68
  end
69
69
 
70
70
  module ClassMethods
71
- def set_cell_name(value)
72
- self.custom_cell_name = value
71
+ def set_cell_section_name(value)
72
+ self.custom_cell_section_name = value
73
73
  end
74
74
  end
75
75
  end
@@ -35,6 +35,11 @@ module MotionPrime
35
35
  self.container_gesture_recognizers << {element: element, action: action, receiver: receiver}
36
36
  end
37
37
 
38
+ def clear_gesture_for_receiver(receiver)
39
+ return unless self.container_gesture_recognizers
40
+ self.container_gesture_recognizers.delete_if { |recognizer| recognizer[:receiver] == receiver }
41
+ end
42
+
38
43
  def prerender_elements_for_state(state = :normal)
39
44
  scale = UIScreen.mainScreen.scale
40
45
  space = CGColorSpaceCreateDeviceRGB()
@@ -60,10 +65,6 @@ module MotionPrime
60
65
  @cached_draw_image ||= MotionSupport::HashWithIndifferentAccess.new
61
66
  end
62
67
 
63
- def strong_references
64
- [self, screen.main_controller].map(&:strong_ref)
65
- end
66
-
67
68
  private
68
69
  def set_container_gesture_recognizer
69
70
  single_tap = UITapGestureRecognizer.alloc.initWithTarget(self, action: 'on_container_tap_gesture:')
@@ -74,7 +75,14 @@ module MotionPrime
74
75
 
75
76
  def on_container_tap_gesture(recognizer)
76
77
  target = Array.wrap(container_gesture_recognizers).detect do |gesture_data|
77
- CGRectContainsPoint(gesture_data[:element].computed_frame, recognizer.locationInView(container_view))
78
+ point = recognizer.locationInView(container_view)
79
+ element = gesture_data[:element]
80
+ section = element.section
81
+ if section.has_container_bounds?
82
+ point.x -= section.container_bounds.origin.x
83
+ point.y -= section.container_bounds.origin.y
84
+ end
85
+ CGRectContainsPoint(element.computed_frame, point)
78
86
  end
79
87
  (target[:receiver] || self).send(target[:action], recognizer, target[:element]) if target
80
88
  end
@@ -35,18 +35,36 @@ module MotionPrime
35
35
  @name = options[:name] ||= default_name
36
36
  @options_block = options[:block]
37
37
  end
38
+
39
+ if Prime.env.development?
40
+ @_section_info = "#{@name} #{screen.try(:class)}"
41
+ @@_allocated_sections ||= []
42
+ @@_allocated_sections << @_section_info
43
+ end
38
44
  end
39
45
 
40
46
  def dealloc
47
+ if Prime.env.development?
48
+ index = @@_allocated_sections.index(@_section_info)
49
+ @@_allocated_sections.delete_at(index)
50
+ end
41
51
  Prime.logger.dealloc_message :section, self, self.name
42
52
  NSNotificationCenter.defaultCenter.removeObserver self # unbinding events created in bind_keyboard_events
43
53
  super
44
54
  end
45
55
 
56
+ def strong_references
57
+ [screen, screen.main_controller]
58
+ end
59
+
46
60
  def container_bounds
47
61
  options[:container_bounds] or raise "You must pass `container bounds` option to prerender base section"
48
62
  end
49
63
 
64
+ def has_container_bounds?
65
+ options[:container_bounds].present?
66
+ end
67
+
50
68
  # Get computed container options
51
69
  #
52
70
  # @return options [Hash] computed options
@@ -101,33 +119,31 @@ module MotionPrime
101
119
  # they will be rendered immediately after that or rendered async later, based on type of section.
102
120
  #
103
121
  # @return result [Boolean] true if has been loaded by this thread.
104
- def load_section
122
+ def create_elements
105
123
  return if @section_loaded
106
124
  if @section_loading
107
125
  sleep 0.1
108
- return @section_loaded ? false : load_section
126
+ return @section_loaded ? false : create_elements
109
127
  end
110
128
  @section_loading = true
111
- create_elements
129
+
130
+ self.elements = {}
131
+ elements_options.each do |key, opts|
132
+ add_element(key, opts)
133
+ end
134
+ self.instance_eval(&@options_block) if @options_block.is_a?(Proc)
135
+
112
136
  @section_loading = false
113
137
  return @section_loaded = true
114
138
  end
115
139
 
116
- # Force load section
117
- #
118
- # @return result [Boolean] true if has been loaded by this thread.
119
- def load_section!
120
- @section_loaded = false
121
- load_section
122
- end
123
-
124
140
  # Force reload section, will also re-render elements.
125
141
  # For table view cells will also reload it's table data.
126
142
  def reload_section
127
143
  self.elements_to_render.values.map(&:view).flatten.each do |view|
128
144
  view.removeFromSuperview if view
129
145
  end
130
- load_section!
146
+ create_elements!
131
147
  run_callbacks :render do
132
148
  render!
133
149
  end
@@ -138,14 +154,6 @@ module MotionPrime
138
154
  end
139
155
  end
140
156
 
141
- def create_elements
142
- self.elements = {}
143
- elements_options.each do |key, opts|
144
- add_element(key, opts)
145
- end
146
- self.instance_eval(&@options_block) if @options_block.is_a?(Proc)
147
- end
148
-
149
157
  def add_element(key, options = {})
150
158
  return unless render_element?(key)
151
159
  opts = options.clone
@@ -159,16 +167,7 @@ module MotionPrime
159
167
  else
160
168
  self.elements[key] = element
161
169
  end
162
- end
163
-
164
- def build_element(options = {})
165
- type = options.delete(:type)
166
- render_as = options.delete(:as).to_s
167
- if self.is_a?(BaseFieldSection) || self.is_a?(BaseHeaderSection) || render_as == 'view'
168
- BaseElement.factory(type, options)
169
- else
170
- DrawElement.factory(type, options) || BaseElement.factory(type, options)
171
- end
170
+ element
172
171
  end
173
172
 
174
173
  def render_element?(element_name)
@@ -183,7 +182,7 @@ module MotionPrime
183
182
  end
184
183
 
185
184
  def render(container_options = {})
186
- load_section
185
+ create_elements
187
186
  self.container_options.merge!(container_options)
188
187
  run_callbacks :render do
189
188
  render!
@@ -191,8 +190,8 @@ module MotionPrime
191
190
  end
192
191
 
193
192
  def render_container(options = {}, &block)
194
- if should_render_container?
195
- element = self.container_element || self.init_container_element(options)
193
+ if should_render_container? && !self.container_element.try(:view)
194
+ element = self.init_container_element(options)
196
195
  element.render do
197
196
  block.call
198
197
  end
@@ -210,7 +209,8 @@ module MotionPrime
210
209
  end
211
210
 
212
211
  def element(name)
213
- elements[name.to_sym]
212
+ self.elements ||= {}
213
+ self.elements[name.to_sym]
214
214
  end
215
215
 
216
216
  def view(name)
@@ -262,7 +262,7 @@ module MotionPrime
262
262
  views = Array.wrap(keyboard_close_bindings_options[:views])
263
263
 
264
264
  elements.each do |el|
265
- views << el.view if %w[text_field text_view].include?(el.view_name) && el.view
265
+ views << el.view if el.try(:view) && %w[text_field text_view].include?(el.view_name)
266
266
  end
267
267
  views.compact.each(&:resignFirstResponder)
268
268
  end
@@ -272,7 +272,15 @@ module MotionPrime
272
272
  end
273
273
 
274
274
  def elements_to_render
275
- self.elements.select { |key, element| element.is_a?(BaseElement) }
275
+ self.elements.except(*elements_to_draw.keys)
276
+ end
277
+
278
+ def current_input_view_height
279
+ App.shared.windows.last.subviews.first.try(:height) || KEYBOARD_HEIGHT_PORTRAIT
280
+ end
281
+
282
+ def screen?
283
+ screen && screen.weakref_alive?
276
284
  end
277
285
 
278
286
  protected
@@ -312,6 +320,24 @@ module MotionPrime
312
320
  end
313
321
  end
314
322
 
323
+ # Force load section
324
+ #
325
+ # @return result [Boolean] true if has been loaded by this thread.
326
+ def create_elements!
327
+ @section_loaded = false
328
+ create_elements
329
+ end
330
+
331
+ def build_element(options = {})
332
+ type = options.delete(:type)
333
+ render_as = options.delete(:as).to_s
334
+ if render_as != 'draw' && (render_as == 'view' || self.is_a?(BaseFieldSection) || self.is_a?(BaseHeaderSection))
335
+ BaseElement.factory(type, options)
336
+ else
337
+ DrawElement.factory(type, options) || BaseElement.factory(type, options)
338
+ end
339
+ end
340
+
315
341
  def compute_container_options!
316
342
  raw_options = {}
317
343
  raw_options.merge!(self.class.container_options.try(:clone) || {})