Last active
August 23, 2023 14:31
-
-
Save JadenGeller/1e551d5a6a7353bfc5186b52750658b1 to your computer and use it in GitHub Desktop.
SwiftUI editing Binding for UIControl firstResponder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
import Dispatch | |
extension View { | |
// Warning: This will affect the layout of the view that's wrapped! :( | |
public func editing(_ isEditing: Binding<Bool>) -> some View { | |
EditingProxy(rootView: self, isEditing: isEditing) | |
} | |
} | |
fileprivate struct EditingProxy<Content: View>: UIViewControllerRepresentable { | |
let rootView: Content | |
@Binding var isEditing: Bool | |
class Coordinator: NSObject { | |
let proxy: EditingProxy | |
init(_ proxy: EditingProxy) { | |
self.proxy = proxy | |
} | |
func observeEditing(in controller: UIViewController) { | |
DispatchQueue.main.async { [weak self] in | |
guard let self = self else { return } | |
controller.control.addTarget(self, action: #selector(self.editingDidBegin(_:)), for: .editingDidBegin) | |
controller.control.addTarget(self, action: #selector(self.editingDidEnd(_:)), for: .editingDidEnd) | |
} | |
} | |
@objc func editingDidBegin(_ control: UIControl) { | |
withAnimation { | |
proxy.isEditing = true | |
control.isEditing = proxy.isEditing // Binding may be constant! | |
} | |
} | |
@objc func editingDidEnd(_ control: UIControl) { | |
withAnimation { | |
proxy.isEditing = false | |
control.isEditing = proxy.isEditing // Binding may be constant! | |
} | |
} | |
} | |
func makeCoordinator() -> Coordinator { | |
Coordinator(self) | |
} | |
func makeUIViewController(context: Context) -> UIHostingController<Content> { | |
let controller = UIHostingController(rootView: rootView) | |
context.coordinator.observeEditing(in: controller) | |
DispatchQueue.main.async { [isEditing] in // Wait for SwiftUI to build view hierachy! | |
controller.control.isEditing = isEditing | |
} | |
return controller | |
} | |
func updateUIViewController(_ controller: UIHostingController<Content>, context: Context) { | |
controller.rootView = rootView | |
context.coordinator.observeEditing(in: controller) | |
DispatchQueue.main.async { [isEditing] in // Wait for SwiftUI to build view hierachy! | |
controller.control.isEditing = isEditing | |
} | |
} | |
} | |
extension UIViewController { | |
fileprivate var control: UIControl { | |
guard let control = view.firstDescendant(where: { $0 is UIControl }) else { | |
preconditionFailure("Could not find control in subview hierahcy") | |
} | |
return control as! UIControl | |
} | |
} | |
extension UIControl { | |
fileprivate var isEditing: Bool { | |
get { | |
isFirstResponder | |
} | |
set { | |
guard newValue != isEditing else { | |
return | |
} | |
if newValue { | |
becomeFirstResponder() | |
} else { | |
resignFirstResponder() | |
} | |
} | |
} | |
} | |
extension UIView { | |
fileprivate func descendants(atDepth depth: Int) -> [UIView] { | |
guard depth > 0 else { | |
return [self] | |
} | |
var result: [UIView] = [] | |
for subview in subviews { | |
result.append(contentsOf: subview.descendants(atDepth: depth - 1)) | |
} | |
return result | |
} | |
fileprivate func firstDescendant(where condition: (UIView) -> Bool) -> UIView? { | |
for depth in 0... { | |
let descendents = descendants(atDepth: depth) | |
guard !descendents.isEmpty else { return nil } | |
if let result = descendents.first(where: condition) { | |
return result | |
} | |
} | |
fatalError("Unreachable code") | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct ContentView: View { | |
@State var isEditingMain: Bool = false | |
@State var mainText: String = "" | |
@State var otherText: String = "" | |
var body: some View { | |
Form { | |
TextField("Main", text: $mainText) | |
.editing($isEditingMain) | |
TextField("Other", text: $otherText) | |
Toggle(isOn: $isEditingMain) { | |
Text("Editing Main") | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment