Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Returns first responder after UIImagePickerController search bar selection on iOS 16 #2392

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 32 additions & 50 deletions deltachat-ios/Helper/GiveBackMyFirstResponder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,74 +65,56 @@ private class GiveBackMyFirstResponder<VC: UIViewController>: FirstResponderRetu
}

private class FirstResponderReturningViewController: UIViewController {
private var lastResponder: UIResponder?
private var lastResponders: [UIResponder] = {
var lastResponders: [UIResponder] = []
while let next = UIResponder.currentFirstResponder, next.resignFirstResponder() {
lastResponders.append(next)
}
return lastResponders
}()

override func viewDidLoad() {
lastResponder = UIResponder.currentFirstResponder
super.viewDidLoad()
override var isBeingDismissed: Bool {
parent?.isBeingDismissed ?? super.isBeingDismissed || super.isBeingDismissed
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isBeingDismissed, let lastResponder, let newFirstResponder = UIResponder.currentFirstResponder {
if newFirstResponder != lastResponder {
// Resigning here makes the animation smoother when we make lastResponder first responder again
newFirstResponder.resignFirstResponder()
}
if isBeingDismissed, !lastResponders.isEmpty, let newFirstResponder = UIResponder.currentFirstResponder {
// Resigning here makes the animation smoother when we make lastResponders first responder again
newFirstResponder.resignFirstResponder()
}
}

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isBeingDismissed, let lastResponder, !lastResponder.isFirstResponder {
// I don't think this is perfect and it's definitely not how UIKit does it but it works.

// - if the lastResponder failed to become first responder, and
// the next responder is not first responder
// - try to make the next responder first responder
// - if that fails try the next one
// - if it succeeds try again from the start
// This shouldn't get in a loop because if it finds a responder that
// can become first responder it will only loop over the responders
// up to the one that became first responder and if it doesn't find
// any (more) it will set next to nil which will exit the loop.
//
// This is needed because lastResponder might not be on the screen (eg because
// it is in an inputAccessoryView in which case the responder which owns the
// inputAccessoryView needs to become first responder first)
//
// Note that UIResponder.canBecomeFirstResponder is not used because it
// returns true in cases where becomeFirstResponder can still fail (eg the
// case mentioned previously).
var next = lastResponder.next
while !lastResponder.becomeFirstResponder(),
let iterator = next, !iterator.isFirstResponder {
// Failed to make lastResponder first responder so try the next one which
// can cause the lastResponder to become available.
if iterator.becomeFirstResponder() {
// next became first responder so try again
next = lastResponder.next
} else {
next = next?.next
}
}
if isBeingDismissed, !lastResponders.isEmpty {
lastResponders.reversed().forEach { $0.becomeFirstResponder() }
}
}
}


extension UIResponder {
/// Finds the current first responder and returns it.
///
/// If this gets rejected see https://stackoverflow.com/a/50472291/3393964 for alternatives.
/// Note: Do not replace this with the `UIApplication.shared.sendAction(_, to: nil, from: nil, for: nil)` method
/// because that does not work reliably in all cases. eg, when you initialise a UIImagePickerController on iOS 16 it returns nil even if your textfield is still first responder.
static var currentFirstResponder: UIResponder? {
_currentFirstResponder = nil
UIApplication.shared.sendAction(#selector(UIResponder.findFirstResponder(_:)), to: nil, from: nil, for: nil)
return _currentFirstResponder
for window in UIApplication.shared.windows {
if let firstResponder = window.previousFirstResponder {
return firstResponder
}
}
return nil
}
}

private static weak var _currentFirstResponder: UIResponder?
extension UIResponder {
var nextFirstResponder: UIResponder? {
return isFirstResponder ? self : next?.nextFirstResponder
}
}

@objc func findFirstResponder(_ sender: Any) {
UIResponder._currentFirstResponder = self
extension UIView {
var previousFirstResponder: UIResponder? {
return nextFirstResponder ?? subviews.compactMap { $0.previousFirstResponder }.first
}
}