nitron 0.2
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.
- data/.gitignore +2 -0
- data/README.md +83 -0
- data/lib/nitron.rb +20 -0
- data/lib/nitron/data/extensions/app_delegate+core_data.rb +43 -0
- data/lib/nitron/data/model.rb +100 -0
- data/lib/nitron/data/relation.rb +59 -0
- data/lib/nitron/static_table_view_controller.rb +38 -0
- data/lib/nitron/table_view_controller.rb +172 -0
- data/lib/nitron/ui/action_support.rb +49 -0
- data/lib/nitron/ui/data_binder.rb +49 -0
- data/lib/nitron/ui/data_binding_support.rb +33 -0
- data/lib/nitron/ui/data_bound_table_delegate.rb +41 -0
- data/lib/nitron/ui/extensions/app_delegate+storyboard.rb +19 -0
- data/lib/nitron/ui/extensions/ui_bar_button_item.rb +9 -0
- data/lib/nitron/ui/extensions/ui_view.rb +32 -0
- data/lib/nitron/ui/outlet_binder.rb +12 -0
- data/lib/nitron/ui/outlet_support.rb +20 -0
- data/lib/nitron/version.rb +3 -0
- data/lib/nitron/view_controller.rb +12 -0
- data/nitron.gemspec +18 -0
- data/spec/main_spec.rb +9 -0
- metadata +95 -0
data/.gitignore
ADDED
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
Nitron
|
2
|
+
===================
|
3
|
+
|
4
|
+
Introduction
|
5
|
+
----------
|
6
|
+
Nitron is an opinionated, loosely-coupled set of RubyMotion components designed to accelerate iOS
|
7
|
+
development, especially with simpler iOS apps. It provides meaningful
|
8
|
+
abstractions atop the strong foundation present in the iOS SDK.
|
9
|
+
|
10
|
+
This first release focuses on making Storyboard-based workflows enjoyable.
|
11
|
+
|
12
|
+
Installation
|
13
|
+
----------
|
14
|
+
Add the following line to your `Gemfile`:
|
15
|
+
|
16
|
+
`gem "nitron"`
|
17
|
+
|
18
|
+
If you haven't already, update your Rakefile to use Bundler. Insert the
|
19
|
+
following immediately before `Motion::Project::App.setup`:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
require 'rubygems'
|
23
|
+
require 'bundler'
|
24
|
+
|
25
|
+
Bundler.require
|
26
|
+
```
|
27
|
+
|
28
|
+
Example
|
29
|
+
------
|
30
|
+
A modal view controller responsible for creating new `Tasks`:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
class TaskCreateViewController < Nitron::ViewController
|
34
|
+
# The on class method is part of Nitron's Action DSL.
|
35
|
+
# It wires the provided block to be an event handler for the specified outlet using the iOS target/action pattern.
|
36
|
+
on :cancel do
|
37
|
+
close
|
38
|
+
end
|
39
|
+
|
40
|
+
# Nitron emulates 'native' outlet support, allowing you to easily define outlets through Xcode.
|
41
|
+
# The titleField and datePicker methods are created upon initial load by using metadata contained in the Storyboard.
|
42
|
+
on :save do
|
43
|
+
Task.create(title: titleField.text, due: datePicker.date)
|
44
|
+
|
45
|
+
close
|
46
|
+
end
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
Features
|
51
|
+
----------
|
52
|
+
|
53
|
+
* **Data Binding** - declaratively bind your model data to controls, either
|
54
|
+
via code or Interface Builder
|
55
|
+
* **Outlet Support** - expose controls to your controllers via Interface Builder
|
56
|
+
* **Action Support** - Ruby DSL to attach event handlers to outlets
|
57
|
+
* **CoreData Models** - beginnings of a CoreData model abstraction uses
|
58
|
+
XCode's data modeling tools with an ActiveRecord-like syntax
|
59
|
+
|
60
|
+
If you notice, many of these features aim at slimming down your
|
61
|
+
controllers. This is no accident: many iOS controllers have far too many
|
62
|
+
responsibilities. Glue code is a perfect target for metaprogramming, so
|
63
|
+
we're focusing on making beautiful controllers presently.
|
64
|
+
|
65
|
+
We're also careful to make these features modular, so you can mix them
|
66
|
+
into your existing controllers as needed.
|
67
|
+
|
68
|
+
Tutorial
|
69
|
+
----------
|
70
|
+
TBD
|
71
|
+
|
72
|
+
Examples
|
73
|
+
----------
|
74
|
+
https://github.com/mattgreen/nitron-examples
|
75
|
+
|
76
|
+
Caveats
|
77
|
+
---------
|
78
|
+
|
79
|
+
* Data binding doesn't use KVO presently. This is already in the works.
|
80
|
+
* Action support is limited to selecting a button or a table cell.
|
81
|
+
Future releases will expand the DSL to support additional events.
|
82
|
+
* CoreData needs support for relationships and migrations.
|
83
|
+
|
data/lib/nitron.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'nitron/version'
|
2
|
+
|
3
|
+
unless defined?(Motion::Project::Config)
|
4
|
+
raise "This file must be required within a RubyMotion project Rakefile."
|
5
|
+
end
|
6
|
+
|
7
|
+
Motion::Project::App.setup do |app|
|
8
|
+
Dir.glob(File.join(File.dirname(__FILE__), "nitron/**/*.rb")).each do |file|
|
9
|
+
app.files.unshift(file)
|
10
|
+
end
|
11
|
+
|
12
|
+
app.files.unshift(File.join(File.dirname(__FILE__), 'nitron/view_controller.rb'))
|
13
|
+
app.files.unshift(File.join(File.dirname(__FILE__), 'nitron/ui/data_binding_support.rb'))
|
14
|
+
app.files.unshift(File.join(File.dirname(__FILE__), 'nitron/ui/outlet_support.rb'))
|
15
|
+
app.files.unshift(File.join(File.dirname(__FILE__), 'nitron/ui/action_support.rb'))
|
16
|
+
|
17
|
+
unless app.frameworks.include?("CoreData")
|
18
|
+
app.frameworks << "CoreData"
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class AppDelegate
|
2
|
+
def managedObjectContext
|
3
|
+
@managedObjectContext ||= begin
|
4
|
+
applicationName = NSBundle.mainBundle.infoDictionary.objectForKey("CFBundleName")
|
5
|
+
|
6
|
+
documentsDirectory = NSFileManager.defaultManager.URLsForDirectory(NSDocumentDirectory, inDomains:NSUserDomainMask).lastObject;
|
7
|
+
storeURL = documentsDirectory.URLByAppendingPathComponent("#{applicationName}.sqlite")
|
8
|
+
|
9
|
+
error_ptr = Pointer.new(:object)
|
10
|
+
unless persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration:nil, URL:storeURL, options:nil, error:error_ptr)
|
11
|
+
raise "Can't add persistent SQLite store: #{error_ptr[0].description}"
|
12
|
+
end
|
13
|
+
|
14
|
+
context = NSManagedObjectContext.alloc.init
|
15
|
+
context.persistentStoreCoordinator = persistentStoreCoordinator
|
16
|
+
|
17
|
+
context
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def managedObjectModel
|
22
|
+
@managedObjectModel ||= begin
|
23
|
+
model = NSManagedObjectModel.mergedModelFromBundles([NSBundle.mainBundle]).mutableCopy
|
24
|
+
|
25
|
+
model.entities.each do |entity|
|
26
|
+
begin
|
27
|
+
Kernel.const_get(entity.name)
|
28
|
+
entity.setManagedObjectClassName(entity.name)
|
29
|
+
|
30
|
+
rescue NameError
|
31
|
+
entity.setManagedObjectClassName("Model")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
model
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def persistentStoreCoordinator
|
40
|
+
@coordinator ||= NSPersistentStoreCoordinator.alloc.initWithManagedObjectModel(managedObjectModel)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Nitron
|
2
|
+
class Model < NSManagedObject
|
3
|
+
class << self
|
4
|
+
def all
|
5
|
+
Data::Relation.alloc.initWithClass(self)
|
6
|
+
end
|
7
|
+
|
8
|
+
def create(attributes={})
|
9
|
+
model = new(attributes)
|
10
|
+
model.save
|
11
|
+
|
12
|
+
model
|
13
|
+
end
|
14
|
+
|
15
|
+
def destroy(object)
|
16
|
+
if context = object.managedObjectContext
|
17
|
+
context.deleteObject(object)
|
18
|
+
|
19
|
+
error = Pointer.new(:object)
|
20
|
+
context.save(error)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def entityDescription
|
25
|
+
@_metadata ||= UIApplication.sharedApplication.delegate.managedObjectModel.entitiesByName[name]
|
26
|
+
end
|
27
|
+
|
28
|
+
def find(object_id)
|
29
|
+
unless entity = find_by_id(object_id)
|
30
|
+
raise "No record found!"
|
31
|
+
end
|
32
|
+
|
33
|
+
entity
|
34
|
+
end
|
35
|
+
|
36
|
+
def first
|
37
|
+
relation.first
|
38
|
+
end
|
39
|
+
|
40
|
+
def method_missing(method, *args, &block)
|
41
|
+
if method.start_with?("find_by_")
|
42
|
+
attribute = method.gsub("find_by_", "")
|
43
|
+
relation.where("#{attribute} = ?", *args).first
|
44
|
+
else
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def new(attributes={})
|
50
|
+
self.alloc.initWithEntity(entityDescription, insertIntoManagedObjectContext:nil).tap do |model|
|
51
|
+
attributes.each do |keyPath, value|
|
52
|
+
model.setValue(value, forKey:keyPath)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def respond_to?(method)
|
58
|
+
if method.start_with?("find_by_")
|
59
|
+
true
|
60
|
+
else
|
61
|
+
super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def order(*args)
|
66
|
+
relation.order(*args)
|
67
|
+
end
|
68
|
+
|
69
|
+
def where(*args)
|
70
|
+
relation.where(*args)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def relation
|
76
|
+
Data::Relation.alloc.initWithClass(self)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def destroy
|
81
|
+
self.class.destroy(self)
|
82
|
+
end
|
83
|
+
|
84
|
+
def inspect
|
85
|
+
properties = entity.properties.map { |property| "#{property.name}: #{valueForKey(property.name).inspect}" }
|
86
|
+
|
87
|
+
"#<#{entity.name} #{properties.join(", ")}>"
|
88
|
+
end
|
89
|
+
|
90
|
+
def save
|
91
|
+
unless context = managedObjectContext
|
92
|
+
context = UIApplication.sharedApplication.delegate.managedObjectContext
|
93
|
+
context.insertObject(self)
|
94
|
+
end
|
95
|
+
|
96
|
+
error = Pointer.new(:object)
|
97
|
+
context.save(error)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Nitron
|
2
|
+
module Data
|
3
|
+
class Relation < NSFetchRequest
|
4
|
+
def initWithClass(entityClass)
|
5
|
+
if init
|
6
|
+
setEntity(entityClass.entityDescription)
|
7
|
+
end
|
8
|
+
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def all
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def first
|
17
|
+
setFetchLimit(1)
|
18
|
+
|
19
|
+
to_a[0]
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect
|
23
|
+
to_a
|
24
|
+
end
|
25
|
+
|
26
|
+
def order(column, opts={})
|
27
|
+
descriptors = sortDescriptors || []
|
28
|
+
|
29
|
+
descriptors << NSSortDescriptor.alloc.initWithKey(column.to_s, ascending:opts.fetch(:ascending, true))
|
30
|
+
setSortDescriptors(descriptors)
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_a
|
36
|
+
error = Pointer.new(:object)
|
37
|
+
context.executeFetchRequest(self, error:error)
|
38
|
+
end
|
39
|
+
|
40
|
+
def where(format, *args)
|
41
|
+
predicate = NSPredicate.predicateWithFormat(format.gsub("?", "%@"), argumentArray:args)
|
42
|
+
|
43
|
+
if self.predicate
|
44
|
+
self.predicate = NSCompoundPredicate.andPredicateWithSubpredicates([predicate])
|
45
|
+
else
|
46
|
+
self.predicate = predicate
|
47
|
+
end
|
48
|
+
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def context
|
55
|
+
UIApplication.sharedApplication.delegate.managedObjectContext
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Nitron
|
2
|
+
class StaticTableViewController < ViewController
|
3
|
+
def setValue(value, forKey: key)
|
4
|
+
if key == "staticDataSource"
|
5
|
+
@_dataSource = value
|
6
|
+
else
|
7
|
+
super
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def tableView(tableView, didSelectRowAtIndexPath:indexPath)
|
12
|
+
cell = tableView.cellForRowAtIndexPath(indexPath)
|
13
|
+
|
14
|
+
if outlet = cell.outlets.first
|
15
|
+
handler = self.class.outletHandlers[outlet[0]]
|
16
|
+
|
17
|
+
if handler
|
18
|
+
self.instance_eval(&handler[:handler])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def tableView(tableView, heightForRowAtIndexPath:indexPath)
|
24
|
+
cell = @_dataSource.tableView(tableView, cellForRowAtIndexPath:indexPath)
|
25
|
+
|
26
|
+
cell.bounds.size.height
|
27
|
+
end
|
28
|
+
|
29
|
+
def viewWillAppear(animated)
|
30
|
+
view.dataSource = @_dataSource
|
31
|
+
view.delegate = self
|
32
|
+
|
33
|
+
# The data binding module may wrap view.delegate, so run it after we've set up.
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module Nitron
|
2
|
+
class TableViewController < ViewController
|
3
|
+
def self.collection(&block)
|
4
|
+
options[:collection] = block
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.group_by(name, opts={})
|
8
|
+
options[:groupBy] = name.to_s
|
9
|
+
options[:groupIndex] = opts[:index] || false
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.options
|
13
|
+
@options ||= {
|
14
|
+
collection: lambda { [] },
|
15
|
+
groupBy: nil,
|
16
|
+
groupIndex: false,
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def controllerDidChangeContent(controller)
|
23
|
+
view.reloadData()
|
24
|
+
end
|
25
|
+
|
26
|
+
def dataSource
|
27
|
+
@_dataSource ||= begin
|
28
|
+
collection = self.instance_eval(&self.class.options[:collection])
|
29
|
+
|
30
|
+
case collection
|
31
|
+
when Array
|
32
|
+
ArrayDataSource.alloc.initWithCollection(collection, className:self.class.name)
|
33
|
+
when NSFetchRequest
|
34
|
+
CoreDataSource.alloc.initWithRequest(collection, owner:self, sectionNameKeyPath:self.class.options[:groupBy], options:self.class.options)
|
35
|
+
else
|
36
|
+
raise "Collection block must return an Array, or an NSFetchRequest"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def prepareForSegue(segue, sender:sender)
|
42
|
+
model = nil
|
43
|
+
|
44
|
+
if view.respond_to?(:indexPathForSelectedRow)
|
45
|
+
if view.indexPathForSelectedRow
|
46
|
+
model = dataSource.objectAtIndexPath(view.indexPathForSelectedRow)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
if model
|
51
|
+
controller = segue.destinationViewController
|
52
|
+
if controller.respond_to?(:model=)
|
53
|
+
controller.model = model
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def setValue(value, forKey: key)
|
59
|
+
if key == "staticDataSource"
|
60
|
+
raise "Static tables are not supported by TableViewController! Please use StaticTableViewController instead."
|
61
|
+
else
|
62
|
+
super
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def viewDidLoad
|
67
|
+
super
|
68
|
+
|
69
|
+
view.dataSource = dataSource
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
class ArrayDataSource
|
75
|
+
def initWithCollection(collection, className:className)
|
76
|
+
if init
|
77
|
+
@collection = collection
|
78
|
+
@className = className
|
79
|
+
end
|
80
|
+
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
def numberOfSectionsInTableView(tableView)
|
85
|
+
1
|
86
|
+
end
|
87
|
+
|
88
|
+
def objectAtIndexPath(indexPath)
|
89
|
+
@collection[indexPath.row]
|
90
|
+
end
|
91
|
+
|
92
|
+
def sectionForSectionIndexTitle(title, atIndex:index)
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
|
96
|
+
def tableView(tableView, cellForRowAtIndexPath:indexPath)
|
97
|
+
@cellReuseIdentifier ||= "#{@className.gsub("ViewController", "")}Cell"
|
98
|
+
unless cell = tableView.dequeueReusableCellWithIdentifier(@cellReuseIdentifier)
|
99
|
+
puts "Unable to find a cell named #{@cellReuseIdentifier}. Have you set the reuse identifier of the UITableViewCell?"
|
100
|
+
return
|
101
|
+
end
|
102
|
+
|
103
|
+
cell
|
104
|
+
end
|
105
|
+
|
106
|
+
def tableView(tableView, numberOfRowsInSection:section)
|
107
|
+
@collection.size
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class CoreDataSource
|
112
|
+
def initWithRequest(request, owner:owner, sectionNameKeyPath:sectionNameKeyPath, options:options)
|
113
|
+
if init
|
114
|
+
context = UIApplication.sharedApplication.delegate.managedObjectContext
|
115
|
+
|
116
|
+
@className = owner.class.name
|
117
|
+
@controller = NSFetchedResultsController.alloc.initWithFetchRequest(request,
|
118
|
+
managedObjectContext:context,
|
119
|
+
sectionNameKeyPath:sectionNameKeyPath,
|
120
|
+
cacheName:nil)
|
121
|
+
@controller.delegate = owner
|
122
|
+
@options = options
|
123
|
+
|
124
|
+
errorPtr = Pointer.new(:object)
|
125
|
+
unless @controller.performFetch(errorPtr)
|
126
|
+
raise "Error fetching data"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
self
|
131
|
+
end
|
132
|
+
|
133
|
+
def numberOfSectionsInTableView(tableView)
|
134
|
+
@controller.sections.size
|
135
|
+
end
|
136
|
+
|
137
|
+
def objectAtIndexPath(indexPath)
|
138
|
+
@controller.objectAtIndexPath(indexPath)
|
139
|
+
end
|
140
|
+
|
141
|
+
def sectionForSectionIndexTitle(title, atIndex:index)
|
142
|
+
@collection.sectionForSectionIndexTitle(title, atIndex:index)
|
143
|
+
end
|
144
|
+
|
145
|
+
def sectionIndexTitlesForTableView(tableView)
|
146
|
+
if @options[:groupIndex]
|
147
|
+
@controller.sectionIndexTitles
|
148
|
+
else
|
149
|
+
nil
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def tableView(tableView, cellForRowAtIndexPath:indexPath)
|
154
|
+
@cellReuseIdentifier ||= "#{@className.gsub("ViewController", "")}Cell"
|
155
|
+
unless cell = tableView.dequeueReusableCellWithIdentifier(@cellReuseIdentifier)
|
156
|
+
puts "Unable to find a cell named #{@cellReuseIdentifier}. Have you set the reuse identifier of the UITableViewCell?"
|
157
|
+
return nil
|
158
|
+
end
|
159
|
+
|
160
|
+
cell
|
161
|
+
end
|
162
|
+
|
163
|
+
def tableView(tableView, numberOfRowsInSection:section)
|
164
|
+
@controller.sections[section].numberOfObjects
|
165
|
+
end
|
166
|
+
|
167
|
+
def tableView(tableView, titleForHeaderInSection:section)
|
168
|
+
@controller.sections[section].name
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Nitron
|
2
|
+
module UI
|
3
|
+
module ActionSupport
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def on(outlet, &block)
|
10
|
+
actions[outlet.to_s] = { :handler => block }
|
11
|
+
end
|
12
|
+
|
13
|
+
def actions
|
14
|
+
@_actions ||= {
|
15
|
+
"cancel" => { :handler => proc { close }, :default => true },
|
16
|
+
"done" => { :handler => proc { close }, :default => true }
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def _dispatch(sender)
|
22
|
+
if action = @_actions[sender]
|
23
|
+
instance_eval &action[:handler]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def dealloc
|
28
|
+
@_actions.clear
|
29
|
+
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def viewDidLoad
|
34
|
+
super
|
35
|
+
|
36
|
+
@_actions = {}
|
37
|
+
|
38
|
+
self.class.actions.each do |outlet, action|
|
39
|
+
if respond_to?(outlet)
|
40
|
+
target = send(outlet)
|
41
|
+
@_actions[target] = self.class.actions[outlet]
|
42
|
+
|
43
|
+
target.addTarget(self, action:"_dispatch:", forControlEvents:UIControlEventTouchUpInside)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Nitron
|
2
|
+
module UI
|
3
|
+
class DataBinder
|
4
|
+
def self.shared
|
5
|
+
@singleton ||= alloc.init
|
6
|
+
end
|
7
|
+
|
8
|
+
def bind(model, view, options={})
|
9
|
+
if view.is_a?(UITableView)
|
10
|
+
view.delegate = DataBoundTableDelegate.alloc.initWithDelegate(view.delegate)
|
11
|
+
return [view.delegate]
|
12
|
+
end
|
13
|
+
|
14
|
+
view.dataBindings.each do |keyPath, subview|
|
15
|
+
bindControl(model, subview, keyPath)
|
16
|
+
end
|
17
|
+
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def bindControl(model, control, keyPath)
|
24
|
+
value = model.valueForKeyPath(keyPath)
|
25
|
+
|
26
|
+
if control.respond_to?(:text=)
|
27
|
+
control.text = value
|
28
|
+
elsif control.respond_to?(:image=)
|
29
|
+
control.image = value
|
30
|
+
elsif control.respond_to?(:value=)
|
31
|
+
control.value = value
|
32
|
+
elsif control.respond_to?(:on=)
|
33
|
+
control.on = value
|
34
|
+
elsif control.respond_to?(:progress=)
|
35
|
+
control.progress = value
|
36
|
+
elsif control.respond_to?(:date=)
|
37
|
+
control.date = value
|
38
|
+
else
|
39
|
+
puts "Sorry, data binding is not supported for an instance of '#{control.class.name}' :("
|
40
|
+
end
|
41
|
+
|
42
|
+
rescue
|
43
|
+
puts "***ERROR: Failed to bind value #{value.inspect} (read from '#{model.class.name}.#{keyPath}') to #{control.inspect}"
|
44
|
+
|
45
|
+
raise
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Nitron
|
2
|
+
module UI
|
3
|
+
module DataBindingSupport
|
4
|
+
def dealloc
|
5
|
+
if @_bindings
|
6
|
+
@_bindings = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
if @_model
|
10
|
+
@_model = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def model
|
17
|
+
@_model
|
18
|
+
end
|
19
|
+
|
20
|
+
def model=(model)
|
21
|
+
@_model = model
|
22
|
+
|
23
|
+
DataBinder.shared.bind(model, view)
|
24
|
+
end
|
25
|
+
|
26
|
+
def viewDidLoad
|
27
|
+
super
|
28
|
+
|
29
|
+
@_bindings = DataBinder.shared.bind(model, view)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Nitron
|
2
|
+
module UI
|
3
|
+
class DataBoundTableDelegate
|
4
|
+
def initWithDelegate(delegate)
|
5
|
+
if init
|
6
|
+
@delegate = delegate
|
7
|
+
end
|
8
|
+
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(method, *args, &block)
|
13
|
+
if @delegate
|
14
|
+
@delegate.send(method, *args, &block)
|
15
|
+
else
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def respond_to?(method)
|
21
|
+
if method == "tableView:willDisplayCell:forRowAtIndexPath"
|
22
|
+
true
|
23
|
+
elsif @delegate
|
24
|
+
@delegate.respond_to?(method)
|
25
|
+
else
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def tableView(tableView, willDisplayCell:cell, forRowAtIndexPath:indexPath)
|
31
|
+
if @delegate && @delegate.respond_to?("tableView:willDisplayCell:forRowAtIndexPath:")
|
32
|
+
@delegate.tableView(tableView, willDisplayCell:cell, forRowAtIndexPath:indexPath)
|
33
|
+
end
|
34
|
+
|
35
|
+
model = tableView.dataSource.objectAtIndexPath(indexPath)
|
36
|
+
|
37
|
+
Nitron::UI::DataBinder.shared.bind(model, cell)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class AppDelegate
|
2
|
+
def application(application, didFinishLaunchingWithOptions:launchOptions)
|
3
|
+
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
|
4
|
+
|
5
|
+
if storyboard
|
6
|
+
@window.rootViewController = storyboard.instantiateInitialViewController
|
7
|
+
end
|
8
|
+
|
9
|
+
@window.rootViewController.wantsFullScreenLayout = true
|
10
|
+
@window.makeKeyAndVisible
|
11
|
+
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
def storyboard
|
16
|
+
@storyboard ||= UIStoryboard.storyboardWithName("MainStoryboard", bundle:nil)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class UIView
|
2
|
+
def dataBindings
|
3
|
+
@_dataBindings ||= {}
|
4
|
+
end
|
5
|
+
|
6
|
+
def outlets
|
7
|
+
@_outlets ||= {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def setValue(value, forUndefinedKey:key)
|
11
|
+
if key == "dataBinding" || key == "outlet"
|
12
|
+
raise "Runtime attribute '#{key}' must be a String (declared on #{self.class.name})" unless value.is_a?(String)
|
13
|
+
|
14
|
+
container = self
|
15
|
+
while container.superview
|
16
|
+
container = container.superview
|
17
|
+
end
|
18
|
+
|
19
|
+
if key == "dataBinding"
|
20
|
+
unless value.start_with?("model.")
|
21
|
+
raise "Data binding expression must start with 'model.'; you provided '#{value}'"
|
22
|
+
end
|
23
|
+
|
24
|
+
container.dataBindings[value[6..-1]] = self
|
25
|
+
else
|
26
|
+
container.outlets[value] = self
|
27
|
+
end
|
28
|
+
else
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Nitron
|
2
|
+
module UI
|
3
|
+
module OutletSupport
|
4
|
+
def setValue(value, forUndefinedKey:key)
|
5
|
+
unless self.class.respond_to?(key)
|
6
|
+
self.class.send(:attr_reader, key)
|
7
|
+
end
|
8
|
+
|
9
|
+
instance_variable_set("@#{key}", value)
|
10
|
+
end
|
11
|
+
|
12
|
+
def viewDidLoad
|
13
|
+
super
|
14
|
+
|
15
|
+
outletBinder = OutletBinder.new
|
16
|
+
outletBinder.bind(self, view)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/nitron.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/nitron/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Matt Green"]
|
6
|
+
gem.email = ["mattgreenrocks@gmail.com"]
|
7
|
+
gem.description = "Turbocharged iOS development via RubyMotion"
|
8
|
+
gem.summary = "Turbocharged iOS development via RubyMotion"
|
9
|
+
gem.homepage = "https://github.com/mattgreen/nitron"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
13
|
+
gem.name = "nitron"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = Nitron::VERSION
|
16
|
+
|
17
|
+
gem.add_dependency 'motion-cocoapods', '>= 1.0.1'
|
18
|
+
end
|
data/spec/main_spec.rb
ADDED
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nitron
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 2
|
8
|
+
version: "0.2"
|
9
|
+
platform: ruby
|
10
|
+
authors:
|
11
|
+
- Matt Green
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
|
16
|
+
date: 2012-06-12 00:00:00 -04:00
|
17
|
+
default_executable:
|
18
|
+
dependencies:
|
19
|
+
- !ruby/object:Gem::Dependency
|
20
|
+
name: motion-cocoapods
|
21
|
+
prerelease: false
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
segments:
|
27
|
+
- 1
|
28
|
+
- 0
|
29
|
+
- 1
|
30
|
+
version: 1.0.1
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
description: Turbocharged iOS development via RubyMotion
|
34
|
+
email:
|
35
|
+
- mattgreenrocks@gmail.com
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files: []
|
41
|
+
|
42
|
+
files:
|
43
|
+
- .gitignore
|
44
|
+
- README.md
|
45
|
+
- lib/nitron.rb
|
46
|
+
- lib/nitron/data/extensions/app_delegate+core_data.rb
|
47
|
+
- lib/nitron/data/model.rb
|
48
|
+
- lib/nitron/data/relation.rb
|
49
|
+
- lib/nitron/static_table_view_controller.rb
|
50
|
+
- lib/nitron/table_view_controller.rb
|
51
|
+
- lib/nitron/ui/action_support.rb
|
52
|
+
- lib/nitron/ui/data_binder.rb
|
53
|
+
- lib/nitron/ui/data_binding_support.rb
|
54
|
+
- lib/nitron/ui/data_bound_table_delegate.rb
|
55
|
+
- lib/nitron/ui/extensions/app_delegate+storyboard.rb
|
56
|
+
- lib/nitron/ui/extensions/ui_bar_button_item.rb
|
57
|
+
- lib/nitron/ui/extensions/ui_view.rb
|
58
|
+
- lib/nitron/ui/outlet_binder.rb
|
59
|
+
- lib/nitron/ui/outlet_support.rb
|
60
|
+
- lib/nitron/version.rb
|
61
|
+
- lib/nitron/view_controller.rb
|
62
|
+
- nitron.gemspec
|
63
|
+
- spec/main_spec.rb
|
64
|
+
has_rdoc: true
|
65
|
+
homepage: https://github.com/mattgreen/nitron
|
66
|
+
licenses: []
|
67
|
+
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
version: "0"
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
version: "0"
|
87
|
+
requirements: []
|
88
|
+
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 1.3.6
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Turbocharged iOS development via RubyMotion
|
94
|
+
test_files:
|
95
|
+
- spec/main_spec.rb
|