Skip to content

Instantly share code, notes, and snippets.

@iband
Forked from JadenGeller/Editing.swift
Last active December 14, 2023 14:48
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iband/0f794e213bb2c61465c66fbf4410b81f to your computer and use it in GitHub Desktop.
Save iband/0f794e213bb2c61465c66fbf4410b81f to your computer and use it in GitHub Desktop.
SwiftUI editing Binding for UITextView (UIResponder) 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, 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")
}
}
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