You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 01:18:11