motion-prime 0.1.0

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 (65) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +13 -0
  3. data/Gemfile.lock +83 -0
  4. data/README.md +67 -0
  5. data/Rakefile +23 -0
  6. data/app/app_delegate.rb +5 -0
  7. data/doc/SECTION.md +7 -0
  8. data/doc/STYLE.md +39 -0
  9. data/files/Gemfile +3 -0
  10. data/files/Rakefile +16 -0
  11. data/files/app/app_delegate.rb +4 -0
  12. data/files/app/screens/application_screen.rb +3 -0
  13. data/lib/motion-prime.rb +14 -0
  14. data/lib/view_styler.rb +141 -0
  15. data/motion-prime.gemspec +27 -0
  16. data/motion-prime/app_delegate.rb +56 -0
  17. data/motion-prime/elements/base.rb +94 -0
  18. data/motion-prime/elements/button.rb +7 -0
  19. data/motion-prime/elements/draw.rb +56 -0
  20. data/motion-prime/elements/draw/image.rb +43 -0
  21. data/motion-prime/elements/draw/label.rb +13 -0
  22. data/motion-prime/elements/image.rb +14 -0
  23. data/motion-prime/elements/label.rb +20 -0
  24. data/motion-prime/elements/text_field.rb +7 -0
  25. data/motion-prime/elements/text_view.rb +7 -0
  26. data/motion-prime/helpers/has_authorization.rb +10 -0
  27. data/motion-prime/helpers/has_search_bar.rb +25 -0
  28. data/motion-prime/models/base.rb +220 -0
  29. data/motion-prime/screens/_aliases_mixin.rb +32 -0
  30. data/motion-prime/screens/_base_mixin.rb +119 -0
  31. data/motion-prime/screens/_navigation_bar_mixin.rb +57 -0
  32. data/motion-prime/screens/_navigation_mixin.rb +118 -0
  33. data/motion-prime/screens/_orientations_mixin.rb +39 -0
  34. data/motion-prime/screens/base_screen.rb +22 -0
  35. data/motion-prime/screens/sidebar_container_screen.rb +58 -0
  36. data/motion-prime/sections/base.rb +101 -0
  37. data/motion-prime/sections/draw.rb +62 -0
  38. data/motion-prime/sections/form.rb +103 -0
  39. data/motion-prime/sections/form/base_field_section.rb +26 -0
  40. data/motion-prime/sections/form/password_field_section.rb +33 -0
  41. data/motion-prime/sections/form/select_field_section.rb +40 -0
  42. data/motion-prime/sections/form/string_field_section.rb +32 -0
  43. data/motion-prime/sections/form/submit_field_section.rb +20 -0
  44. data/motion-prime/sections/form/text_field_section.rb +33 -0
  45. data/motion-prime/sections/table.rb +97 -0
  46. data/motion-prime/sections/table/refresh_mixin.rb +13 -0
  47. data/motion-prime/styles/forms.rb +93 -0
  48. data/motion-prime/support/_key_value_store.rb +10 -0
  49. data/motion-prime/support/dm_button.rb +22 -0
  50. data/motion-prime/support/dm_cell_with_section.rb +12 -0
  51. data/motion-prime/support/dm_text_field.rb +30 -0
  52. data/motion-prime/support/dm_text_view.rb +93 -0
  53. data/motion-prime/support/dm_view_controller.rb +50 -0
  54. data/motion-prime/support/dm_view_with_section.rb +11 -0
  55. data/motion-prime/support/navigation_controller.rb +4 -0
  56. data/motion-prime/support/ui_search_bar_custom.rb +10 -0
  57. data/motion-prime/support/ui_view.rb +59 -0
  58. data/motion-prime/version.rb +3 -0
  59. data/motion-prime/views/layout.rb +45 -0
  60. data/motion-prime/views/styles.rb +44 -0
  61. data/motion-prime/views/view_builder.rb +80 -0
  62. data/motion-prime/views/view_styler.rb +141 -0
  63. data/resources/Default-568h@2x.png +0 -0
  64. data/spec/main_spec.rb +9 -0
  65. metadata +245 -0
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../motion-prime/version', __FILE__)
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "motion-prime"
6
+ spec.version = MotionPrime::VERSION
7
+ spec.authors = ["Iskander Haziev"]
8
+ spec.email = ["gvalmon@gmail.com"]
9
+ spec.description = %q{RubyMotion apps development framework}
10
+ spec.summary = %q{RubyMotion apps development framework}
11
+ spec.homepage = ""
12
+ spec.license = ""
13
+
14
+ spec.files = `git ls-files`.split($\)
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_development_dependency "rake"
20
+ spec.add_dependency "cocoapods"
21
+ spec.add_dependency "motion-cocoapods"
22
+ spec.add_dependency "motion-require"
23
+ spec.add_dependency "motion-support"
24
+ spec.add_dependency 'bubble-wrap'
25
+ spec.add_dependency 'sugarcube'
26
+ spec.add_dependency 'nano-store'
27
+ end
@@ -0,0 +1,56 @@
1
+ motion_require './helpers/has_authorization'
2
+ module MotionPrime
3
+ class BaseAppDelegate
4
+ include MotionPrime::HasAuthorization
5
+
6
+ attr_accessor :window, :sidebar_container
7
+
8
+ def application(application, didFinishLaunchingWithOptions:launch_options)
9
+ on_load(application, launch_options)
10
+ true
11
+ end
12
+
13
+ def app_delegate
14
+ UIApplication.sharedApplication.delegate
15
+ end
16
+
17
+ def app_window
18
+ self.app_delegate.window
19
+ end
20
+
21
+ def open_root_screen(screen)
22
+ screen.send(:on_screen_load) if screen.respond_to?(:on_screen_load)
23
+ screen = screen.main_controller if screen.respond_to?(:main_controller)
24
+
25
+ self.window ||= UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
26
+ self.window.rootViewController = screen
27
+ self.window.makeKeyAndVisible
28
+ screen
29
+ end
30
+
31
+ def open_screen(screen)
32
+ if sidebar?
33
+ sidebar_container.content_controller = screen
34
+ else
35
+ open_root_screen(screen)
36
+ end
37
+ end
38
+
39
+ def sidebar?
40
+ !sidebar_container.nil?
41
+ end
42
+
43
+ def open_with_sidebar(content, menu, options={})
44
+ self.sidebar_container = SidebarContainerScreen.new(menu, content, options)
45
+ open_root_screen(sidebar_container)
46
+ end
47
+
48
+ def show_sidebar
49
+ sidebar_container.show_sidebar
50
+ end
51
+
52
+ def hide_sidebar
53
+ sidebar_container.hide_sidebar
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,94 @@
1
+ module MotionPrime
2
+ class BaseElement
3
+ # MotionPrime::BaseElement is container for UIView class elements with options.
4
+ # Elements are located inside Sections
5
+
6
+ include ::MotionSupport::Callbacks
7
+ attr_accessor :options, :section, :name,
8
+ :view_class, :view, :styles, :screen
9
+
10
+ define_callbacks :render
11
+
12
+ def initialize(options = {})
13
+ @options = options
14
+ @section = WeakRef.new(options.delete(:section))
15
+ @name = options[:name]
16
+ @block = options.delete(:block)
17
+ @view_class = options.delete(:view_class) || "UIView"
18
+ @view_name = self.class.name.demodulize.underscore.gsub('_element', '')
19
+ end
20
+
21
+ def render(options = {}, &block)
22
+ self.screen = options[:to]
23
+ run_callbacks :render do
24
+ render!(&block)
25
+ end
26
+ end
27
+
28
+ def render!(&block)
29
+ @view = screen.add_view view_class.constantize, computed_options, &block
30
+ end
31
+
32
+ # Lazy-computing options
33
+ def computed_options
34
+ compute_options! if @computed_options.blank?
35
+ @computed_options
36
+ end
37
+
38
+ def compute_options!
39
+ @computed_options = options
40
+ compute_block_options
41
+ compute_style_options
42
+ @computed_options = normalize_options(@computed_options)
43
+ end
44
+
45
+ # Compute options sent inside block, e.g.
46
+ # element :button do
47
+ # {name: model.name}
48
+ # end
49
+ def compute_block_options
50
+ if block = @block
51
+ @computed_options.merge!(section.send :instance_eval, &block)
52
+ end
53
+ end
54
+
55
+ def compute_style_options
56
+ @styles = [:"base_#{@view_name}"]
57
+ @styles += Array.wrap(@computed_options.delete(:styles))
58
+ @styles += [:"#{section.name}_#{name}"] if section.present?
59
+ @computed_options.merge!(style_options)
60
+ end
61
+
62
+ def style_options
63
+ Styles.for(styles)
64
+ end
65
+
66
+ def normalize_options(options)
67
+ options.each do |key, option|
68
+ options[key] = if option.is_a?(Proc) && key != :block
69
+ section.send :instance_eval, &option
70
+ else
71
+ option
72
+ end
73
+ end
74
+ end
75
+
76
+ class << self
77
+ def factory(type, options = {})
78
+ class_name = "#{type.classify}Element"
79
+ options.merge!({view_class: "UI#{type.classify}"})
80
+ if MotionPrime.const_defined?(class_name)
81
+ "MotionPrime::#{class_name}".constantize.new(options)
82
+ else
83
+ self.new(options)
84
+ end
85
+ end
86
+ def before_render(method_name)
87
+ set_callback :render, :before, method_name
88
+ end
89
+ def after_render(method_name)
90
+ set_callback :render, :after, method_name
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,7 @@
1
+ module MotionPrime
2
+ class ButtonElement < BaseElement
3
+ def view_class
4
+ "DMButton"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,56 @@
1
+ module MotionPrime
2
+ class DrawElement < BaseElement
3
+ # MotionPrime::DrawElement is container for drawRect method options.
4
+ # Elements are located inside Sections
5
+
6
+ def render!
7
+ end
8
+
9
+ def view
10
+ @view ||= section.container_view
11
+ end
12
+
13
+ def computed_left
14
+ width = computed_options[:width]
15
+ left = computed_options[:left]
16
+ right = computed_options[:right]
17
+ return left if left
18
+ return 0 if right.nil?
19
+
20
+ width = 0.0 if width.nil?
21
+ max_width = view.bounds.size.width
22
+
23
+ # calculate left if width is relative, e.g 0.7
24
+ if width > 0 && width <= 1
25
+ max_width - (max_width * width) - right
26
+ else
27
+ max_width - width - right
28
+ end
29
+ end
30
+
31
+ def computed_top
32
+ height = computed_options[:height]
33
+ top = computed_options[:top]
34
+ bottom = computed_options[:bottom]
35
+ return top if top
36
+ return 0 if bottom.nil?
37
+
38
+ height = 0.0 if height.nil?
39
+ max_height = view.bounds.size.height
40
+
41
+ # calculate top if height is relative, e.g 0.7
42
+ if height > 0 && height <= 1
43
+ max_height - (max_height * height) - bottom
44
+ else
45
+ max_height - height - bottom
46
+ end
47
+ end
48
+
49
+ class << self
50
+ def factory(type, options = {})
51
+ class_name = "#{type.classify}DrawElement"
52
+ "MotionPrime::#{class_name}".constantize.new(options)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,43 @@
1
+ motion_require '../draw.rb'
2
+ module MotionPrime
3
+ class ImageDrawElement < DrawElement
4
+ attr_accessor :image_data
5
+ def draw_in(rect)
6
+ image_rect = CGRectMake(
7
+ computed_left,
8
+ computed_top,
9
+ computed_options[:width],
10
+ computed_options[:height]
11
+ )
12
+ # draw already initialized image
13
+ if image_data
14
+ image_data.drawInRect(image_rect)
15
+ # draw image from resources
16
+ elsif computed_options[:image]
17
+ self.image_data = computed_options[:image].uiimage
18
+ image_data.drawInRect(image_rect)
19
+ # show default image and download image from url
20
+ elsif computed_options[:url]
21
+ if computed_options[:default]
22
+ computed_options[:default].uiimage.drawInRect(image_rect)
23
+ end
24
+ manager = SDWebImageManager.sharedManager
25
+ manager.downloadWithURL(computed_options[:url],
26
+ options: 0,
27
+ progress: lambda{ |r_size, e_size| },
28
+ completed: lambda{ |image, error, type, finished|
29
+ if image
30
+ self.image_data = image
31
+ if type == SDImageCacheTypeNone || type == SDImageCacheTypeDisk
32
+ # if it's first call, we should redraw view, because it's async
33
+ section.container_view.setNeedsDisplay
34
+ else
35
+ # if it's second call, we should just draw image
36
+ self.image_data.drawInRect(image_rect)
37
+ end
38
+ end
39
+ } )
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ motion_require '../draw.rb'
2
+ module MotionPrime
3
+ class LabelDrawElement < DrawElement
4
+ def draw_in(rect)
5
+ color = computed_options[:color] || computed_options[:text_color]
6
+ color.uicolor.set if color
7
+ computed_options[:text].to_s.drawAtPoint(
8
+ CGPointMake(computed_left, computed_top),
9
+ withFont: computed_options[:font]
10
+ )
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module MotionPrime
2
+ class ImageElement < BaseElement
3
+ after_render :fetch_image
4
+ def view_class
5
+ "UIImageView"
6
+ end
7
+
8
+ def fetch_image
9
+ return unless computed_options[:url]
10
+ view.setImageWithURL NSURL.URLWithString(computed_options[:url]),
11
+ placeholderImage: computed_options[:default].uiimage
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ module MotionPrime
2
+ class LabelElement < BaseElement
3
+ after_render :size_to_fit
4
+
5
+ def size_to_fit
6
+ if computed_options[:size_to_fit] || style_options[:size_to_fit]
7
+ view.sizeToFit
8
+ end
9
+ end
10
+
11
+ def height
12
+ width = computed_options[:width]
13
+ font = computed_options[:font] || :system.uifont
14
+ raise "Please set element width for height calculation" unless width
15
+ computed_options[:text].sizeWithFont(font,
16
+ constrainedToSize: [width, Float::MAX],
17
+ lineBreakMode:UILineBreakModeWordWrap).height
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ module MotionPrime
2
+ class TextFieldElement < BaseElement
3
+ def view_class
4
+ "DMTextField"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module MotionPrime
2
+ class TextViewElement < BaseElement
3
+ def view_class
4
+ "DMTextView"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ module MotionPrime
2
+ module HasAuthorization
3
+ def current_user
4
+ @current_user = User.current
5
+ end
6
+ def api_client
7
+ @api_client ||= ApiClient.new(access_token: current_user.access_token)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ # This module adds search functionality, to Screen or TableSection
2
+ module MotionPrime
3
+ module HasSearchBar
4
+ def add_search_bar(&block)
5
+ search_bar = create_search_bar
6
+ search_bar.delegate = self
7
+ self.table_view.tableHeaderView = search_bar if is_a?(TableSection)
8
+ @search_callback = block
9
+ end
10
+
11
+ def create_search_bar
12
+ name = is_a?(TableSection) ? name : self.class.name.underscore
13
+ screen = is_a?(TableSection) ? self.screen : self
14
+ screen.search_bar(styles: [:"base_search_bar", :"#{name}_search_bar"]).view
15
+ end
16
+
17
+ def searchBar(search_bar, textDidChange: text)
18
+ @search_callback.call(text)
19
+ end
20
+
21
+ def searchBarSearchButtonClicked(search_bar)
22
+ search_bar.resignFirstResponder
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,220 @@
1
+ motion_require '../helpers/has_authorization'
2
+ module MotionPrime
3
+ class BaseModel < NanoStore::Model
4
+ class_attribute :sync_url
5
+ class_attribute :sync_attributes
6
+ class_attribute :_associations
7
+ alias_method :attributes, :info
8
+ include MotionPrime::HasAuthorization
9
+
10
+ def sync_url
11
+ self.class.sync_url.to_s.gsub(':id', id.to_s)
12
+ end
13
+
14
+ def model_name
15
+ self.class.name.underscore
16
+ end
17
+
18
+ def new_record?
19
+ id.blank?
20
+ end
21
+
22
+ def destroy(&block)
23
+ use_callback = block_given?
24
+ api_client.delete(sync_url) do
25
+ block.call() if use_callback
26
+ end
27
+ delete
28
+ end
29
+
30
+ # fetch attributes from url
31
+ def sync_with_url(url, &block)
32
+ api_client.get(url) do |data|
33
+ if data.present?
34
+ sync_with_attributes(data, &block)
35
+ end
36
+ end
37
+ end
38
+
39
+ def update_with_url(url, &block)
40
+ use_callback = block_given?
41
+ post_data = { model_name => filtered_sync_attributes}
42
+ api_client.send(id ? :put : :post, url, post_data) do |data|
43
+ self.id ||= data['id']
44
+ block.call() if use_callback
45
+ end
46
+ end
47
+
48
+ # set attributes
49
+ def sync_with_attributes(attrs, &block)
50
+ attrs.each do |key, value|
51
+ if respond_to?(:"sync_#{key}")
52
+ self.send(:"sync_#{key}", value)
53
+ elsif respond_to?(:"#{key}=")
54
+ self.send(:"#{key}=", value)
55
+ end
56
+ end
57
+ block.call(self) if block_given?
58
+ end
59
+
60
+ def sync!(sync_options = {}, &block)
61
+ sync(sync_options.merge(save: true), &block)
62
+ end
63
+
64
+ # sync with url and
65
+ # TODO: order of fetch/update should be based on updated time
66
+ def sync(sync_options = {}, &block)
67
+ use_callback = block_given?
68
+ should_fetch = sync_options[:fetch]
69
+ should_update = sync_options[:update]
70
+
71
+ should_fetch = !new_record? if should_fetch.nil?
72
+ should_update = new_record? if should_update.nil?
73
+
74
+ sync_with_url self.sync_url do
75
+ save if sync_options[:save]
76
+ block.call if use_callback
77
+ end if should_fetch
78
+ update_with_url self.sync_url do
79
+ save if sync_options[:save]
80
+ block.call if use_callback
81
+ end if should_update
82
+
83
+ sync_associations(sync_options)
84
+ end
85
+
86
+ def sync_associations(sync_options = {})
87
+ (self.class._associations || []).each do |key, options|
88
+ sync_association(key, sync_options)
89
+ end
90
+ end
91
+
92
+ def sync_association(key, sync_options = {}, &block)
93
+ options = self.class._associations[key]
94
+ return unless options[:sync_url]
95
+ options[:type] == :many ?
96
+ sync_has_many(key, options, sync_options, &block) :
97
+ sync_has_one(key, options, sync_options, &block)
98
+ end
99
+
100
+ def sync_has_many(key, options = {}, sync_options = {}, &block)
101
+ old_collection = self.send(key)
102
+ use_callback = block_given?
103
+ puts "SYNC: started sync for #{key} in #{self.class.name}"
104
+ api_client.get(options[:sync_url]) do |data|
105
+ if data.present?
106
+ # Update/Create existing records
107
+ data.each do |attributes|
108
+ model = old_collection.detect{ |model| model.id == attributes[:id]}
109
+ unless model
110
+ model = key.singularize.to_s.classify.constantize.new
111
+ self.send(:"#{key}_bag") << model
112
+ end
113
+ model.sync_with_attributes(attributes)
114
+ model.save if sync_options[:save]
115
+ end
116
+ old_collection.each do |old_model|
117
+ model = data.detect{ |model| model[:id] == old_model.id}
118
+ unless model
119
+ old_model.delete
120
+ end
121
+ end
122
+ save if sync_options[:save]
123
+ puts "SYNC: finished sync for #{key} in #{self.class.name}"
124
+ block.call if use_callback
125
+ else
126
+ puts "SYNC ERROR: failed sync for #{key} in #{self.class.name}"
127
+ block.call if use_callback
128
+ end
129
+ end
130
+ end
131
+
132
+ def sync_has_one(key, options = {}, &block)
133
+ # TODO: add implementation
134
+ end
135
+
136
+ def inspect
137
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}> " + BW::JSON.generate(attributes)
138
+ end
139
+
140
+ # NOTE: .clear method doesn't work, using removeArray hack for now
141
+ def _clear_bag(bag_name)
142
+ bag = self.send(bag_name.to_sym)
143
+ bag_copy = bag.to_a.clone
144
+ bag - bag.to_a # this removes association from model
145
+ bag_copy.each(&:delete) # this removes collection from db
146
+ end
147
+
148
+ def filtered_sync_attributes
149
+ return attributes if self.class.sync_attributes.blank?
150
+ attributes.reject do |key, value|
151
+ self.class.sync_attributes.exclude?(key.to_sym)
152
+ end
153
+ end
154
+
155
+ class << self
156
+ def sync_url(url = nil)
157
+ url ? self.sync_url = url : super
158
+ end
159
+
160
+ def sync_attributes(*attrs)
161
+ attrs ? self.sync_attributes = attrs : super
162
+ end
163
+
164
+ def has_one(association_name, options = {})
165
+ bag_name = "#{association_name.pluralize}_bag"
166
+ self.bag bag_name.to_sym
167
+
168
+ self._associations ||= {}
169
+ self._associations[association_name] = options.merge(type: :one)
170
+
171
+ define_method("#{association_name}=") do |value|
172
+ self._clear_bag(bag_name)
173
+
174
+ self.send(:"#{bag_name}") << value
175
+ value
176
+ end
177
+ define_method("#{association_name}_attributes=") do |value|
178
+ self._clear_bag(bag_name)
179
+
180
+ association = association_name.classify.constantize.new
181
+ association.sync_with_attributes(value)
182
+ association.save
183
+ self.send(:"#{bag_name}") << association
184
+ association
185
+ end
186
+ define_method("#{association_name}") do
187
+ self.send(:"#{bag_name}").to_a.first
188
+ end
189
+ end
190
+
191
+ def has_many(association_name, options = {})
192
+ bag_name = "#{association_name}_bag"
193
+ self.bag bag_name.to_sym
194
+
195
+ self._associations ||= {}
196
+ self._associations[association_name] = options.merge(type: :many)
197
+
198
+ define_method("#{association_name}_attributes=") do |value|
199
+ self._clear_bag(bag_name)
200
+
201
+ association = []
202
+ value.each do |attrs|
203
+ model = association_name.classify.constantize.new
204
+ model.sync_with_attributes(attrs)
205
+ association << model
206
+ end
207
+ self.send(:"#{bag_name}=", association)
208
+ association
209
+ end
210
+ define_method("#{association_name}=") do |value|
211
+ self._clear_bag(bag_name)
212
+ self.send(:"#{bag_name}=", value)
213
+ end
214
+ define_method("#{association_name}") do
215
+ self.send(:"#{bag_name}").to_a
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end