Skip to content

Instantly share code, notes, and snippets.

@gngrwzrd
Last active June 1, 2021 01:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gngrwzrd/f5a868844e22d0a2992138dac8e2137c to your computer and use it in GitHub Desktop.
Save gngrwzrd/f5a868844e22d0a2992138dac8e2137c to your computer and use it in GitHub Desktop.
extension Notification.Name {
///Post this to go next.
static let FormFieldNavigationNext = Notification.Name("FormFieldNavigationNext")
///Post this to go previous.
static let FormFieldNavigationPrevious = Notification.Name("FormFieldNavigationPrevious")
///Post this to end editing.
static let FormFieldNavigationDone = Notification.Name("FormFieldNavigationDone")
}
class FormFieldNavigator {
weak var view:UIView? {
didSet {
if let _ = view {
update()
} else {
inputs = []
}
}
}
weak var firstResponder:UIView? {
get {
return _firstResponder()
} set {
_firstResponderStorage = newValue
_firstResponderStorage?.becomeFirstResponder()
}
}
private var _firstResponderStorage:UIView?
///All inputs found.
private var inputs:[UIView] = []
init(view:UIView? = nil) {
NotificationCenter.default.addObserver(self, selector: #selector(goNext(notification:)), name: .BLFormFieldNavigationNext, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(goPrevious(notification:)), name: .BLFormFieldNavigationPrevious, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(onDone(notification:)), name: .BLFormFieldNavigationDone, object: nil)
self.view = view
}
private func allInputs(view:UIView) -> [UIView] {
var inputs:[UIView] = []
for view in view.subviews {
if view is UITextField || view is UITextView {
inputs.append(view)
}
let subInputs = allInputs(view: view)
if subInputs.count > 0 {
inputs.append(contentsOf: subInputs)
}
}
return inputs
}
func update() {
guard let view = view else { return }
inputs = allInputs(view: view)
inputs.sort { view1, view2 in
let view1Frame = view1.convert(CGPoint.zero, to: nil)
let view2Frame = view2.convert(CGPoint.zero, to: nil)
if view1Frame.y == view2Frame.y {
return view1Frame.x < view2Frame.x
}
return view1Frame.y < view2Frame.y
}
}
private func nextResponderIndex(_ responder:UIView) -> Int? {
if let idx = indexOfResponder(responder) {
let next = idx + 1
if next < responders.count {
return next
}
}
return nil
}
private func prevResponderIndex(_ responder:UIView) -> Int? {
if let idx = indexOfResponder(responder), idx > 0 {
return idx - 1
}
return nil
}
private func nextResponder(from responder:UIView) -> UIView? {
if let nextIndex = nextResponderIndex(responder) {
return responders[nextIndex]
}
return nil
}
private func previousResponder(from responder:UIView) -> UIView? {
if let prevIndex = prevResponderIndex(responder) {
return responders[prevIndex]
}
return nil
}
private func _firstResponder() -> UIView? {
if let firstResponder = responders.filter({ $0.isFirstResponder }).first {
return firstResponder
}
return nil
}
private func resignFirstResponder() {
guard let first = _firstResponder() else { return }
first.endEditing(true)
}
@objc private func goNext(notification:Notification) {
guard let first = _firstResponder() else { return }
goNext(view: first)
}
private func goNext(view:UIView) {
if let next = nextResponder(from: view) {
if next.isHiddenInSuperChain() {
goNext(view: next)
} else {
next.becomeFirstResponder()
}
} else {
resignFirstResponder()
}
}
@objc private func goPrevious(notification:Notification) {
guard let first = _firstResponder() else { return }
goPrev(view: first)
}
private func goPrev(view:UIView) {
if let prev = previousResponder(from: view) {
if prev.isHiddenInSuperChain() {
goPrev(view: view)
} else {
prev.becomeFirstResponder()
}
} else {
resignFirstResponder()
}
}
@objc private func onDone(notification:Notification) {
guard let first = _firstResponder() else { return }
first.endEditing(true)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment