motion-prime 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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