Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add actual Router files + tests

  • Loading branch information...
commit d1c296825e3bcab437e6ba45c0900e6b1576c3fd 1 parent 2b5de42
@clayallsopp authored
View
1  Rakefile
@@ -4,4 +4,5 @@ require 'motion/project'
Motion::Project::App.setup do |app|
# Use `rake config' to see complete project settings.
app.name = 'Routable'
+ app.files += Dir.glob(File.join(app.project_dir, 'lib/**/*.rb'))
end
View
10 app/app_delegate.rb
@@ -1,5 +1,15 @@
class AppDelegate
+ attr_reader :navigation_controller
+
def application(application, didFinishLaunchingWithOptions:launchOptions)
+ @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
+ @window.rootViewController = self.navigation_controller
+ @window.rootViewController.wantsFullScreenLayout = true
+ @window.makeKeyAndVisible
true
end
+
+ def navigation_controller
+ @navigation_controller ||= UINavigationController.alloc.init
+ end
end
View
23 lib/ns_object.rb
@@ -0,0 +1,23 @@
+class NSObject
+ def add_block_method(sym, &block)
+ block_methods[sym] = block
+ nil
+ end
+
+ def method_missing(sym, *args, &block)
+ if block_methods.keys.member? sym
+ return block_methods[sym].call(*args, &block)
+ end
+ raise NoMethodError.new("undefined method `#{sym}' for " + "#{self.inspect}:#{self.class.name}")
+ end
+
+ def methods
+ methods = super
+ methods + block_methods.keys
+ end
+
+ private
+ def block_methods
+ @block_methods ||= {}
+ end
+end
View
154 lib/router.rb
@@ -0,0 +1,154 @@
+module Routable
+ class Router
+ # Singleton, for practical use (you might not want)
+ # to have more than one router.
+ class << self
+ def router
+ @router ||= Router.new
+ end
+ end
+
+ # The root UINavigationController we use to push/pop view controllers
+ attr_accessor :navigation_controller
+
+ # Hash of URL => UIViewController classes
+ # EX
+ # {"users/:id" => UsersController,
+ # "users/:id/posts" => PostsController,
+ # "users/:user_id/posts/:id" => PostController }
+ def routes
+ @routes ||= {}
+ end
+
+ # Map a URL to a UIViewController
+ # EX
+ # router.map "/users/:id", UsersController
+ # OPTIONS
+ # :modal => true/false
+ # - We present the VC modally (router is not shared between the new nav VC)
+ # :shared => true/false
+ # - If URL is called again, we pop to that VC if it's in memory.
+
+ def map(url, klass, options = {})
+ format = url
+ self.routes[format] = options.merge!(klass: klass)
+ end
+
+ # Push the UIViewController for the given url
+ # EX
+ # router.open("users/3")
+ # => router.navigation_controller pushes a UsersController
+ def open(url, animated = true)
+ controller_options = options_for_url(url)
+ controller = controller_for_url(url)
+ if self.navigation_controller.modalViewController
+ self.navigation_controller.dismissModalViewControllerAnimated(animated)
+ end
+ if controller_options[:modal]
+ if controller.class == UINavigationController
+ self.navigation_controller.presentModalViewController(controller, animated: animated)
+ else
+ tempNavigationController = UINavigationController.alloc.init
+ tempNavigationController.pushViewController(controller, animated: false)
+ self.navigation_controller.presentModalViewController(tempNavigationController, animated: animated)
+ end
+ else
+ if self.navigation_controller.viewControllers.member? controller
+ self.navigation_controller.popToViewController(controller, animated:animated)
+ else
+ self.navigation_controller.pushViewController(controller, animated:animated)
+ end
+ end
+ end
+
+ def options_for_url(url)
+ # map of url => options
+
+ @url_options_cache ||= {}
+ if @url_options_cache[url]
+ return @url_options_cache[url]
+ end
+
+ parts = url.split("/")
+
+ open_options = nil
+ open_params = {}
+
+ self.routes.each { |format, options|
+
+ # If the # of path components isn't the same, then
+ # it for sure isn't a match.
+ format_parts = format.split("/")
+ if format_parts.count != parts.count
+ next
+ end
+
+ matched = true
+ format_params = {}
+ # go through each of the path compoenents and
+ # check if they match up (symbols aside)
+ format_parts.each_with_index {|format_part, index|
+ check_part = parts[index]
+
+ # if we're looking at a symbol (ie :user_id),
+ # then note it and move on.
+ if format_part[0] == ":"
+ format_params[format_part[1..-1].to_sym] = check_part
+ next
+ end
+
+ # if we're looking at normal strings,
+ # check equality.
+ if format_part != check_part
+ matched = false
+ break
+ end
+ }
+
+ if !matched
+ next
+ end
+
+ open_options = options
+ open_params = format_params
+ }
+
+ if open_options == nil
+ raise "No route found for url #{url}"
+ end
+
+ @url_options_cache[url] = open_options.merge(open_params: open_params)
+ end
+
+ # Returns a UIViewController for the given url
+ # EX
+ # router.controller_for_url("users/3")
+ # => #<UsersController @id='3'>
+ def controller_for_url(url)
+ return shared_vc_cache[url] if shared_vc_cache[url]
+
+ open_options = options_for_url(url)
+ open_klass = open_options[:klass]
+ controller = open_klass.alloc.init(open_options[:open_params])
+ if open_options[:shared]
+ shared_vc_cache[url] = controller
+ # when controller.viewDidUnload called, remove from cache.
+ controller.add_block_method :new_dealloc do
+ shared_vc_cache.delete url
+ end
+ controller.instance_eval do
+ def viewDidUnload
+ new_dealloc
+ super
+ end
+ end
+ end
+ controller
+ end
+
+ private
+ def shared_vc_cache
+ @shared_vc_cache ||= {}
+ end
+ end
+end
View
62 spec/main_spec.rb
@@ -1,9 +1,61 @@
-describe "Application 'Routable'" do
+class UsersTestController < UIViewController
+ attr_accessor :user_id
+
+ def init(params = {})
+ super()
+ self.user_id = params[:user_id]
+ self
+ end
+end
+
+describe "the url router" do
before do
- @app = UIApplication.sharedApplication
+ @router = Routable::Router.new
+
+ @nav_controller = UIApplication.sharedApplication.delegate.navigation_controller
+ @nav_controller.setViewControllers([], animated: false)
+ @nav_controller.viewControllers.count.should == 0
end
- it "has one window" do
- @app.windows.size.should == 1
+ def make_test_controller_route
+ format = "users/:user_id"
+ @router.map(format, UsersTestController)
+ @router.routes.should == {format => {:klass => UsersTestController}}
end
-end
+
+ it "maps the correct urls" do
+ make_test_controller_route
+
+ user_id = "3"
+ controller = @router.controller_for_url("users/#{user_id}")
+ controller.class.should == UsersTestController
+ controller.user_id.should == user_id
+ end
+
+ it "opens nav controller to url" do
+ @router.navigation_controller = @nav_controller
+ make_test_controller_route
+ @router.open("users/3")
+
+ @nav_controller.viewControllers.count.should == 1
+ @nav_controller.viewControllers.last.class.should == UsersTestController
+ end
+
+ it "uses the shared properties correctly" do
+ shared_format = "users"
+ format= "users/:user_id"
+
+ @router.map(format, UsersTestController)
+ @router.map(shared_format, UsersTestController, shared: true)
+ @router.navigation_controller = @nav_controller
+ @router.open("users/3")
+ @router.open("users/4")
+ @router.open("users")
+ @router.open("users/5")
+
+ @nav_controller.viewControllers.count.should == 4
+
+ @router.open("users")
+ @nav_controller.viewControllers.count.should == 3
+ end
+end
View
21 spec/ns_object_spec.rb
@@ -0,0 +1,21 @@
+describe "the ns object hack router" do
+ it "alias method trick works" do
+ object = "hello"
+ side_effect = false
+
+ object.add_block_method :new_upcase! do
+ side_effect = true
+ end
+
+ object.instance_eval do
+ def upcase!

I'm not sure if RubyMotion actually supports instance_eval as it doesn't support eval or define_method.

You may need to add methods in some other manor (include).

Cheers

@clayallsopp Owner

@RobertLowe actually it does, along with class_eval. you can confirm it with this test (which passes) or this test I just tried https://gist.github.com/2772440

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ new_upcase!
+ super
+ end
+ end
+
+ object.upcase!
+ side_effect.should == true
+ object.should == "HELLO"
+ end
+end
@RobertLowe

I'm not sure if RubyMotion actually supports instance_eval as it doesn't support eval or define_method.

You may need to add methods in some other manor (include).

Cheers

@clayallsopp

@RobertLowe actually it does, along with class_eval. you can confirm it with this test (which passes) or this test I just tried https://gist.github.com/2772440

Please sign in to comment.
Something went wrong with that request. Please try again.