diff --git a/.gitignore b/.gitignore index fdf1ba9..b79da83 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ resources/*.storyboardc *.gem .bundle Gemfile.lock -pkg/* \ No newline at end of file +pkg/* +.rvmrc \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..8f33266 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in motion-settings.gemspec +gemspec \ No newline at end of file diff --git a/app/app_delegate.rb b/app/app_delegate.rb index 19f4c79..459e8c2 100644 --- a/app/app_delegate.rb +++ b/app/app_delegate.rb @@ -29,6 +29,12 @@ def application(application, didFinishLaunchingWithOptions:launchOptions) title: "Switch", key: :switch, type: :switch, + }, { + title: "Bio", + key: :bio, + type: :text, + placeholder: "Enter your Bio here...", + rowHeight: 100 }] }, { title: "Account Type", diff --git a/examples/KitchenSink/app/app_delegate.rb b/examples/KitchenSink/app/app_delegate.rb index d6de6ae..31867ff 100644 --- a/examples/KitchenSink/app/app_delegate.rb +++ b/examples/KitchenSink/app/app_delegate.rb @@ -43,6 +43,23 @@ def application(application, didFinishLaunchingWithOptions:launchOptions) placeholder: "required", type: :string, secure: true + }, { + title: "Row Height", + key: :row_height, + placeholder: "60px", + type: :string, + rowHeight: 60 + }, { + title: "Text", + key: :text, + type: :text, + placeholder: "Enter your text here", + rowHeight: 100 + }, { + title: "Check", + key: :check, + type: :check, + value: true }, { title: "Remember?", key: :remember, diff --git a/lib/formotion.rb b/lib/formotion.rb index bb724ae..c872341 100644 --- a/lib/formotion.rb +++ b/lib/formotion.rb @@ -2,7 +2,7 @@ require 'bubble-wrap/core' BW.require File.expand_path('../formotion/**/*.rb', __FILE__) do - ['form.rb', 'row.rb', 'section.rb'].each {|file| + ['form/form.rb', 'row/row.rb', 'section/section.rb'].each {|file| file("lib/formotion/#{file}").depends_on 'lib/formotion/base.rb' } file("lib/formotion/form_controller.rb").depends_on 'lib/formotion/patch/ui_text_field.rb' diff --git a/lib/formotion/form.rb b/lib/formotion/form/form.rb similarity index 100% rename from lib/formotion/form.rb rename to lib/formotion/form/form.rb diff --git a/lib/formotion/form_delegate.rb b/lib/formotion/form/form_delegate.rb similarity index 74% rename from lib/formotion/form_delegate.rb rename to lib/formotion/form/form_delegate.rb index a156791..55384c1 100644 --- a/lib/formotion/form_delegate.rb +++ b/lib/formotion/form/form_delegate.rb @@ -63,25 +63,16 @@ def tableView(tableView, cellForRowAtIndexPath:indexPath) cell end + def tableView(tableView, heightForRowAtIndexPath: indexPath) + row = row_for_index_path(indexPath) + row.rowHeight || tableView.rowHeight + end + # UITableViewDelegate Methods def tableView(tableView, didSelectRowAtIndexPath:indexPath) tableView.deselectRowAtIndexPath(indexPath, animated:true) row = row_for_index_path(indexPath) - if row.submit_button? - self.submit - elsif row.checkable? - if row.section.select_one and !row.value - row.section.rows.each {|other_row| - other_row.value = (other_row == row) - Formotion::RowCellBuilder.make_check_cell(other_row, tableView.cellForRowAtIndexPath(other_row.index_path)) - } - elsif !row.section.select_one - row.value = !row.value - Formotion::RowCellBuilder.make_check_cell(row, tableView.cellForRowAtIndexPath(row.index_path)) - end - elsif row.editable? - row.text_field.becomeFirstResponder - end + row.object.on_select(tableView, self) end end end \ No newline at end of file diff --git a/lib/formotion/patch/object.rb b/lib/formotion/patch/object.rb new file mode 100644 index 0000000..fae2918 --- /dev/null +++ b/lib/formotion/patch/object.rb @@ -0,0 +1,9 @@ +class Object + # Creates an alias for :method with the form + # old_#{method} + # Instance evals the block. + def swizzle(method, &block) + self.class.send(:alias_method, "old_#{method.to_s}".to_sym, method) + self.instance_eval &block + end +end \ No newline at end of file diff --git a/lib/formotion/patch/ui_text_view.rb b/lib/formotion/patch/ui_text_view.rb new file mode 100644 index 0000000..dd34498 --- /dev/null +++ b/lib/formotion/patch/ui_text_view.rb @@ -0,0 +1,122 @@ +# Methods which use blocks for UITextViewDelegate methods. +# EX +# field.should_end? do |text_field| +# if text_field.text != "secret" +# return false +# end +# true +# end +# +# Also includes an on_change method, which calls after the text +# has changed (there is no UITextViewDelegate equivalent.) +# EX +# field.on_change do |text_field| +# p text_field.text +# end + +class UITextView + attr_accessor :menu_options_enabled + + def canPerformAction(action, withSender:sender) + self.menu_options_enabled + end + + # block takes argument textView; should return true/false + def should_begin?(&block) + add_delegate_method do + @delegate.textViewShouldBeginEditing_callback = block + end + end + + # block takes argument textView + def on_begin(&block) + add_delegate_method do + @delegate.textViewDidBeginEditing_callback = block + end + end + + # block takes argument textView; should return true/false + def should_end?(&block) + add_delegate_method do + @delegate.textViewShouldEndEditing_callback = block + end + end + + # block takes argument textView + def on_end(&block) + add_delegate_method do + @delegate.textViewDidBeginEditing_callback = block + end + end + + # block takes argument textView + def on_change(&block) + add_delegate_method do + @delegate.textViewDidChange_callback = block + end + end + + # block takes argument textView, range [NSRange], and string; should return true/false + def should_change?(&block) + add_delegate_method do + @delegate.shouldChangeCharactersInRange_callback = block + end + end + + private + def add_delegate_method + # create strong reference to the delegate + # (.delegate= only creates a weak reference) + @delegate ||= UITextView_Delegate.new + yield + self.delegate = @delegate + end +end + +class UITextView_Delegate + [:textViewShouldBeginEditing, :textViewDidBeginEditing, + :textViewShouldEndEditing, :textViewDidEndEditing, + :textViewDidChange, :shouldChangeCharactersInRange].each {|method| + attr_accessor (method.to_s + "_callback").to_sym + } + + def textViewShouldBeginEditing(theTextView) + if self.textViewShouldBeginEditing_callback + return self.textViewShouldBeginEditing_callback.call(theTextView) + end + true + end + + def textViewDidBeginEditing(theTextView) + if self.textViewDidBeginEditing_callback + return self.textViewDidBeginEditing_callback.call(theTextView) + end + end + + def textViewShouldEndEditing(theTextView) + if self.textViewShouldEndEditing_callback + return self.textViewShouldEndEditing_callback.call(theTextView) + end + true + end + + def textView(textView, shouldChangeTextInRange:range, replacementText:text) + if self.shouldChangeCharactersInRange_callback + return self.shouldChangeCharactersInRange_callback.call(textView, range, text) + end + true + end + + def textViewDidEndEditing(theTextView) + if self.textViewDidEndEditing_callback + return self.textViewDidEndEditing_callback.call(theTextView) + end + end + + def textViewDidChange(theTextView) + if self.textViewDidChange_callback + self.textViewDidChange_callback.call(theTextView) + end + end + +end \ No newline at end of file diff --git a/lib/formotion/patch/ui_text_view_placeholder.rb b/lib/formotion/patch/ui_text_view_placeholder.rb new file mode 100644 index 0000000..43f0b76 --- /dev/null +++ b/lib/formotion/patch/ui_text_view_placeholder.rb @@ -0,0 +1,65 @@ +class UITextView + attr_reader :placeholder + attr_accessor :placeholder_color + + alias_method :old_initWithCoder, :initWithCoder + def initWithCoder(decoder) + old_initWithCoder(decoder) + setup + self + end + + alias_method :old_initWithFrame, :initWithFrame + def initWithFrame(frame) + old_initWithFrame(frame) + setup + self + end + + def setup + @foreground_observer = NSNotificationCenter.defaultCenter.observe UITextViewTextDidChangeNotification do |notification| + updateShouldDrawPlaceholder + end + end + + alias_method :old_drawRect, :drawRect + def drawRect(rect) + old_drawRect(rect) + + if (@shouldDrawPlaceholder) + self.placeholder_color.set + self.placeholder.drawInRect(placeholder_rect, withFont:self.font) + end + end + + + alias_method :old_setText, :setText + def setText(text) + old_setText(text) + + updateShouldDrawPlaceholder + end + + def placeholder=(placeholder) + return if @placeholder == placeholder + + @placeholder = placeholder + + updateShouldDrawPlaceholder + end + + def placeholder_rect + CGRectMake(self.contentInset.left + 10.0, self.contentInset.top, self.frame.size.width - self.contentInset.left - self.contentInset.right - 16.0, self.frame.size.height - self.contentInset.top - self.contentInset.bottom - 16.0) + end + + def placeholder_color + @placeholder_color ||= UIColor.lightGrayColor + end + + def updateShouldDrawPlaceholder + prev = @shouldDrawPlaceholder; + @shouldDrawPlaceholder = self.placeholder && self.text.length == 0 + + self.setNeedsDisplay if (prev != @shouldDrawPlaceholder) + end +end \ No newline at end of file diff --git a/lib/formotion/row.rb b/lib/formotion/row/row.rb similarity index 90% rename from lib/formotion/row.rb rename to lib/formotion/row/row.rb index fa431ae..5bb6b11 100644 --- a/lib/formotion/row.rb +++ b/lib/formotion/row/row.rb @@ -6,7 +6,7 @@ class Row < Formotion::Base # the user's (or configured) value for this row. :value, # set as cell.titleLabel.text - :title, + :title, # set as cell.detailLabel.text :subtitle, # configures the type of input this is (string, phone, switch, etc) @@ -19,7 +19,7 @@ class Row < Formotion::Base # placeholder text :placeholder, # whether or not the entry field is secure (like a password) - :secure, + :secure, # given by a UIReturnKey___ integer, string, or symbol # EX :default, :google :return_key, @@ -32,7 +32,12 @@ class Row < Formotion::Base # field.clearButtonMode; given by a UITextFieldViewMode__ integer, string, symbol # EX :never, :while_editing # DEFAULT is nil, which is used as :while_editing - :clear_button] + :clear_button, + # row height as integer; used for heightForRowAtIndexPath + # EX 200 + # DEFAULT is nil, which is used as the tableView.rowHeight + :rowHeight, + ] PROPERTIES.each {|prop| attr_accessor prop } @@ -62,6 +67,9 @@ class Row < Formotion::Base # starts editing #text_field. attr_accessor :on_begin_callback + # row type object + attr_accessor :object + def initialize(params = {}) super @@ -74,7 +82,7 @@ def initialize(params = {}) # these should be done with alias_method but there's currently a bug # in RM which messes up attr_accessors with alias_method # EX - # row.editable? + # row.secure? # => true # row.checkable? # => nil @@ -103,7 +111,7 @@ def reuse_identifier def next_row # if there are more rows in this section, use that. - return self.section.rows[self.index + 1] if self.index < (self.section.rows.count - 1) + return self.section.rows[self.index + 1] if self.index < (self.section.rows.count - 1) # if there are more sections, then use the first row of that section. return self.section.next_section.rows[0] if self.section.next_section @@ -112,7 +120,7 @@ def next_row end def previous_row - return self.section.rows[self.index - 1] if self.index > 0 + return self.section.rows[self.index - 1] if self.index > 0 # if there are more sections, then use the first row of that section. return self.section.previous_section.rows[-1] if self.section.previous_section @@ -120,41 +128,30 @@ def previous_row nil end - def editable? - Formotion::RowType::TEXT_FIELD_TYPES.member? self.type - end - def submit_button? - self.type == Formotion::RowType::SUBMIT - end - - def switchable? - self.type == Formotion::RowType::SWITCH - end - - def checkable? - self.type == Formotion::RowType::CHECK + object.submit_button? end ######################### # setter overrides def type=(type) - @type = Formotion::RowType.for(type) + @object = Formotion::RowType.for(type).new(self) + @type = type end def return_key=(value) @return_key = const_int_get("UIReturnKey", value) end - def auto_correction=(value) + def auto_correction=(value) @auto_correction = const_int_get("UITextAutocorrectionType", value) end - def auto_capitalization=(value) + def auto_capitalization=(value) @auto_capitalization = const_int_get("UITextAutocapitalizationType", value) end - def clear_button=(value) + def clear_button=(value) @clear_button = const_int_get("UITextFieldViewMode", value) end @@ -197,12 +194,12 @@ def const_int_get(base, value) # directly in your code, they don't get added # to Kernel and const_int_get crashes. def load_constants_hack - [UITextAutocapitalizationTypeNone, UITextAutocapitalizationTypeWords, + [UITextAutocapitalizationTypeNone, UITextAutocapitalizationTypeWords, UITextAutocapitalizationTypeSentences,UITextAutocapitalizationTypeAllCharacters, UITextAutocorrectionTypeNo, UITextAutocorrectionTypeYes, UITextAutocorrectionTypeDefault, - UIReturnKeyDefault, UIReturnKeyGo, UIReturnKeyGoogle, UIReturnKeyJoin, + UIReturnKeyDefault, UIReturnKeyGo, UIReturnKeyGoogle, UIReturnKeyJoin, UIReturnKeyNext, UIReturnKeyRoute, UIReturnKeySearch, UIReturnKeySend, - UIReturnKeyYahoo, UIReturnKeyDone, UIReturnKeyEmergencyCall, + UIReturnKeyYahoo, UIReturnKeyDone, UIReturnKeyEmergencyCall, UITextFieldViewModeNever, UITextFieldViewModeAlways, UITextFieldViewModeWhileEditing, UITextFieldViewModeUnlessEditing ] diff --git a/lib/formotion/row/row_cell_builder.rb b/lib/formotion/row/row_cell_builder.rb new file mode 100644 index 0000000..debd2b0 --- /dev/null +++ b/lib/formotion/row/row_cell_builder.rb @@ -0,0 +1,28 @@ +################# +# +# Formotion::RowCellBuilder +# RowCellBuilder handles taking Formotion::Rows +# and configuring UITableViewCells based on their properties. +# +################# +module Formotion + class RowCellBuilder + + # PARAMS row.is_a? Formotion::Row + # RETURNS [cell configured to that row, a UITextField for that row if applicable or nil] + def self.make_cell(row) + cell, text_field = nil + + cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier:row.reuse_identifier) + + cell.accessoryType = UITableViewCellAccessoryNone + cell.textLabel.text = row.title + cell.detailTextLabel.text = row.subtitle + + edit_field = row.object.build_cell(cell) + + [cell, edit_field] + end + + end +end \ No newline at end of file diff --git a/lib/formotion/row_cell_builder.rb b/lib/formotion/row_cell_builder.rb deleted file mode 100644 index cf33158..0000000 --- a/lib/formotion/row_cell_builder.rb +++ /dev/null @@ -1,168 +0,0 @@ -################# -# -# Formotion::RowCellBuilder -# RowCellBuilder handles taking Formotion::Rows -# and configuring UITableViewCells based on their properties. -# -################# -module Formotion - class RowCellBuilder - # The new UITextField in a UITableViewCell - # will be assigned this tag, if applicable. - TEXT_FIELD_TAG=1000 - - # PARAMS row.is_a? Formotion::Row - # RETURNS [cell configured to that row, a UITextField for that row if applicable or nil] - def self.make_cell(row) - cell, text_field = nil - - cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier:row.reuse_identifier) - - cell.accessoryType = UITableViewCellAccessoryNone - cell.textLabel.text = row.title - cell.detailTextLabel.text = row.subtitle - - if row.submit_button? - make_submit_cell(row, cell) - elsif row.switchable? - make_switch_cell(row, cell) - elsif row.checkable? - make_check_cell(row, cell) - elsif row.editable? - text_field = make_text_field(row, cell) - end - [cell, text_field] - end - - # This is actually called whenever again cell is checked/unchecked - # in the UITableViewDelegate callbacks. So (for now) don't - # instantiate long-lived objects in them. - # Maybe that logic should be moved elsewhere? - def self.make_check_cell(row, cell) - cell.accessoryType = row.value ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone - end - - def self.make_switch_cell(row, cell) - cell.selectionStyle = UITableViewCellSelectionStyleNone - switchView = UISwitch.alloc.initWithFrame(CGRectZero) - cell.accessoryView = switchView - switchView.setOn(row.value || false, animated:false) - switchView.when(UIControlEventValueChanged) do - row.value = switchView.isOn - end - end - - # Does a clever little trick to override #layoutSubviews - # for just this one UITableViewCell object, in order to - # center it's labels horizontally. - def self.make_submit_cell(row, cell) - cell.class.send(:alias_method, :old_layoutSubviews, :layoutSubviews) - cell.instance_eval do - def layoutSubviews - old_layoutSubviews - - center = lambda {|frame, dimen| - ((self.frame.size.send(dimen) - frame.size.send(dimen)) / 2.0) - } - - self.textLabel.center = CGPointMake(self.frame.size.width / 2 - 10, self.textLabel.center.y) - self.detailTextLabel.center = CGPointMake(self.frame.size.width / 2 - 10, self.detailTextLabel.center.y) - end - end - end - - # Configures the cell to have a new UITextField - # which is used to enter data. Consists of - # 1) setting up that field with the appropriate properties - # specified by `row` 2) configures the callbacks on the field - # to call any callbacks `row` listens for. - # Also does the layoutSubviews swizzle trick - # to size the UITextField so it won't bump into the titleLabel. - def self.make_text_field(row, cell) - field = UITextField.alloc.initWithFrame(CGRectZero) - field.tag = TEXT_FIELD_TAG - - field.placeholder = row.placeholder - field.text = row.value - - field.clearButtonMode = UITextFieldViewModeWhileEditing - field.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter - field.textAlignment = UITextAlignmentRight - - case row.type - when RowType::EMAIL - field.keyboardType = UIKeyboardTypeEmailAddress - when RowType::PHONE - field.keyboardType = UIKeyboardTypePhonePad - when RowType::NUMBER - field.keyboardType = UIKeyboardTypeDecimalPad - else - field.keyboardType = UIKeyboardTypeDefault - end - - field.secureTextEntry = true if row.secure? - field.returnKeyType = row.return_key || UIReturnKeyNext - field.autocapitalizationType = row.auto_capitalization if row.auto_capitalization - field.autocorrectionType = row.auto_correction if row.auto_correction - field.clearButtonMode = row.clear_button || UITextFieldViewModeWhileEditing - - if row.on_enter_callback - field.should_return? do |text_field| - if row.on_enter_callback.arity == 0 - row.on_enter_callback.call - elsif row.on_enter_callback.arity == 1 - row.on_enter_callback.call(row) - end - false - end - elsif field.returnKeyType == UIReturnKeyDone - field.should_return? do |text_field| - text_field.resignFirstResponder - false - end - else - field.should_return? do |text_field| - if row.next_row && row.next_row.text_field - row.next_row.text_field.becomeFirstResponder - else - text_field.resignFirstResponder - end - true - end - end - - field.on_begin do |text_field| - row.on_begin_callback && row.on_begin_callback.call - end - - field.should_begin? do |text_field| - row.section.form.active_row = row - true - end - - field.on_change do |text_field| - row.value = text_field.text - end - - cell.class.send(:alias_method, :old_layoutSubviews, :layoutSubviews) - cell.instance_eval do - def layoutSubviews - old_layoutSubviews - - # viewWithTag is terrible, but I think it's ok to use here... - formotion_field = self.viewWithTag(Formotion::RowCellBuilder::TEXT_FIELD_TAG) - formotion_field.sizeToFit - - field_frame = formotion_field.frame - field_frame.origin.x = self.textLabel.frame.origin.x + self.textLabel.frame.size.width + 20 - field_frame.origin.y = ((self.frame.size.height - field_frame.size.height) / 2.0).round - field_frame.size.width = self.frame.size.width - field_frame.origin.x - 20 - formotion_field.frame = field_frame - end - end - - cell.addSubview(field) - field - end - end -end \ No newline at end of file diff --git a/lib/formotion/row_type.rb b/lib/formotion/row_type.rb deleted file mode 100644 index fa81696..0000000 --- a/lib/formotion/row_type.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Formotion - class RowType - STRING=0 - EMAIL=1 - PHONE=2 - NUMBER=3 - SUBMIT=4 - SWITCH=5 - CHECK=6 - STATIC=100 - - TYPES = [STRING, EMAIL, PHONE, NUMBER, SUBMIT, SWITCH, CHECK, STATIC] - TEXT_FIELD_TYPES=[STRING, EMAIL, PHONE, NUMBER] - - class << self - def for(string_or_sym_or_int) - type = string_or_sym_or_int - - if type.is_a?(Symbol) or type.is_a? String - string = type.to_s.upcase - if not const_defined? string - raise Formotion::InvalidClassError, "Invalid RowType value #{string_or_sym}" - end - Formotion::RowType.const_get(string) - elsif type.is_a? Integer and TYPES.member? type - TYPES[type] - else - raise Formotion::InvalidClassError, "Attempted row type #{type.inspect} is not a valid RowType." - end - end - end - end -end \ No newline at end of file diff --git a/lib/formotion/row_type/base.rb b/lib/formotion/row_type/base.rb new file mode 100644 index 0000000..594fb8a --- /dev/null +++ b/lib/formotion/row_type/base.rb @@ -0,0 +1,28 @@ +module Formotion + module RowType + class Base + attr_accessor :row, :tableView + + def initialize(row) + @row = row + end + + def submit_button? + false + end + + # builder method for row cell specific implementation + def build_cell(cell) + # implement in row class + nil + end + + # method gets triggered when tableView(tableView, didSelectRowAtIndexPath:indexPath) + # in UITableViewDelegate is executed + def on_select(tableView, tableViewDelegate) + # implement in row class + end + + end + end +end \ No newline at end of file diff --git a/lib/formotion/row_type/character.rb b/lib/formotion/row_type/character.rb new file mode 100644 index 0000000..87c27cc --- /dev/null +++ b/lib/formotion/row_type/character.rb @@ -0,0 +1,104 @@ +module Formotion + module RowType + class Character < Base + + # The new UITextField in a UITableViewCell + # will be assigned this tag, if applicable. + TEXT_FIELD_TAG=1000 + + def keyboardType + UIKeyboardTypeDefault + end + + # Configures the cell to have a new UITextField + # which is used to enter data. Consists of + # 1) setting up that field with the appropriate properties + # specified by `row` 2) configures the callbacks on the field + # to call any callbacks `row` listens for. + # Also does the layoutSubviews swizzle trick + # to size the UITextField so it won't bump into the titleLabel. + def build_cell(cell) + field = UITextField.alloc.initWithFrame(CGRectZero) + field.tag = TEXT_FIELD_TAG + + field.placeholder = row.placeholder + field.text = row.value + + field.clearButtonMode = UITextFieldViewModeWhileEditing + field.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter + field.textAlignment = UITextAlignmentRight + + field.keyboardType = keyboardType + + field.secureTextEntry = true if row.secure? + field.returnKeyType = row.return_key || UIReturnKeyNext + field.autocapitalizationType = row.auto_capitalization if row.auto_capitalization + field.autocorrectionType = row.auto_correction if row.auto_correction + field.clearButtonMode = row.clear_button || UITextFieldViewModeWhileEditing + + if row.on_enter_callback + field.should_return? do |text_field| + if row.on_enter_callback.arity == 0 + row.on_enter_callback.call + elsif row.on_enter_callback.arity == 1 + row.on_enter_callback.call(row) + end + false + end + elsif field.returnKeyType == UIReturnKeyDone + field.should_return? do |text_field| + text_field.resignFirstResponder + false + end + else + field.should_return? do |text_field| + if row.next_row && row.next_row.text_field + row.next_row.text_field.becomeFirstResponder + else + text_field.resignFirstResponder + end + true + end + end + + field.on_begin do |text_field| + row.on_begin_callback && row.on_begin_callback.call + end + + field.should_begin? do |text_field| + row.section.form.active_row = row + true + end + + field.on_change do |text_field| + row.value = text_field.text + end + + cell.swizzle(:layoutSubviews) do + def layoutSubviews + old_layoutSubviews + + # viewWithTag is terrible, but I think it's ok to use here... + formotion_field = self.viewWithTag(TEXT_FIELD_TAG) + formotion_field.sizeToFit + + field_frame = formotion_field.frame + field_frame.origin.x = self.textLabel.frame.origin.x + self.textLabel.frame.size.width + 20 + field_frame.origin.y = ((self.frame.size.height - field_frame.size.height) / 2.0).round + field_frame.size.width = self.frame.size.width - field_frame.origin.x - 20 + formotion_field.frame = field_frame + end + end + + cell.addSubview(field) + field + + end + + def on_select(tableView, tableViewDelegate) + row.text_field.becomeFirstResponder + end + + end + end +end \ No newline at end of file diff --git a/lib/formotion/row_type/check_row.rb b/lib/formotion/row_type/check_row.rb new file mode 100644 index 0000000..d21ffee --- /dev/null +++ b/lib/formotion/row_type/check_row.rb @@ -0,0 +1,30 @@ +module Formotion + module RowType + class CheckRow < Base + + # This is actually called whenever again cell is checked/unchecked + # in the UITableViewDelegate callbacks. So (for now) don't + # instantiate long-lived objects in them. + # Maybe that logic should be moved elsewhere? + def build_cell(cell) + cell.accessoryType = row.value ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone + nil + end + + def on_select(tableView, tableViewDelegate) + if row.section.select_one and !row.value + row.section.rows.each do |other_row| + other_row.value = (other_row == row) + + cell = tableView.cellForRowAtIndexPath(other_row.index_path) + other_row.object.build_cell(cell) if cell + end + elsif !row.section.select_one + row.value = !row.value + build_cell(tableView.cellForRowAtIndexPath(row.index_path)) + end + end + + end + end +end \ No newline at end of file diff --git a/lib/formotion/row_type/email_row.rb b/lib/formotion/row_type/email_row.rb new file mode 100644 index 0000000..7d72f9d --- /dev/null +++ b/lib/formotion/row_type/email_row.rb @@ -0,0 +1,11 @@ +module Formotion + module RowType + class EmailRow < Character + + def keyboardType + UIKeyboardTypeEmailAddress + end + + end + end +end \ No newline at end of file diff --git a/lib/formotion/row_type/number_row.rb b/lib/formotion/row_type/number_row.rb new file mode 100644 index 0000000..4ac6206 --- /dev/null +++ b/lib/formotion/row_type/number_row.rb @@ -0,0 +1,11 @@ +module Formotion + module RowType + class NumberRow < Character + + def keyboardType + UIKeyboardTypeDecimalPad + end + + end + end +end \ No newline at end of file diff --git a/lib/formotion/row_type/phone_row.rb b/lib/formotion/row_type/phone_row.rb new file mode 100644 index 0000000..1785f65 --- /dev/null +++ b/lib/formotion/row_type/phone_row.rb @@ -0,0 +1,11 @@ +module Formotion + module RowType + class PhoneRow < Character + + def keyboardType + UIKeyboardTypePhonePad + end + + end + end +end \ No newline at end of file diff --git a/lib/formotion/row_type/row_type.rb b/lib/formotion/row_type/row_type.rb new file mode 100644 index 0000000..b40cb0d --- /dev/null +++ b/lib/formotion/row_type/row_type.rb @@ -0,0 +1,21 @@ +module Formotion + module RowType + ROW_TYPES = Formotion::RowType.constants(false).select { |constant_name| constant_name =~ /Row$/ } + + class << self + def for(string_or_sym) + type = string_or_sym + + if type.is_a?(Symbol) or type.is_a? String + string = "#{type.to_s.downcase}_row".camelize + if not const_defined? string + raise Formotion::InvalidClassError, "Invalid RowType value #{string_or_sym}. Create a class called #{string}" + end + Formotion::RowType.const_get(string) + else + raise Formotion::InvalidClassError, "Attempted row type #{type.inspect} is not a valid RowType." + end + end + end + end +end \ No newline at end of file diff --git a/lib/formotion/row_type/static_row.rb b/lib/formotion/row_type/static_row.rb new file mode 100644 index 0000000..5a01856 --- /dev/null +++ b/lib/formotion/row_type/static_row.rb @@ -0,0 +1,6 @@ +module Formotion + module RowType + class StaticRow < Base + end + end +end \ No newline at end of file diff --git a/lib/formotion/row_type/string_row.rb b/lib/formotion/row_type/string_row.rb new file mode 100644 index 0000000..0dced7f --- /dev/null +++ b/lib/formotion/row_type/string_row.rb @@ -0,0 +1,6 @@ +module Formotion + module RowType + class StringRow < Character + end + end +end \ No newline at end of file diff --git a/lib/formotion/row_type/submit_row.rb b/lib/formotion/row_type/submit_row.rb new file mode 100644 index 0000000..be9c587 --- /dev/null +++ b/lib/formotion/row_type/submit_row.rb @@ -0,0 +1,34 @@ +module Formotion + module RowType + class SubmitRow < Base + + def submit_button? + true + end + + # Does a clever little trick to override #layoutSubviews + # for just this one UITableViewCell object, in order to + # center it's labels horizontally. + def build_cell(cell) + cell.swizzle(:layoutSubviews) do + def layoutSubviews + old_layoutSubviews + + center = lambda {|frame, dimen| + ((self.frame.size.send(dimen) - frame.size.send(dimen)) / 2.0) + } + + self.textLabel.center = CGPointMake(self.frame.size.width / 2 - 10, self.textLabel.center.y) + self.detailTextLabel.center = CGPointMake(self.frame.size.width / 2 - 10, self.detailTextLabel.center.y) + end + end + nil + end + + def on_select(tableView, tableViewDelegate) + tableViewDelegate.submit + end + + end + end +end \ No newline at end of file diff --git a/lib/formotion/row_type/switch_row.rb b/lib/formotion/row_type/switch_row.rb new file mode 100644 index 0000000..d11222d --- /dev/null +++ b/lib/formotion/row_type/switch_row.rb @@ -0,0 +1,22 @@ +module Formotion + module RowType + class SwitchRow < Base + + def switchable + true + end + + def build_cell(cell) + cell.selectionStyle = UITableViewCellSelectionStyleNone + switchView = UISwitch.alloc.initWithFrame(CGRectZero) + cell.accessoryView = switchView + switchView.setOn(row.value || false, animated:false) + switchView.when(UIControlEventValueChanged) do + row.value = switchView.isOn + end + nil + end + + end + end +end \ No newline at end of file diff --git a/lib/formotion/row_type/text_row.rb b/lib/formotion/row_type/text_row.rb new file mode 100644 index 0000000..8a7f847 --- /dev/null +++ b/lib/formotion/row_type/text_row.rb @@ -0,0 +1,70 @@ +module Formotion + module RowType + class TextRow < Base + + TEXT_VIEW_TAG=1000 + + attr_accessor :field + + def build_cell(cell) + + @field = UITextView.alloc.initWithFrame(CGRectZero) + field.backgroundColor = UIColor.clearColor + field.editable = true + field.tag = TEXT_VIEW_TAG + + field.text = row.value + + field.returnKeyType = row.return_key || UIReturnKeyDefault + field.autocapitalizationType = row.auto_capitalization if row.auto_capitalization + field.autocorrectionType = row.auto_correction if row.auto_correction + field.placeholder = row.placeholder + + field.on_begin do |text_field| + row.on_begin_callback && row.on_begin_callback.call + end + + field.should_begin? do |text_field| + row.section.form.active_row = row + true + end + + field.on_change do |text_field| + row.value = text_field.text + end + + tap = UITapGestureRecognizer.alloc.initWithTarget self, action:'dismissKeyboard' + cell.addGestureRecognizer tap + + cell.swizzle(:layoutSubviews) do + def layoutSubviews + old_layoutSubviews + + # viewWithTag is terrible, but I think it's ok to use here... + formotion_field = self.viewWithTag(TEXT_VIEW_TAG) + formotion_field.sizeToFit + + field_frame = formotion_field.frame + field_frame.origin.y = 10 + field_frame.origin.x = self.textLabel.frame.origin.x + self.textLabel.frame.size.width + 20 + field_frame.size.width = self.frame.size.width - field_frame.origin.x - 20 + field_frame.size.height = self.frame.size.height - 20 + formotion_field.frame = field_frame + end + end + + cell.addSubview(field) + field + end + + def on_select(tableView, tableViewDelegate) + row.text_field.becomeFirstResponder + end + + def dismissKeyboard + field.resignFirstResponder + end + + end + end +end \ No newline at end of file diff --git a/lib/formotion/section.rb b/lib/formotion/section/section.rb similarity index 100% rename from lib/formotion/section.rb rename to lib/formotion/section/section.rb diff --git a/spec/form_spec.rb b/spec/form_spec.rb index cf76882..b2f2a97 100644 --- a/spec/form_spec.rb +++ b/spec/form_spec.rb @@ -56,8 +56,9 @@ it "render works correctly" do @form = Formotion::Form.new(sections: [{ rows: [{ - key: :email, - editable: true, + key: :email, + type: :email, + editable: true, title: 'Email' }]}]) @@ -74,13 +75,13 @@ rows: [{ title: "Email", placeholder: "me@mail.com", - type: Formotion::RowType::EMAIL, + type: :email, auto_correction: UITextAutocorrectionTypeNo, auto_capitalization: UITextAutocapitalizationTypeNone }, { title: "Password", placeholder: "required", - type: Formotion::RowType::STRING, + type: :string, secure: true }, { title: "Remember me?",