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

Keyboard Accessory Buttons - Cancel / Clear / Done

Forums > SwiftUI

A "working" version of Keyboard Accessory Buttons. Probably lots of mistakes in here, and items that could be streamlined to make it work better. As I mentioned in another post, I'd like the usage to have the ability to add modifiers (background color, style, etc) so it would be more versatile. Any thoughts?

KeyboardApp.swift

import SwiftUI

@main
struct KeyboardApp: App {
    let keyboard = KeyboardResponder()

    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(keyboard)
        }
    }
}

KeyboardResponder.swift (Also includes TextFieldView and TextEditorView)

import SwiftUI

// MARK: - TextField

struct TextFieldView: View {

    let fontsize: CGFloat = 14
    let backgroundColor = Color.blue
    let textColor = Color.white

    @Binding var field: String
    @EnvironmentObject var keyboard: KeyboardResponder
    @State var bottom = 0

    var body: some View {
        ZStack {
            GeometryReader { geometry in
                Color(.clear)
                    .onAppear() {
                        bottom = Int(geometry.frame(in: CoordinateSpace.global).maxY)
                    }
            }

            TextField(field, text: $field)
                .font(Font.system(size: fontsize))
                .padding()
                .background(RoundedRectangle(cornerRadius: 10).fill(backgroundColor))
                .foregroundColor(textColor)
                .padding()
                .onTapGesture {
                    print("On Tap Gesture - \(field)")
                    keyboard.fieldBeingEdited = $field
                    keyboard.currentText = field
                    keyboard.bottom = bottom
                }
        }
    }
}

struct TextEditorView: View {

    let fontsize: CGFloat = 14
    let width: CGFloat = 350
    let height: CGFloat = 200
    let backgroundColor = Color.blue
    let textColor = Color.white

    @Binding var field: String
    @EnvironmentObject var keyboard: KeyboardResponder
    @State var bottom = 0

    var body: some View {
        ZStack {
            GeometryReader { geometry in
                Color(.clear)
                    .onAppear() {
                        bottom = Int(geometry.frame(in: CoordinateSpace.global).maxY)
                    }
            }

            TextEditor(text: $field)
                .frame(width: width, height: height, alignment: .leading)
                .font(Font.system(size: fontsize))
                .background(RoundedRectangle(cornerRadius: 10).fill(backgroundColor))
                .foregroundColor(textColor)
                .padding()
                .onTapGesture {
                    keyboard.fieldBeingEdited = $field
                    keyboard.currentText = field
                    keyboard.bottom = bottom
                }
        }
    }
}

// MARK: - Keyboard Buttons

struct KeyboardButtonsView: View {

    var keyboard = KeyboardResponder()
    let keyboardButtonsHeight = 50

    var body: some View {
        HStack {
            Button(action: {
                self.keyboard.cancel()
                UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
            }) {
                Text("Cancel")
            }
            Spacer()
            Button(action: {
                self.keyboard.clear()
            }) {
                Text("Clear")
            }
            Spacer()
            Button(action: {
                UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
            }) {
                Text("Done")
            }
        }
        .padding()
        .frame(height: CGFloat(keyboardButtonsHeight))
        .background(Color("KeyboardBackground"))
    }
}

// MARK: - Keyboard

final class KeyboardResponder: ObservableObject {

    private var notificationCenter: NotificationCenter
    var fieldBeingEdited: Binding<String> = .constant("init")
    @Published private(set) var currentHeight: CGFloat = 0
    @Published private(set) var keyboardHeight: CGFloat = 0
    @Published var keyboardIsShowing: Bool = false
    @Published var textNeedsToMove: Bool = false

    var currentText = ""
    var screenHeight = UIScreen.main.bounds.height
    var bottom = 0

    func clear() {
        fieldBeingEdited.wrappedValue = ""
    }

    func cancel() {
        fieldBeingEdited.wrappedValue = currentText
    }

    init(center: NotificationCenter = .default) {

        notificationCenter = center
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        notificationCenter.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    deinit {
        notificationCenter.removeObserver(self)
    }

    @objc func keyBoardWillShow(notification: Notification) {

        let keyboardButtonsHeight = 50
        let buffer = 10

        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {

            keyboardHeight = keyboardSize.height
            let keyboardTotalHeight = Int(keyboardHeight) + keyboardButtonsHeight + buffer

            let topOfKeyboard = Int(screenHeight) - keyboardTotalHeight

            if bottom > Int(screenHeight) - keyboardTotalHeight {

                currentHeight = CGFloat(bottom - topOfKeyboard)
                print("Current Height = \(currentHeight)")
                keyboardIsShowing = true
                textNeedsToMove = true

            } else {
                print("Setting currentHeight to ZERO")
                currentHeight = 0
                keyboardIsShowing = true
                textNeedsToMove = false
            }
        }
    }

    @objc func keyBoardWillHide(notification: Notification) {
        currentHeight = 0
        keyboardIsShowing = false
        textNeedsToMove = false
    }
}

Usage - ContentView.swift

import SwiftUI

struct ContentView: View {

    @EnvironmentObject var keyboard: KeyboardResponder

    @State private var firstName = "ShadowDES"
    @State private var firstLikes = "Likes @TwoStraws"
    @State private var lastName = "Dennis"
    @State private var lastLikes = "Lots of other things."

    @State var screenHeight = 0
    @State var screenWidth = 0

    init() {
        UITextView.appearance().backgroundColor = .clear
    }

    var body: some View {
        ZStack {

            ScrollView(.vertical, showsIndicators: false) {
                VStack {
                    TextEditorView(field:$firstLikes)
                        .padding()
                    TextFieldView(field: $firstName)
                        .padding()
                    TextEditorView(field:$lastLikes)
                        .padding()
                    TextFieldView(field: $lastName)
                        .padding()
                }

            }// ScrollView
            .offset(x: 0, y: keyboard.textNeedsToMove ? -keyboard.currentHeight : 0)
            .edgesIgnoringSafeArea(.bottom)
            .animation(.easeOut(duration: 0.16))

            VStack {

                // Keyboard Buttons

                Spacer()
                if keyboard.keyboardIsShowing {
                    KeyboardButtonsView(keyboard: keyboard)
                }
            }
            .padding(.bottom, keyboard.keyboardIsShowing ? keyboard.keyboardHeight : 0)
            .edgesIgnoringSafeArea(.bottom)
            .animation(.easeOut(duration: 0.16))

        }
    }
}

   

I'm using a custom input accessory doneAccessory and attaching that to UIKit text fields in SwiftUI. Seems pretty straight forward, so I've stuck with it. I also have an extension for a View that lets me programmatically hide the keybard, for when I hit save etc and want the keyboard to close.

extension View {
    func hideKeyboard() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}
extension UITextField{
    @IBInspectable var doneAccessory: Bool{
        get{
            return self.doneAccessory
        }
        set (hasDone) {
            if hasDone{
                addDoneButtonOnKeyboard()
            }
        }
    }
    func addDoneButtonOnKeyboard()
    {
        let doneToolbar: UIToolbar = UIToolbar(frame: CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
        doneToolbar.barStyle = .default
        let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        let done: UIBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.doneButtonAction))
        let items = [flexSpace, done]
        doneToolbar.items = items
        doneToolbar.sizeToFit()
        self.inputAccessoryView = doneToolbar
    }
    @objc func doneButtonAction()
    {
        self.resignFirstResponder()
    }
}

   

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.