routable 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ .repl_history
2
+ build
3
+ resources/*.nib
4
+ resources/*.momd
5
+ resources/*.storyboardc
6
+ *.gem
7
+ .bundle
8
+ Gemfile.lock
9
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in Routable.gemspec
4
+ gemspec
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Routable
2
+ VERSION = "0.0.1"
3
+ 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