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