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.
- checksums.yaml +8 -8
- data/CHANGELOG.md +6 -0
- data/Gemfile.lock +1 -1
- data/ROADMAP.md +3 -0
- data/files/Gemfile +1 -1
- data/motion-prime/api_client.rb +5 -2
- data/motion-prime/core_ext/kernel.rb +36 -0
- data/motion-prime/elements/_content_text_mixin.rb +32 -11
- data/motion-prime/elements/base_element.rb +33 -16
- data/motion-prime/elements/draw.rb +10 -4
- data/motion-prime/elements/draw/image.rb +26 -15
- data/motion-prime/elements/map.rb +5 -0
- data/motion-prime/models/_association_mixin.rb +0 -3
- data/motion-prime/models/_base_mixin.rb +24 -3
- data/motion-prime/models/_filter_mixin.rb +28 -0
- data/motion-prime/models/_sync_mixin.rb +13 -10
- data/motion-prime/models/association_collection.rb +41 -16
- data/motion-prime/models/errors.rb +46 -24
- data/motion-prime/models/model.rb +8 -0
- data/motion-prime/screens/_sections_mixin.rb +1 -1
- data/motion-prime/screens/extensions/_indicators_mixin.rb +4 -2
- data/motion-prime/screens/extensions/_navigation_bar_mixin.rb +1 -1
- data/motion-prime/screens/screen.rb +4 -0
- data/motion-prime/sections/_async_form_mixin.rb +12 -0
- data/motion-prime/sections/_async_table_mixin.rb +193 -0
- data/motion-prime/sections/_cell_section_mixin.rb +6 -6
- data/motion-prime/sections/_draw_section_mixin.rb +13 -5
- data/motion-prime/sections/base_section.rb +62 -36
- data/motion-prime/sections/form.rb +19 -35
- data/motion-prime/sections/form/base_field_section.rb +42 -34
- data/motion-prime/sections/form/static_field_section.rb +9 -0
- data/motion-prime/sections/table.rb +143 -201
- data/motion-prime/sections/table/table_delegate.rb +15 -15
- data/motion-prime/services/table_data_indexes.rb +12 -2
- data/motion-prime/styles/base.rb +1 -1
- data/motion-prime/styles/form.rb +6 -6
- data/motion-prime/version.rb +1 -1
- data/motion-prime/views/layout.rb +5 -2
- data/motion-prime/views/view_styler.rb +2 -0
- data/spec/models/association_collection_spec.rb +28 -6
- metadata +6 -2
@@ -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 :
|
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(:
|
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
|
31
|
-
self.class.
|
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
|
72
|
-
self.
|
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
|
-
|
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
|
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 :
|
126
|
+
return @section_loaded ? false : create_elements
|
109
127
|
end
|
110
128
|
@section_loading = true
|
111
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
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)
|
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.
|
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) || {})
|