Home > Software design >  Offset couldn't work as expected in SwiftUI
Offset couldn't work as expected in SwiftUI

Time:01-12

I'm trying to create a color PickerView like in iOS 16: enter image description here

The x position of the circle view in the slidebar is based on a property passed from outside, if the property value is 0.5, the circle will place on the center of the bar, if it's 0, the circle will placed in the beginning. Below is the sample code:

struct ColorSliderView: View {

@State private var offsetX: CGFloat

init(saturation: CGFloat, availableSize: CGSize) {
    self.offsetX = saturation * availableSize.width
}


  var body: some View {
    ZStack(alignment: .leading) {
      RoundedRectangle(cornerRadius: 20)
        .fill(gradient)
        .frame(width: size.width, height: strokeWidth)
        .overlay {
          RoundedRectangle(cornerRadius: 20)
            .stroke(.black.opacity(0.02), lineWidth: 2)
        }
      Circle()
        .fill(.white)
        .overlay {
          Circle()
            .fill(slidingColor)
            .frame(width: dragCircleSize - 4)
        }
        .frame(width: dragCircleSize)
        .offset(x: offsetX)
        .gesture(DragGesture().onChanged(onDrag(value:)))
  //    .onChange(of: initColor) { _ in
  //          delay {
  //            withAnimation {
  //              self.offsetX = progress  * (size.width - dragCircleSize)
  //            }
  //            log("onChange: offsetX: \(offsetX)")
  //          }
  //        }
  //        .onAppear {
  //          self.offsetX = progress  * (size.width - dragCircleSize)
   //          log("onAppear: offsetX: \(offsetX)")
   //        }

}

And ColorSliderView will be used in another View named ColorPannel

ColorSliderView(saturation: someSaturation, size: someSize)

And when user select other color dot, the someSaturation value will changed. This will cause the init method of ColorSliderView be called, and I expect the circleView in the slidebar be placed in the right place, but it didn't, it just kept stay in the last place. I print the offset value and debugged the code, it show the offset value is right, but the colorDot circle just didn't moved.

I tried to delay change the offset, but it also didn't work. I think the code is not complex and I just couldn't understand why it didn't work.

Please help if you have any idea, and thanks you all in advance.

CodePudding user response:

I think it's better to pass the saturation value as a @Binding to the slider, because you sure want to use it somewhere in the parent view. And you might also want to pass the base color.

I took the freedom to optimize some other parts of the code:

enter image description here

struct ContentView: View {
    
    @State private var saturation: Double = 0

    var body: some View {
        VStack {

            HStack { // set saturation in Content view, Slider will react
                Button("10%") { saturation = 0.1 }
                Button("50%") { saturation = 0.5 }
                Button("70%") { saturation = 0.7 }
            }
            
            ColorSliderView(saturation: $saturation,
                            color: .yellow,
                            size: CGSize(width: 300, height: 100))
            Text(saturation.formatted(.percent))
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.gray.opacity(0.5))
    }
}


struct ColorSliderView: View {
    
    @Binding var saturation: Double
    let color: Color
    let size: CGSize
    
    let strokeWidth = 20.0
    let dragCircleSize = 40.0

    var gradient: LinearGradient {
        LinearGradient(colors: [.white, color], startPoint: .leading, endPoint: .trailing)
    }
    
    var fill: Color {
        color.opacity(saturation   dragSaturationOffset)
    }
    
    var effectiveWidth: Double { size.width - dragCircleSize }
    
    @State private var dragSaturationOffset = 0.0
    
    var body: some View {
        ZStack(alignment: .leading) {
            RoundedRectangle(cornerRadius: 20)
                .fill(gradient)
                .frame(width: size.width, height: strokeWidth)
                .overlay {
                    RoundedRectangle(cornerRadius: 20)
                        .stroke(.black.opacity(0.02), lineWidth: 2)
                }
            Circle()
                .fill(.white)
                .overlay {
                    Circle()
                        .fill(fill)
                        .frame(width: dragCircleSize - 4)
                }
                .frame(width: dragCircleSize)
                .offset(x: effectiveWidth * (saturation   dragSaturationOffset))
            
                .gesture(DragGesture()
                    .onChanged { value in
                        dragSaturationOffset = value.translation.width / effectiveWidth
                        dragSaturationOffset = max(dragSaturationOffset, -saturation)
                        dragSaturationOffset = min(dragSaturationOffset, 1-saturation)
                    }
                    .onEnded { _ in
                        saturation  = dragSaturationOffset
                        dragSaturationOffset = 0
                    }
                )
        }
    }
}
  • Related