1

I'm trying to create my own keyboard view (I need a decimal keyboard, with a guaranteed . and not , which is shown in Europe).

I have it working great except in landscape one of the keys is not the correct size. (Colours to show frame sizes)

enter image description here enter image description here

The problem seems to happen on iOS 15 when the height of the full view shrinks to around 200px. On iOS 14 you can see it's causing issue as it has padding on the bottom row. Removing the .resizable() form the Image seems to fix that so not sure if it's related.

I've simplified the code here to a view that shows the issue.

Here is the code that builds each key:

enum KeyboardKey: String, CaseIterable {
    
    case one = "1"
    case two = "2"
    case three = "3"
    case four = "4"
    case five = "5"
    case six = "6"
    case seven = "7"
    case eight = "8"
    case nine = "9"
    case zero = "0"
    case dot = "."
    case backspace = "backspace"
    
    @ViewBuilder
    func view() -> some View {
        switch self {
        case .one, .two, .three, .four, .five, .six, .seven,
                .eight, .nine, .zero:
            Text(self.rawValue)
                .font(.title)
                .foregroundColor(.black)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.white)
                .cornerRadius(6)
                .shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1)
        case .dot:
            Text(self.rawValue)
                .font(.title)
                .foregroundColor(.black)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.red)
                .cornerRadius(6)
        case .backspace:
            HStack(alignment: .center, spacing: 0) {
                Image(systemName: "delete.left")
                    .resizable()
                    .frame(width: 20, height: 20)
                    .background(Color.green)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.red)
            .cornerRadius(6)
        }
    }
}

and the keyboard view is built up with this:

struct IPKeyboardButtons: View {
    
    let rows: [[KeyboardKey]] = [
        [.one, .two, .three],
        [.four, .five, .six],
        [.seven, .eight, .nine],
        [.dot, .zero, .backspace]
    ]
    
    var body: some View {
        VStack {
            ForEach(0..<rows.count, id: \.self) { rowIndex in
                
                let row = rows[rowIndex]
                HStack(spacing: 5) {
                    ForEach(row, id: \.self) { key in
                        Button(action: {
                            print("Tapped: \(key)")
                        }) {
                            key.view()
                        }
                    }
                }
            }
        }
        .padding(5)
        .padding(.bottom, 40)
        .background(Color.keyboardBackground)
    }
}

You can view it in a SwiftUI preview with this:

struct App_Previews: PreviewProvider {
    static var previews: some View {
        IPKeyboardButtons()
            .frame(height: 200)
    }
}

Removing the .frame(height: 200) make it look perfect.

I've tried using a ZStack, VStack, AnyView() you name it, to wrap the key, but they all have the same effect. I want the image to be smaller than the button, so I set it to be 20x20 then set the container view to .infinity but it seems to be ignored.

10
  • A minimal reproducible example would be helpful. You left rows out or your code. Commented Sep 8, 2021 at 0:40
  • You are right, I assumed I was missing something really obvious but have now added the full code for the keyboard. Commented Sep 8, 2021 at 10:13
  • Have you considered, just replacing coma with dot in text field instead of creating custom keyboard? Commented Sep 8, 2021 at 10:14
  • It's for entering IP addresses. Yes I could use the decimal keypad but the user will see a comma when they know they want to press a dot. It's not a great UX Commented Sep 8, 2021 at 10:15
  • I've just twigged that this could be an iOS beta issue. I'm using Xcode 13 beta 5. I've just built it with Xcode 12 and the spacing is all over the place, but that button looks fine. Commented Sep 8, 2021 at 10:24

1 Answer 1

1

I would use a LazyVGrid to handle this. The solution you are trying to use was really an iOS 13 workaround for making grids. LazyVGrid handles this elegantly.

struct IPKeyboardButtons: View {
    
    let columns = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible())
    ]
    
    var body: some View {
        LazyVGrid(columns: columns, alignment: .center, spacing: 20) {
            ForEach(KeyboardKey.allCases, id: \.self) { key in
                Button(action: {
                    print("Tapped: \(key)")
                }) {
                    key.view()
                }
            }
        }
        .padding(5)
        .padding()
        .background(Color.gray)
    }
}

I changed the enums view() function slightly to add some padding around the symbols(number or SF Font). Also, you can treat an SF Font like text. They are designed to be used right along with text and using text modifiers like .font:

enum KeyboardKey: String, CaseIterable {
    
    case one = "1"
    case two = "2"
    case three = "3"
    case four = "4"
    case five = "5"
    case six = "6"
    case seven = "7"
    case eight = "8"
    case nine = "9"
    case zero = "0"
    case dot = "."
    case backspace = "backspace"
    
    @ViewBuilder
    func view() -> some View {
        switch self {
        case .one, .two, .three, .four, .five, .six, .seven,
                .eight, .nine, .zero:
            Text(self.rawValue)
                .font(.title)
                .foregroundColor(Color.black)
                .padding() // optional, but this will increase the size of the button itself
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.white)
                .cornerRadius(6)
                .shadow(color: Color.black.opacity(0.5), radius: 1, x: 0, y: 1)
        case .dot:
            Text(self.rawValue)
                .font(.title)
                .foregroundColor(Color.black)
                .padding() // optional, but this will increase the size of the button itself
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.red)
                .cornerRadius(6)
        case .backspace:
            Image(systemName: "delete.left")
                .font(.title)
                .background(Color.green)
                .padding() // optional, but this will increase the size of the button itself
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.red)
                .cornerRadius(6)
        }
    }
}

This is the view I put it in so it would behave in a real world test case. I would stay away from forcing frames in the preview as that will give artificial constraints and does not respect safe areas:

struct KeyboardView: View {
    
    @State var textBinding = ""
    
    var body: some View {
        VStack {
            TextField("TextField", text: $textBinding)
            Spacer()
            IPKeyboardButtons()
        }
    }
}

This code is much simpler and easier to maintain.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you. As soon as I saw the notification that said "I would use a LazyVGrid" I added the LazyVGrid to my project and it worked :) I will look though and add some of your changes too, but the Lazy grid was the answer. Thank you

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.