routable 0.0.1
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 +9 -0
- data/Gemfile +4 -0
- data/README.md +87 -0
- data/Rakefile +1 -0
- data/Routable.gemspec +16 -0
- data/app/app_delegate.rb +15 -0
- data/lib/routable/ns_object.rb +23 -0
- data/lib/routable/router.rb +160 -0
- data/lib/routable/version.rb +3 -0
- data/lib/routable.rb +11 -0
- data/spec/main_spec.rb +71 -0
- data/spec/ns_object_spec.rb +21 -0
- metadata +67 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# Routable
|
2
|
+
A UIViewController->URL router.
|
3
|
+
|
4
|
+
```ruby
|
5
|
+
@router.map("profile/:id", ProfileController)
|
6
|
+
|
7
|
+
# Later on...
|
8
|
+
|
9
|
+
# Pushes a ProfileController with .initWithParams(:id => 189)
|
10
|
+
@router.open("profile/189")
|
11
|
+
```
|
12
|
+
|
13
|
+
Why is this awesome? Because now you can push any view controller from any part of the app with just a string: buttons, push notifications, anything.
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem install routable
|
19
|
+
```
|
20
|
+
|
21
|
+
And now in your Rakefile, require `routable`:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
$:.unshift("/Library/RubyMotion/lib")
|
25
|
+
require 'motion/project'
|
26
|
+
require 'routable'
|
27
|
+
|
28
|
+
Motion::Project::App.setup do |app|
|
29
|
+
...
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
## Setup
|
34
|
+
|
35
|
+
For every UIViewController you want routable with `:symbolic` params, you need to define `.initWithParams({})`.
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
class ProfileController < UIViewController
|
39
|
+
attr_accessor :user_id
|
40
|
+
|
41
|
+
def initWithParams(params = {})
|
42
|
+
init()
|
43
|
+
self.user_id = params[:user_id]
|
44
|
+
self
|
45
|
+
end
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
Here's an example of how you could setup Routable for the entire application:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
class AppDelegate
|
53
|
+
def application(application, didFinishLaunchingWithOptions:launchOptions)
|
54
|
+
|
55
|
+
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
|
56
|
+
@window.makeKeyAndVisible
|
57
|
+
|
58
|
+
# Make our URLs
|
59
|
+
map_urls
|
60
|
+
|
61
|
+
# .open(url, animated)
|
62
|
+
if User.logged_in_user
|
63
|
+
@router.open("menu", false)
|
64
|
+
else
|
65
|
+
@router.open("login", false)
|
66
|
+
end
|
67
|
+
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
def map_urls
|
72
|
+
@router = Routable::Router.router
|
73
|
+
@router.navigation_controller = UINavigationController.alloc.init
|
74
|
+
|
75
|
+
# :modal means we push it modally.
|
76
|
+
@router.map("login", LoginController, modal: true)
|
77
|
+
# :shared means it will only keep one instance of this VC in the hierarchy;
|
78
|
+
# if we push it again later, it will pop any covering VCs.
|
79
|
+
@router.map("menu", MenuController, shared: true)
|
80
|
+
@router.map("profile/:id", ProfileController)
|
81
|
+
@router.map("messages", MessagesController)
|
82
|
+
@router.map("message/:id", MessageThreadController)
|
83
|
+
|
84
|
+
@window.rootViewController = @router.navigation_controller
|
85
|
+
end
|
86
|
+
end
|
87
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/Routable.gemspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/routable/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "routable"
|
6
|
+
s.version = Routable::VERSION
|
7
|
+
s.authors = ["Clay Allsopp"]
|
8
|
+
s.email = ["clay.allsopp@gmail.com"]
|
9
|
+
s.homepage = "https://github.com/clayallsopp/Routable"
|
10
|
+
s.summary = "A RubyMotion UIViewController -> URL router"
|
11
|
+
s.description = "A RubyMotion UIViewController -> URL router"
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split($\)
|
14
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
15
|
+
s.require_paths = ["lib"]
|
16
|
+
end
|
data/app/app_delegate.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
class AppDelegate
|
2
|
+
attr_reader :navigation_controller
|
3
|
+
|
4
|
+
def application(application, didFinishLaunchingWithOptions:launchOptions)
|
5
|
+
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
|
6
|
+
@window.rootViewController = self.navigation_controller
|
7
|
+
@window.rootViewController.wantsFullScreenLayout = true
|
8
|
+
@window.makeKeyAndVisible
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
def navigation_controller
|
13
|
+
@navigation_controller ||= UINavigationController.alloc.init
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class NSObject
|
2
|
+
def add_block_method(sym, &block)
|
3
|
+
block_methods[sym] = block
|
4
|
+
nil
|
5
|
+
end
|
6
|
+
|
7
|
+
def method_missing(sym, *args, &block)
|
8
|
+
if block_methods.keys.member? sym
|
9
|
+
return block_methods[sym].call(*args, &block)
|
10
|
+
end
|
11
|
+
raise NoMethodError.new("undefined method `#{sym}' for " + "#{self.inspect}:#{self.class.name}")
|
12
|
+
end
|
13
|
+
|
14
|
+
def methods
|
15
|
+
methods = super
|
16
|
+
methods + block_methods.keys
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def block_methods
|
21
|
+
@block_methods ||= {}
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module Routable
|
2
|
+
class Router
|
3
|
+
# Singleton, for practical use (you might not want)
|
4
|
+
# to have more than one router.
|
5
|
+
class << self
|
6
|
+
def router
|
7
|
+
@router ||= Router.new
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# The root UINavigationController we use to push/pop view controllers
|
12
|
+
attr_accessor :navigation_controller
|
13
|
+
|
14
|
+
# Hash of URL => UIViewController classes
|
15
|
+
# EX
|
16
|
+
# {"users/:id" => UsersController,
|
17
|
+
# "users/:id/posts" => PostsController,
|
18
|
+
# "users/:user_id/posts/:id" => PostController }
|
19
|
+
def routes
|
20
|
+
@routes ||= {}
|
21
|
+
end
|
22
|
+
|
23
|
+
# Map a URL to a UIViewController
|
24
|
+
# EX
|
25
|
+
# router.map "/users/:id", UsersController
|
26
|
+
# OPTIONS
|
27
|
+
# :modal => true/false
|
28
|
+
# - We present the VC modally (router is not shared between the new nav VC)
|
29
|
+
# :shared => true/false
|
30
|
+
# - If URL is called again, we pop to that VC if it's in memory.
|
31
|
+
|
32
|
+
def map(url, klass, options = {})
|
33
|
+
format = url
|
34
|
+
self.routes[format] = options.merge!(klass: klass)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Push the UIViewController for the given url
|
38
|
+
# EX
|
39
|
+
# router.open("users/3")
|
40
|
+
# => router.navigation_controller pushes a UsersController
|
41
|
+
def open(url, animated = true)
|
42
|
+
controller_options = options_for_url(url)
|
43
|
+
controller = controller_for_url(url)
|
44
|
+
if self.navigation_controller.modalViewController
|
45
|
+
self.navigation_controller.dismissModalViewControllerAnimated(animated)
|
46
|
+
end
|
47
|
+
if controller_options[:modal]
|
48
|
+
if controller.class == UINavigationController
|
49
|
+
self.navigation_controller.presentModalViewController(controller, animated: animated)
|
50
|
+
else
|
51
|
+
tempNavigationController = UINavigationController.alloc.init
|
52
|
+
tempNavigationController.pushViewController(controller, animated: false)
|
53
|
+
self.navigation_controller.presentModalViewController(tempNavigationController, animated: animated)
|
54
|
+
end
|
55
|
+
else
|
56
|
+
if self.navigation_controller.viewControllers.member? controller
|
57
|
+
self.navigation_controller.popToViewController(controller, animated:animated)
|
58
|
+
else
|
59
|
+
self.navigation_controller.pushViewController(controller, animated:animated)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def options_for_url(url)
|
65
|
+
# map of url => options
|
66
|
+
|
67
|
+
@url_options_cache ||= {}
|
68
|
+
if @url_options_cache[url]
|
69
|
+
return @url_options_cache[url]
|
70
|
+
end
|
71
|
+
|
72
|
+
parts = url.split("/")
|
73
|
+
|
74
|
+
open_options = nil
|
75
|
+
open_params = {}
|
76
|
+
|
77
|
+
self.routes.each { |format, options|
|
78
|
+
|
79
|
+
# If the # of path components isn't the same, then
|
80
|
+
# it for sure isn't a match.
|
81
|
+
format_parts = format.split("/")
|
82
|
+
if format_parts.count != parts.count
|
83
|
+
next
|
84
|
+
end
|
85
|
+
|
86
|
+
matched = true
|
87
|
+
format_params = {}
|
88
|
+
# go through each of the path compoenents and
|
89
|
+
# check if they match up (symbols aside)
|
90
|
+
format_parts.each_with_index {|format_part, index|
|
91
|
+
check_part = parts[index]
|
92
|
+
|
93
|
+
# if we're looking at a symbol (ie :user_id),
|
94
|
+
# then note it and move on.
|
95
|
+
if format_part[0] == ":"
|
96
|
+
format_params[format_part[1..-1].to_sym] = check_part
|
97
|
+
next
|
98
|
+
end
|
99
|
+
|
100
|
+
# if we're looking at normal strings,
|
101
|
+
# check equality.
|
102
|
+
if format_part != check_part
|
103
|
+
matched = false
|
104
|
+
break
|
105
|
+
end
|
106
|
+
}
|
107
|
+
|
108
|
+
if !matched
|
109
|
+
next
|
110
|
+
end
|
111
|
+
|
112
|
+
open_options = options
|
113
|
+
open_params = format_params
|
114
|
+
}
|
115
|
+
|
116
|
+
if open_options == nil
|
117
|
+
raise "No route found for url #{url}"
|
118
|
+
end
|
119
|
+
|
120
|
+
@url_options_cache[url] = open_options.merge(open_params: open_params)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns a UIViewController for the given url
|
124
|
+
# EX
|
125
|
+
# router.controller_for_url("users/3")
|
126
|
+
# => #<UsersController @id='3'>
|
127
|
+
def controller_for_url(url)
|
128
|
+
return shared_vc_cache[url] if shared_vc_cache[url]
|
129
|
+
|
130
|
+
open_options = options_for_url(url)
|
131
|
+
open_params = open_options[:open_params]
|
132
|
+
open_klass = open_options[:klass]
|
133
|
+
controller = open_klass.alloc
|
134
|
+
if controller.respond_to? :initWithParams
|
135
|
+
controller = controller.initWithParams(open_params)
|
136
|
+
else
|
137
|
+
controller = controller.init
|
138
|
+
end
|
139
|
+
if open_options[:shared]
|
140
|
+
shared_vc_cache[url] = controller
|
141
|
+
# when controller.viewDidUnload called, remove from cache.
|
142
|
+
controller.add_block_method :new_dealloc do
|
143
|
+
shared_vc_cache.delete url
|
144
|
+
end
|
145
|
+
controller.instance_eval do
|
146
|
+
def viewDidUnload
|
147
|
+
new_dealloc
|
148
|
+
super
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
controller
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
def shared_vc_cache
|
157
|
+
@shared_vc_cache ||= {}
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/lib/routable.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "routable/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__), 'routable/*.rb')).each do |file|
|
9
|
+
app.files.unshift(file)
|
10
|
+
end
|
11
|
+
end
|
data/spec/main_spec.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
class UsersTestController < UIViewController
|
2
|
+
attr_accessor :user_id
|
3
|
+
|
4
|
+
def initWithParams(params = {})
|
5
|
+
init()
|
6
|
+
self.user_id = params[:user_id]
|
7
|
+
self
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class NoParamsController < UIViewController
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "the url router" do
|
15
|
+
before do
|
16
|
+
@router = Routable::Router.new
|
17
|
+
|
18
|
+
@nav_controller = UINavigationController.alloc.init
|
19
|
+
@nav_controller.setViewControllers([], animated: false)
|
20
|
+
@nav_controller.viewControllers.count.should == 0
|
21
|
+
end
|
22
|
+
|
23
|
+
def make_test_controller_route
|
24
|
+
format = "users/:user_id"
|
25
|
+
@router.map(format, UsersTestController)
|
26
|
+
@router.routes.should == {format => {:klass => UsersTestController}}
|
27
|
+
end
|
28
|
+
|
29
|
+
it "maps the correct urls" do
|
30
|
+
make_test_controller_route
|
31
|
+
|
32
|
+
user_id = "1001"
|
33
|
+
controller = @router.controller_for_url("users/#{user_id}")
|
34
|
+
controller.class.should == UsersTestController
|
35
|
+
controller.user_id.should == user_id
|
36
|
+
end
|
37
|
+
|
38
|
+
it "opens urls with no params" do
|
39
|
+
@router.navigation_controller = @nav_controller
|
40
|
+
@router.map("url", NoParamsController)
|
41
|
+
@router.open("url")
|
42
|
+
@router.navigation_controller.viewControllers.count.should == 1
|
43
|
+
end
|
44
|
+
|
45
|
+
it "opens nav controller to url" do
|
46
|
+
@router.navigation_controller = @nav_controller
|
47
|
+
make_test_controller_route
|
48
|
+
@router.open("users/3")
|
49
|
+
|
50
|
+
@nav_controller.viewControllers.count.should == 1
|
51
|
+
@nav_controller.viewControllers.last.class.should == UsersTestController
|
52
|
+
end
|
53
|
+
|
54
|
+
it "uses the shared properties correctly" do
|
55
|
+
shared_format = "users"
|
56
|
+
format= "users/:user_id"
|
57
|
+
|
58
|
+
@router.map(format, UsersTestController)
|
59
|
+
@router.map(shared_format, UsersTestController, shared: true)
|
60
|
+
@router.navigation_controller = @nav_controller
|
61
|
+
@router.open("users/3")
|
62
|
+
@router.open("users/4")
|
63
|
+
@router.open("users")
|
64
|
+
@router.open("users/5")
|
65
|
+
|
66
|
+
@nav_controller.viewControllers.count.should == 4
|
67
|
+
|
68
|
+
@router.open("users")
|
69
|
+
@nav_controller.viewControllers.count.should == 3
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
describe "the ns object hack router" do
|
2
|
+
it "alias method trick works" do
|
3
|
+
object = "hello"
|
4
|
+
side_effect = false
|
5
|
+
|
6
|
+
object.add_block_method :new_upcase! do
|
7
|
+
side_effect = true
|
8
|
+
end
|
9
|
+
|
10
|
+
object.instance_eval do
|
11
|
+
def upcase!
|
12
|
+
new_upcase!
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
object.upcase!
|
18
|
+
side_effect.should == true
|
19
|
+
object.should == "HELLO"
|
20
|
+
end
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: routable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Clay Allsopp
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2012-05-23 00:00:00 Z
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A RubyMotion UIViewController -> URL router
|
17
|
+
email:
|
18
|
+
- clay.allsopp@gmail.com
|
19
|
+
executables: []
|
20
|
+
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files: []
|
24
|
+
|
25
|
+
files:
|
26
|
+
- .gitignore
|
27
|
+
- Gemfile
|
28
|
+
- README.md
|
29
|
+
- Rakefile
|
30
|
+
- Routable.gemspec
|
31
|
+
- app/app_delegate.rb
|
32
|
+
- lib/routable.rb
|
33
|
+
- lib/routable/ns_object.rb
|
34
|
+
- lib/routable/router.rb
|
35
|
+
- lib/routable/version.rb
|
36
|
+
- spec/main_spec.rb
|
37
|
+
- spec/ns_object_spec.rb
|
38
|
+
homepage: https://github.com/clayallsopp/Routable
|
39
|
+
licenses: []
|
40
|
+
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
requirements: []
|
59
|
+
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 1.8.21
|
62
|
+
signing_key:
|
63
|
+
specification_version: 3
|
64
|
+
summary: A RubyMotion UIViewController -> URL router
|
65
|
+
test_files:
|
66
|
+
- spec/main_spec.rb
|
67
|
+
- spec/ns_object_spec.rb
|