SwiftUI中如何将滑块固定范围映射至动态范围?
滑块与画布拖拽的映射实现需求及代码
需求说明
- 紫色圆形置于左侧、黄色圆形置于右侧
- 将取值范围为-1.0到1.0的滑块数值映射至两个圆形:
- 滑块值为1.0时,黄色圆形处于可见画布的右边界
- 滑块值为-1.0时,紫色圆形处于可见画布的左边界
- 圆形支持拖动调整位置,画布支持拖拽操作
适配iOS 15.0、macOS 12.0的实现代码:
import SwiftUI struct ContentView: View { private let maxCanvasVisibilitySize: CGSize = CGSize(width: 400.0, height: 300.0) @State private var maxCanvasImaginarySize: CGSize = CGSize(width: 400.0, height: 300.0) @State private var canvasLastOffset: CGSize = CGSize() @State private var canvasCurrentOffset: CGSize = CGSize() @State private var canvasIsDragging: Bool = Bool() private let circleSize: CGSize = CGSize(width: 50.0, height: 50.0) @State private var circleLastOffset: CGSize = CGSize(width: 500.0, height: .zero) @State private var circleCurrentOffset: CGSize = CGSize(width: 500.0, height: .zero) @State private var circleIsDragging: Bool = Bool() private let circle2Size: CGSize = CGSize(width: 100.0, height: 100.0) @State private var circle2LastOffset: CGSize = CGSize(width: -400.0, height: .zero) @State private var circle2CurrentOffset: CGSize = CGSize(width: -400.0, height: .zero) @State private var circle2IsDragging: Bool = Bool() @State private var percentage: CGFloat = CGFloat() @State private var max: CGFloat = CGFloat() @State private var min: CGFloat = CGFloat() var body: some View { VStack { GeometryReader { geometryValue in ZStack { Color.black .gesture(canvasDragGesture) Circle() .fill(Color.yellow) .frame(width: circleSize.width, height: circleSize.height) .offset(circleCurrentOffset) .offset(canvasCurrentOffset) .gesture(circleDragGesture) Circle() .fill(Color.purple) .frame(width: circle2Size.width, height: circle2Size.height) .offset(circle2CurrentOffset) .offset(canvasCurrentOffset) .gesture(circle2DragGesture) } .overlay(screenRectangle(string: "VisibilitySize", size: maxCanvasVisibilitySize, cornerRadius: 0, color: .red, topOffset: false).overlay(Color.red.frame(width: 4.0).opacity(0.5))) .overlay( screenRectangle(string: "max - min Size:", size: CGSize(width: (max - min), height: maxCanvasVisibilitySize.height), cornerRadius: 0, color: Color.green, dash: true) .overlay(Color.green.frame(width: 4.0).opacity(0.5)) .offset(x: min + (max - min)/2.0) .offset(canvasCurrentOffset) ) } .frame(width: maxCanvasVisibilitySize.width, height: maxCanvasVisibilitySize.height) SliderView(percentage: $percentage) .frame(width: maxCanvasVisibilitySize.width) } .padding() .onChange(of: percentage) { newValue in if (!canvasIsDragging) { canvasCurrentOffset.width = newValue*((max - min)/2.0) canvasLastOffset = canvasCurrentOffset } } .onAppear { makeMaxMin() canvasCurrentOffset.width = -(min + (max - min)/2.0) canvasLastOffset = canvasCurrentOffset percentage = canvasCurrentOffset.width/((max - min)/2.0) } } private func makeMaxMin() { if (circleCurrentOffset.width >= circle2CurrentOffset.width) { max = circleCurrentOffset.width + circleSize.width/2.0 } else { max = circle2CurrentOffset.width + circle2Size.width/2.0 } if (circleCurrentOffset.width <= circle2CurrentOffset.width) { min = circleCurrentOffset.width - circleSize.width/2.0 } else { min = circle2CurrentOffset.width - circle2Size.width/2.0 } } private var circleDragGesture: some Gesture { return DragGesture(minimumDistance: CGFloat.zero, coordinateSpace: .local) .onChanged() { gestureValue in circleIsDragging = true circleCurrentOffset = CGSize(width: gestureValue.translation.width + circleLastOffset.width, height: .zero) makeMaxMin() percentage = canvasCurrentOffset.width/((max - min)/2.0) } .onEnded() { gestureValue in circleLastOffset = circleCurrentOffset circleIsDragging = false } } private var circle2DragGesture: some Gesture { return DragGesture(minimumDistance: CGFloat.zero, coordinateSpace: .local) .onChanged() { gestureValue in circle2IsDragging = true circle2CurrentOffset = CGSize(width: gestureValue.translation.width + circle2LastOffset.width, height: .zero) makeMaxMin() percentage = canvasCurrentOffset.width/((max - min)/2.0) } .onEnded() { gestureValue in circle2LastOffset = circle2CurrentOffset circle2IsDragging = false } } private var canvasDragGesture: some Gesture { return DragGesture(minimumDistance: CGFloat.zero, coordinateSpace: .local) .onChanged() { gestureValue in canvasIsDragging = true canvasCurrentOffset = CGSize(width: gestureValue.translation.width + canvasLastOffset.width, height: .zero) makeMaxMin() percentage = canvasCurrentOffset.width/((max - min)/2.0) } .onEnded() { gestureValue in canvasLastOffset = canvasCurrentOffset canvasIsDragging = false } } } struct SliderView: View { init(percentage: Binding<CGFloat>) { self._percentage = percentage } @Binding var percentage: CGFloat @State private var knobLastOffset: CGFloat = CGFloat() @State private var knobCurrentOffset: CGFloat = CGFloat() @State private var knobIsDragging: Bool = Bool() private let knobSize: CGSize = CGSize(width: 50.0, height: 30.0) @State private var sliderGeometrySize: CGSize = CGSize() var body: some View { GeometryReader { geometryValue in ZStack { Color.white Color.blue.opacity(0.75) .frame(width: knobSize.width) .offset(x: knobCurrentOffset) .gesture(knobDragGesture) } .onAppear { sliderGeometrySize = geometryValue.size } .onChange(of: geometryValue.size) { newValue in sliderGeometrySize = newValue let upperRangeOffset = (newValue.width - knobSize.width)/2.0 let lowerRangeOffset = -upperRangeOffset knobCurrentOffset = offsetRegulatorWithPercentage(percentage: percentage, upperRangeOffset: upperRangeOffset, lowerRangeOffset: lowerRangeOffset) knobLastOffset = knobCurrentOffset } } .frame(height: knobSize.height) .onChange(of: percentage) { newValue in if (!knobIsDragging) { let upperRangeOffset = (sliderGeometrySize.width - knobSize.width)/2.0 let lowerRangeOffset = -upperRangeOffset knobCurrentOffset = offsetRegulatorWithPercentage(percentage: newValue, upperRangeOffset: upperRangeOffset, lowerRangeOffset: lowerRangeOffset) knobLastOffset = knobCurrentOffset } } } private var knobDragGesture: some Gesture { return DragGesture(minimumDistance: CGFloat.zero, coordinateSpace: .local) .onChanged() { gestureValue in knobIsDragging = true let internalOffset = gestureValue.translation.width + knobLastOffset let upperRangeOffset = (sliderGeometrySize.width - knobSize.width)/2.0 let lowerRangeOffset = -upperRangeOffset knobCurrentOffset = offsetRegulator(internalOffset: internalOffset, upperRangeOffset: upperRangeOffset, lowerRangeOffset: lowerRangeOffset) percentage = -1.0 * knobCurrentOffset / ((sliderGeometrySize.width - knobSize.width)/2.0) } .onEnded() { gestureValue in knobLastOffset = knobCurrentOffset knobIsDragging = false } } private func offsetRegulator(internalOffset: CGFloat, upperRangeOffset: CGFloat, lowerRangeOffset: CGFloat) -> CGFloat { if (internalOffset >= .zero) { if (internalOffset < upperRangeOffset) { return internalOffset } else { return upperRangeOffset } } else { if (internalOffset > lowerRangeOffset) { return internalOffset } else { return lowerRangeOffset } } } private func offsetRegulatorWithPercentage(percentage: CGFloat, upperRangeOffset: CGFloat, lowerRangeOffset: CGFloat) -> CGFloat { let internalOffset: CGFloat = -1.0 * percentage * upperRangeOffset return offsetRegulator(internalOffset: internalOffset, upperRangeOffset: upperRangeOffset, lowerRangeOffset: lowerRangeOffset) } } func screenRectangle(string: String, size: CGSize, cornerRadius: CGFloat, color: Color, dash: Bool = false, topOffset: Bool = true) -> some View { RoundedRectangle(cornerRadius: cornerRadius) .stroke(color, style: dash ? StrokeStyle(lineWidth: 4.0, lineCap: .round, dash: [25.0, 25.0]) : StrokeStyle(lineWidth: 4.0)) .frame(width: size.width, height: size.height) .overlay(alignment: .top ) { Text("\(string): \(String(format: "%.2f", size.width)) X \(String(format: "%.2f", size.height))").myTextStyle(color: color).offset(y: topOffset ? -30.0 : 15.0) } } extension View { func myTextStyle(color: Color = Color.black) -> some View { return self .font(Font.system(.body, design: .monospaced).weight(.bold)) .foregroundColor(color) .lineLimit(1) .fixedSize() } }
内容的提问来源于stack exchange,提问作者swiftPunk
相关产品推荐
相关产品推荐

