0

In my app I have the need to custom handle text input for which I am using a UIView that conforms to UIKeyInput. This is then wrapped with UIViewRepresentable for use with SwiftUI. I would like to get SwiftUI to add a keyboard menu when my view is selected -- but have been unable to do so.

I have put together a tiny demo app that shows the difference between how a standard TextField and my custom view are treated. (Using custom text view from hackingwithswift.com as a standin for my actual use case.) The app includes a TextField - when it is active, a system toolbar is added as well as a custom toolbar. Though, the toolbar shows two buttons - one that I intended for the TextField, and also the one I intended for my custom view. When my custom view is activated, no toolbars are added. Mainly, I am after getting the custom toolbar (with blue buttons) added.

How do I get my view to be treated similar to a TextField?

TextField vs custom text view

ContentView:

import SwiftUI
    
    enum LocFields {
        case textField
        case myTextView
    }
    
    struct ContentView: View {
        @State private var tfText = ""
        @State private var isEditingTvText = false
        @FocusState private var focusedField: LocFields?
    
        var body: some View {
            VStack {
                TextField("Enter text", text: $tfText)
                    .textFieldStyle(.roundedBorder)
                    .focused($focusedField, equals: .textField)
                    .toolbar {
                        ToolbarItemGroup(placement: .keyboard) {
                            Button("Tap me 1!") {
                                print("Tapped 1")
                            }
                        }
                    }
    
                TextView(isEditing: isEditingTvText)
                    .background(Color.gray.opacity(0.2))
                    .focused($focusedField, equals: .myTextView)
                    .toolbar {
                        ToolbarItemGroup(placement: .keyboard) {
                            Button("Tap me 2!") {
                                print("Tapped 2")
                            }
                        }
                    }
                    .onTapGesture {
                        isEditingTvText.toggle()
                    }
            }
            .padding()
            .onChange(of: focusedField) { _, newValue in
                switch newValue {
                    case .textField:
                        isEditingTvText = false
                    case .myTextView:
                        break
                    default:    // Set to nil case
                        isEditingTvText = false
                }
            }
        }
    }

TextView:

import SwiftUI

struct TextView: UIViewRepresentable {
    var isEditing: Bool

    // NOTE: TextRenderingView from:
    // https://www.hackingwithswift.com/example-code/uikit/how-to-create-custom-text-input-using-uikeyinput

    func makeUIView(context: Context) -> TextRenderingView {
        let vw = TextRenderingView()
        vw.backgroundColor = .clear
        return vw
    }
    
    func updateUIView(_ uiView: TextRenderingView, context: Context) {
        let uiViewIsFirst = uiView.isFirstResponder
        if isEditing {
            if !uiViewIsFirst {
                DispatchQueue.main.async {
                    uiView.becomeFirstResponder()
                }
            }
        } else {
            if uiViewIsFirst {
                DispatchQueue.main.async {
                    uiView.resignFirstResponder()
                }
            }
        }
    }

}

TextRenderingView:

import UIKit

// From: https://www.hackingwithswift.com/example-code/uikit/how-to-create-custom-text-input-using-uikeyinput

class TextRenderingView: UIView, UIKeyInput {
    // the string we'll be drawing
    var input = ""

    override var canBecomeFirstResponder: Bool {
        true
    }

    var hasText: Bool {
        input.isEmpty == false
    }

    func insertText(_ text: String) {
        input += text
        setNeedsDisplay()
    }

    func deleteBackward() {
        _ = input.popLast()
        setNeedsDisplay()
    }

    override func draw(_ rect: CGRect) {
        let attrs: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 32)]
        let attributedString = NSAttributedString(string: input, attributes: attrs)
        attributedString.draw(in: rect)
    }
}
2
  • Since the view came from UIKit, SwiftUI toolbar does not work on it. Just set inputAccessoryView to whatever you want. Commented Jun 10 at 17:36
  • Thanks for putting me onto inputAccessoryView. Figured out how to turn a SwiftUI view into a UIView and add that. Will demonstrate in code below. Commented Jun 10 at 23:45

1 Answer 1

0

One can create a SwiftUI "toolbar", convert it to a UIView, and add it as an inputAccessoryView.

Using above code, make the following changes:

TextRenderingView: (add following to override inputAccessoryView)

    var inputAcc: UIView?

    override var inputAccessoryView: UIView? {
        get {
            return self.inputAcc
        }
        set {
            self.inputAcc = newValue
        }
    }

TextView: (Convert to accept a View for the keyboard, convert to UIHostingController and add as inputAccessoryView)

import SwiftUI

struct TextView<Content: View>: UIViewRepresentable {
    var isEditing: Bool
    var swiftUIView: Content

    // NOTE: TextRenderingView from:
    // https://www.hackingwithswift.com/example-code/uikit/how-to-create-custom-text-input-using-uikeyinput

    func makeUIView(context: Context) -> TextRenderingView {
        let vw = TextRenderingView()
        vw.backgroundColor = .clear
        let hostingController = UIHostingController(rootView: swiftUIView)
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        vw.inputAccessoryView = hostingController.view
        return vw
    }

    func updateUIView(_ uiView: TextRenderingView, context: Context) {
        let uiViewIsFirst = uiView.isFirstResponder
        if isEditing {
            if !uiViewIsFirst {
                DispatchQueue.main.async {
                    uiView.becomeFirstResponder()
                }
            }
        } else {
            if uiViewIsFirst {
                DispatchQueue.main.async {
                    uiView.resignFirstResponder()
                }
            }
        }
    }

}

ContentView: (Add a function to create HStack for keyboard toolbar. Use that function in making TextView.)

            TextView(isEditing: isEditingTvText, swiftUIView: swiftUIKeyboardBar())
                .background(Color.gray.opacity(0.2))
                .focused($focusedField, equals: .myTextView)
                .onTapGesture {
                    isEditingTvText.toggle()
                }
    func swiftUIKeyboardBar() -> some View {
        HStack {
            Button("Tap Me 1!!") {
                print("1 tapped")
            }.padding(8)
            Spacer()
            Button("Tap Me 2!!") {
                print("2 tapped")
            }.padding(8)
        }
        .background(Color.gray.opacity(0.2))
    }
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.