motion-prime 1.0.3 → 1.0.4

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/Gemfile.lock +1 -1
  4. data/ROADMAP.md +3 -0
  5. data/files/Gemfile +1 -1
  6. data/lib/motion-prime.rb +1 -1
  7. data/motion-prime/api_client.rb +7 -5
  8. data/motion-prime/elements/_content_text_mixin.rb +14 -4
  9. data/motion-prime/elements/_text_mixin.rb +6 -2
  10. data/motion-prime/elements/base_element.rb +37 -12
  11. data/motion-prime/elements/button.rb +1 -1
  12. data/motion-prime/elements/draw.rb +6 -0
  13. data/motion-prime/elements/draw/_draw_background_mixin.rb +33 -2
  14. data/motion-prime/elements/draw/image.rb +9 -5
  15. data/motion-prime/elements/draw/label.rb +1 -1
  16. data/motion-prime/elements/label.rb +1 -1
  17. data/motion-prime/helpers/has_normalizer.rb +2 -1
  18. data/motion-prime/helpers/has_style_options.rb +16 -0
  19. data/motion-prime/helpers/has_styles.rb +5 -1
  20. data/motion-prime/models/_association_mixin.rb +17 -2
  21. data/motion-prime/models/_base_mixin.rb +5 -1
  22. data/motion-prime/models/_dirty_mixin.rb +1 -1
  23. data/motion-prime/models/_nano_bag_mixin.rb +18 -5
  24. data/motion-prime/models/_sync_mixin.rb +3 -3
  25. data/motion-prime/models/_timestamps_mixin.rb +3 -3
  26. data/motion-prime/sections/_draw_section_mixin.rb +20 -3
  27. data/motion-prime/sections/_section_with_container_mixin.rb +1 -1
  28. data/motion-prime/sections/abstract_collection.rb +6 -2
  29. data/motion-prime/sections/base_section.rb +6 -6
  30. data/motion-prime/sections/collection/collection_delegate.rb +6 -4
  31. data/motion-prime/sections/form/base_field_section.rb +8 -0
  32. data/motion-prime/sections/page_view.rb +19 -5
  33. data/motion-prime/sections/page_view/page_view_delegate.rb +19 -6
  34. data/motion-prime/sections/table/refresh_mixin.rb +1 -1
  35. data/motion-prime/sections/table/table_delegate.rb +6 -4
  36. data/motion-prime/styles/base.rb +1 -1
  37. data/motion-prime/support/consts.rb +6 -1
  38. data/motion-prime/support/mp_table_view.rb +4 -4
  39. data/motion-prime/version.rb +1 -1
  40. data/motion-prime/views/styles.rb +1 -1
  41. data/motion-prime/views/view_builder.rb +9 -5
  42. data/motion-prime/views/view_styler.rb +16 -15
  43. metadata +37 -36
@@ -29,7 +29,8 @@ module MotionPrime
29
29
  end
30
30
  end
31
31
 
32
- def normalize_value(object, receiver)
32
+ def normalize_value(object, receiver = nil)
33
+ receiver ||= self
33
34
  if element?
34
35
  receiver.send(:instance_exec, section || screen, self, &object)
35
36
  else
@@ -0,0 +1,16 @@
1
+ module MotionPrime
2
+ module HasStyleOptions
3
+ def extract_font_from(options, prefix = nil)
4
+ options ||= {}
5
+ return options[:font] if options[:font].present?
6
+
7
+ name_key = [prefix, 'font_name'].compact.join('_').to_sym
8
+ size_key = [prefix, 'font_size'].compact.join('_').to_sym
9
+ if options.slice(size_key, name_key).any?
10
+ font_name = options[name_key] || :system
11
+ font_size = options[size_key] || 14
12
+ font_name.uifont(font_size)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -3,10 +3,14 @@ module MotionPrime
3
3
  def prepare_gradient(options)
4
4
  colors = options[:colors].map(&:uicolor).map(&:cgcolor)
5
5
  locations = options[:locations] if options[:locations]
6
+ type = options[:type].to_s
6
7
 
7
8
  if self.is_a?(ViewStyler)
8
9
  gradient = CAGradientLayer.layer
9
-
10
+ if type == 'horizontal'
11
+ gradient.startPoint = CGPointMake(0, 0.5)
12
+ gradient.endPoint = CGPointMake(1.0, 0.5)
13
+ end
10
14
  gradient.frame = if options[:frame_width]
11
15
  CGRectMake(options[:frame_x].to_f, options[:frame_y].to_f, options[:frame_width].to_f, options[:frame_height].to_f)
12
16
  else
@@ -6,6 +6,15 @@ module MotionPrime
6
6
  @_bags ||= {}
7
7
  end
8
8
 
9
+ def bags_attributes
10
+ # retrieving has_one/has_many bag keys
11
+ self.class._associations.keys.inject({}) do |result, association_name|
12
+ key = :"#{association_name.pluralize}_bag"
13
+ result[key] = self.info[key] if self.info[key].present?
14
+ result
15
+ end
16
+ end
17
+
9
18
  def bag_key_for(bag_name)
10
19
  self.info[bag_name]
11
20
  end
@@ -13,7 +22,7 @@ module MotionPrime
13
22
  # Saves model and all associations to store.
14
23
  #
15
24
  # @return [Prime::Model] model
16
- def save
25
+ def save!
17
26
  _bags.values.each do |bag|
18
27
  bag.store = self.store
19
28
  bag.save
@@ -78,9 +87,10 @@ module MotionPrime
78
87
  value
79
88
  end
80
89
  define_method("#{association_name}_attributes=") do |value|
90
+ bags_attributes = self.send(association_name).try(:bags_attributes) || {}
81
91
  self.send(bag_name).clear
82
-
83
92
  association = association_name.classify.constantize.new
93
+ association.info.merge!(bags_attributes)
84
94
  association.fetch_with_attributes(value)
85
95
  self.send(:"#{bag_name}") << association
86
96
  association
@@ -102,11 +112,16 @@ module MotionPrime
102
112
  self._associations[association_name] = options.merge(type: :many)
103
113
 
104
114
  define_method("#{association_name}_attributes=") do |value|
115
+ bags_attributes = self.send(bag_name).to_a.inject({}) do |result, item|
116
+ result[item.id] = item.bags_attributes if item.id
117
+ result
118
+ end
105
119
  self.send(bag_name).clear
106
120
 
107
121
  pending_save_counter = 0
108
122
  collection = value.inject({}) do |result, attrs|
109
123
  model = association_name.classify.constantize.new
124
+ model.info.merge!(bags_attributes.fetch(attrs[:id], {}))
110
125
  model.fetch_with_attributes(attrs)
111
126
  unique_key = model.id || "pending_#{pending_save_counter+=1}"
112
127
  result.merge(unique_key => model)
@@ -6,10 +6,14 @@ module MotionPrime
6
6
  base.class_attribute :default_sort_options
7
7
  end
8
8
 
9
+ def save
10
+ self.performSelectorOnMainThread :save!, withObject: nil, waitUntilDone: true
11
+ end
12
+
9
13
  # Saves model to default store.
10
14
  #
11
15
  # @return [Prime::Model] model
12
- def save
16
+ def save!
13
17
  set_default_id_if_needed
14
18
  raise StoreError, 'No store provided' unless self.store
15
19
  error_ptr = Pointer.new(:id)
@@ -47,7 +47,7 @@ module MotionPrime
47
47
  end
48
48
  end
49
49
 
50
- def save
50
+ def save!
51
51
  super
52
52
  reset_changed_attributes
53
53
  self
@@ -34,8 +34,9 @@ module MotionPrime
34
34
  # @return self [Prime::Model]
35
35
  def add(object_or_array, options = {})
36
36
  error_ptr = Pointer.new(:id)
37
- options[:existed_ids] ||= []
38
- options[:existed_ids] += filter_array(self.to_a, bag_key: self.key).map(&:id)
37
+ options[:existed_ids] ||= filter_array(self.to_a, bag_key: self.key).inject({}) do |result, item|
38
+ result.merge(item.id => item)
39
+ end
39
40
  prepared = prepare_for_store(object_or_array, options)
40
41
 
41
42
  if object_or_array.is_a?(Array)
@@ -54,9 +55,17 @@ module MotionPrime
54
55
  object.map { |entity| prepare_for_store(entity, options) }.compact
55
56
  else
56
57
  object.bag_key = self.key
57
- if object.id.present? && Array.wrap(options[:existed_ids]).include?(object.id)
58
- return if options[:silent_validation]
59
- raise StoreError, "duplicated item added `#{object.class_name_without_kvo}` with `id` = #{object.id}"
58
+ if object.id.present? && options[:existed_ids].include?(object.id)
59
+ if options[:silent_validation]
60
+ return
61
+ elsif options[:replace]
62
+ replace = options[:existed_ids][object.id]
63
+ replace.delete
64
+ delete_key(replace.key)
65
+ object
66
+ else
67
+ raise StoreError, "duplicated item added `#{object.class_name_without_kvo}` with `id` = #{object.id}"
68
+ end
60
69
  end
61
70
  object
62
71
  end
@@ -117,6 +126,10 @@ module MotionPrime
117
126
  end
118
127
 
119
128
  def save
129
+ self.performSelectorOnMainThread :save!, withObject: nil, waitUntilDone: true
130
+ end
131
+
132
+ def save!
120
133
  self.store ||= MotionPrime::Store.shared_store
121
134
  error_ptr = Pointer.new(:id)
122
135
  result = self.saveAndReturnError(error_ptr)
@@ -124,10 +124,10 @@ module MotionPrime
124
124
 
125
125
  method = options[:method] || (persisted? ? :put : :post)
126
126
  api_client.send(method, url, post_data, options) do |data, status_code|
127
- save_response = !options.has_key?(:save_response) || options[:save_response]
128
- if save_response && status_code.to_s =~ /20\d/ && data.is_a?(Hash)
127
+ assign_response_data = options.fetch(:save_response, true)
128
+ if assign_response_data && status_code.to_s =~ /20\d/ && data.is_a?(Hash)
129
129
  set_attributes_from_response(data)
130
- save
130
+ save if options[:save_response]
131
131
  end
132
132
  block.call(data, status_code, data) if use_callback
133
133
  end
@@ -6,7 +6,7 @@ module MotionPrime
6
6
  base.class_attribute :_timestamp_attributes
7
7
  end
8
8
 
9
- def save
9
+ def save!
10
10
  time = Time.now
11
11
  trigger_timestamp(:save, time)
12
12
  trigger_timestamp(:create, time) if new_record?
@@ -18,13 +18,13 @@ module MotionPrime
18
18
  return unless field
19
19
  self.send(:"#{field}=", time)
20
20
  end
21
-
21
+
22
22
  module ClassMethods
23
23
  def timestamp_attributes(actions = nil)
24
24
  self._timestamp_attributes ||= {}
25
25
  actions ||= {save: :saved_at, create: :created_at}
26
26
  actions.each do |action_name, field|
27
- _timestamp_attributes[action_name.to_sym] = field
27
+ self._timestamp_attributes[action_name.to_sym] = field
28
28
  self.attribute field, type: :time
29
29
  end
30
30
  end
@@ -28,6 +28,14 @@ module MotionPrime
28
28
  end
29
29
  end
30
30
 
31
+ def before_element_render(element)
32
+ return unless element == container_element
33
+ self.container_gesture_recognizers = nil
34
+ elements_to_draw.values.each(&:on_container_render)
35
+ end
36
+
37
+ def after_element_render(element); end
38
+
31
39
  def bind_gesture_on_container_for(element, action, receiver = nil)
32
40
  self.container_gesture_recognizers ||= begin
33
41
  set_container_gesture_recognizer
@@ -87,7 +95,11 @@ module MotionPrime
87
95
  end
88
96
  CGRectContainsPoint(element.computed_frame, point)
89
97
  end
90
- (target[:receiver] || self).send(target[:action], recognizer, target[:element]) if target
98
+ stop_propagation = false
99
+ if target
100
+ stop_propagation = !(target[:receiver] || self).send(target[:action], recognizer, target[:element])
101
+ end
102
+ recognizer.cancelsTouchesInView = stop_propagation
91
103
  end
92
104
 
93
105
  def draw_elements(rect)
@@ -103,8 +115,13 @@ module MotionPrime
103
115
  background_color = options[:background_color].try(:uicolor)
104
116
 
105
117
  if gradient_options = options[:gradient]
106
- start_point = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect))
107
- end_point = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect))
118
+ if gradient_options[:type].to_s == 'horizontal'
119
+ start_point = CGPointMake(0, 0.5)
120
+ end_point = CGPointMake(1.0, 0.5)
121
+ else
122
+ start_point = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect))
123
+ end_point = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect))
124
+ end
108
125
 
109
126
  # CGContextSaveGState(context)
110
127
  CGContextAddRect(context, rect)
@@ -18,7 +18,7 @@ module MotionPrime
18
18
  has_drawn_content: true
19
19
  })
20
20
  container_element_options = self.class.container_element_options.clone
21
- options = (container_element_options || {}).merge(options)
21
+ options = (container_element_options || {}).deep_merge(options)
22
22
  type = options.delete(:type)
23
23
  MotionPrime::BaseElement.factory(type, options)
24
24
  end
@@ -93,7 +93,9 @@ module MotionPrime
93
93
  def reset_collection_data
94
94
  @did_appear = false
95
95
  Array.wrap(@data).flatten.each do |section|
96
- section.container_element.try(:update_options, reuse_identifier: nil)
96
+ next unless element = section.container_element
97
+ element.update_options(reuse_identifier: nil)
98
+ element.view.try(:removeFromSuperview)
97
99
  end
98
100
  @data = nil
99
101
  @data_stamp = nil
@@ -225,7 +227,9 @@ module MotionPrime
225
227
  display_pending_cells
226
228
  end
227
229
 
228
- def scroll_view_did_scroll(scroll)
230
+ def scroll_view_did_scroll(scroll); end
231
+
232
+ def update_pull_to_refresh_after_scroll(scroll)
229
233
  return unless refresh_view = collection_view.try(:pullToRefreshView)
230
234
  return refresh_view.alpha = 1 if refresh_view.state == SVPullToRefreshStateLoading
231
235
 
@@ -199,7 +199,7 @@ module MotionPrime
199
199
 
200
200
  def render(container_options = {}, force = false)
201
201
  force ? create_elements! : create_elements
202
- self.container_options.merge!(container_options)
202
+ self.container_options.deep_merge!(container_options)
203
203
  run_callbacks :render do
204
204
  render!
205
205
  end
@@ -209,12 +209,12 @@ module MotionPrime
209
209
  render_container(container_options) do
210
210
  elements_to_render.each do |key, element|
211
211
  element.render
212
- on_element_render(element)
213
212
  end
214
213
  end
215
214
  end
216
215
 
217
- def on_element_render(element)
216
+ def after_element_render(element)
217
+ super
218
218
  return unless callbacks = elements_callbacks.try(:[], element.name)
219
219
  callbacks.each do |options|
220
220
  options[:method].to_proc.call(options[:target] || self)
@@ -375,15 +375,15 @@ module MotionPrime
375
375
 
376
376
  def compute_container_options!
377
377
  raw_options = {}
378
- raw_options.merge!(self.class.container_options.try(:clone) || {})
379
- raw_options.merge!(options[:container] || {})
378
+ raw_options.deep_merge!(self.class.container_options.try(:clone) || {})
379
+ raw_options.deep_merge!(options[:container] || {})
380
380
  # allow to pass styles as proc
381
381
  normalize_options(raw_options, elements_eval_object, nil, [:styles])
382
382
  @container_options = raw_options # must be here because section_styles may use container_options for custom styles
383
383
 
384
384
  container_options_from_styles = Styles.for(section_styles.values.flatten)[:container] if section_styles
385
385
  if container_options_from_styles.present?
386
- @container_options = container_options_from_styles.merge(@container_options)
386
+ @container_options = container_options_from_styles.deep_merge(@container_options)
387
387
  end
388
388
  normalize_options(@container_options, elements_eval_object)
389
389
  end
@@ -5,13 +5,14 @@ module MotionPrime
5
5
 
6
6
  def initialize(options)
7
7
  self.collection_section = options[:section].try(:weak_ref)
8
+ @_section_info = collection_section.to_s
8
9
  @section_instance = collection_section.to_s
9
10
  end
10
11
 
11
- # def dealloc
12
- # pp 'Deallocating collection_delegate for ', @section_instance
13
- # super
14
- # end
12
+ def dealloc
13
+ Prime.logger.dealloc_message :collection_delegate, @_section_info
14
+ super
15
+ end
15
16
 
16
17
  def numberOfSectionsInCollectionView(table)
17
18
  collection_section.number_of_groups
@@ -45,6 +46,7 @@ module MotionPrime
45
46
 
46
47
  def scrollViewDidScroll(scroll)
47
48
  collection_section.scroll_view_did_scroll(scroll)
49
+ collection_section.update_pull_to_refresh_after_scroll(scroll)
48
50
  end
49
51
 
50
52
  def scrollViewWillBeginDragging(scroll)
@@ -156,6 +156,14 @@ module MotionPrime
156
156
  observing_errors_for.errors.info.slice(*errors_observer_fields).values.flatten
157
157
  end
158
158
 
159
+ def style_suffixes
160
+ suffixes = @options[:style_suffixes]
161
+ suffixes = normalize_value(suffixes, collection_section) if suffixes.is_a?(Proc)
162
+ suffixes = Array.wrap(suffixes).clone
163
+ suffixes << 'with_errors' if has_errors?
164
+ suffixes
165
+ end
166
+
159
167
  def reload_section
160
168
  clear_observers
161
169
  form.hard_reload_cell_section(self)
@@ -27,17 +27,26 @@ module MotionPrime
27
27
  end
28
28
 
29
29
  def set_page(index, animated = false, &block)
30
- block ||= proc{|a|}
31
30
  page = page_for_index(index)
32
- current_index = index_for_page(page_controller.viewControllers.last).to_i
33
- direction = current_index <= index ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse
34
- page_controller.setViewControllers([page], direction: direction, animated: animated, completion: block)
31
+ set_view_controllers([page], animated, &block)
35
32
  end
36
33
 
37
34
  def reload_collection_data
38
- page_controller.setViewControllers(page_controller.viewControllers, direction: UIPageViewControllerNavigationDirectionForward, animated: false, completion: proc{|a|})
35
+ set_view_controllers(page_controller.viewControllers, false)
39
36
  end
40
37
 
38
+ def set_view_controllers(controllers, animated = false, &completion)
39
+ completion ||= proc{|a|}
40
+ index = index_for_page(controllers.last)
41
+ current_index = index_for_page(page_controller.viewControllers.last).to_i
42
+ direction = current_index <= index ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse
43
+ page_controller.setViewControllers(controllers, direction: direction, animated: animated, completion: completion)
44
+ page_did_set(index)
45
+ end
46
+
47
+ def page_did_set(index); end
48
+ def page_will_set(index); end
49
+
41
50
  def add_pages(sections, follow = false)
42
51
  @data += Array.wrap(sections)
43
52
  if follow
@@ -50,6 +59,10 @@ module MotionPrime
50
59
  end
51
60
  end
52
61
 
62
+ def current_page_id
63
+ index_for_page(page_controller.viewControllers.last)
64
+ end
65
+
53
66
  # Delegate
54
67
  def page_for_index(index)
55
68
  return nil if !index || data.length == 0 || index < 0 || index >= data.size
@@ -58,6 +71,7 @@ module MotionPrime
58
71
  @view_controllers[index]
59
72
  else
60
73
  controller = MotionPrime::Screen.new
74
+ controller.parent_screen = self.screen
61
75
  section = data[index]
62
76
  section.screen = controller.weak_ref
63
77
  controller.set_section :main, instance: section
@@ -5,13 +5,14 @@ module MotionPrime
5
5
 
6
6
  def initialize(options)
7
7
  self.collection_section = options[:section].try(:weak_ref)
8
+ @_section_info = collection_section.to_s
8
9
  @section_instance = collection_section.to_s
9
10
  end
10
11
 
11
- # def dealloc
12
- # pp 'Deallocating page_view_delegate for ', @section_instance
13
- # super
14
- # end
12
+ def dealloc
13
+ Prime.logger.dealloc_message :collection_delegate, @_section_info
14
+ super
15
+ end
15
16
 
16
17
  def viewControllerAtIndex(index, storyboard:storyboard)
17
18
  collection_section.page_for_index(index)
@@ -39,7 +40,7 @@ module MotionPrime
39
40
  UIDevice.currentDevice.orientation == UIDeviceOrientationPortraitUpsideDown ||
40
41
  UIDevice.currentDevice.orientation == UIDeviceOrientationUnknown
41
42
  if is_portrait
42
- page_view_controller.setViewControllers([current], direction:UIPageViewControllerNavigationDirectionForward, animated:true, completion:lambda{|a|})
43
+ collection_section.reload_collection_data
43
44
  page_view_controller.doubleSided = false
44
45
  return UIPageViewControllerSpineLocationMin
45
46
  else
@@ -51,10 +52,22 @@ module MotionPrime
51
52
  prev_vc = pageViewController(page_view_controller, viewControllerBeforeViewController: current)
52
53
  viewControllers = [prev_vc, current]
53
54
  end
54
- page_view_controller.setViewControllers(viewControllers, direction:UIPageViewControllerNavigationDirectionForward, animated:true, completion:lambda{|a|})
55
+ collection_section.set_view_controllers(viewControllers, true)
55
56
  page_view_controller.doubleSided = true
56
57
  return UIPageViewControllerSpineLocationMid
57
58
  end
58
59
  end
60
+
61
+ def pageViewController(pvc, didFinishAnimating: finished, previousViewControllers: previous_view_controllers, transitionCompleted: completed)
62
+ if completed
63
+ index = collection_section.index_for_page(collection_section.page_controller.viewControllers.last)
64
+ collection_section.page_did_set(index)
65
+ end
66
+ end
67
+
68
+ def pageViewController(pvc, willTransitionToViewControllers: pending_view_controllers)
69
+ index = collection_section.index_for_page(pending_view_controllers.last)
70
+ collection_section.page_will_set(index)
71
+ end
59
72
  end
60
73
  end