UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: SWIFTUI/iOS16 ToolbarItem placement .keyboard; button disappears (real device only)

Forums > SwiftUI

I added a toolbar button to the keyboard and on tapping it, the focus shifts to the next field. Tapping it again shift to the next field, and so on... On the simulator this works flawlessly. In fact, the keyboard stays up all the time and the button is always there (as it should be). HOWEVER! On a real device after tapping the button the keyboard shortly disappears and when it pops up again, the next button is no longer there. Only exiting the view completely and re-entering will make the button show again, once!

NOTE: The button disappears also when I manually tap/enter into a new TextField!

         .toolbar {
                ToolbarItemGroup(placement: .keyboard){
                    HStack {
                        EmptyView()
                            .frame(maxWidth: .infinity, alignment: .leading)

                        Button{
                            setNextFocus()
                        } label: {
                            HStack {
                                Text("Next")
                                Image(systemName: "rectangle.trailinghalf.inset.filled.arrow.trailing")
                            }
                            .foregroundColor(.orange)
                        }.frame(maxWidth: .infinity, alignment: .trailing)
                    }
                }
            }

Been at this for a whole day now trying all kinds of workarounds, seems like a genuine bug? Simulator works fine, real phone not so ...

2      

Just checked the below code and it works on the device as expected. You seem have redundant HStack in and EmptyView in ToolbarItemGroup, so I removed from my example. Not knowing the logic in changing focus state it's difficult to say what is the reason for such behavior you described...

enum NameField {
    case focus1
    case focus2
    case focus3
}

struct ContentView: View {
    @State private var text1 = ""
    @State private var text2 = ""
    @State private var text3 = ""

    @FocusState private var nameField: NameField?

    var body: some View {
        NavigationStack {
            VStack {
                TextField("Field One", text: $text1)
                    .focused($nameField, equals: .focus1)
                TextField("Field Two", text: $text2)
                    .focused($nameField, equals: .focus2)
                TextField("Field Three", text: $text3)
                    .focused($nameField, equals: .focus3)
            }
            .textFieldStyle(.roundedBorder)
            .padding(.horizontal)
            .toolbar {
                ToolbarItemGroup(placement: .keyboard){
                    Spacer()

                    Button{
                        setNextFocus()
                    } label: {
                        HStack {
                            Text("Next")
                            Image(systemName: "rectangle.trailinghalf.inset.filled.arrow.trailing")
                        }
                        .foregroundColor(.orange)
                    }
                }
            }
        }
    }

    func setNextFocus() {
        if nameField == .focus1 {
            nameField = .focus2
        } else if nameField == .focus2 {
            nameField = .focus3
        } else if nameField == .focus3 {
            nameField = .focus1
        } else {
            // choose what you want to do
        }
    }
}

3      

Thanks for trying to help me! The EmptyView() is there because when I put a Spacer() there, the button disappears completely!

I've a slightly different setup of focusState.... The View is presented as a sheet (FullScreenCover)

Here's the complete View Code (it's a bit big, will be refactored at some point, for now I'm trying to get the last bits working and improved!)

import SwiftUI
import Combine

struct GameDetailsView: View {
    // MARK: - Properties & State
    var game: Game

    @State private var selectedImage: UIImage?

    @EnvironmentObject var gamesViewModel: GamesViewModel
    @EnvironmentObject var dataController: DataController
    @Environment(\.dismiss) var dismiss

    @State private var showingDeleteAlert = false
    @State private var showingImagePickerOptions = false
    @State private var imagePickerSource: ImagePickerSource? = nil

    @FocusState private var focus: AnyKeyPath?

    @State private var title: String
    @State private var comments: String
    @State private var platform: String
    @State private var genre: String
    @State private var year: String
    @State private var publisher: String
    @State private var developer: String
    @State private var iconImageURL: String
    @State private var iconImage: Data
    @State private var creationDate: Date
    @State private var lastEdited: Date

    // MARK: - Computed Properties (views)
    var gameImage: Image {
        if let selectedImage = selectedImage {
            return Image(uiImage: selectedImage)
        } else {
            return Image(uiImage: game.CoreDataImage)
        }
    }

    // MARK: - Body
    var body: some View {
        NavigationStack {
            ZStack(alignment: .bottom) {
                VStack {
                    // Dismiss
                    HStack {
                        Spacer()

                        Button {
                            dismiss()
                        } label: {
                            Image(systemName: "x.square")
                        }
                        .font(.title2)
                        .foregroundColor(.systemPurple)
                    }
                    .padding([.horizontal, .top])
                    .padding(.bottom, 5)

                    ScrollView {
                        // Required Game details
                        VStack {
                           Text("Required Game details")
                                .padding(.top)
                                .font(.title2)

                            Text("Tap on the logo image to select an image for the game!")
                                .font(.caption)
                                .lineLimit(2)

                            HStack {
                                Spacer()

                                gameImage
                                    .resizable()
                                    .aspectRatio(contentMode: .fill)
                                    .frame(width: 150, height: 150)
                                    .cornerRadius(8)
                                    .onTapGesture {
                                        showingImagePickerOptions = true
                                    }

                                Spacer()
                            }
                            .padding()

                            TextField("Game title", text: $title, onCommit: setNextFocus)
                                .focused($focus, equals: \Self.title)

                            TextField("Platform", text: $platform, onCommit: setNextFocus)
                                .focused($focus, equals: \Self.platform)

                            TextField("Year of release", text: $year.max(4), onCommit: setNextFocus)
                                .focused($focus, equals: \Self.year)
                                .keyboardType(.numbersAndPunctuation)
                            //limit to numbers only (on all platforms)
                                .onReceive(Just(year)) { newValue in
                                    let filtered = newValue.filter { Set("0123456789").contains($0) }
                                    if filtered != newValue {
                                        self.year = filtered
                                    }
                                }
                        }
                        .padding(.bottom)
                        .background(
                            RoundedRectangle(cornerRadius: 8)
                                .strokeBorder(.orange, lineWidth: 2)
                        )
                        .padding()

                        // Additional details
                        VStack {
                            Text("Additional details")
                                .padding(.top)
                                .font(.title2)

                            TextEditor(text: $comments)
                                .focused($focus, equals: \Self.comments)
                                .padding(.horizontal)
                                .frame(minHeight: 88)
                                .scrollContentBackground(.hidden)
                                .background(.orange.opacity(0.2))
                                .cornerRadius(8)
                                .foregroundColor(.white)
                                .padding(.horizontal)
                                .overlay(alignment: .leading) {
                                    (comments.isReallyEmpty ? Text("Comments").foregroundColor(.white.opacity(0.3)) : Text("")).padding(.leading, 30)
                                }

                            TextField("Genre", text: $genre, onCommit: setNextFocus)
                                .focused($focus, equals: \Self.genre)

                            TextField("publisher", text: $publisher, onCommit: setNextFocus)
                                .focused($focus, equals: \Self.publisher)

                            TextField("Developer", text: $developer, onCommit: setNextFocus)
                                .focused($focus, equals: \Self.developer)
                        }
                        .padding(.bottom)
                        .background(RoundedRectangle(cornerRadius: 8)
                            .strokeBorder(.orange, lineWidth: 2))
                        .padding()

                        // Dates
                        VStack {
                            Text("Created")
                                .padding(.top)
                                .padding(.bottom, 5)
                                .font(.title2)

                            Text("Created on \(creationDate.customMediumToString)\nLast edited on \(lastEdited.customMediumToString)")
                                .font(.caption)
                                .lineLimit(2)

                        }
                        .frame(maxWidth: .infinity)
                        .padding(.bottom)
                        .background(RoundedRectangle(cornerRadius: 8)
                            .strokeBorder(.orange, lineWidth: 2))
                        .padding()
                        // Make sure we can scroll all the way down and not be obscured by the buttons
                        .padding(.bottom, 80)
                    }
                }
                .textFieldStyle(GamesTextFieldStyle())
                .alert(isPresented: $showingDeleteAlert) {
                    Alert(title: Text("Delete this Game?"),
                          message: Text("Are you sure you want to delete this Game?\nAll related annotations and images will also be deleted!\nThis can not be undone!"),
                          primaryButton: .cancel(),
                          secondaryButton: .destructive(Text("Delete"), action: delete))
                }
                .confirmationDialog("Select", isPresented: $showingImagePickerOptions) {
                    Button("Camera") { imagePickerSource = .camera }
                    Button("Photo library") { imagePickerSource = .photos }
                    Button("Documents") { imagePickerSource = .documents }
                    Button("Web") { imagePickerSource = .web }
                    Button("Paste from clipboard") { pasteImageFromClipboard() }
                    Button("Cancel", role: .cancel) { }
                } message: {
                    Text("Select an Image source...")
                }
                .sheet(item: $imagePickerSource) { source in
                    switch source {
                    case .camera:
                        CameraPicker(selectedImage: $selectedImage)
                    case .photos:
                        CameraPicker(sourceType: .photoLibrary, selectedImage: $selectedImage)
                    case .documents:
                        DocumentsPicker(selectedImage: $selectedImage)
                    case .web:
                        SafariWebPickerWithOverlay(selectedImage: $selectedImage)
                    }
                }
                .onDisappear(perform: deleteInvalidNewGame)
            }
            // Hide the keyboard when tapped anywhere in the view not user interactive!
            .onTapGesture {
                withAnimation(.easeInOut) {
                    self.hideKeyboard()
                }
            }
            .toolbar {
                #warning("TO DO: Bug, on real devices/iphones the keyboard button disappears upon losing focus by either manually tapping on another field or tapping the button")
                ToolbarItemGroup(placement: .keyboard) {
                    HStack {
                        EmptyView()
                            .frame(maxWidth: .infinity, alignment: .leading)

                        Button {
                            setNextFocus()
                        } label: {
                            HStack {
                                Text("Next")
                                Image(systemName: "rectangle.trailinghalf.inset.filled.arrow.trailing")
                            }
                            .foregroundColor(.systemOrange)
                        }
                    }
                    .frame(maxWidth: .infinity, alignment: .trailing)
                }
            }
            .safeAreaInset(edge: .bottom) {
                // Save and Delete Buttons
                // Slide up with the keyboard when it appears
                HStack {
                    Button {
                        print("Save tapped")
                        save()
                    } label: {
                        HStack {
                            Text("Save")
                            AppSymbols.save
                        }
                    }
                    .frame(height: 44)
                    .frame(maxWidth: .infinity)
                    .background {
                        RoundedRectangle(cornerRadius: 8)
                            .fill(.orange)
                    }
                    .disabled(missingRequiredGameDetails())
                    .opacity(missingRequiredGameDetails() ? 0.3 : 1)

                    Spacer(minLength: 20)

                    Button {
                        print("Delete tapped")
                        showingDeleteAlert.toggle()
                    } label: {
                        HStack {
                            Text("Delete")
                            AppSymbols.delete
                        }
                    }
                    .frame(height: 44)
                    .frame(maxWidth: .infinity)
                    .background {
                        RoundedRectangle(cornerRadius: 8)
                            .fill(.red)
                    }
                }
                .padding()
                .background(.ultraThinMaterial)
                .foregroundColor(.white)
            }
            .foregroundColor(.white)
            .background(
                LinearGradient(colors: [
                    Color.systemOrange,
                    Color.systemPurple,
                    Color.systemPurple,
                    Color.systemPurple,
                    Color.systemPurple
                ], startPoint: .top, endPoint: .bottom)
        )
        }
    }

    // MARK: - Init
    init(game: Game) {
        self.game = game

        //populate the local @State properties
        _title = State(wrappedValue: game.viewTitle)
        _comments = State(wrappedValue: game.viewComments)
        _platform = State(wrappedValue: game.viewPlatform)
        _genre = State(wrappedValue: game.viewGenre)
        _year = State(wrappedValue: game.viewYear)
        _publisher = State(wrappedValue: game.viewPublisher)
        _developer = State(wrappedValue: game.viewDeveloper)
        _iconImageURL = State(wrappedValue: game.viewIconImageURL)
        _iconImage = State(wrappedValue: game.viewIconImage)
        _creationDate = State(wrappedValue: game.viewCreationDate)
        _lastEdited = State(wrappedValue: game.viewLastEdited)

        // enable different background colors form / TextEditor
        UITableView.appearance().backgroundColor = .clear
        UITextView.appearance().backgroundColor = .clear
    }

    // MARK: - Methods
    // Shifts focus to next Textfield on enter
    func setNextFocus() {
        switch focus {
        case \Self.title:
            focus = \Self.platform
        case \Self.platform:
            focus = \Self.year
        case \Self.year:
            focus = \Self.comments
        case \Self.comments:
            focus = \Self.genre
        case \Self.genre:
            focus = \Self.publisher
        case \Self.publisher:
            focus = \Self.developer
        default:
            focus = \Self.title
        }
    }

    private func pasteImageFromClipboard() {
        let pasteboard = UIPasteboard.general

        //image was returned by Copy
        if pasteboard.hasImages {
            guard let image = pasteboard.image else { return }
            selectedImage = image
            //Image Url was returned by Copy
        } else if pasteboard.hasURLs {
            guard let url = pasteboard.url else { return }
            if let data = try? Data(contentsOf: url) {
                if let image = UIImage(data: data) {
                    selectedImage = image
                }
            }
        }
        pasteboard.items.removeAll()
    }

    // Minimum details required to be a valid game
    private func missingRequiredGameDetails() -> Bool {
        title.isReallyEmpty || platform.isReallyEmpty || year.isReallyEmpty
    }

    // On dismiss , delete a newly added Game that hasn't the minimum required details
    private func deleteInvalidNewGame() {
        guard title == "New Game", missingRequiredGameDetails() == true else { return }
        gamesViewModel.delete(game)
    }

    func update() {
        game.title = title
        game.comments = comments
        game.platform = platform
        game.genre = genre
        game.year = year
        game.publisher = publisher
        game.developer = developer
        game.lastEdited = Date.now
        if let selectedImage = selectedImage {
            game.iconImage = selectedImage.jpegData(compressionQuality: 1.0)
        }
    }

    func save() {
        update()
        gamesViewModel.save(game)
        dismiss()
    }

    func delete() {
        gamesViewModel.delete(game)
        dismiss()
    }
}

3      

Your simplified View works on my real device as intended! My View works like that only in the Simulator. What's the difference ??

2      

I solved it! It's a navigation issue: The GameDetailsView is presented as a FullScreenCover. The View that presents it is part of a NavigationStack with a path (stored in A Router object, which is part of the Environment)

class Router: ObservableObject {
    @Published var path = NavigationPath()

    func reset() {
      path = NavigationPath()
    }
}

The solution is to add the environment variable to the top of the View and replacing plain NavigationStack with the router parameter:

@EnvironmentObject var router: Router

(...)

NavigationStack(path: $router.path) {
   // View Contents
}

This also makes that using a Spacer() works like it should! The Toolbar code is now:

            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                        Spacer()
                        Button {
                            setNextFocus()
                        } label: {
                            HStack {
                                Text("Next")
                                Image(systemName: "rectangle.trailinghalf.inset.filled.arrow.trailing")
                            }
                            .foregroundColor(.systemOrange)
                        }
                }
            }

So glad! Thanks!

2      

I like this. I like a game called geometry dash. and i want to make some changes in the apk version of this game. So how can I do it?

2      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

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.