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.
- 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) || {})
|