I'm trying to create a color PickerView like in iOS 16:
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:
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
}
)
}
}
}