Skip to content

Instantly share code, notes, and snippets.

@shaps80
Created November 5, 2018 19:26
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save shaps80/c17ddb6596bc138e44037c820242553d to your computer and use it in GitHub Desktop.
Save shaps80/c17ddb6596bc138e44037c820242553d to your computer and use it in GitHub Desktop.
A convenient controller for working with the responder chain on iOS
import UIKit
/// Provides a convenient controller for working with the responder chain
public final class ResponderController: NSObject, NSCopying {
/// When true, the responder will loop through the responders when reaching the ends
@IBInspectable
public var isContinuous: Bool = true
private var responders: [UIResponder] {
guard Thread.isMainThread else { fatalError("Responder Controller should not be called on a background thread") }
guard let view = view else { return [] }
return view.childResponders
.sorted { $0.isPositionedBefore($1) }
}
/// Only responders inside this view will be considered by this controller
@IBOutlet public weak var view: UIView?
/// Makes a new responder controller
@objc public override init() {
super.init()
}
@objc public init(view: UIView) {
self.view = view
super.init()
}
public override func copy() -> Any {
return ResponderController()
}
public func copy(with zone: NSZone? = nil) -> Any {
return copy()
}
/// Makes the responder before `currernt` the first responder
@IBAction public func previous(_ sender: Any?) {
previous?.becomeFirstResponder()
}
/// Makes the responder after `currernt` the first responder
@IBAction public func next(_ sender: Any?) {
next?.becomeFirstResponder()
}
/// Resigns the current first responder
@IBAction public func resign(_ sender: Any?) {
current?.resignFirstResponder()
}
/// Makes the first visible control the first responder
@IBAction public func begin(_ sender: Any?) {
first?.becomeFirstResponder()
}
}
extension ResponderController {
/// Returns the first responder
@objc public var first: UIResponder? {
guard Thread.isMainThread else { fatalError("Responder Controller should not be called on a background thread") }
return responders.first
}
/// Returns the last responder
@objc public var last: UIResponder? {
guard Thread.isMainThread else { fatalError("Responder Controller should not be called on a background thread") }
return responders.last
}
/// Returns the current responder
@objc public var current: UIResponder? {
guard Thread.isMainThread else { fatalError("Responder Controller should not be called on a background thread") }
guard let index = currentIndex else { return nil }
return responders[index]
}
/// Returns the responder before `current`
@objc public var previous: UIResponder? {
guard Thread.isMainThread else { fatalError("Responder Controller should not be called on a background thread") }
guard let prev = previousIndex else { return nil }
return responders[prev]
}
/// Returns the responder after `current`
@objc public var next: UIResponder? {
guard Thread.isMainThread else { fatalError("Responder Controller should not be called on a background thread") }
guard let next = nextIndex else { return nil }
return responders[next]
}
}
private extension ResponderController {
var currentIndex: Int? {
return responders.index { $0.isFirstResponder }
}
var previousIndex: Int? {
guard let index = currentIndex else { return nil }
let responders = self.responders
let prev = responders.index(before: index)
if prev < responders.startIndex {
if isContinuous {
return responders.index(before: responders.endIndex)
} else {
return nil
}
}
return prev
}
var nextIndex: Int? {
guard let index = currentIndex else { return nil }
let responders = self.responders
let next = responders.index(after: index)
if next >= responders.endIndex {
if isContinuous {
return responders.startIndex
} else {
return nil
}
}
return next
}
}
private extension UIView {
func nearestAncestor(sharedBy otherView: UIView) -> UIView? {
let mine = sequence(first: self, next: { $0.superview })
let theirs = Set(sequence(first: otherView, next: { $0.superview }))
return mine.first(where: theirs.contains)
}
func isPositionedBefore(_ view: UIView) -> Bool {
guard let ancestor = nearestAncestor(sharedBy: view) else { return false }
let lhs = relativeFrame(in: ancestor)
let rhs = view.relativeFrame(in: ancestor)
switch ancestor.effectiveUserInterfaceLayoutDirection {
case .leftToRight:
return lhs.maxY < rhs.minY || lhs.maxX < rhs.minX
case .rightToLeft:
return lhs.maxY < rhs.minY || rhs.maxX < lhs.minX
}
}
}
private extension UIView {
/// Returns the frame in relation to the specified view. This is essentially the 'visual' frame for this view
///
/// - Parameter view: The view to use for frame conversion
/// - Returns: A visual frame in `view`
func relativeFrame(in view: UIView) -> CGRect {
return convert(bounds, to: view)
}
/// Returns all child responders where `isResponder == true`
@objc var childResponders: [UIView] {
return [self].filter { $0.isValidResponder }
+ subviews.flatMap { $0.childResponders }
}
/// Returns true if this view is a valid responder, false otherwise
///
/// Returns true if `self` satisfies the following requirements:
/// 1. canBecomeFirstResponder == true
/// 2. alpha > 0
/// 3. isHidden == false
@objc var isValidResponder: Bool {
return canBecomeFirstResponder
&& alpha > 0
&& !isHidden
}
}
private extension UIControl {
/// Returns true is this control is a valid responder, false otherwise
///
/// Returns true if `self` satisfies the following requirements:
/// 1. super.isResponder == true
/// 2. isEnabled == true
override var isValidResponder: Bool {
return super.isValidResponder && isEnabled
}
}
@shaps80
Copy link
Author

shaps80 commented Nov 5, 2018

Read about this on my blog and download the Live Playground.
https://shaps.me/blog/2018/11/5/responder-controller

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment