formotion 0.0.1

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.
@@ -0,0 +1,211 @@
1
+ module Formotion
2
+ class Row < Formotion::Base
3
+ PROPERTIES = [
4
+ # @form.render will contain row's value as the value for this key.
5
+ :key,
6
+ # the user's (or configured) value for this row.
7
+ :value,
8
+ # set as cell.titleLabel.text
9
+ :title,
10
+ # set as cell.detailLabel.text
11
+ :subtitle,
12
+ # configures the type of input this is (string, phone, switch, etc)
13
+ # either Formotion::RowType or a string/symbol representation of one
14
+ # see row_type.rb
15
+ :type,
16
+
17
+ # The following apply only to text-input fields
18
+
19
+ # placeholder text
20
+ :placeholder,
21
+ # whether or not the entry field is secure (like a password)
22
+ :secure,
23
+ # given by a UIReturnKey___ integer, string, or symbol
24
+ # EX :default, :google
25
+ :return_key,
26
+ # given by a UITextAutocorrectionType___ integer, string, or symbol
27
+ # EX :yes, :no, :default
28
+ :auto_correction,
29
+ # given by a UITextAutocapitalizationType___ integer, string, or symbol
30
+ # EX :none, :words
31
+ :auto_capitalization,
32
+ # field.clearButtonMode; given by a UITextFieldViewMode__ integer, string, symbol
33
+ # EX :never, :while_editing
34
+ # DEFAULT is nil, which is used as :while_editing
35
+ :clear_button]
36
+ PROPERTIES.each {|prop|
37
+ attr_accessor prop
38
+ }
39
+ BOOLEAN_PROPERTIES = [:secure]
40
+ SERIALIZE_PROPERTIES = PROPERTIES
41
+
42
+ # Reference to the row's section
43
+ attr_accessor :section
44
+
45
+ # Index of the row in the section
46
+ attr_accessor :index
47
+
48
+ # The reuse-identifier used in UITableViews
49
+ # By default is a stringification of section.index and row.index,
50
+ # thus is unique per row (bad for memory, to fix later.)
51
+ attr_accessor :reuse_identifier
52
+
53
+ # The following apply only to text-input fields
54
+
55
+ # reference to the row's UITextField.
56
+ attr_reader :text_field
57
+
58
+ # callback for what happens when the user
59
+ # hits the enter key while editing #text_field
60
+ attr_accessor :on_enter_callback
61
+ # callback for what happens when the user
62
+ # starts editing #text_field.
63
+ attr_accessor :on_begin_callback
64
+
65
+ def initialize(params = {})
66
+ super
67
+
68
+ BOOLEAN_PROPERTIES.each {|prop|
69
+ Formotion::Conditions.assert_nil_or_boolean(self.send(prop))
70
+ }
71
+ end
72
+
73
+ # Makes all ::BOOLEAN_PROPERTIES queriable with an appended ?
74
+ # these should be done with alias_method but there's currently a bug
75
+ # in RM which messes up attr_accessors with alias_method
76
+ # EX
77
+ # row.editable?
78
+ # => true
79
+ # row.checkable?
80
+ # => nil
81
+ def method_missing(method, *args, &block)
82
+ boolean_method = (method.to_s[0..-2]).to_sym
83
+ if BOOLEAN_PROPERTIES.member? boolean_method
84
+ return self.send(boolean_method)
85
+ end
86
+ super
87
+ end
88
+
89
+ #########################
90
+ # pseudo-properties
91
+
92
+ def index_path
93
+ NSIndexPath.indexPathForRow(self.index, inSection:self.section.index)
94
+ end
95
+
96
+ def form
97
+ self.section.form
98
+ end
99
+
100
+ def reuse_identifier
101
+ @reuse_identifier || "SECTION_#{self.section.index}_ROW_#{self.index}"
102
+ end
103
+
104
+ def next_row
105
+ # if there are more rows in this section, use that.
106
+ return self.section.rows[self.index + 1] if self.index < (self.section.rows.count - 1)
107
+
108
+ # if there are more sections, then use the first row of that section.
109
+ return self.section.next_section.rows[0] if self.section.next_section
110
+
111
+ nil
112
+ end
113
+
114
+ def previous_row
115
+ return self.section.rows[self.index - 1] if self.index > 0
116
+
117
+ # if there are more sections, then use the first row of that section.
118
+ return self.section.previous_section.rows[-1] if self.section.previous_section
119
+
120
+ nil
121
+ end
122
+
123
+ def editable?
124
+ Formotion::RowType::TEXT_FIELD_TYPES.member? self.type
125
+ end
126
+
127
+ def submit_button?
128
+ self.type == Formotion::RowType::SUBMIT
129
+ end
130
+
131
+ def switchable?
132
+ self.type == Formotion::RowType::SWITCH
133
+ end
134
+
135
+ def checkable?
136
+ self.type == Formotion::RowType::CHECK
137
+ end
138
+
139
+ #########################
140
+ # setter overrides
141
+ def type=(type)
142
+ @type = Formotion::RowType.for(type)
143
+ end
144
+
145
+ def return_key=(value)
146
+ @return_key = const_int_get("UIReturnKey", value)
147
+ end
148
+
149
+ def auto_correction=(value)
150
+ @auto_correction = const_int_get("UITextAutocorrectionType", value)
151
+ end
152
+
153
+ def auto_capitalization=(value)
154
+ @auto_capitalization = const_int_get("UITextAutocapitalizationType", value)
155
+ end
156
+
157
+ def clear_button=(value)
158
+ @clear_button = const_int_get("UITextFieldViewMode", value)
159
+ end
160
+
161
+ #########################
162
+ # setters for callbacks
163
+
164
+ def on_enter(&block)
165
+ self.on_enter_callback = block
166
+ end
167
+
168
+ def on_begin(&block)
169
+ self.on_begin_callback = block
170
+ end
171
+
172
+ #########################
173
+ # Methods for making cells
174
+ # Called in UITableViewDataSource methods
175
+ # in form_delegate.rb
176
+ def make_cell
177
+ cell, text_field = Formotion::RowCellBuilder.make_cell(self)
178
+ @text_field = text_field
179
+ cell
180
+ end
181
+
182
+ #########################
183
+ # Retreiving data
184
+ def to_hash
185
+ super
186
+ end
187
+
188
+ private
189
+ def const_int_get(base, value)
190
+ return value if value.is_a? Integer
191
+ value = value.to_s.camelize
192
+ Kernel.const_get("#{base}#{value}")
193
+ end
194
+
195
+ # Looks like RubyMotion adds UIKit constants
196
+ # at compile time. If you don't use these
197
+ # directly in your code, they don't get added
198
+ # to Kernel and const_int_get crashes.
199
+ def load_constants_hack
200
+ [UITextAutocapitalizationTypeNone, UITextAutocapitalizationTypeWords,
201
+ UITextAutocapitalizationTypeSentences,UITextAutocapitalizationTypeAllCharacters,
202
+ UITextAutocorrectionTypeNo, UITextAutocorrectionTypeYes, UITextAutocorrectionTypeDefault,
203
+ UIReturnKeyDefault, UIReturnKeyGo, UIReturnKeyGoogle, UIReturnKeyJoin,
204
+ UIReturnKeyNext, UIReturnKeyRoute, UIReturnKeySearch, UIReturnKeySend,
205
+ UIReturnKeyYahoo, UIReturnKeyDone, UIReturnKeyEmergencyCall,
206
+ UITextFieldViewModeNever, UITextFieldViewModeAlways, UITextFieldViewModeWhileEditing,
207
+ UITextFieldViewModeUnlessEditing
208
+ ]
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,168 @@
1
+ #################
2
+ #
3
+ # Formotion::RowCellBuilder
4
+ # RowCellBuilder handles taking Formotion::Rows
5
+ # and configuring UITableViewCells based on their properties.
6
+ #
7
+ #################
8
+ module Formotion
9
+ class RowCellBuilder
10
+ # The new UITextField in a UITableViewCell
11
+ # will be assigned this tag, if applicable.
12
+ TEXT_FIELD_TAG=1000
13
+
14
+ # PARAMS row.is_a? Formotion::Row
15
+ # RETURNS [cell configured to that row, a UITextField for that row if applicable or nil]
16
+ def self.make_cell(row)
17
+ cell, text_field = nil
18
+
19
+ cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier:row.reuse_identifier)
20
+
21
+ cell.accessoryType = UITableViewCellAccessoryNone
22
+ cell.textLabel.text = row.title
23
+ cell.detailTextLabel.text = row.subtitle
24
+
25
+ if row.submit_button?
26
+ make_submit_cell(row, cell)
27
+ elsif row.switchable?
28
+ make_switch_cell(row, cell)
29
+ elsif row.checkable?
30
+ make_check_cell(row, cell)
31
+ elsif row.editable?
32
+ text_field = make_text_field(row, cell)
33
+ end
34
+ [cell, text_field]
35
+ end
36
+
37
+ # This is actually called whenever again cell is checked/unchecked
38
+ # in the UITableViewDelegate callbacks. So (for now) don't
39
+ # instantiate long-lived objects in them.
40
+ # Maybe that logic should be moved elsewhere?
41
+ def self.make_check_cell(row, cell)
42
+ cell.accessoryType = row.value ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone
43
+ end
44
+
45
+ def self.make_switch_cell(row, cell)
46
+ cell.selectionStyle = UITableViewCellSelectionStyleNone
47
+ switchView = UISwitch.alloc.initWithFrame(CGRectZero)
48
+ cell.accessoryView = switchView
49
+ switchView.setOn(row.value || false, animated:false)
50
+ switchView.when(UIControlEventValueChanged) do
51
+ row.value = switchView.isOn
52
+ end
53
+ end
54
+
55
+ # Does a clever little trick to override #layoutSubviews
56
+ # for just this one UITableViewCell object, in order to
57
+ # center it's labels horizontally.
58
+ def self.make_submit_cell(row, cell)
59
+ cell.class.send(:alias_method, :old_layoutSubviews, :layoutSubviews)
60
+ cell.instance_eval do
61
+ def layoutSubviews
62
+ old_layoutSubviews
63
+
64
+ center = lambda {|frame, dimen|
65
+ ((self.frame.size.send(dimen) - frame.size.send(dimen)) / 2.0)
66
+ }
67
+
68
+ self.textLabel.center = CGPointMake(self.frame.size.width / 2 - 10, self.textLabel.center.y)
69
+ self.detailTextLabel.center = CGPointMake(self.frame.size.width / 2 - 10, self.detailTextLabel.center.y)
70
+ end
71
+ end
72
+ end
73
+
74
+ # Configures the cell to have a new UITextField
75
+ # which is used to enter data. Consists of
76
+ # 1) setting up that field with the appropriate properties
77
+ # specified by `row` 2) configures the callbacks on the field
78
+ # to call any callbacks `row` listens for.
79
+ # Also does the layoutSubviews swizzle trick
80
+ # to size the UITextField so it won't bump into the titleLabel.
81
+ def self.make_text_field(row, cell)
82
+ field = UITextField.alloc.initWithFrame(CGRectZero)
83
+ field.tag = TEXT_FIELD_TAG
84
+
85
+ field.placeholder = row.placeholder
86
+ field.text = row.value
87
+
88
+ field.clearButtonMode = UITextFieldViewModeWhileEditing
89
+ field.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter
90
+ field.textAlignment = UITextAlignmentRight
91
+
92
+ case row.type
93
+ when RowType::EMAIL
94
+ field.keyboardType = UIKeyboardTypeEmailAddress
95
+ when RowType::PHONE
96
+ field.keyboardType = UIKeyboardTypePhonePad
97
+ when RowType::NUMBER
98
+ field.keyboardType = UIKeyboardTypeDecimalPad
99
+ else
100
+ field.keyboardType = UIKeyboardTypeDefault
101
+ end
102
+
103
+ field.secureTextEntry = true if row.secure?
104
+ field.returnKeyType = row.return_key || UIReturnKeyNext
105
+ field.autocapitalizationType = row.auto_capitalization if row.auto_capitalization
106
+ field.autocorrectionType = row.auto_correction if row.auto_correction
107
+ field.clearButtonMode = row.clear_button || UITextFieldViewModeWhileEditing
108
+
109
+ if row.on_enter_callback
110
+ field.should_return? do |text_field|
111
+ if row.on_enter_callback.arity == 0
112
+ row.on_enter_callback.call
113
+ elsif row.on_enter_callback.arity == 1
114
+ row.on_enter_callback.call(row)
115
+ end
116
+ false
117
+ end
118
+ elsif field.returnKeyType == UIReturnKeyDone
119
+ field.should_return? do |text_field|
120
+ text_field.resignFirstResponder
121
+ false
122
+ end
123
+ else
124
+ field.should_return? do |text_field|
125
+ if row.next_row && row.next_row.text_field
126
+ row.next_row.text_field.becomeFirstResponder
127
+ else
128
+ text_field.resignFirstResponder
129
+ end
130
+ true
131
+ end
132
+ end
133
+
134
+ field.on_begin do |text_field|
135
+ row.on_begin_callback && row.on_begin_callback.call
136
+ end
137
+
138
+ field.should_begin? do |text_field|
139
+ row.section.form.active_row = row
140
+ true
141
+ end
142
+
143
+ field.on_change do |text_field|
144
+ row.value = text_field.text
145
+ end
146
+
147
+ cell.class.send(:alias_method, :old_layoutSubviews, :layoutSubviews)
148
+ cell.instance_eval do
149
+ def layoutSubviews
150
+ old_layoutSubviews
151
+
152
+ # viewWithTag is terrible, but I think it's ok to use here...
153
+ formotion_field = self.viewWithTag(Formotion::RowCellBuilder::TEXT_FIELD_TAG)
154
+ formotion_field.sizeToFit
155
+
156
+ field_frame = formotion_field.frame
157
+ field_frame.origin.x = self.textLabel.frame.origin.x + self.textLabel.frame.size.width + 20
158
+ field_frame.origin.y = ((self.frame.size.height - field_frame.size.height) / 2.0).round
159
+ field_frame.size.width = self.frame.size.width - field_frame.origin.x - 20
160
+ formotion_field.frame = field_frame
161
+ end
162
+ end
163
+
164
+ cell.addSubview(field)
165
+ field
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,33 @@
1
+ module Formotion
2
+ class RowType
3
+ STRING=0
4
+ EMAIL=1
5
+ PHONE=2
6
+ NUMBER=3
7
+ SUBMIT=4
8
+ SWITCH=5
9
+ CHECK=6
10
+ STATIC=100
11
+
12
+ TYPES = [STRING, EMAIL, PHONE, NUMBER, SUBMIT, SWITCH, CHECK, STATIC]
13
+ TEXT_FIELD_TYPES=[STRING, EMAIL, PHONE, NUMBER]
14
+
15
+ class << self
16
+ def for(string_or_sym_or_int)
17
+ type = string_or_sym_or_int
18
+
19
+ if type.is_a?(Symbol) or type.is_a? String
20
+ string = type.to_s.upcase
21
+ if not const_defined? string
22
+ raise Formotion::InvalidClassError, "Invalid RowType value #{string_or_sym}"
23
+ end
24
+ Formotion::RowType.const_get(string)
25
+ elsif type.is_a? Integer and TYPES.member? type
26
+ TYPES[type]
27
+ else
28
+ raise Formotion::InvalidClassError, "Attempted row type #{type.inspect} is not a valid RowType."
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,108 @@
1
+ module Formotion
2
+ class Section < Formotion::Base
3
+ PROPERTIES = [
4
+ # Displayed in the section header row
5
+ :title,
6
+ # Arranges the section as a 'radio' section,
7
+ # such that only one row can be checked at a time.
8
+ :select_one,
9
+ # IF :select_one is true, then @form.render will contain
10
+ # the checked row's value as the value for this key.
11
+ # ELSE it does nothing.
12
+ :key
13
+ ]
14
+ PROPERTIES.each {|prop|
15
+ attr_accessor prop
16
+ }
17
+ SERIALIZE_PROPERTIES = PROPERTIES + [:rows]
18
+
19
+ # Relationships
20
+
21
+ # This section's form
22
+ attr_accessor :form
23
+
24
+ # This section's index in it's form.
25
+ attr_accessor :index
26
+
27
+ def initialize(params = {})
28
+ super
29
+
30
+ Formotion::Conditions.assert_nil_or_boolean(self.select_one)
31
+
32
+ rows = params[:rows] || params["rows"]
33
+ rows && rows.each {|row_hash|
34
+ row = create_row(row_hash)
35
+ }
36
+ end
37
+
38
+ def build_row(&block)
39
+ row = create_row
40
+ block.call(row)
41
+ row
42
+ end
43
+
44
+ def create_row(hash = {})
45
+ row = hash
46
+ if hash.class == Hash
47
+ row = Formotion::Row.new(hash)
48
+ end
49
+ row.section = self
50
+ row.index = self.rows.count
51
+ self.rows << row
52
+ row
53
+ end
54
+
55
+ #########################
56
+ # attribute overrides
57
+
58
+ def rows
59
+ @rows ||= []
60
+ end
61
+
62
+ def rows=(rows)
63
+ rows.each {|row|
64
+ Formotion::Conditions.assert_class(row, Formotion::Row)
65
+ }
66
+ @rows = rows
67
+ end
68
+
69
+ def index=(index)
70
+ @index = index.to_i
71
+ end
72
+
73
+ #########################
74
+ # pseudo-properties
75
+
76
+ # should be done with alias_method but there's currently a bug
77
+ # in RM which messes up attr_accessors with alias_method
78
+ def select_one?
79
+ self.select_one
80
+ end
81
+
82
+ def next_section
83
+ # if there are more sections in this form, use that.
84
+ if self.index < self.form.sections.count - 1
85
+ return self.form.sections[self.index + 1]
86
+ end
87
+
88
+ nil
89
+ end
90
+
91
+ def previous_section
92
+ # if there are more sections in this form, use that.
93
+ if self.index > 0
94
+ return self.form.sections[self.index - 1]
95
+ end
96
+
97
+ nil
98
+ end
99
+
100
+ #########################
101
+ # Retreiving data
102
+ def to_hash
103
+ h = super
104
+ h[:rows] = self.rows.collect {|row| row.to_hash}
105
+ h
106
+ end
107
+ end
108
+ end