formotion 0.0.1

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