ProMotion 2.4.2 → 2.5.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +15 -13
- data/lib/ProMotion.rb +40 -14
- data/lib/ProMotion/cocoatouch/collection_view_cell.rb +8 -0
- data/lib/ProMotion/cocoatouch/collection_view_controller.rb +49 -0
- data/lib/ProMotion/cocoatouch/view_controller.rb +5 -0
- data/lib/ProMotion/collection/cell/collection_view_cell_module.rb +17 -0
- data/lib/ProMotion/collection/collection.rb +106 -0
- data/lib/ProMotion/collection/collection_builder.rb +39 -0
- data/lib/ProMotion/collection/collection_class_methods.rb +38 -0
- data/lib/ProMotion/collection/collection_screen.rb +8 -0
- data/lib/ProMotion/collection/data/collection_data.rb +32 -0
- data/lib/ProMotion/collection/data/collection_data_builder.rb +18 -0
- data/lib/ProMotion/delegate/delegate_module.rb +1 -1
- data/lib/ProMotion/ipad/split_screen.rb +0 -1
- data/lib/ProMotion/repl/live_reloader.rb +76 -0
- data/lib/ProMotion/repl/repl.rb +65 -0
- data/lib/ProMotion/screen/nav_bar_module.rb +8 -1
- data/lib/ProMotion/screen/screen_module.rb +26 -3
- data/lib/ProMotion/screen/screen_navigation.rb +3 -0
- data/lib/ProMotion/table/cell/table_view_cell_module.rb +11 -0
- data/lib/ProMotion/table/extensions/searchable.rb +11 -0
- data/lib/ProMotion/table/table.rb +12 -1
- data/lib/ProMotion/table/table_builder.rb +7 -5
- data/lib/ProMotion/tabs/tabs.rb +6 -3
- data/lib/ProMotion/version.rb +1 -1
- data/spec/functional/func_screen_spec.rb +25 -20
- data/spec/functional/func_split_screen_spec.rb +2 -2
- data/spec/unit/collection_screen_spec.rb +133 -0
- data/spec/unit/screen_helpers_spec.rb +1 -1
- data/spec/unit/screen_spec.rb +44 -6
- data/spec/unit/tables/table_module_spec.rb +28 -2
- data/spec/unit/tables/table_refreshable_spec.rb +25 -0
- data/spec/unit/tables/table_screen_spec.rb +0 -56
- data/spec/unit/tables/table_searchable_spec.rb +41 -0
- data/spec/unit/tables/table_view_cell_spec.rb +20 -1
- metadata +22 -5
@@ -0,0 +1,32 @@
|
|
1
|
+
module ProMotion
|
2
|
+
class CollectionData
|
3
|
+
include ProMotion::Table::Utils
|
4
|
+
include ProMotion::CollectionDataBuilder
|
5
|
+
|
6
|
+
attr_accessor :data, :collection_view
|
7
|
+
|
8
|
+
def initialize(data, collection_view)
|
9
|
+
self.data = data
|
10
|
+
self.collection_view = WeakRef.new(collection_view)
|
11
|
+
end
|
12
|
+
|
13
|
+
def section(index)
|
14
|
+
sections.at(index) || []
|
15
|
+
end
|
16
|
+
|
17
|
+
def sections
|
18
|
+
self.data
|
19
|
+
end
|
20
|
+
|
21
|
+
def section_length(index)
|
22
|
+
section(index).length
|
23
|
+
end
|
24
|
+
|
25
|
+
def cell(params={})
|
26
|
+
params = index_path_to_section_index(params)
|
27
|
+
section = self.data[params[:section]]
|
28
|
+
c = section.at(params[:index].to_i)
|
29
|
+
set_data_cell_defaults(c)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ProMotion
|
2
|
+
module CollectionDataBuilder
|
3
|
+
def set_data_cell_defaults(data_cell)
|
4
|
+
data_cell[:cell_identifier] ||= PM::CollectionViewCell::KIdentifier
|
5
|
+
data_cell[:properties] ||= data_cell[:style] || data_cell[:styles]
|
6
|
+
|
7
|
+
data_cell[:accessory] = {
|
8
|
+
view: data_cell[:accessory],
|
9
|
+
value: data_cell[:accessory_value],
|
10
|
+
action: data_cell[:accessory_action],
|
11
|
+
arguments: data_cell[:accessory_arguments]
|
12
|
+
} unless data_cell[:accessory].nil? || data_cell[:accessory].is_a?(Hash)
|
13
|
+
|
14
|
+
data_cell
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -2,7 +2,7 @@ module ProMotion
|
|
2
2
|
module DelegateModule
|
3
3
|
include ProMotion::Support
|
4
4
|
include ProMotion::Tabs
|
5
|
-
include ProMotion::SplitScreen if UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad
|
5
|
+
include ProMotion::SplitScreen if UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad || (UIDevice.currentDevice.systemVersion.to_i >= 8 )
|
6
6
|
|
7
7
|
attr_accessor :window, :home_screen
|
8
8
|
|
@@ -0,0 +1,76 @@
|
|
1
|
+
class LiveReloader
|
2
|
+
attr_reader :path_query, :opts
|
3
|
+
|
4
|
+
def initialize(path, opts={})
|
5
|
+
@path_query = path
|
6
|
+
@opts = opts
|
7
|
+
end
|
8
|
+
|
9
|
+
def watch(&callback)
|
10
|
+
log path_query
|
11
|
+
log live_file_paths
|
12
|
+
|
13
|
+
@live_reload_timer = every(opts[:interval] || 0.5) do
|
14
|
+
live_files.each do |live_file, modified_date|
|
15
|
+
if File.exist?(live_file) && File.mtime(live_file) > modified_date
|
16
|
+
new_code = File.read(live_file)
|
17
|
+
eval(new_code)
|
18
|
+
callback.call *[live_file, new_code, parse_class_names(new_code)].first(callback.arity)
|
19
|
+
reload_live_files
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
log "Watching #{path_query}."
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def stop
|
29
|
+
@live_reload_timer.invalidate if @live_reload_timer
|
30
|
+
@live_reload_timer = nil
|
31
|
+
log "Stopped."
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def debug?
|
36
|
+
@opts[:debug]
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def every(interval, &callback)
|
42
|
+
NSTimer.scheduledTimerWithTimeInterval(interval, target: callback, selector: 'call:', userInfo: nil, repeats: true)
|
43
|
+
end
|
44
|
+
|
45
|
+
def live_files
|
46
|
+
@live_files ||= live_file_paths.inject({}) do |out, live_file_path_file|
|
47
|
+
out[live_file_path_file] = File.mtime(live_file_path_file)
|
48
|
+
out
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def reload_live_files
|
53
|
+
@live_files = nil
|
54
|
+
live_files
|
55
|
+
end
|
56
|
+
|
57
|
+
def log(s)
|
58
|
+
puts s.inspect if debug?
|
59
|
+
s
|
60
|
+
end
|
61
|
+
|
62
|
+
def project_root_path
|
63
|
+
NSBundle.mainBundle.infoDictionary["ProjectRootPath"]
|
64
|
+
end
|
65
|
+
|
66
|
+
def live_file_paths
|
67
|
+
log Dir.glob("#{project_root_path}/#{path_query}").to_a
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_class_names(code)
|
71
|
+
log code.split("\n").map do |code_line|
|
72
|
+
matched = code_line.match(/^\s*class\s+(\S+)\s+</)
|
73
|
+
matched[1] if matched
|
74
|
+
end.to_a.compact.to_a
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
if RUBYMOTION_ENV == "development"
|
2
|
+
puts "Type `pm_live` to enable ProMotion's live reload system."
|
3
|
+
module Kernel
|
4
|
+
def pm_live(opts={})
|
5
|
+
@screen_watcher.stop if @screen_watcher
|
6
|
+
@view_watcher.stop if @view_watcher
|
7
|
+
if opts == false || opts.to_s.downcase == "off"
|
8
|
+
"Live reloading of PM screens is now off."
|
9
|
+
else
|
10
|
+
@screen_watcher = LiveReloader.new("app/screens/**/*.rb", opts).watch do |reloaded_file, new_code, class_names|
|
11
|
+
vcs = pm_all_view_controllers(UIApplication.sharedApplication.delegate.window.rootViewController)
|
12
|
+
vcs.each do |vc|
|
13
|
+
if pm_is_in_ancestry?(vc, class_names)
|
14
|
+
puts "Sending :on_live_reload to #{vc.inspect}." if opts[:debug]
|
15
|
+
vc.send(:on_live_reload) if vc.respond_to?(:on_live_reload)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
@view_watcher = LiveReloader.new("app/views/**/*.rb", opts).watch do |reloaded_file, new_code, class_names|
|
21
|
+
views = pm_all_views(UIApplication.sharedApplication.delegate.window)
|
22
|
+
views.each do |view|
|
23
|
+
if pm_is_in_ancestry?(view, class_names)
|
24
|
+
puts "Sending :on_live_reload to #{view.inspect}." if opts[:debug]
|
25
|
+
view.send(:on_live_reload) if view.respond_to?(:on_live_reload)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
"Live reloading of screens and views is now on."
|
31
|
+
end
|
32
|
+
end
|
33
|
+
alias_method :pm_live_screens, :pm_live
|
34
|
+
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Very permissive. Might get unnecessary reloads. That's okay.
|
39
|
+
def pm_is_in_ancestry?(vc, screen_names)
|
40
|
+
screen_names.any? do |screen_name|
|
41
|
+
vc.class.to_s.include?(screen_name) ||
|
42
|
+
vc.class.ancestors.any? do |ancestor|
|
43
|
+
screen_name.include?(ancestor.to_s)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def pm_all_view_controllers(root_view_controller)
|
49
|
+
vcs = [ root_view_controller ]
|
50
|
+
if root_view_controller.respond_to?(:viewControllers)
|
51
|
+
vcs = vcs + root_view_controller.viewControllers.map{|vc| pm_all_view_controllers(vc) }
|
52
|
+
end
|
53
|
+
if root_view_controller.respond_to?(:childViewControllers)
|
54
|
+
vcs = vcs + root_view_controller.childViewControllers.map{|vc| pm_all_view_controllers(vc) }
|
55
|
+
end
|
56
|
+
vcs.flatten.uniq
|
57
|
+
end
|
58
|
+
|
59
|
+
def pm_all_views(root_view)
|
60
|
+
views = [ root_view ]
|
61
|
+
views = views + views.map{|v| v.subviews.map{|sv| pm_all_views(sv) } }
|
62
|
+
views.flatten.uniq
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -53,6 +53,8 @@ module ProMotion
|
|
53
53
|
alias_method :set_toolbar_button, :set_toolbar_items
|
54
54
|
|
55
55
|
def add_nav_bar(args = {})
|
56
|
+
args = self.class.get_nav_bar.merge(args)
|
57
|
+
return unless args[:nav_bar]
|
56
58
|
self.navigationController ||= begin
|
57
59
|
self.first_screen = true if self.respond_to?(:first_screen=)
|
58
60
|
nav = (args[:nav_controller] || NavigationController).alloc.initWithRootViewController(self)
|
@@ -61,7 +63,12 @@ module ProMotion
|
|
61
63
|
nav
|
62
64
|
end
|
63
65
|
self.navigationController.toolbarHidden = !args[:toolbar] unless args[:toolbar].nil?
|
64
|
-
|
66
|
+
end
|
67
|
+
|
68
|
+
def view_will_appear(animated)
|
69
|
+
if @screen_options && !@screen_options[:hide_nav_bar].nil?
|
70
|
+
self.navigationController.setNavigationBarHidden(@screen_options[:hide_nav_bar], animated: false)
|
71
|
+
end
|
65
72
|
end
|
66
73
|
|
67
74
|
private
|
@@ -5,15 +5,16 @@ module ProMotion
|
|
5
5
|
include ProMotion::Styling
|
6
6
|
include ProMotion::NavBarModule
|
7
7
|
include ProMotion::Tabs
|
8
|
-
include ProMotion::SplitScreen if UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad
|
8
|
+
include ProMotion::SplitScreen if UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad || (UIDevice.currentDevice.systemVersion.to_i >= 8 )
|
9
9
|
|
10
10
|
attr_accessor :parent_screen, :first_screen, :modal, :split_screen
|
11
11
|
|
12
12
|
def screen_init(args = {})
|
13
|
+
@screen_options = args
|
13
14
|
check_ancestry
|
14
15
|
resolve_title
|
15
16
|
apply_properties(args)
|
16
|
-
add_nav_bar(args)
|
17
|
+
add_nav_bar(args)
|
17
18
|
add_nav_bar_buttons
|
18
19
|
tab_bar_setup
|
19
20
|
try :on_init
|
@@ -38,6 +39,7 @@ module ProMotion
|
|
38
39
|
end
|
39
40
|
|
40
41
|
def view_will_appear(animated)
|
42
|
+
super
|
41
43
|
resolve_status_bar
|
42
44
|
self.will_appear
|
43
45
|
|
@@ -70,6 +72,18 @@ module ProMotion
|
|
70
72
|
def on_disappear; end
|
71
73
|
def on_dismiss; end
|
72
74
|
|
75
|
+
def did_receive_memory_warning
|
76
|
+
self.on_memory_warning
|
77
|
+
end
|
78
|
+
def on_memory_warning
|
79
|
+
mp "Received memory warning in #{self.inspect}. You should implement on_memory_warning in your secreen.", force_color: :red
|
80
|
+
end
|
81
|
+
|
82
|
+
def on_live_reload
|
83
|
+
self.view.subviews.each(&:removeFromSuperview)
|
84
|
+
on_load
|
85
|
+
end
|
86
|
+
|
73
87
|
def should_rotate(orientation)
|
74
88
|
case orientation
|
75
89
|
when UIInterfaceOrientationPortrait
|
@@ -175,6 +189,7 @@ module ProMotion
|
|
175
189
|
status_bar_hidden false
|
176
190
|
status_bar_style UIStatusBarStyleDefault
|
177
191
|
else
|
192
|
+
return status_bar_hidden true if UIApplication.sharedApplication.isStatusBarHidden
|
178
193
|
status_bar_hidden false
|
179
194
|
global_style = NSBundle.mainBundle.objectForInfoDictionaryKey("UIStatusBarStyle")
|
180
195
|
status_bar_style global_style ? Object.const_get(global_style) : UIStatusBarStyleDefault
|
@@ -218,7 +233,7 @@ module ProMotion
|
|
218
233
|
end
|
219
234
|
@title = t if t
|
220
235
|
@title_type = :text if t
|
221
|
-
@title
|
236
|
+
@title
|
222
237
|
end
|
223
238
|
|
224
239
|
def title_type
|
@@ -251,6 +266,14 @@ module ProMotion
|
|
251
266
|
@status_bar_animation || UIStatusBarAnimationSlide
|
252
267
|
end
|
253
268
|
|
269
|
+
def nav_bar(enabled, args={})
|
270
|
+
@nav_bar_args = ({ nav_bar: enabled }).merge(args)
|
271
|
+
end
|
272
|
+
|
273
|
+
def get_nav_bar
|
274
|
+
@nav_bar_args ||= { nav_bar: false }
|
275
|
+
end
|
276
|
+
|
254
277
|
def nav_bar_button(side, args={})
|
255
278
|
@nav_bar_button_args = args
|
256
279
|
@nav_bar_button_args[:side] = side
|
@@ -82,6 +82,9 @@ module ProMotion
|
|
82
82
|
# Instantiate screen if given a class
|
83
83
|
screen = screen.new if screen.respond_to?(:new)
|
84
84
|
|
85
|
+
# Store screen options
|
86
|
+
screen.instance_variable_set(:@screen_options, args)
|
87
|
+
|
85
88
|
# Set parent
|
86
89
|
screen.parent_screen = self if screen.respond_to?(:parent_screen=)
|
87
90
|
|
@@ -19,6 +19,17 @@ module ProMotion
|
|
19
19
|
set_accessory_type
|
20
20
|
end
|
21
21
|
|
22
|
+
def layoutSubviews
|
23
|
+
super
|
24
|
+
|
25
|
+
# Support changing sizes of the image view
|
26
|
+
if (data_cell[:image] && data_cell[:image].is_a?(Hash) && data_cell[:image][:size])
|
27
|
+
self.imageView.bounds = CGRectMake(0, 0, data_cell[:image][:size], data_cell[:image][:size]);
|
28
|
+
elsif (data_cell[:remote_image] && data_cell[:remote_image][:size])
|
29
|
+
self.imageView.bounds = CGRectMake(0, 0, data_cell[:remote_image][:size], data_cell[:remote_image][:size]);
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
22
33
|
protected
|
23
34
|
|
24
35
|
# TODO: Remove this in ProMotion 2.1. Just for migration purposes.
|
@@ -11,6 +11,8 @@ module ProMotion
|
|
11
11
|
search_bar.placeholder = params[:search_bar][:placeholder]
|
12
12
|
end
|
13
13
|
|
14
|
+
@no_results_text = params[:search_bar][:no_results] if params[:search_bar][:no_results]
|
15
|
+
|
14
16
|
@table_search_display_controller = UISearchDisplayController.alloc.initWithSearchBar(search_bar, contentsController: params[:content_controller])
|
15
17
|
@table_search_display_controller.delegate = params[:delegate]
|
16
18
|
@table_search_display_controller.searchResultsDataSource = params[:data_source]
|
@@ -39,10 +41,19 @@ module ProMotion
|
|
39
41
|
search_bar
|
40
42
|
end
|
41
43
|
|
44
|
+
def set_no_results_text(controller)
|
45
|
+
Dispatch::Queue.main.async do
|
46
|
+
controller.searchResultsTableView.subviews.each do |v|
|
47
|
+
v.text = @no_results_text if v.is_a?(UILabel)
|
48
|
+
end
|
49
|
+
end if @no_results_text
|
50
|
+
end
|
51
|
+
|
42
52
|
######### iOS methods, headless camel case #######
|
43
53
|
|
44
54
|
def searchDisplayController(controller, shouldReloadTableForSearchString:search_string)
|
45
55
|
self.promotion_table_data.search(search_string)
|
56
|
+
set_no_results_text(controller) if @no_results_text
|
46
57
|
true
|
47
58
|
end
|
48
59
|
|
@@ -23,6 +23,10 @@ module ProMotion
|
|
23
23
|
set_up_row_height
|
24
24
|
end
|
25
25
|
|
26
|
+
def on_live_reload
|
27
|
+
update_table_data
|
28
|
+
end
|
29
|
+
|
26
30
|
def check_table_data
|
27
31
|
mp("Missing #table_data method in TableScreen #{self.class.to_s}.", force_color: :red) unless self.respond_to?(:table_data)
|
28
32
|
end
|
@@ -287,6 +291,7 @@ module ProMotion
|
|
287
291
|
section = promotion_table_data.section(index)
|
288
292
|
view = section[:title_view]
|
289
293
|
view = section[:title_view].new if section[:title_view].respond_to?(:new)
|
294
|
+
view.on_load if view.respond_to?(:on_load)
|
290
295
|
view.title = section[:title] if view.respond_to?(:title=)
|
291
296
|
view
|
292
297
|
end
|
@@ -294,7 +299,13 @@ module ProMotion
|
|
294
299
|
def tableView(_, heightForHeaderInSection: index)
|
295
300
|
section = promotion_table_data.section(index)
|
296
301
|
if section[:title_view] || section[:title].to_s.length > 0
|
297
|
-
section[:title_view_height]
|
302
|
+
if section[:title_view_height]
|
303
|
+
section[:title_view_height]
|
304
|
+
elsif (section_header = tableView(_, viewForHeaderInSection: index)) && section_header.respond_to?(:height)
|
305
|
+
section_header.height
|
306
|
+
else
|
307
|
+
tableView.sectionHeaderHeight
|
308
|
+
end
|
298
309
|
else
|
299
310
|
0.0
|
300
311
|
end
|
@@ -1,11 +1,13 @@
|
|
1
1
|
module ProMotion
|
2
2
|
module TableBuilder
|
3
3
|
def trigger_action(action, arguments, index_path)
|
4
|
-
|
5
|
-
case
|
6
|
-
when 0 then
|
7
|
-
when 2 then
|
8
|
-
else
|
4
|
+
action = (action.is_a?(Proc) ? action : method(action))
|
5
|
+
case arity = action.arity
|
6
|
+
when 0 then action.call # Just call the proc or the method
|
7
|
+
when 2 then action.call(arguments, index_path) # Send arguments and index path
|
8
|
+
else
|
9
|
+
mp("Action should not have optional parameters: #{action.to_s} in #{self.inspect}", force_color: :yellow) if arity < 0
|
10
|
+
action.call(arguments) # Send arguments
|
9
11
|
end
|
10
12
|
end
|
11
13
|
|
data/lib/ProMotion/tabs/tabs.rb
CHANGED
@@ -50,10 +50,13 @@ module ProMotion
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def create_tab_bar_item(tab={})
|
53
|
-
if tab[:system_icon]
|
54
|
-
mp("`system_icon:` no longer supported. Use `system_item:` instead.", force_color: :yellow)
|
55
|
-
mp("`icon:` no longer supported. Use `item:` instead.", force_color: :yellow) if tab[:icon]
|
53
|
+
if tab[:system_icon]
|
54
|
+
mp("`system_icon:` no longer supported. Use `system_item:` instead.", force_color: :yellow)
|
56
55
|
tab[:system_item] ||= tab[:system_icon]
|
56
|
+
end
|
57
|
+
|
58
|
+
if tab[:icon]
|
59
|
+
mp("`icon:` no longer supported. Use `item:` instead.", force_color: :yellow)
|
57
60
|
tab[:item] ||= tab[:icon]
|
58
61
|
end
|
59
62
|
|