Skip to content

Instantly share code, notes, and snippets.

@JadenGeller
Last active August 23, 2023 14:31
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save JadenGeller/1e551d5a6a7353bfc5186b52750658b1 to your computer and use it in GitHub Desktop.
Save JadenGeller/1e551d5a6a7353bfc5186b52750658b1 to your computer and use it in GitHub Desktop.
SwiftUI editing Binding for UIControl firstResponder
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")
}
}
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