最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

ios - SwiftUI LazyHStack in ScrollView Causes Scrolling Issues in Custom Wheel Picker - Stack Overflow

programmeradmin5浏览0评论

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.

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.

Share Improve this question asked Mar 24 at 12:13 john smithjohn smith 1516 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 0

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(...)

If you use an HStack with .scrollPosition(id:_), it won't know which one is selected on appear, since the scroll position binding is initially nil. To fix this, you'd normally set a value for the scroll position binding on appear, which is not possible with the custom binding as you used it.

To avoid other headaches, I'd simply create a local state and then "sync" it with the original binding:

@State private var currentValue: Int?

Use it for the .scrollPosition:

.scrollPosition(id: $currentValue, anchor: .center)

Then set a value on appear and update the original binding whenever it changes:

.onAppear {
    currentValue = value
}
.onChange(of: currentValue) {
    if let newValue = currentValue {
        value = newValue
    }
}

As for the rest of it, see the full code below on how it could be structured to avoid having to use set frame width and height and have more flexibility:

import SwiftUI

struct WheelPicker: View {
    
    //Parameters
    var count: Int
    var spacing: CGFloat = 80
    @Binding var value: Int
    
    @State private var currentValue: Int?
    
    //Body
    var body: some View {
        
        GeometryReader { geometry in
            let size = geometry.size    //Size of the entire Parent Container
            let horizontalPadding = (size.width - spacing) / 2 // <-  subtract the width of the content
            
            VStack(spacing: 5) {
                Rectangle()
                    .frame(width: 1)
                
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(spacing: 0) {
                        ForEach(0..<count, id: \.self) { index in
                            
                            Color.clear// <- fixed width container since numbers may vary in size/width
                                .containerRelativeFrame(.horizontal, alignment: .center)
                                .overlay {
                                    Text("\(index)")
                                }
                                //Use scrollTransition to scale instead of font size
                                .scrollTransition { content, phase in
                                    content
                                        .scaleEffect(phase.isIdentity ? 1.3 : 1)
                                        .opacity(phase.isIdentity ? 1 : 0.4)
                                }
                        }
                    }
                    .scrollTargetLayout()
                }
                .scrollIndicators(.hidden)
                .scrollTargetBehavior(.viewAligned)
                .scrollPosition(id: $currentValue, anchor: .leading)
                .sensoryFeedback(.alignment, trigger: value) // <- haptic feedback
                .contentMargins(.horizontal, horizontalPadding) //makes it start and end in the center
            }
            .onAppear {
                currentValue = value
            }
            .onChange(of: currentValue) {
                if let newValue = currentValue {
                    value = newValue
                }
            }
            .frame(height: 100) // <- height of the picker
            .containerRelativeFrame(.vertical) // <- optional, to center everything vertically
        }
    }
}

#Preview {
    @Previewable @State var count: Int = 30
    @Previewable @State var value: Int = 8
    WheelPicker(count: count, spacing: 80, value: $value)
        .onChange(of: value) {
            print("value is: \(value)") // <- this can be used to update other things, like UserDefaults
        }
}
发布评论

评论列表(0)

  1. 暂无评论