1

The toolbar shows if I use a TextField, but not with UITextView. I'm not sure whether this is a bug, or I'm just not bridging UITextView properly with SwiftUI. Any idea on how to make .toolbar() work with UITextView? Here's my code:

struct MyUITextView: UIViewRepresentable {
    @Binding var text: String
    private let editor = UITextView()
    
    func makeUIView(context: Context) -> UITextView {
        editor.delegate = context.coordinator
        
        return editor
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(text: $text)
    }
    
    func updateUIView(_ editor: UITextView, context: Context) {
        editor.text = text
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        @Binding private var text: String
        
        init(text: Binding<String>) {
            self._text = text
        }
        
        func textViewDidChange(_ editor: UITextView) {
            text = editor.text
        }
    }
}

struct ContentView: View {
    @State var uitextView = "UITextView"
    @State var textfield = "TextField"
    var body: some View {
        VStack {
            MyUITextView(text: $uitextView) // Toolbar doesn't show up when focused
                .border(.black, width: 1)
                .frame(maxHeight: 40)
            TextField("", text: $textfield) // Toolbar shows up when focused
                .border(.black, width: 1)
        }
        .toolbar {
            ToolbarItem(placement: .keyboard) {
                Button("Click") {}
            }
        }
    }
}

Demo

Konrad
  • 13
  • 2
  • 1
    I don't think `toolbar` works with things ported from UIKit? Use the UIKit APIs to make your toolbar. – Sweeper Sep 02 '23 at 02:58

1 Answers1

0

SwiftUI's toolbar doesn't know about UIKit views, so it doesn't add toolbars to your UITextView.

You can still add a toolbar using the UIKit APIs:

let toolbar = UIToolbar()
toolbar.items = [...]
editor.inputAccessoryView = toolbar
toolbar.translatesAutoresizingMaskIntoConstraints = false

If you want to use SwiftUI to write the toolbar's contents, you'd have to add a UIHostingController in the coordinator.

For some reason, adding a single UIBarButtonItem(customView: hostingController.view) makes the user unable to press the SwiftUI Button. You have to make the entire inputAccessoryView a SwiftUI instead.

Example:

// UIViewRepresentable and Coordinator design inspired by https://stackoverflow.com/a/74788978/5133585
struct MyUITextView<Toolbar: View>: UIViewRepresentable {
    init(text: Binding<String>, @ViewBuilder toolbar: @escaping () -> Toolbar) {
        self._text = text
        self.toolbar = toolbar
    }
    
    @Binding var text: String
    let toolbar: () -> Toolbar
    
    func makeCoordinator() -> Coordinator {
        Coordinator(hostingVC: UIHostingController(rootView: toolbar()))
    }
    
    func makeUIView(context: Context) -> UITextView {
        context.coordinator.editor
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.text = text
         context.coordinator.stringDidChange = { string in
            text = string
        }
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        
        let hostingVC: UIHostingController<Toolbar>
        
        init(hostingVC: UIHostingController<Toolbar>) {
            self.hostingVC = hostingVC
            hostingVC.sizingOptions = [.intrinsicContentSize]
        }
        
        lazy var editor: UITextView = {
            let editor = UITextView()
            editor.delegate = self
            editor.inputAccessoryView = hostingVC.view
            editor.inputAccessoryView?.translatesAutoresizingMaskIntoConstraints = false
            return editor
        }()
        
        var stringDidChange: ((String) -> ())?
        
        func textViewDidChange(_ textView: UITextView) {
            stringDidChange?(textView.text)
        }
    }
}
MyUITextView(text: $uitextView) {
    // this HStack is for imitating the UIToolbar
    HStack {
        Button("Click") { print("Foo") }
            .padding()
        Spacer()
    }
    .background(.bar.shadow(.drop(radius: 1)))
}
.border(.black, width: 1)
.frame(maxHeight: 40)
Sweeper
  • 213,210
  • 22
  • 193
  • 313