motion-prime 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
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) || {})