ProMotion 2.4.2 → 2.5.0.beta1

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -13
  3. data/lib/ProMotion.rb +40 -14
  4. data/lib/ProMotion/cocoatouch/collection_view_cell.rb +8 -0
  5. data/lib/ProMotion/cocoatouch/collection_view_controller.rb +49 -0
  6. data/lib/ProMotion/cocoatouch/view_controller.rb +5 -0
  7. data/lib/ProMotion/collection/cell/collection_view_cell_module.rb +17 -0
  8. data/lib/ProMotion/collection/collection.rb +106 -0
  9. data/lib/ProMotion/collection/collection_builder.rb +39 -0
  10. data/lib/ProMotion/collection/collection_class_methods.rb +38 -0
  11. data/lib/ProMotion/collection/collection_screen.rb +8 -0
  12. data/lib/ProMotion/collection/data/collection_data.rb +32 -0
  13. data/lib/ProMotion/collection/data/collection_data_builder.rb +18 -0
  14. data/lib/ProMotion/delegate/delegate_module.rb +1 -1
  15. data/lib/ProMotion/ipad/split_screen.rb +0 -1
  16. data/lib/ProMotion/repl/live_reloader.rb +76 -0
  17. data/lib/ProMotion/repl/repl.rb +65 -0
  18. data/lib/ProMotion/screen/nav_bar_module.rb +8 -1
  19. data/lib/ProMotion/screen/screen_module.rb +26 -3
  20. data/lib/ProMotion/screen/screen_navigation.rb +3 -0
  21. data/lib/ProMotion/table/cell/table_view_cell_module.rb +11 -0
  22. data/lib/ProMotion/table/extensions/searchable.rb +11 -0
  23. data/lib/ProMotion/table/table.rb +12 -1
  24. data/lib/ProMotion/table/table_builder.rb +7 -5
  25. data/lib/ProMotion/tabs/tabs.rb +6 -3
  26. data/lib/ProMotion/version.rb +1 -1
  27. data/spec/functional/func_screen_spec.rb +25 -20
  28. data/spec/functional/func_split_screen_spec.rb +2 -2
  29. data/spec/unit/collection_screen_spec.rb +133 -0
  30. data/spec/unit/screen_helpers_spec.rb +1 -1
  31. data/spec/unit/screen_spec.rb +44 -6
  32. data/spec/unit/tables/table_module_spec.rb +28 -2
  33. data/spec/unit/tables/table_refreshable_spec.rb +25 -0
  34. data/spec/unit/tables/table_screen_spec.rb +0 -56
  35. data/spec/unit/tables/table_searchable_spec.rb +41 -0
  36. data/spec/unit/tables/table_view_cell_spec.rb +20 -1
  37. metadata +22 -5
@@ -0,0 +1,8 @@
1
+ module ProMotion
2
+ class CollectionScreen < CollectionViewController
3
+ include ProMotion::ScreenModule
4
+ include ProMotion::CollectionBuilder
5
+ include ProMotion::Collection
6
+ end
7
+ end
8
+
@@ -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
 
@@ -58,7 +58,6 @@ module ProMotion
58
58
  end
59
59
 
60
60
  def split_screen_setup(split, args)
61
- args[:icon] ||= args[:item] # TODO: Remove in PM 2.2+.
62
61
  if (args[:item] || args[:title]) && respond_to?(:create_tab_bar_item)
63
62
  split.tabBarItem = create_tab_bar_item(args)
64
63
  end
@@ -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
- self.navigationController.setNavigationBarHidden(args[:hide_nav_bar], animated: false) unless args[:hide_nav_bar].nil?
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) if args[:nav_bar]
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 ||= self.to_s
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] || tableView.sectionHeaderHeight
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
- return mp("Action not implemented: #{action.to_s}", force_color: :green) unless self.respond_to?(action)
5
- case self.method(action).arity
6
- when 0 then self.send(action) # Just call the method
7
- when 2 then self.send(action, arguments, index_path) # Send arguments and index path
8
- else self.send(action, arguments) # Send arguments
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
 
@@ -50,10 +50,13 @@ module ProMotion
50
50
  end
51
51
 
52
52
  def create_tab_bar_item(tab={})
53
- if tab[:system_icon] || tab[:icon]
54
- mp("`system_icon:` no longer supported. Use `system_item:` instead.", force_color: :yellow) if tab[:system_icon]
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