class MotionKit::BaseLayout
Abstract base class, responsible for “registration” of layout classes with the class `targets` method.
Very few methods are defined on BaseLayout
, and any unknown methods are delegated to the 'apply' method, which accepts a method name, arguments, and an optional block to set the new context.
The TreeLayout
subclass defines methods that are appropriate for adding and removing views to a view hierarchy.
Attributes
Public Class Methods
Prevents infinite loops when methods that are defined on Object/Kernel are not properly delegated to the target.
# File lib/motion-kit/helpers/base_layout.rb, line 319 def delegate_method_fix(method_name) running_name = "motion_kit_is_calling_#{method_name}" define_method(method_name) do |*args, &block| if target.motion_kit_meta[running_name] if block apply_with_context(method_name, *args, &block) else apply_with_target(method_name, *args) end else target.motion_kit_meta[running_name] = true retval = apply(method_name, *args, &block) target.motion_kit_meta[running_name] = false return retval end end end
# File lib/motion-kit/helpers/base_layout.rb, line 19 def initialize(args={}) # @layout is the object we look in for style methods @layout = nil # the Layout object that implements custom style methods. Leave this as nil # in the initializer. @layout_delegate = nil @layout_state = :initial # You can set a root view by using .new(root: some_view) # Explicit roots will not have a strong reference from # MotionKit, so retain one yourself from your controller # or other view to prevent deallocation. @preset_root = args[:root] end
Public Instance Methods
Only intended for private use
# File lib/motion-kit/helpers/base_layout.rb, line 137 def add_deferred_block(context, &block) context ||= @context if context.nil? && @assign_root context ||= self.create_default_root_context end raise InvalidDeferredError.new('deferred must be run inside of a context') unless context raise ArgumentError.new('Block required') unless block self.deferred_blocks << [context, block] end
Tries to call the setter (`foo 'value'` => `view.setFoo('value')`), or assignment method (`foo 'value'` => `view.foo = 'value'`), or if a block is given, then the object returned by 'method_name' is assigned as the new context, and the block is executed.
You can call this method directly, but usually it is called via method_missing.
# File lib/motion-kit/helpers/base_layout.rb, line 226 def apply(method_name, *args, &block) method_name = method_name.to_s raise ApplyError.new("Cannot apply #{method_name.inspect} to instance of #{target.class.name}") if method_name.length == 0 # if there is no target, than we should raise the NoMethodError; someone # called a method on the layout directly. begin target = self.target rescue NoContextError => e raise NoMethodError.new("undefined method `#{method_name}' for #{self}:#{self.class}", method_name) end objc_method_name, objc_method_args = objc_version(method_name, args) ruby_method_name, ruby_method_args = ruby_version(method_name, args) @layout_delegate ||= Layout.layout_for(target.class) if @layout_delegate @layout_delegate.set_parent_layout(parent_layout) if objc_method_name && @layout_delegate.respond_to?(objc_method_name) return @layout_delegate.send(objc_method_name, *objc_method_args, &block) elsif @layout_delegate.respond_to?(ruby_method_name) return @layout_delegate.send(ruby_method_name, *ruby_method_args, &block) end end if block apply_with_context(method_name, *args, &block) else apply_with_target(method_name, *args, &block) end end
# File lib/motion-kit/helpers/base_layout.rb, line 258 def apply_with_context(method_name, *args, &block) objc_method_name, objc_method_args = objc_version(method_name, args) ruby_method_name, ruby_method_args = ruby_version(method_name, args) if objc_method_name && target.respond_to?(objc_method_name) new_context = target.send(objc_method_name, *objc_method_args) self.context(new_context, &block) elsif target.respond_to?(ruby_method_name) new_context = target.send(ruby_method_name, *ruby_method_args) self.context(new_context, &block) elsif ruby_method_name.include?('_') objc_name = MotionKit.objective_c_method_name(ruby_method_name) self.apply(objc_name, *args, &block) else raise ApplyError.new("Cannot apply #{ruby_method_name.inspect} to instance of #{target.class.name}") end end
# File lib/motion-kit/helpers/base_layout.rb, line 276 def apply_with_target(method_name, *args, &block) objc_method_name, objc_method_args = objc_version(method_name, args) ruby_method_name, ruby_method_args = ruby_version(method_name, args) objc_setter = objc_method_name && MotionKit.setter(objc_method_name) setter = MotionKit.setter(ruby_method_name) assign = "#{ruby_method_name}=" # The order is important here. # - unchanged method name if no args are passed (e.g. `layer`) # - setter (`setLayer(val)`) # - assign (`layer=val`) # - unchanged method name *again*, because many Ruby classes provide a # combined getter/setter (`layer(val)`) # - lastly, try again after converting to camelCase if objc_method_name && target.respond_to?(objc_method_name) target.send(objc_method_name, *objc_method_args, &block) elsif args.empty? && target.respond_to?(ruby_method_name) target.send(ruby_method_name, *ruby_method_args, &block) elsif objc_setter && target.respond_to?(objc_setter) target.send(objc_setter, *objc_method_args, &block) elsif target.respond_to?(setter) target.send(setter, *args, &block) elsif target.respond_to?(assign) target.send(assign, *args, &block) elsif target.respond_to?(ruby_method_name) target.send(ruby_method_name, *ruby_method_args, &block) # UIAppearance classes are a whole OTHER thing; they never return 'true' elsif target.is_a?(MotionKit.appearance_class) target.send(setter, *args, &block) # Finally, try again with camel case if there's an underscore. elsif method_name.include?('_') objc_name = MotionKit.objective_c_method_name(method_name) self.apply(objc_name, *args, &block) else target.send(setter, *args, &block) end end
Runs a block of code with a new object as the 'context'. Methods from the Layout
classes are applied to this target object, and missing methods are delegated to a new Layout
instance that is created based on the new context.
This method is part of the public API, you can pass in any object to have it become the 'context'.
Example:
def table_view_style content = target.contentView if content context(content) do background_color UIColor.clearColor end end # usually you use 'context' automatically via method_missing, by # passing a block to a method that returns an object. That object becomes # the new context. layer do # target is now a CALayer, and methods are delegated to CALayerHelpers corner_radius 5 end end
# File lib/motion-kit/helpers/base_layout.rb, line 91 def context(new_target, &block) return new_target unless block # this little line is incredibly important; the context is only set on # the top-level Layout object. # mp "MOTIONKIT CONTEXT is #{new_target} meta: #{new_target.motion_kit_meta}" return parent_layout.context(new_target, &block) unless is_parent_layout? if new_target.is_a?(Symbol) new_target = self.get_view(new_target) end context_was, parent_was, delegate_was = @context, @parent, @layout_delegate prev_should_run = @should_run_deferred if @should_run_deferred.nil? @should_run_deferred = true else @should_run_deferred = false end @parent = MK::Parent.new(context_was) @context = new_target @context.motion_kit_meta[:delegate] ||= Layout.layout_for(@context.class) @layout_delegate = @context.motion_kit_meta[:delegate] if @layout_delegate @layout_delegate.set_parent_layout(parent_layout) end yield @layout_delegate, @context, @parent = delegate_was, context_was, parent_was if @should_run_deferred run_deferred(new_target) end @should_run_deferred = prev_should_run new_target end
Blocks passed to `deferred` are run at the end of a “session”, usually after a call to Layout#layout.
# File lib/motion-kit/helpers/base_layout.rb, line 130 def deferred(context=nil, &block) context ||= @context parent_layout.add_deferred_block(context, &block) return self end
Only intended for private use
# File lib/motion-kit/helpers/base_layout.rb, line 149 def deferred_blocks @deferred_blocks ||= [] end
# File lib/motion-kit/helpers/base_layout.rb, line 45 def has_context? if is_parent_layout? !!@context else parent_layout.has_context? end end
# File lib/motion-kit-ios/helpers/layout_device.rb, line 25 def ipad? UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad end
# File lib/motion-kit-ios/helpers/layout_device.rb, line 21 def iphone35? iphone? && UIScreen.mainScreen.bounds.size.width == 320 && UIScreen.mainScreen.bounds.size.height == 480 end
# File lib/motion-kit-ios/helpers/layout_device.rb, line 13 def iphone47? iphone? && UIScreen.mainScreen.bounds.size.width == 375 && UIScreen.mainScreen.bounds.size.height == 667 end
# File lib/motion-kit-ios/helpers/layout_device.rb, line 17 def iphone4? iphone? && UIScreen.mainScreen.bounds.size.width == 320 && UIScreen.mainScreen.bounds.size.height == 568 end
# File lib/motion-kit-ios/helpers/layout_device.rb, line 9 def iphone55? iphone? && UIScreen.mainScreen.bounds.size.width == 414 && UIScreen.mainScreen.bounds.size.height == 736 end
# File lib/motion-kit-ios/helpers/layout_device.rb, line 5 def iphone? UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone end
# File lib/motion-kit/helpers/base_layout.rb, line 41 def is_parent_layout? @layout.nil? || @layout == self end
@example
def login_button_style frame [[0, 0], [100, 20]] title 'Login' end
Methods that style the view start out as missing methods. This just calls 'apply', which searches for the method in the delegate (`@layout_delegate`) or using inspection (`respond_to?(:setFoo)`).
# File lib/motion-kit/helpers/base_layout.rb, line 176 def method_missing(method_name, *args, &block) self.apply(method_name, *args, &block) end
# File lib/motion-kit/helpers/base_layout.rb, line 180 def objc_version(method_name, args) if method_name.count(':') > 1 objc_method_name = method_name elsif args.length == 2 && args[1].is_a?(Hash) && !args[1].empty? objc_method_name = "#{method_name}:#{args[1].keys.join(':')}:" else return nil, nil end if args[1].is_a?(Hash) objc_method_args = [args[0]].concat args[1].values else objc_method_args = args end return objc_method_name, objc_method_args end
This method is used to check the orientation. On an ipad, this method returns true for :portrait if the device is “upside down”, but it returns false in the same situation on an iphone.
# File lib/motion-kit-ios/helpers/layout_orientation.rb, line 8 def orientation?(value) if target.is_a?(UIView) && target.nextResponder && target.nextResponder.is_a?(UIViewController) interface_orientation = target.nextResponder.interfaceOrientation else interface_orientation = UIApplication.sharedApplication.statusBarOrientation end return case value when :portrait if ipad? interface_orientation == UIInterfaceOrientationPortrait || interface_orientation == UIInterfaceOrientationPortraitUpsideDown else interface_orientation == UIInterfaceOrientationPortrait end when :upright, UIInterfaceOrientationPortrait interface_orientation == UIInterfaceOrientationPortrait when :landscape interface_orientation == UIInterfaceOrientationLandscapeLeft || interface_orientation == UIInterfaceOrientationLandscapeRight when :landscape_left, UIInterfaceOrientationLandscapeLeft interface_orientation == UIInterfaceOrientationLandscapeLeft when :landscape_right, UIInterfaceOrientationLandscapeRight interface_orientation == UIInterfaceOrientationLandscapeRight when :upside_down, UIInterfaceOrientationPortraitUpsideDown interface_orientation == UIInterfaceOrientationPortraitUpsideDown end end
# File lib/motion-kit-ios/helpers/layout_orientation.rb, line 44 def orientation_block(orientation, block) block = block.weak! always do if orientation?(orientation) block.call end end end
# File lib/motion-kit/helpers/base_layout.rb, line 37 def parent_layout @layout || self end
# File lib/motion-kit-ios/helpers/layout_device.rb, line 29 def retina? UIScreen.mainScreen.respond_to?(:scale) && UIScreen.mainScreen.scale == 2 end
# File lib/motion-kit/helpers/base_layout.rb, line 198 def ruby_version(method_name, args) if method_name.count(':') > 1 parts = method_name.split(':') ruby_method_name = parts.first if args[1].is_a?(Hash) ruby_method_args = args else keys = parts[1..-1].map(&:to_sym) ruby_method_args = [args[0]] << Hash[keys.zip args[1..-1]] end elsif method_name.count(':') == 1 ruby_method_name = method_name.split(':').first ruby_method_args = args else ruby_method_name = method_name ruby_method_args = args end return ruby_method_name, ruby_method_args end
Only intended for private use
# File lib/motion-kit/helpers/base_layout.rb, line 154 def run_deferred(top_level_context) deferred_blocks = self.deferred_blocks @deferred_blocks = nil deferred_blocks.each do |target, block| context(target, &block) end if @deferred_blocks run_deferred(top_level_context) end end
# File lib/motion-kit/helpers/base_layout.rb, line 33 def set_parent_layout(layout) @layout = WeakRef.new(layout) end
# File lib/motion-kit/helpers/base_layout.rb, line 53 def target if is_parent_layout? # only the "root layout" instance is allowed to change the context. # if there isn't a context set, try and create a root instance; this # will fail if we're not in a state that allows the root to be created @context ||= create_default_root_context else # child layouts get the context from the root layout parent_layout.target end end
# File lib/motion-kit-tvos/helpers/layout_device.rb, line 5 def tv? UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomTV end
# File lib/motion-kit/helpers/base_layout.rb, line 64 def v ; target ; end
Protected Instance Methods
# File lib/motion-kit/helpers/base_layout.rb, line 341 def preset_root # Set in the initializer # TreeLayout.new(root: some_view) @preset_root end