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 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