Skip to content

Instantly share code, notes, and snippets.

@colinta
Last active December 11, 2015 00:49
Show Gist options
  • Save colinta/4519385 to your computer and use it in GitHub Desktop.
Save colinta/4519385 to your computer and use it in GitHub Desktop.
ExposeController
# given a target and slideView, this class will manage a slide out view.
#
# ExposeController.new(target, slideView, options)
#
# target: this is the view that the pan gesture will be added to, can be the same as the slideView.
# slideView: this is the view that will be moved to the right, should be a container view of some sort.
# delegate: the expose controller delegate
#
# It is assumed that the *exposed view* is organized below the slideView, and
# so it will be exposed when the slideView is moved.
#
# Options:
#
# margin: the minimum visible width of the slideView. default: 50
# direction: `:left` or `:right`. The direction the slideView moves in order to expose the view beneath.
#
# Delegate methods:
#
# :didCloseSlideMenu
# :didOpenSlideMenu
# :didSlideMenu
# :willCloseSlideMenu
# :willOpenSlideMenu
# :willSlideMenu
#
# Example:
#
# # navbar is a UINavigationBar instance
# # containerView is the view you want to move out of the way
# @slidemenu = ExposeController.new(navbar, containerView, margin: SidemenuMargin)
#
# Memory:
#
# If you want good memory handling, I recommend you implement these two delegate methods:
# def willOpenSlideMenu(menu)
# background_view.hide
# end
#
# def didCloseSlideMenu(menu)
# background_view.show
# end
#
# Todo:
#
# Add support for dragging from side of the screen, rather than using a target
# view. This can be 'faked' by having a transparent view on the top all your
# other views, and passing that in as the target.
#
class ExposeController
# When the slideView is "exposing" the view beneath, this view covers the
# visible portion of slideView. The horizontal pan gesture is attached to it
attr :coverView
# The delegate gets notified before and after slide events
attr_accessor :delegate
def initialize(target, slideView, options)
default_options = {
margin: 50,
direction: :right,
delegate: nil,
}
@options = default_options.merge(options)
@slideView = slideView
@coverView = UIControl.alloc.initWithFrame(CGRect.empty)
@coverView.on :touch_down {
toggle
}
target.on_gesture(HorizontalPanGestureRecognizer) { |event|
case event.state
when :began.uigesturerecognizerstate
start_gesture(event)
when :changed.uigesturerecognizerstate
update_state(event)
when :ended.uigesturerecognizerstate
move_to_state(@last_direction)
end
}
# reset state
if open_direction == :right
@state = :left
@last_direction = :right
else
@state = :right
@last_direction = :left
end
move_to_state(closed_direction)
end
def toggle
delegate_send :willSlideMenu
if @state == closed_direction
delegate_send :willOpenSlideMenu
move_to_state open_direction
else
delegate_send :willCloseSlideMenu
move_to_state closed_direction
end
end
def slideClosed
return if @state == closed_direction
delegate_send :willSlideMenu
delegate_send :willOpenSlideMenu
move_to_state(closed_direction)
end
def slideOpen
return if @state == open_direction
delegate_send :willSlideMenu
delegate_send :willCloseSlideMenu
move_to_state(open_direction)
end
private
def delegate_send(method, *args)
# method = "#{method}:"
@delegate && @delegate.respond_to?(method) && @delegate.send(method, self, *args)
end
def open_direction
@options[:direction]
end
def closed_direction
if open_direction == :right
:left
else
:right
end
end
def min_x
case open_direction
when :right
@slideView.superview.bounds.min_x
when :left
@slideView.superview.bounds.min_x - @slideView.bounds.width + @options[:margin]
end
end
def max_x
case open_direction
when :right
@slideView.bounds.max_x - @options[:margin]
when :left
@slideView.superview.bounds.min_x
end
end
def start_gesture(event)
@start = event.locationInView(@slideView.superview).x
@last_direction = nil
# send 'will' event to delegate
delegate_send :willSlideMenu
if @state == open_direction
delegate_send :willCloseSlideMenu
else
delegate_send :willOpenSlideMenu
end
end
def update_state(event)
panning_location = event.locationInView(@slideView.superview).x
position_delta = panning_location - @start
if position_delta > 0
movement = :right
elsif position_delta < 0
movement = :left
else
movement = nil
end
new_x = @slideView.frame.min_x + position_delta
if (movement == :right and new_x <= max_x) || (movement == :left and new_x >= min_x)
@slideView.slide(:right, {size: position_delta, duration: 0.1, options: UIViewAnimationOptionCurveLinear })
@last_direction = movement
end
@start = panning_location
end
def coverViewFrame
cover_size = [@options[:margin], @slideView.bounds.height]
if open_direction == :right
cover_frame = [[0, 0], cover_size]
else
cover_frame = [[@slideView.bounds.width - @options[:margin], 0], cover_size]
end
end
def move_to_state(direction)
position = @slideView.frame.origin
original_x = position.x
case direction
when :left
position.x = min_x
when :right
position.x = max_x
end
distance = (position.x - original_x).abs
duration = distance / (@slideView.bounds.width - @options[:margin]) * 0.25
@slideView.move_to(position, duration: duration, options: UIViewAnimationOptionCurveEaseOut) {
# event is done, so send 'did' event to delegate
if @state == open_direction
delegate_send :didOpenSlideMenu
else
delegate_send :didCloseSlideMenu
end
}
@state = direction
#
# also, add the cover view if the state is 'open'. Touching the coverView
# will immediately close the slideView
delegate_send :didSlideMenu
if @state == open_direction
@coverView.frame = coverViewFrame
@slideView << @coverView
else
@coverView.removeFromSuperview
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment