formotion 0.0.3 → 0.5

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