formotion 0.0.3 → 0.5

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 (69) hide show
  1. data/.gitignore +2 -1
  2. data/CHANGELOG.md +10 -0
  3. data/Gemfile +6 -0
  4. data/LIST_OF_ROW_TYPES.md +163 -0
  5. data/NEW_ROW_TYPES.md +73 -0
  6. data/README.md +11 -14
  7. data/Rakefile +6 -0
  8. data/app/app_delegate.rb +22 -0
  9. data/examples/KitchenSink/Rakefile +1 -1
  10. data/examples/KitchenSink/app/app_delegate.rb +50 -2
  11. data/examples/Login/.gitignore +5 -0
  12. data/examples/Login/Rakefile +9 -0
  13. data/examples/Login/app/app_delegate.rb +16 -0
  14. data/examples/Login/app/login_controller.rb +47 -0
  15. data/examples/Login/app/user_controller.rb +38 -0
  16. data/examples/Login/spec/main_spec.rb +9 -0
  17. data/lib/formotion.rb +7 -1
  18. data/lib/formotion/{form.rb → form/form.rb} +0 -0
  19. data/lib/formotion/{form_delegate.rb → form/form_delegate.rb} +10 -15
  20. data/lib/formotion/patch/object.rb +9 -0
  21. data/lib/formotion/patch/ui_action_sheet.rb +27 -0
  22. data/lib/formotion/patch/ui_text_view.rb +122 -0
  23. data/lib/formotion/patch/ui_text_view_placeholder.rb +65 -0
  24. data/lib/formotion/{row.rb → row/row.rb} +37 -27
  25. data/lib/formotion/row/row_cell_builder.rb +28 -0
  26. data/lib/formotion/row_type/base.rb +41 -0
  27. data/lib/formotion/row_type/check_row.rb +30 -0
  28. data/lib/formotion/row_type/date_row.rb +61 -0
  29. data/lib/formotion/row_type/email_row.rb +11 -0
  30. data/lib/formotion/row_type/image_row.rb +99 -0
  31. data/lib/formotion/row_type/number_row.rb +11 -0
  32. data/lib/formotion/row_type/options_row.rb +24 -0
  33. data/lib/formotion/row_type/phone_row.rb +11 -0
  34. data/lib/formotion/row_type/row_type.rb +21 -0
  35. data/lib/formotion/row_type/slider_row.rb +43 -0
  36. data/lib/formotion/row_type/static_row.rb +6 -0
  37. data/lib/formotion/row_type/string_row.rb +112 -0
  38. data/lib/formotion/row_type/submit_row.rb +34 -0
  39. data/lib/formotion/row_type/switch_row.rb +19 -0
  40. data/lib/formotion/row_type/text_row.rb +76 -0
  41. data/lib/formotion/{section.rb → section/section.rb} +3 -0
  42. data/lib/formotion/version.rb +1 -1
  43. data/spec/form_spec.rb +5 -4
  44. data/spec/functional/character_spec.rb +70 -0
  45. data/spec/functional/check_row_spec.rb +42 -0
  46. data/spec/functional/date_row_spec.rb +63 -0
  47. data/spec/functional/image_row_spec.rb +82 -0
  48. data/spec/functional/options_row_spec.rb +27 -0
  49. data/spec/functional/slider_row_spec.rb +27 -0
  50. data/spec/functional/submit_row_spec.rb +30 -0
  51. data/spec/functional/switch_row_spec.rb +28 -0
  52. data/spec/functional/text_row_spec.rb +60 -0
  53. data/spec/row_type/check_spec.rb +27 -0
  54. data/spec/row_type/date_spec.rb +69 -0
  55. data/spec/row_type/email_spec.rb +22 -0
  56. data/spec/row_type/image_spec.rb +43 -0
  57. data/spec/row_type/number_spec.rb +22 -0
  58. data/spec/row_type/options_spec.rb +37 -0
  59. data/spec/row_type/phone_spec.rb +22 -0
  60. data/spec/row_type/slider_spec.rb +47 -0
  61. data/spec/row_type/static_spec.rb +15 -0
  62. data/spec/row_type/string_spec.rb +52 -0
  63. data/spec/row_type/submit_spec.rb +28 -0
  64. data/spec/row_type/switch_spec.rb +27 -0
  65. data/spec/row_type/text_spec.rb +41 -0
  66. data/spec/support/ui_control_wrap_extension.rb +7 -0
  67. metadata +88 -9
  68. data/lib/formotion/row_cell_builder.rb +0 -168
  69. data/lib/formotion/row_type.rb +0 -33
@@ -0,0 +1,28 @@
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
+
11
+ # PARAMS row.is_a? Formotion::Row
12
+ # RETURNS [cell configured to that row, a UITextField for that row if applicable or nil]
13
+ def self.make_cell(row)
14
+ cell, text_field = nil
15
+
16
+ cell = UITableViewCell.alloc.initWithStyle(row.object.cell_style, reuseIdentifier:row.reuse_identifier)
17
+
18
+ cell.accessoryType = UITableViewCellAccessoryNone
19
+ cell.textLabel.text = row.title
20
+ cell.detailTextLabel.text = row.subtitle
21
+
22
+ edit_field = row.object.build_cell(cell)
23
+
24
+ [cell, edit_field]
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ module Formotion
2
+ module RowType
3
+ class Base
4
+ attr_accessor :row, :tableView
5
+
6
+ def tableView
7
+ @tableView ||= self.row.form.table
8
+ end
9
+
10
+ def initialize(row)
11
+ @row = row
12
+ end
13
+
14
+ def submit_button?
15
+ false
16
+ end
17
+
18
+ # RowCellBuilder uses this to instantiate the UITableViewCell.
19
+ def cell_style
20
+ UITableViewCellStyleSubtitle
21
+ end
22
+
23
+ # builder method for row cell specific implementation
24
+ def build_cell(cell)
25
+ # implement in row class
26
+ nil
27
+ end
28
+
29
+ # called by the Row after all the setup and connections are made
30
+ # in #make_cell
31
+ def after_build(cell)
32
+ end
33
+
34
+ # method gets triggered when tableView(tableView, didSelectRowAtIndexPath:indexPath)
35
+ # in UITableViewDelegate is executed
36
+ def on_select(tableView, tableViewDelegate)
37
+ # implement in row class
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,30 @@
1
+ module Formotion
2
+ module RowType
3
+ class CheckRow < Base
4
+
5
+ # This is actually called whenever again cell is checked/unchecked
6
+ # in the UITableViewDelegate callbacks. So (for now) don't
7
+ # instantiate long-lived objects in them.
8
+ # Maybe that logic should be moved elsewhere?
9
+ def build_cell(cell)
10
+ cell.accessoryType = row.value ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone
11
+ nil
12
+ end
13
+
14
+ def on_select(tableView, tableViewDelegate)
15
+ if row.section.select_one and !row.value
16
+ row.section.rows.each do |other_row|
17
+ other_row.value = (other_row == row)
18
+
19
+ cell = tableView.cellForRowAtIndexPath(other_row.index_path)
20
+ other_row.object.build_cell(cell) if cell
21
+ end
22
+ elsif !row.section.select_one
23
+ row.value = !row.value
24
+ build_cell(tableView.cellForRowAtIndexPath(row.index_path))
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,61 @@
1
+ module Formotion
2
+ module RowType
3
+ class DateRow < StringRow
4
+ # overwrite Character on_change method
5
+ def on_change(text_field)
6
+ end
7
+
8
+ def update
9
+ self.row.text_field && self.row.text_field.text = self.formatted_value
10
+ end
11
+
12
+ def date_value
13
+ value = self.row.value
14
+ if value.is_a? Numeric
15
+ NSDate.dateWithTimeIntervalSince1970(value.to_i)
16
+ else
17
+ nil
18
+ end
19
+ end
20
+
21
+ def formatter
22
+ @formatter ||= begin
23
+ formatter = NSDateFormatter.new
24
+
25
+ date_style = self.row.format
26
+ if date_style && date_style.to_s[-5..-1] != "style"
27
+ date_style = (date_style.to_s + "_style").to_sym
28
+ end
29
+ formatter.dateStyle = self.row.send(:const_int_get, "NSDateFormatter", date_style || NSDateFormatterShortStyle)
30
+ formatter
31
+ end
32
+ end
33
+
34
+ def formatted_value
35
+ return formatter.stringFromDate(self.date_value) if self.date_value
36
+ self.row.value
37
+ end
38
+
39
+ def after_build(cell)
40
+ self.row.text_field.inputView = self.picker
41
+ update
42
+ end
43
+
44
+ def picker
45
+ @picker ||= begin
46
+ picker = UIDatePicker.alloc.initWithFrame(CGRectZero)
47
+ picker.datePickerMode = UIDatePickerModeDate
48
+ picker.hidden = false
49
+ picker.date = self.date_value || NSDate.date
50
+
51
+ picker.when(UIControlEventValueChanged) do
52
+ self.row.value = @picker.date.timeIntervalSince1970.to_i
53
+ update
54
+ end
55
+
56
+ picker
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,11 @@
1
+ module Formotion
2
+ module RowType
3
+ class EmailRow < StringRow
4
+
5
+ def keyboardType
6
+ UIKeyboardTypeEmailAddress
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,99 @@
1
+ module Formotion
2
+ module RowType
3
+ class ImageRow < Base
4
+ include BW::KVO
5
+
6
+ IMAGE_VIEW_TAG=1100
7
+
8
+ def build_cell(cell)
9
+ add_plus_accessory(cell)
10
+
11
+ observe(self.row, "value") do |old_value, new_value|
12
+ @image_view.image = new_value
13
+ if new_value
14
+ self.row.rowHeight = 200
15
+ cell.accessoryView = nil
16
+ else
17
+ self.row.rowHeight = 44
18
+ add_plus_accessory(cell)
19
+ end
20
+ row.form.reload_data
21
+ end
22
+
23
+ @image_view = UIImageView.alloc.init
24
+ @image_view.image = row.value if row.value
25
+ @image_view.tag = IMAGE_VIEW_TAG
26
+ @image_view.contentMode = UIViewContentModeScaleAspectFit
27
+ @image_view.backgroundColor = UIColor.clearColor
28
+ cell.addSubview(@image_view)
29
+
30
+ cell.swizzle(:layoutSubviews) do
31
+ def layoutSubviews
32
+ old_layoutSubviews
33
+
34
+ # viewWithTag is terrible, but I think it's ok to use here...
35
+ formotion_field = self.viewWithTag(IMAGE_VIEW_TAG)
36
+
37
+ field_frame = formotion_field.frame
38
+ field_frame.origin.y = 10
39
+ field_frame.origin.x = self.textLabel.frame.origin.x + self.textLabel.frame.size.width + 20
40
+ field_frame.size.width = self.frame.size.width - field_frame.origin.x - 20
41
+ field_frame.size.height = self.frame.size.height - 20
42
+ formotion_field.frame = field_frame
43
+ end
44
+ end
45
+ end
46
+
47
+ def on_select(tableView, tableViewDelegate)
48
+ @action_sheet = UIActionSheet.alloc.init
49
+ @action_sheet.delegate = self
50
+
51
+ @action_sheet.destructiveButtonIndex = (@action_sheet.addButtonWithTitle "Delete") if row.value
52
+ @action_sheet.addButtonWithTitle "Take" if BW::Device.camera.front? or BW::Device.camera.rear?
53
+ @action_sheet.addButtonWithTitle "Choose"
54
+ @action_sheet.cancelButtonIndex = (@action_sheet.addButtonWithTitle "Cancel")
55
+
56
+ @action_sheet.showInView @image_view
57
+ end
58
+
59
+ def actionSheet actionSheet, clickedButtonAtIndex: index
60
+ source = nil
61
+
62
+ if index == actionSheet.destructiveButtonIndex
63
+ row.value = nil
64
+ return
65
+ end
66
+
67
+ case actionSheet.buttonTitleAtIndex(index)
68
+ when "Take"
69
+ source = :camera
70
+ when "Choose"
71
+ source = :photo_library
72
+ when "Cancel"
73
+ else
74
+ p "Unrecognized button title #{actionSheet.buttonTitleAtIndex(index)}"
75
+ end
76
+
77
+ if source
78
+ @camera = BW::Device.camera.any
79
+ @camera.picture(source_type: source, media_types: [:image]) do |result|
80
+ if result[:original_image]
81
+ row.value = result[:original_image]
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ def add_plus_accessory(cell)
88
+ @add_button ||= begin
89
+ button = UIButton.buttonWithType(UIButtonTypeContactAdd)
90
+ button.when(UIControlEventTouchUpInside) do
91
+ self.on_select(nil, nil)
92
+ end
93
+ button
94
+ end
95
+ cell.accessoryView = @add_button
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,11 @@
1
+ module Formotion
2
+ module RowType
3
+ class NumberRow < StringRow
4
+
5
+ def keyboardType
6
+ UIKeyboardTypeDecimalPad
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ module Formotion
2
+ module RowType
3
+ class OptionsRow < Base
4
+
5
+ SLIDER_VIEW_TAG = 1200
6
+
7
+ def build_cell(cell)
8
+ cell.selectionStyle = UITableViewCellSelectionStyleNone
9
+ slideView = UISegmentedControl.alloc.initWithItems(row.items || [])
10
+ slideView.selectedSegmentIndex = row.items.index(row.value) if row.value
11
+ slideView.segmentedControlStyle = UISegmentedControlStyleBar
12
+ cell.accessoryView = slideView
13
+
14
+ slideView.when(UIControlEventValueChanged) do
15
+ row.value = row.items[slideView.selectedSegmentIndex]
16
+ end
17
+
18
+ nil
19
+ end
20
+
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ module Formotion
2
+ module RowType
3
+ class PhoneRow < StringRow
4
+
5
+ def keyboardType
6
+ UIKeyboardTypePhonePad
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ module Formotion
2
+ module RowType
3
+ ROW_TYPES = Formotion::RowType.constants(false).select { |constant_name| constant_name =~ /Row$/ }
4
+
5
+ class << self
6
+ def for(string_or_sym)
7
+ type = string_or_sym
8
+
9
+ if type.is_a?(Symbol) or type.is_a? String
10
+ string = "#{type.to_s.downcase}_row".camelize
11
+ if not const_defined? string
12
+ raise Formotion::InvalidClassError, "Invalid RowType value #{string_or_sym}. Create a class called #{string}"
13
+ end
14
+ Formotion::RowType.const_get(string)
15
+ else
16
+ raise Formotion::InvalidClassError, "Attempted row type #{type.inspect} is not a valid RowType."
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,43 @@
1
+ module Formotion
2
+ module RowType
3
+ class SliderRow < Base
4
+
5
+ SLIDER_VIEW_TAG = 1200
6
+
7
+ def build_cell(cell)
8
+ cell.selectionStyle = UITableViewCellSelectionStyleNone
9
+ slideView = UISlider.alloc.initWithFrame(CGRectZero)
10
+ cell.accessoryView = slideView
11
+ row.range ||= (1..10)
12
+ slideView.minimumValue = row.range.first
13
+ slideView.maximumValue = row.range.last
14
+ slideView.tag = SLIDER_VIEW_TAG
15
+ slideView.setValue(row.value, animated:true) if row.value
16
+ slideView.accessibilityLabel = row.title + " Slider"
17
+
18
+ slideView.when(UIControlEventValueChanged) do
19
+ row.value = slideView.value
20
+ end
21
+
22
+ cell.swizzle(:layoutSubviews) do
23
+ def layoutSubviews
24
+ old_layoutSubviews
25
+
26
+ # viewWithTag is terrible, but I think it's ok to use here...
27
+ formotion_field = self.viewWithTag(SLIDER_VIEW_TAG)
28
+ formotion_field.sizeToFit
29
+
30
+ field_frame = formotion_field.frame
31
+ field_frame.origin.y = 10
32
+ field_frame.origin.x = self.textLabel.frame.origin.x + self.textLabel.frame.size.width + 20
33
+ field_frame.size.width = self.frame.size.width - field_frame.origin.x - 20
34
+ field_frame.size.height = self.frame.size.height - 20
35
+ formotion_field.frame = field_frame
36
+ end
37
+ end
38
+ nil
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,6 @@
1
+ module Formotion
2
+ module RowType
3
+ class StaticRow < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,112 @@
1
+ module Formotion
2
+ module RowType
3
+ class StringRow < Base
4
+
5
+ # The new UITextField in a UITableViewCell
6
+ # will be assigned this tag, if applicable.
7
+ TEXT_FIELD_TAG=1000
8
+
9
+ def keyboardType
10
+ UIKeyboardTypeDefault
11
+ end
12
+
13
+ # Configures the cell to have a new UITextField
14
+ # which is used to enter data. Consists of
15
+ # 1) setting up that field with the appropriate properties
16
+ # specified by `row` 2) configures the callbacks on the field
17
+ # to call any callbacks `row` listens for.
18
+ # Also does the layoutSubviews swizzle trick
19
+ # to size the UITextField so it won't bump into the titleLabel.
20
+ def build_cell(cell)
21
+ field = UITextField.alloc.initWithFrame(CGRectZero)
22
+ field.tag = TEXT_FIELD_TAG
23
+
24
+ field.placeholder = row.placeholder
25
+ field.text = row.value.to_s
26
+
27
+ field.clearButtonMode = UITextFieldViewModeWhileEditing
28
+ field.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter
29
+ field.textAlignment = UITextAlignmentRight
30
+
31
+ field.keyboardType = keyboardType
32
+
33
+ field.secureTextEntry = true if row.secure?
34
+ field.returnKeyType = row.return_key || UIReturnKeyNext
35
+ field.autocapitalizationType = row.auto_capitalization if row.auto_capitalization
36
+ field.autocorrectionType = row.auto_correction if row.auto_correction
37
+ field.clearButtonMode = row.clear_button || UITextFieldViewModeWhileEditing
38
+
39
+ add_callbacks(field)
40
+
41
+ cell.swizzle(:layoutSubviews) do
42
+ def layoutSubviews
43
+ old_layoutSubviews
44
+
45
+ # viewWithTag is terrible, but I think it's ok to use here...
46
+ formotion_field = self.viewWithTag(TEXT_FIELD_TAG)
47
+ formotion_field.sizeToFit
48
+
49
+ field_frame = formotion_field.frame
50
+ field_frame.origin.x = self.textLabel.frame.origin.x + self.textLabel.frame.size.width + 20
51
+ field_frame.origin.y = ((self.frame.size.height - field_frame.size.height) / 2.0).round
52
+ field_frame.size.width = self.frame.size.width - field_frame.origin.x - 20
53
+ formotion_field.frame = field_frame
54
+ end
55
+ end
56
+
57
+ cell.addSubview(field)
58
+ field
59
+
60
+ end
61
+
62
+ def add_callbacks(field)
63
+ if row.on_enter_callback
64
+ field.should_return? do |text_field|
65
+ if row.on_enter_callback.arity == 0
66
+ row.on_enter_callback.call
67
+ elsif row.on_enter_callback.arity == 1
68
+ row.on_enter_callback.call(row)
69
+ end
70
+ false
71
+ end
72
+ elsif field.returnKeyType == UIReturnKeyDone
73
+ field.should_return? do |text_field|
74
+ text_field.resignFirstResponder
75
+ false
76
+ end
77
+ else
78
+ field.should_return? do |text_field|
79
+ if row.next_row && row.next_row.text_field
80
+ row.next_row.text_field.becomeFirstResponder
81
+ else
82
+ text_field.resignFirstResponder
83
+ end
84
+ true
85
+ end
86
+ end
87
+
88
+ field.on_begin do |text_field|
89
+ row.on_begin_callback && row.on_begin_callback.call
90
+ end
91
+
92
+ field.should_begin? do |text_field|
93
+ row.section.form.active_row = row
94
+ true
95
+ end
96
+
97
+ field.on_change do |text_field|
98
+ on_change(text_field)
99
+ end
100
+ end
101
+
102
+ def on_change(text_field)
103
+ row.value = text_field.text
104
+ end
105
+
106
+ def on_select(tableView, tableViewDelegate)
107
+ row.text_field.becomeFirstResponder
108
+ end
109
+
110
+ end
111
+ end
112
+ end