Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI: Number pad layout

Tags:

swift

swiftui

I'm building a custom number pad in Swift UI. I want the buttons to be arranged in a grid that fills the screen. The only way I've found to do this for far is to use GeometryReader but this seems like overkill for this simple task. Is there a better way to write this?

GeometryReader { geometry in
    HStack(spacing: 0) {
        ForEach([a, b, c]) { n in
            Button(action: { self.add(n) }) { Text("\(n)") }
                .frame(width: geometry.size.width/3)
        }
    }
}.frame(height: 80)

This results in this, which is how I want it to look. Just curious if there is a good way to do this without GeometryReader.

enter image description here

like image 993
keegan3d Avatar asked Dec 03 '22 10:12

keegan3d


1 Answers

Yes, you can do this without GeometryReader. You just have to make each key view expandable. Then your HStack and VStack will take care of the rest.

A Button tightly wraps its label subview, so you need to make the label subview expandable. A Color is a View that expands to fill whatever space it's given, so let's use Color.clear as the button's label, and overlay the real Text label on the Color. I think we should define a KeyPadButton View for this:

struct KeyPadButton: View {
    var key: String

    var body: some View {
        Button(action: { self.action(self.key) }) {
            Color.clear
                .overlay(RoundedRectangle(cornerRadius: 12)
                    .stroke(Color.accentColor))
                .overlay(Text(key))
        }
    }

    enum ActionKey: EnvironmentKey {
        static var defaultValue: (String) -> Void { { _ in } }
    }

    @Environment(\.keyPadButtonAction) var action: (String) -> Void
}

extension EnvironmentValues {
    var keyPadButtonAction: (String) -> Void {
        get { self[KeyPadButton.ActionKey.self] }
        set { self[KeyPadButton.ActionKey.self] = newValue }
    }
}

#if DEBUG
struct KeyPadButton_Previews: PreviewProvider {
    static var previews: some View {
        KeyPadButton(key: "8")
            .padding()
            .frame(width: 80, height: 80)
            .previewLayout(.sizeThatFits)
    }
}
#endif

I've defined an EnvironmentKey so that we can use the environment to pass the action callback from the higher-level keypad view to all the buttons.

KeyPadButton looks like this:

KeyPadButton preview

If you don't like the border, just remove that modifier.

Note that I've manually set the size of the button in the PreviewProvider. Since this view is expandable, it'll preview at the device size by default, and we don't need to see one giant button.

Now let's define a KeyPadRow view to lay out one row of buttons:

struct KeyPadRow: View {
    var keys: [String]

    var body: some View {
        HStack {
            ForEach(keys, id: \.self) { key in
                KeyPadButton(key: key)
            }
        }
    }
}

Now we can define the KeyPad view to lay out the entire keypad and provide the action for the buttons:

struct KeyPad: View {
    @Binding var string: String

    var body: some View {
        VStack {
            KeyPadRow(keys: ["1", "2", "3"])
            KeyPadRow(keys: ["4", "5", "6"])
            KeyPadRow(keys: ["7", "8", "9"])
            KeyPadRow(keys: [".", "0", "⌫"])
        }.environment(\.keyPadButtonAction, self.keyWasPressed(_:))
    }

    private func keyWasPressed(_ key: String) {
        switch key {
        case "." where string.contains("."): break
        case "." where string == "0": string += key
        case "⌫":
            string.removeLast()
            if string.isEmpty { string = "0" }
        case _ where string == "0": string = key
        default: string += key
        }
    }
}

Finally, let's define a ContentView that shows the string above the keypad:

struct ContentView : View {
    var body: some View {
        VStack {
            HStack {
                Spacer()
                Text(string)
            }.padding([.leading, .trailing])
            Divider()
            KeyPad(string: $string)
        }
        .font(.largeTitle)
            .padding()
    }

    @State private var string = "0"

}


#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
        }
    }
}
#endif

Here's the final result:

demo

like image 116
rob mayoff Avatar answered Dec 29 '22 03:12

rob mayoff