iosswiftswiftui

SwiftUI LazyHStack in ScrollView Causes Scrolling Issues in Custom Wheel Picker


I have a Custom Horizontal Scroll Wheel that connects a model that saves value in a UserDefault using didset. When I use a regular HStack the value is correct and saves correctly to UserDefaults when you scroll to a new number but the problem is when the view is initially shown its always at a 0 when when I confirm the value passed in isnt zero. When I change it to a LazyHStack it works as indented but comes with weird bugs where it does not scroll to the correct position and sometimes breaks the scrolling. :

Where the userDefaults is being set.

   @Published var captureInterval: Int = 1 {
           didSet {
               UserDefaults.standard.set(captureInterval, forKey: "captureInterval")
           }
       }
    @Published var startCaptureInterval: Int = 2 {
           didSet {
               UserDefaults.standard.set(startCaptureInterval, forKey: "startCaptureInterval")
           }
       }
    

The View in Question:

struct WheelPicker: View {
    
    var count: Int      //(20 passed in)
    var spacing: CGFloat = 80
    @Binding var value: Int
    
    //TODO: Add some Haptic Feedback
    
    var body: some View {
        GeometryReader { geometry in
            let size = geometry.size    //Size of the entire Parent Container
            let horizontalPadding = geometry.size.width / 2
            
            ScrollView(.horizontal, showsIndicators: false) {
               LazyHStack(spacing: spacing) {
                    ForEach(0..<count, id: \.self) { index in
                        Divider()
                            .foregroundStyle(.blue)
                            .frame(width: 0, height: 30, alignment: .center)
                            .frame(maxHeight: 30, alignment: .bottom)
                            .overlay(alignment: .bottom) {
                                Text("\(index)")
                                    .font(.system(size: index == value ? 25 : 20))
                                    //.fontWeight(.semibold)
                                    .textScale(.secondary)
                                    .fixedSize()    //Not letting any parent Resize the text
                                    //.offset(y: 38)   //how much to push off the bottom of the text from bottom of Divider
                                    .offset(y: index == value ? 43 : 38)    //adjusting for the 5 extra points the bigger text has
                            }
                           
                    }
                }
                .scrollTargetLayout()
                .frame(height: size.height)
                
                
            }
            .scrollIndicators(.hidden)
            .scrollTargetBehavior(.viewAligned)                     //will help us know which index is at the center
            .scrollPosition(id: Binding<Int?>(get: {    //needed because scrollPositon wont accept a Binding int
                let position: Int? = value      
                return position
            }, set: { newValue in
                    if let newValue {
                    value = newValue    //simply taking in the new value and pass it back to the binding
                }
            }))
            .overlay(alignment: .center, content: {
                Rectangle()     //will height the active index in wheelpicker by drawing a small rectangle over it
                    .frame(width: 1, height: 45) //you can adjust its height to make it bigger
            })
            .safeAreaPadding(.horizontal, horizontalPadding)        //makes it start and end in the center
            
          
        }
    }
    
    
   
}

#Preview {
    @Previewable @State var count: Int = 30
    @Previewable @State var value: Int = 5
    WheelPicker(count: count, value: $value)
}

What I have tried so far its to create an Int? variable that I initialize in .task to equals he value, pass it in as a the scrollPosition and add an OnTap In the HStack to set both the value and new Int? variable. This makes the scrolling and initial position work perfect but wont set the userDefaults anymore.


Solution

  • It seems like the scroll just isn't scrolling to the initial scroll position for some reason. You can scroll it manually in onAppear using a ScrollViewReader.

    ScrollViewReader { scrollProxy in
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: spacing) {
                // ....
            }
            .scrollTargetLayout()
            .frame(height: size.height)
        }
        .scrollIndicators(.hidden)
        .scrollTargetBehavior(.viewAligned)
        .scrollPosition(id: Binding($value)) // you can just create a Binding<Int?> like this
        .onAppear {
            scrollProxy.scrollTo(value) // manually scroll to the initial position
        }
    }
    .overlay(...)