0

I am developing a basic passcode entry screen, consisting off a top Stack to display currently entry, then some HStack's displaying the numbers

    VStack(){
        HStack(spacing: 20){
            ForEach(codes,id: \.self){i in
                Text("*")
            }
        }
        
        HStack(){
            <Number 1 - 3>
        }
        HStack(){
            <Number 4 - 6>
        }
        HStack(){
            <Number 7 - 9>
        }
        HStack(){
            <Number 0>
        }
    }

This issue im facing is when there is no passcode entered the top HStack dosnt use up any space, so has a vertical height of 0, when I enter a code, it forces the whole view to jump a little as the view resizes.

How can I stop that so

Display Name
  • 1,025
  • 2
  • 15
  • 34

1 Answers1

2

If I'm being honest, it was quiet fun to build ! Don't forget to mark this answer as the right one if it solved your issue. ✅

PROBLEM

The jumping effect is due to SwiftUI updating all views positions based on available space calculated based on your content (passcode digits). The font, font weight, text size, etc… all has an effect on the available space left for other views.

SOLUTION

To avoid that, you need to a predefined frame that will let the parent view know that your digits will never take more space. Doing so, each update won't effect the position of any other view because the allocated top space would always be size you specified and not the digits sizes (or absence).

CODE

import SwiftUI
import Combine


// Using Combine to manage digits and future network calls…
class PasscodeManager: ObservableObject {
    let codesQuantity = 4
    @Published var codes = [Int]()
}


struct PasscodeView: View {

    @StateObject private var manager = PasscodeManager()

    var body: some View {
        VStack {
            Spacer()
            // Dots placeholders and passcode digits
            selectedCodes
            Spacer()
            // Numberpad
            PasscodeLine(numbers: 1...3) { add(number: $0) }
            PasscodeLine(numbers: 4...6) { add(number: $0) }
            PasscodeLine(numbers: 7...9) { add(number: $0) }
            PasscodeLine(numbers: 0...0) { add(number: $0) }
            Spacer()
        }
        .padding()
    }

    var selectedCodes: some View {
        let minDots = manager.codes.count == manager.codesQuantity ? 0:1
        let maxDots = manager.codesQuantity - manager.codes.count
        return HStack(spacing: 32) {
            ForEach(manager.codes, id: \.self) { Text("\($0)") }
            if maxDots != 0 {
                ForEach(minDots...maxDots, id: \.self) { _ in
                    Circle().frame(width: 12)
                }
            }
        }
        .font(.title.bold())
        // Setting a default height should fix your problem.  
        .frame(height: 70)
    }

    func add(number: Int) {
        guard manager.codes.count < manager.codesQuantity else { return }
        manager.codes.append(number)
    }

}

struct PasscodeLine: View {
    let numbers: ClosedRange<Int>
    var select: (Int) -> Void
    var body: some View {
        HStack {
            ForEach(numbers, id: \.self) { number in
                Spacer()
                Button(action: { select(number) },
                       label: {
                        Text("\(number)")
                            .font(.title)
                            .fontWeight(.medium)
                            .foregroundColor(Color(.label))
                            .padding(32)
                            .background(Color(.quaternarySystemFill))
                            .clipShape(Circle())
                       })
            }
            Spacer()
        }
    }
}

RESULT

enter image description here

Hans Rietmann
  • 396
  • 5
  • 13