WWDC21 SALE: Save 50% on all my Swift books and bundles! >>

Keyboard Buttons - UIViewRepresentable TextField

Forums > SwiftUI

Until SwiftUI gives us Accessory buttons for the keyboard, It looks like the best option is to use a UIViewRepresentable. My goal is to still have a cancel / clear / done button on top of the keyboard. The buttons show up on the keyboard, but when clicked do nothing. The textFieldShouldReturn is called and works properly. What am I missing? Oh, and I know the functions don't actually do anything but print to the console, but I can get that working.

import SwiftUI

struct TestTextfield: UIViewRepresentable {

    @Binding var text: String
    var keyType: UIKeyboardType

    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField()
        textField.delegate = context.coordinator
        textField.keyboardType = keyType

        let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textField.frame.size.width, height: 44))
        let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)

        let cancelButton = UIBarButtonItem(title: "Cancel", style: .done, target: self, action: #selector(context.coordinator.cancelButtonTapped(button:)))
        let clearButton = UIBarButtonItem(title: "Clear", style: .done, target: self, action: #selector(context.coordinator.clearButtonTapped(button:)))
        let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(context.coordinator.doneButtonTapped(button:)))

        toolBar.setItems([cancelButton, spacer, clearButton, spacer, doneButton], animated: true)
        textField.inputAccessoryView = toolBar

        return textField
    }

    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    class Coordinator: NSObject, UITextFieldDelegate {

        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            print("TextField Should Return")
            textField.resignFirstResponder()
            return true
        }

        override init() {
            print("Keyboard Buttons")
        }

        @objc func cancelButtonTapped(button:UIBarButtonItem) -> Void {
           print("Cancel Button Tapped")
        }

        @objc func clearButtonTapped(button:UIBarButtonItem) -> Void {
           print("Clear Button Tapped")
        }

        @objc func doneButtonTapped(button:UIBarButtonItem) -> Void {
            print("Done Button Tapped")
        }
    }
}

struct ContentView: View {

    @State private var textfield1 = "First"
    @State private var textfield2 = "Second"
    @State private var textfield3 = "Third"

    var body: some View {
        ScrollView {
            Rectangle()
                .fill(Color.blue)
                .frame(width: 200, height: 400, alignment: .center)
            TestTextfield(text: $textfield1, keyType: UIKeyboardType.default)
                .darkTextFieldStyle()
            Spacer()
            TestTextfield(text: $textfield2, keyType: UIKeyboardType.default)
                .darkTextFieldStyle()
            Spacer()
            TestTextfield(text: $textfield3, keyType: UIKeyboardType.default)
                .darkTextFieldStyle()
        }
    }
}

struct darkTextField: ViewModifier {
    func body(content: Content) -> some View {

        content
            .padding(.horizontal, 30)
            .padding(.vertical, 5)
            .background(Color.white)
            .cornerRadius(10)
    }
}

extension View {
    func darkTextFieldStyle() -> some View {
        self.modifier(darkTextField())
    }
}

   

Hey,

It seems like you're missing some of the conceptual basics regarding Cocoa's target-action system. The target is supposed to be the actual object your function exists and should be called on, the action is just essentially the name of the function. When you pass self as the target and #selector(context.coordinator.cancelButtonTapped(button:)) as your action, then the context.coordinator. part of the selector only helps the Swift compiler check that such a function exists, but it doesn't tell Cocoa to look there for it. To make it work, you need to pass context.coordinator as your target:

let cancelButton = UIBarButtonItem(title: "Cancel", style: .done, target: context.coordinator, action: #selector(context.coordinator.cancelButtonTapped(button:)))
let clearButton = UIBarButtonItem(title: "Clear", style: .done, target: context.coordinator, action: #selector(context.coordinator.clearButtonTapped(button:)))
let doneButton = UIBarButtonItem(title: "Done", style: .done, target: context.coordinator, action: #selector(context.coordinator.doneButtonTapped(button:)))

But since you don't seem to have much experience with target-action, I suggest you take a look at the "new" UIAction api. This lets you use normal closures as onclick methods for buttons, which makes it a lot easier to use IMO. Here's what the above buttons would look like with it (I chose to show a different approach for each button, just so you get to pick which would suit you best):

let cancelButton = UIBarButtonItem(title: "Cancel", image: nil, primaryAction: UIAction { action in
    context.coordinator.cancelButtonTapped(action: action)
})
let clearButton = UIBarButtonItem(title: "Clear", image: nil, primaryAction: UIAction(handler: context.coordinator.clearButtonTapped(action:)))
let doneButton = UIBarButtonItem(title: "Done", image: nil, primaryAction: context.coordinator.doneAction)

...

class Coordinator: NSObject, UITextFieldDelegate {
    lazy var doneAction = UIAction(handler: doneButtonTapped(action:))

    ...

    func cancelButtonTapped(action: UIAction) -> Void {
       print("Cancel Button Tapped")
    }

    func clearButtonTapped(action: UIAction) -> Void {
       print("Clear Button Tapped")
    }

    private func doneButtonTapped(action: UIAction) -> Void {
        print("Done Button Tapped")
    }
}

Target-action is of course still a very valid approach, just throught I'd throw this here for completeness.

   

Save money with our WWDC sale!

SAVE 50% To celebrate WWDC21, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.