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
@@ -29,31 +29,26 @@ module MotionPrime
29
29
  end
30
30
  end
31
31
 
32
- def reload_cell(section)
32
+ def reload_cell_section(section)
33
33
  field = section.name.to_sym
34
- index = field_indexes[field].split('_').map(&:to_i)
35
- path = NSIndexPath.indexPathForRow(index.last, inSection: index.first)
34
+ path = field_indexes[field]
36
35
  section.cell.try(:removeFromSuperview)
37
36
 
38
37
  fields[field] = load_field(self.class.fields_options[field])
39
- fields[field].load_section
38
+ fields[field].create_elements
40
39
  if flat_data?
41
40
  @data[path.row] = fields[field]
42
41
  else
43
42
  @data[path.section][path.row] = fields[field]
44
43
  end
45
44
 
46
- set_data_stamp(field_indexes[field])
45
+ set_data_stamp(fields[field].object_id)
47
46
 
48
47
  # table_view.beginUpdates
49
48
  table_view.reloadRowsAtIndexPaths([path], withRowAnimation: UITableViewRowAnimationNone)
50
49
  # table_view.endUpdates
51
50
  end
52
51
 
53
- def reset_data_stamps
54
- set_data_stamp(self.field_indexes.values)
55
- end
56
-
57
52
  # Returns element based on field name and element name
58
53
  #
59
54
  # Examples:
@@ -169,18 +164,13 @@ module MotionPrime
169
164
 
170
165
  def reload_data
171
166
  @groups_count = nil
167
+ init_form_fields # must be before resetting to reflect changes on @data
172
168
  reset_data
173
- init_form_fields
174
169
  reload_table_data
175
170
  end
176
171
 
177
- def reset_data
178
- super
179
- self.fields.values.each(&:clear_observers)
180
- end
181
-
182
172
  def has_many_sections?
183
- section_header_options.present? || grouped_data.count > 1
173
+ group_header_options.present? || grouped_data.count > 1
184
174
  end
185
175
 
186
176
  def render_table
@@ -188,27 +178,13 @@ module MotionPrime
188
178
  super
189
179
  end
190
180
 
191
- def reload_table_data
192
- return super unless async_data?
193
- sections = NSMutableIndexSet.new
194
- number_of_sections.times do |section_id|
195
- sections.addIndex(section_id)
196
- end
197
- table_view.reloadSections sections, withRowAnimation: UITableViewRowAnimationFade
198
- end
199
-
200
181
  # Table View Delegate
201
182
  # ---------------------
202
183
 
203
- def number_of_sections(table = nil)
184
+ def number_of_groups(table = nil)
204
185
  has_many_sections? ? grouped_data.compact.count : 1
205
186
  end
206
187
 
207
- def height_for_index(table, index)
208
- section = load_cell_by_index(index, preload: false)
209
- section.container_height
210
- end
211
-
212
188
  class << self
213
189
  def inherited(subclass)
214
190
  super
@@ -217,6 +193,11 @@ module MotionPrime
217
193
  subclass.text_view_limits = self.text_view_limits.try(:clone)
218
194
  end
219
195
 
196
+ def async_table_data(options = {})
197
+ super
198
+ self.send :include, Prime::AsyncFormMixin
199
+ end
200
+
220
201
  def field(name, options = {}, &block)
221
202
  options[:name] = name
222
203
  options[:type] ||= :string
@@ -237,9 +218,11 @@ module MotionPrime
237
218
  end
238
219
 
239
220
  private
240
- def load_sections; end
221
+ def create_section_elements; end
241
222
 
242
223
  def init_form_fields
224
+ self.fields.values.each(&:clear_observers) if fields.present?
225
+
243
226
  self.fields = {}
244
227
  self.field_indexes = {}
245
228
  self.grouped_data = []
@@ -254,7 +237,7 @@ module MotionPrime
254
237
 
255
238
  section = load_field(field)
256
239
  self.fields[key] = section
257
- self.field_indexes[key] = "#{section_id}_#{section_indexes[section_id]}"
240
+ self.field_indexes[key] = NSIndexPath.indexPathForRow(section_indexes[section_id], inSection: section_id)
258
241
  grouped_data[section_id][section_indexes[section_id]] = section
259
242
 
260
243
  section_indexes[section_id] += 1
@@ -264,8 +247,9 @@ module MotionPrime
264
247
  end
265
248
 
266
249
  def init_form_headers
267
- options = Array.wrap(self.class.section_header_options).clone
268
- self.section_header_options = options.delete_if.each_with_index { |opts, id| grouped_data[id].nil? }
250
+ options = Array.wrap(self.class.group_header_options).clone
251
+ options.compact.each { |opts| normalize_options(opts) }
252
+ self.group_header_options = options.delete_if.each_with_index { |opts, id| grouped_data[id].nil? }
269
253
  end
270
254
  end
271
255
  end
@@ -36,11 +36,11 @@ module MotionPrime
36
36
  # @param height [Integet] new height of field
37
37
  # @return [MotionPrime::BaseFieldSection]
38
38
  def update_height(height)
39
+ return if container_options[:height] == height
39
40
  container_options[:height] = height
40
- field_index = form.field_indexes[name]
41
- index = field_index.split('_').map(&:to_i)
42
- path = NSIndexPath.indexPathForRow(index.last, inSection: index.first)
43
- form.table_view.reloadRowsAtIndexPaths([path], withRowAnimation: UITableViewRowAnimationFade)
41
+ index = form.field_indexes[name]
42
+ form.send :set_data_stamp, self.object_id
43
+ form.table_view.reloadRowsAtIndexPaths([index], withRowAnimation: UITableViewRowAnimationFade)
44
44
  self
45
45
  end
46
46
 
@@ -52,18 +52,19 @@ module MotionPrime
52
52
  def observe_model_errors
53
53
  return unless observing_errors?
54
54
  on_error_change = proc { |old_value, new_value|
55
- next if old_value == new_value
56
- if @status_for_updated == :rendered
57
- reload_section
58
- else
59
- load_section!
60
- form.reload_table_data
55
+ changes = observing_errors_for.errors.changes
56
+ errors_observer_fields.each do |field|
57
+ next unless changes.has_key?(field)
58
+ if @status_for_updated == :rendered
59
+ reload_section
60
+ else
61
+ create_elements!
62
+ form.reload_table_data
63
+ end
61
64
  end
62
65
  }.weak!
63
66
 
64
- errors_observer_fields.each do |field|
65
- observe observing_errors_for.errors, observing_errors_for.errors.unique_key(field), &on_error_change
66
- end
67
+ observe observing_errors_for.errors, :info, &on_error_change
67
68
  end
68
69
 
69
70
  def dealloc
@@ -75,18 +76,32 @@ module MotionPrime
75
76
  form.name
76
77
  end
77
78
 
78
- def focus(begin_editing = true)
79
- # scroll to cell
80
- path = form.table_view.indexPathForCell(cell)
81
- form.table_view.scrollToRowAtIndexPath path,
82
- atScrollPosition: UITableViewScrollPositionTop, animated: true
79
+ def focus(begin_editing = false)
83
80
  # focus on text field
84
- return unless begin_editing
85
- elements.values.each do |element|
81
+ elements.values.detect do |element|
86
82
  if element.view.is_a?(UITextField) || element.view.is_a?(UITextView)
87
- element.view.becomeFirstResponder and return
83
+ element.view.becomeFirstResponder
88
84
  end
85
+ end if begin_editing
86
+
87
+ # scroll to cell
88
+ # path = form.table_view.indexPathForCell(cell)
89
+ # form.table_view.scrollToRowAtIndexPath path,
90
+ # atScrollPosition: UITableViewScrollPositionTop, animated: true
91
+ return self unless input_view = element(:input).try(:view)
92
+ origin = input_view.frame.origin
93
+ point = container_view.convertPoint(origin, toView: form.table_view)
94
+
95
+ nav_bar_height = screen.navigationController ? screen.navigationController.navigationBar.frame.size.height : 0
96
+ nav_bar_height += 20 # status bar height
97
+
98
+ offset = form.table_view.contentOffset
99
+ new_top_offset = point.y - nav_bar_height
100
+ unless new_top_offset == offset.y
101
+ offset.y = new_top_offset
102
+ form.table_view.setContentOffset(offset, animated: true)
89
103
  end
104
+
90
105
  self
91
106
  rescue
92
107
  NSLog("can't focus on element #{self.class_name_without_kvo}")
@@ -104,7 +119,7 @@ module MotionPrime
104
119
  end
105
120
 
106
121
  def default_label_options
107
- label_options = options[:label]
122
+ label_options = options[:label] || {}
108
123
  if label_options.has_key?(:text)
109
124
  label_options
110
125
  else
@@ -114,7 +129,7 @@ module MotionPrime
114
129
 
115
130
  def bind_text_input
116
131
  view(:input).on :change do |view|
117
- focus
132
+ focus if options[:auto_focus]
118
133
  form.on_input_change(view(:input))
119
134
  end
120
135
  end
@@ -125,9 +140,7 @@ module MotionPrime
125
140
 
126
141
  def has_errors?
127
142
  return false unless observing_errors?
128
- errors_observer_fields.any? do |field|
129
- observing_errors_for.errors[field].present?
130
- end
143
+ observing_errors_for.errors.info.slice(*errors_observer_fields).values.any?(&:present?)
131
144
  end
132
145
 
133
146
  def errors_observer_fields
@@ -145,23 +158,18 @@ module MotionPrime
145
158
  def all_errors
146
159
  return [] unless observing_errors?
147
160
 
148
- errors_observer_fields.map do |field|
149
- observing_errors_for.errors[field]
150
- end
161
+ observing_errors_for.errors.info.slice(*errors_observer_fields).values.flatten
151
162
  end
152
163
 
153
164
  def reload_section
154
165
  clear_observers
155
- form.reload_cell(self)
166
+ form.reload_cell_section(self)
156
167
  end
157
168
 
158
169
  def clear_observers
159
170
  return unless observing_errors?
160
171
  # unobserve_all cause double dealloc, following code is a replacement
161
- block = proc { |field|
162
- unobserve observing_errors_for.errors, observing_errors_for.errors.unique_key(field)
163
- }.weak!
164
- errors_observer_fields.each(&block)
172
+ unobserve observing_errors_for.errors, :info
165
173
  # TODO: clear 'on' events
166
174
  end
167
175
 
@@ -0,0 +1,9 @@
1
+ module MotionPrime
2
+ class StaticFieldSection < Section
3
+ include CellSectionMixin
4
+
5
+ def form
6
+ table
7
+ end
8
+ end
9
+ end
@@ -7,65 +7,116 @@ module MotionPrime
7
7
  include HasStyleChainBuilder
8
8
  include HasSearchBar
9
9
 
10
- class_attribute :async_data_options, :section_header_options, :pull_to_refresh_block
10
+ class_attribute :group_header_options, :pull_to_refresh_block
11
11
 
12
- attr_accessor :table_element, :did_appear, :section_headers, :section_header_options
12
+ attr_accessor :table_element, :did_appear, :group_header_sections, :group_header_options
13
13
  attr_reader :decelerating
14
14
  before_render :render_table
15
15
  after_render :init_pull_to_refresh
16
16
  delegate :init_pull_to_refresh, to: :table_delegate
17
17
 
18
+
19
+ # Return sections which will be used to render as table cells.
20
+ #
21
+ # This method should be redefined in your table section and must return array.
22
+ # @return [Array<Prime::Section>] array of sections
18
23
  def table_data
19
24
  @model || []
20
25
  end
21
26
 
22
- def dealloc
23
- Prime.logger.dealloc_message :table, self, self.table_view.to_s
24
- table_delegate.clear_delegated
25
- table_view.setDataSource nil
26
- super
27
+ # Returns cached version of table data
28
+ #
29
+ # @return [Array<Prime::Section>] cached array of sections
30
+ def data
31
+ @data || set_table_data
27
32
  end
28
33
 
29
- def async_data?
30
- self.class.async_data_options
34
+ # IMPORTANT: when you use #map in table_data,
35
+ # then #dealloc of Prime::Section will not be called to section created on that #map.
36
+ # We did not find yet why this happening, for now just using hack.
37
+ def fixed_table_data
38
+ table_data.to_enum.to_a
31
39
  end
32
40
 
33
- def data
34
- @data || set_table_data
41
+ def dealloc
42
+ Prime.logger.dealloc_message :table, self, self.table_element.try(:view).to_s
43
+ table_delegate.clear_delegated
44
+ table_view.setDataSource nil
45
+ super
35
46
  end
36
47
 
48
+ # Reset all table data and reload table view
49
+ #
50
+ # @return [Boolean] true
37
51
  def reload_data
38
52
  reset_data
39
- @async_loaded_data = table_data if async_data?
40
53
  reload_table_data
41
54
  end
42
55
 
56
+ # Reload table view
57
+ #
58
+ # @return [Boolean] true
43
59
  def reload_table_data
44
60
  table_view.reloadData
61
+ true
45
62
  end
46
63
 
64
+ # Reload table view if data was empty before.
65
+ #
66
+ # @return [Boolean] true if reload was happened
47
67
  def refresh_if_needed
48
- reload_table_data if @data.nil?
68
+ @data.nil? ? reload_table_data : false
49
69
  end
50
70
 
71
+ # Reset all table data.
72
+ #
73
+ # @return [Boolean] true
51
74
  def reset_data
52
75
  @did_appear = false
53
76
  @data = nil
54
- @async_loaded_data = nil
55
- @preloader_next_starts_from = nil
56
- @preloader_cancelled = false
57
77
  @data_stamp = nil
58
- @preloader_queue[-1] = :cancelled if @preloader_queue.present?
59
- end
60
-
61
- def add_cells(cells)
62
- prepare_table_cells(cells)
78
+ @reusable_cells.each do |object_id, cell|
79
+ cell.reuseIdentifier = nil
80
+ end if @reusable_cells
81
+ @reusable_cells = nil
82
+ true
83
+ end
84
+
85
+ # Add cells to table view and reload table view.
86
+ #
87
+ # @param cell sections [Prime::Section, Array<Prime::Section>] cells which will be added to table view.
88
+ # @return [Boolean] true
89
+ def add_cell_sections(sections)
90
+ prepare_table_cell_sections(sections)
63
91
  @data ||= []
64
- @data += cells
92
+ @data += sections
65
93
  reload_table_data
66
94
  end
67
95
 
68
- def reload_cell(section)
96
+ # Delete cells from table data and remove them from table view with animation.
97
+ #
98
+ # @param cell sections [Prime::Section, Array<Prime::Section>] cells which will be removed from table view.
99
+ # @return [Array<NSIndexPath>] index paths of removed cells.
100
+ def delete_cell_sections(sections)
101
+ paths = []
102
+ Array.wrap(sections).each do |section|
103
+ paths << NSIndexPath.indexPathForRow(@data.index(section), inSection: 0)
104
+ delete_from_data(section)
105
+ end
106
+ table_view.beginUpdates
107
+ table_view.deleteRowsAtIndexPaths(paths, withRowAnimation: UITableViewRowAnimationLeft)
108
+ table_view.endUpdates
109
+ paths
110
+ end
111
+
112
+ def delete_from_data(section)
113
+ # section will not deallocate if you'll just write @data.delete(section)
114
+ index = @data.index(section)
115
+ @data[index] = nil
116
+ @data.delete_at(index)
117
+ end
118
+
119
+ def reload_cell_section(section)
69
120
  section.elements.values.each(&:compute_options!)
70
121
  section.cached_draw_image = nil
71
122
  # TODO: reset date stamps, reload row
@@ -81,7 +132,7 @@ module MotionPrime
81
132
  {common: base_styles, specific: item_styles}
82
133
  end
83
134
 
84
- def cell_styles(cell)
135
+ def cell_section_styles(section)
85
136
  # type = [`cell`, `header`, `field`]
86
137
 
87
138
  # UserFormSection example: field :email, type: :string
@@ -93,29 +144,29 @@ module MotionPrime
93
144
  # CategoriesTableSection example: table is a `CategoryTableSection`, cell is a `CategoryTitleSection`, element :icon, type: :image
94
145
  # table_name = `categories`
95
146
  # type = `cell` (always true)
96
- # table_cell_name = `title`
97
- type = cell.respond_to?(:cell_type) ? cell.cell_type : 'cell'
147
+ # table_cell_section_name = `title`
148
+ type = section.respond_to?(:cell_type) ? section.cell_type : 'cell'
98
149
  suffixes = [type]
99
- if cell.is_a?(BaseFieldSection)
100
- suffixes << cell.default_name
150
+ if section.is_a?(BaseFieldSection)
151
+ suffixes << section.default_name
101
152
  end
102
153
 
103
154
  styles = {}
104
155
  # table: base_table_<type>
105
156
  # form: base_form_<type>, base_form_<field_type>
106
157
  styles[:common] = build_styles_chain(table_styles[:common], suffixes)
107
- if cell.is_a?(BaseFieldSection)
158
+ if section.is_a?(BaseFieldSection)
108
159
  # form cell: _<type>_<field_name> = `_field_email`
109
- suffixes << :"#{type}_#{cell.name}" if cell.name
110
- elsif cell.respond_to?(:cell_name) # cell section came from table
111
- # table cell: _<table_cell_name> = `_title`
112
- suffixes << cell.cell_name
160
+ suffixes << :"#{type}_#{section.name}" if section.name
161
+ elsif section.respond_to?(:cell_section_name) # cell section came from table
162
+ # table cell: _<table_cell_section_name> = `_title`
163
+ suffixes << section.cell_section_name
113
164
  end
114
- # table: <table_name>_table_<type>, <table_name>_table_<table_cell_name> = `categories_table_cell`, `categories_table_title`
165
+ # table: <table_name>_table_<type>, <table_name>_table_<table_cell_section_name> = `categories_table_cell`, `categories_table_title`
115
166
  # form: <form_name>_form_<type>, <form_name>_form_<field_type>, user_form_<type>_email = `user_form_field`, `user_form_string_field`, `user_form_field_email`
116
167
  styles[:specific] = build_styles_chain(table_styles[:specific], suffixes)
117
168
 
118
- container_options_styles = cell.container_options[:styles]
169
+ container_options_styles = section.container_options[:styles]
119
170
  if container_options_styles.present?
120
171
  styles[:specific] += Array.wrap(container_options_styles)
121
172
  end
@@ -127,17 +178,18 @@ module MotionPrime
127
178
  @table_delegate ||= TableDelegate.new(section: self)
128
179
  end
129
180
 
130
- def render_table
131
- options = {
181
+ def table_element_options
182
+ {
183
+ section: self.weak_ref,
132
184
  styles: table_styles.values.flatten,
133
185
  delegate: table_delegate,
134
186
  data_source: table_delegate,
135
187
  style: (UITableViewStyleGrouped unless flat_data?)
136
188
  }
137
- if async_data? && self.class.async_data_options.has_key?(:estimated_row_height)
138
- options[:estimated_row_height] = self.class.async_data_options[:estimated_row_height]
139
- end
140
- self.table_element = screen.table_view(options)
189
+ end
190
+
191
+ def render_table
192
+ self.table_element = screen.table_view(table_element_options)
141
193
  end
142
194
 
143
195
  def table_view
@@ -152,91 +204,86 @@ module MotionPrime
152
204
  table_view.try(:show)
153
205
  end
154
206
 
155
- def render_cell(index, table)
156
- section = rows_for_section(index.section)[index.row]
207
+ def render_cell(index, table = nil)
208
+ table ||= table_view
209
+ section = cell_sections_for_group(index.section)[index.row]
157
210
  element = section.container_element || section.init_container_element(container_element_options_for(index))
158
211
 
159
212
  view = element.render do
160
213
  section.render
161
214
  end
162
- on_row_render(view, index)
163
- preload_sections_after(index)
215
+
216
+ @reusable_cells ||= {}
217
+ @reusable_cells[section.object_id] = view
218
+ on_cell_render(view, index)
164
219
  view
165
220
  end
166
221
 
167
222
  def render_header(section)
168
- return unless options = self.section_header_options.try(:[], section)
169
- self.section_headers[section] ||= BaseHeaderSection.new(options.merge(screen: screen, table: self.weak_ref))
223
+ return unless options = self.group_header_options.try(:[], section)
224
+ self.group_header_sections[section] ||= BaseHeaderSection.new(options.merge(screen: screen, table: self.weak_ref))
170
225
  end
171
226
 
172
- def header_for_section(section)
173
- self.section_headers ||= []
174
- self.section_headers[section] || render_header(section)
227
+ def header_section_for_group(section)
228
+ self.group_header_sections ||= []
229
+ self.group_header_sections[section] || render_header(section)
175
230
  end
176
231
 
177
- def on_row_render(cell, index); end
232
+ def on_cell_render(cell, index); end
178
233
  def on_appear; end
179
234
  def on_click(table, index); end
180
235
 
181
236
  def has_many_sections?
182
- section_header_options.present? || data.try(:first).is_a?(Array)
237
+ group_header_options.present? || data.try(:first).is_a?(Array)
183
238
  end
184
239
 
185
240
  def flat_data?
186
241
  !has_many_sections?
187
242
  end
188
243
 
189
- def rows_for_section(section)
244
+ def cell_sections_for_group(section)
190
245
  flat_data? ? data : data[section]
191
246
  end
192
247
 
193
- def row_by_index(index)
194
- rows_for_section(index.section)[index.row]
248
+ def cell_section_by_index(index)
249
+ cell_sections_for_group(index.section)[index.row]
195
250
  end
196
251
 
197
- def on_async_data_loaded; end
198
- def on_async_data_preloaded(loaded_index); end
199
-
200
252
  def cell_name(table, index)
201
- record = row_by_index(index)
202
- if record && record.model &&
203
- record.model.respond_to?(:id) && record.model.id.present?
204
- "cell_#{record.model.id}_#{data_stamp_for("#{index.section}_#{index.row}")}"
205
- else
206
- "cell_#{index.section}_#{index.row}_#{data_stamp_for("#{index.section}_#{index.row}")}"
207
- end
253
+ record = cell_section_by_index(index)
254
+ "cell_#{record.object_id}_#{@data_stamp[record.object_id]}"
208
255
  end
209
256
 
210
257
  # Table View Delegate
211
258
  # ---------------------
212
259
 
213
- def number_of_sections(table = nil)
260
+ def number_of_groups(table = nil)
214
261
  has_many_sections? ? data.count : 1
215
262
  end
216
263
 
217
264
  def cell_for_index(table, index)
218
265
  cell = cached_cell(index, table) || render_cell(index, table)
219
-
220
266
  # run table view is appeared callback if needed
221
- if !@did_appear && index.row == rows_for_section(index.section).size - 1
267
+ if !@did_appear && index.row == cell_sections_for_group(index.section).size - 1
222
268
  on_appear
223
269
  end
224
270
  cell.is_a?(UIView) ? cell : cell.view
225
271
  end
226
272
 
227
273
  def height_for_index(table, index)
228
- section = load_cell_by_index(index, preload: true)
274
+ section = cell_section_by_index(index)
275
+ section.create_elements
229
276
  section.container_height
230
277
  end
231
278
 
232
- def view_for_header_in_section(table, section)
233
- return unless header = header_for_section(section)
279
+ def header_cell_in_group(table, group)
280
+ return unless header = header_section_for_group(group)
234
281
 
235
- reuse_identifier = "header_#{section}"
282
+ reuse_identifier = "header_#{group}"
236
283
  cached = table.dequeueReusableHeaderFooterViewWithIdentifier(reuse_identifier)
237
284
  return cached if cached.present?
238
285
 
239
- styles = cell_styles(header).values.flatten
286
+ styles = cell_section_styles(header).values.flatten
240
287
  wrapper = MotionPrime::BaseElement.factory(:table_view_header_footer_view, screen: screen, styles: styles, parent_view: table_view, reuse_identifier: reuse_identifier)
241
288
  wrapper.render do |container_view, container_element|
242
289
  header.container_element = container_element
@@ -244,8 +291,8 @@ module MotionPrime
244
291
  end
245
292
  end
246
293
 
247
- def height_for_header_in_section(table, section)
248
- header_for_section(section).try(:container_height) || 0
294
+ def height_for_header_in_group(table, group)
295
+ header_section_for_group(group).try(:container_height) || 0
249
296
  end
250
297
 
251
298
  def scroll_view_will_begin_dragging(scroll)
@@ -274,49 +321,27 @@ module MotionPrime
274
321
  end
275
322
 
276
323
  def set_table_data
277
- cells = async_data? ? load_sections_async : table_data
278
- prepare_table_cells(cells)
279
- @data = cells
324
+ sections = fixed_table_data
325
+ prepare_table_cell_sections(sections)
326
+ @data = sections
280
327
  reset_data_stamps
281
- load_sections unless async_data?
328
+ create_section_elements
282
329
  @data
283
330
  end
284
331
 
285
- def load_sections_async
286
- @async_loaded_data || begin
287
- BW::Reactor.schedule_on_main do
288
- @async_loaded_data = table_data
289
- @data = nil
290
- reload_table_data
291
- on_async_data_loaded
292
- end
293
- []
294
- end
295
- end
296
-
297
332
  def cached_cell(index, table = nil)
298
333
  table ||= self.table_view
299
334
  table.dequeueReusableCellWithIdentifier(cell_name(table, index))
300
335
  end
301
336
 
302
- def prepare_table_cells(cell)
303
- if cell.is_a?(Array)
304
- cell.each { |c| prepare_table_cells(c) }
305
- else
337
+ def prepare_table_cell_sections(cells)
338
+ Array.wrap(cells.flatten).each do |cell|
306
339
  Prime::Config.prime.cell_section.mixins.each do |mixin|
307
340
  cell.class.send(:include, mixin)
308
341
  end
309
342
  cell.screen ||= screen
310
- cell.table ||= WeakRef.new(self) if cell.respond_to?(:table=)
311
- end
312
- end
313
-
314
- def load_cell_by_index(index, options = {})
315
- section = rows_for_section(index.section)[index.row]
316
- if section.load_section && options[:preload] && !section.container_element && async_data? # perform only if just loaded
317
- section.load_container_with_elements(container: container_element_options_for(index))
343
+ cell.table ||= self.weak_ref if cell.respond_to?(:table=)
318
344
  end
319
- section
320
345
  end
321
346
 
322
347
  def container_element_options_for(index)
@@ -326,125 +351,42 @@ module MotionPrime
326
351
  }
327
352
  end
328
353
 
329
- def data_stamp_for(id)
330
- @data_stamp[id]
331
- end
332
-
333
- def set_data_stamp(cell_ids)
354
+ def set_data_stamp(section_ids)
334
355
  @data_stamp ||= {}
335
- [*cell_ids].each { |id| @data_stamp[id] = Time.now.to_f }
356
+ [*section_ids].each do |id|
357
+ @data_stamp[id] = Time.now.to_f
358
+ end
336
359
  end
337
360
 
338
361
  def reset_data_stamps
339
- keys = @data.each_with_index.map do |row, id|
340
- if row.is_a?(Array)
341
- section = id
342
- rows = (0...row.count)
343
- else
344
- section = 0
345
- rows = [id]
346
- end
347
- rows.map { |row| "#{section}_#{row}" }
348
- end
349
- set_data_stamp(keys.flatten)
362
+ keys = data.flatten.map(&:object_id)
363
+ set_data_stamp(keys)
350
364
  end
351
365
 
352
- def load_sections
353
- return if async_data?
366
+ def create_section_elements
354
367
  if flat_data?
355
- data.each(&:load_section)
368
+ data.each(&:create_elements)
356
369
  else
357
- data.each { |section_data| section_data.each(&:load_section) }
370
+ data.each { |group_sections| group_sections.each(&:create_elements) }
358
371
  end
359
372
  end
360
373
 
361
-
362
- # Preloads sections after rendering cell in current sheduled index or given index.
363
- # TODO: probably should be in separate class.
364
- #
365
- # @param index [NSIndexPath] Value of first index to load if current sheduled index not exists.
366
- # @return [NSIndexPath, Boolean] Index of next sheduled index.
367
- def preload_sections_after(index)
368
- return if !async_data? || @preloader_next_starts_from == false
369
- service = preloader_index_service
370
-
371
- load_limit = self.class.async_data_options.try(:[], :preload_rows_count)
372
- @preloader_next_starts_from ||= index
373
- index_to_start_preloading = service.sum_index(@preloader_next_starts_from, load_limit ? -load_limit/2 : 0)
374
- if service.compare_indexes(index, index_to_start_preloading) >= 0
375
- load_count = preload_sections_schedule_next(@preloader_next_starts_from, load_limit)
376
- @preloader_next_starts_from = service.sum_index(@preloader_next_starts_from, load_count, false)
377
- else
378
- false
379
- end
380
- end
381
-
382
- # Schedules preloading sections starting with given index with given limit.
383
- # TODO: probably should be in separate class.
384
- #
385
- # @param index [NSIndexPath] Value of first index to load.
386
- # @param limit [Integer] Limit of sections to load.
387
- # @return [Integer] Count of sections scheduled to load.
388
- def preload_sections_schedule_next(index, limit)
389
- service = preloader_index_service
390
-
391
- left_to_load = rows_for_section(index.section).count - index.row
392
- load_count = [left_to_load, limit].compact.min
393
- @preloader_cancelled = false
394
- @preloader_queue ||= []
395
- @strong_refs ||= []
396
-
397
- # TODO: we do we need to keep screen ref too?
398
- queue_id = @preloader_queue.count
399
- @strong_refs[queue_id] = [screen.strong_ref, screen.main_controller.strong_ref]
400
- @preloader_queue[queue_id] = :in_progress
401
- BW::Reactor.schedule(queue_id) do |queue_id|
402
- result = load_count.times do |offset|
403
- if @preloader_queue[queue_id] == :cancelled
404
- @strong_refs[queue_id] = nil
405
- break
406
- end
407
- if screen.main_controller.retainCount == 1
408
- @strong_refs[queue_id] = nil
409
- @preloader_queue[queue_id] = :dealloc
410
- break
411
- end
412
-
413
- load_cell_by_index(index, preload: true)
414
- unless offset == load_count - 1
415
- index = service.sum_index(index, 1)
416
- end
417
- end
418
-
419
- if result
420
- on_async_data_preloaded(index)
421
- @preloader_queue[queue_id] = :completed
422
- end
423
- @strong_refs[queue_id] = nil
424
- end
425
- load_count
426
- end
427
-
428
- def preloader_index_service
429
- TableDataIndexes.new(@data)
430
- end
431
-
432
374
  class << self
433
375
  def inherited(subclass)
434
376
  super
435
- subclass.async_data_options = self.async_data_options.try(:clone)
436
- subclass.section_header_options = self.section_header_options.try(:clone)
377
+ subclass.group_header_options = self.group_header_options.try(:clone)
437
378
  end
438
379
 
439
380
  def async_table_data(options = {})
440
- self.async_data_options = options
381
+ self.send :include, Prime::AsyncTableMixin
382
+ self.set_async_data_options options
441
383
  end
442
384
 
443
385
  def group_header(name, options)
444
386
  options[:name] = name
445
- self.section_header_options ||= []
387
+ self.group_header_options ||= []
446
388
  section = options.delete(:id)
447
- self.section_header_options[section] = options
389
+ self.group_header_options[section] = options
448
390
  end
449
391
 
450
392
  def pull_to_refresh(&block)