-
-
Save iband/0f794e213bb2c61465c66fbf4410b81f to your computer and use it in GitHub Desktop.
SwiftUI editing Binding for UITextView (UIResponder) 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, UITextViewDelegate { | |
let proxy: EditingProxy | |
private var textViewDelegate: UITextViewDelegate? | |
init(_ proxy: EditingProxy) { | |
self.proxy = proxy | |
} | |
func observeEditing(in controller: UIViewController) { | |
DispatchQueue.main.async { [weak self] in | |
guard let self = self else { return } | |
if self.textViewDelegate == nil { | |
self.textViewDelegate = controller.control.delegate | |
} | |
controller.control.delegate = self | |
} | |
} | |
func textViewDidBeginEditing(_ textView: UITextView) { | |
textViewDelegate?.textViewDidBeginEditing?(textView) | |
withAnimation { | |
proxy.isEditing = true | |
textView.isEditing = proxy.isEditing // Binding may be constant! | |
} | |
} | |
func textViewDidEndEditing(_ textView: UITextView) { | |
textViewDelegate?.textViewDidEndEditing?(textView) | |
withAnimation { | |
proxy.isEditing = false | |
textView.isEditing = proxy.isEditing // Binding may be constant! | |
} | |
} | |
func textViewDidChange(_ textView: UITextView) { | |
textViewDelegate?.textViewDidChange?(textView) | |
} | |
func textViewDidChangeSelection(_ textView: UITextView) { | |
textViewDelegate?.textViewDidChangeSelection?(textView) | |
} | |
func textViewShouldEndEditing(_ textView: UITextView) -> Bool { | |
return textViewDelegate?.textViewShouldEndEditing?(textView) ?? true | |
} | |
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { | |
return textViewDelegate?.textViewShouldBeginEditing?(textView) ?? true | |
} | |
} | |
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: UITextView { | |
guard let control = view.firstDescendant(where: { $0 is UITextView }) else { | |
preconditionFailure("Could not find control in subview hierahcy") | |
} | |
return control as! UITextView | |
} | |
} | |
extension UIResponder { | |
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
import SwiftUI | |
struct NewPlanView: View { | |
@Environment(\.presentationMode) var presentationMode | |
@State var title: String = "" | |
@State var placeholder = "Some placeholder text" | |
@State var isEditing: Bool = false | |
var body: some View { | |
ZStack() { | |
if title.isEmpty { | |
TextEditor(text: $placeholder) | |
.disabled(true) | |
} | |
TextEditor(text: $title) | |
.editing($isEditing) | |
.opacity(title.isEmpty ? 0.5 : 1) | |
.onAppear() { | |
isEditing = true | |
} | |
} | |
.font(.title2) | |
.padding(.all) | |
} | |
} | |
struct NewPlanView_Previews: PreviewProvider { | |
static var previews: some View { | |
Group { | |
NewPlanView() | |
} | |
.previewLayout(.sizeThatFits) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment