【问题标题】:SwiftUI - 2 Handle Range SliderSwiftUI - 2 处理范围滑块
【发布时间】:2020-10-16 14:43:59
【问题描述】:

有没有办法使用 SwiftUI 创建一个有 2 个句柄的 Slider?

我正在开展一个项目,该项目需要设置一个低点和高点以在其间创建随机值,而滑块似乎完美地满足了这一需求。我目前将它实现为 2 个单独的滑块,但它更愿意让它有 1 个带 2 个手柄的滑块。

我一直在搜索,但在 SwiftUI 中找不到任何示例,但我确实在这里找到了我想要做的网页示例:https://jqueryui.com/slider/#range

这可以通过 SwiftUI 在 iOS 中实现吗?

【问题讨论】:

    标签: ios swift iphone xcode swiftui


    【解决方案1】:

    我为您创建了一个自定义滑块。我希望这足以满足您的需求。如果还有什么我可以做的,请告诉我。

    滑块:

    import SwiftUI
    import Combine
    
    //SliderValue to restrict double range: 0.0 to 1.0
    @propertyWrapper
    struct SliderValue {
        var value: Double
        
        init(wrappedValue: Double) {
            self.value = wrappedValue
        }
        
        var wrappedValue: Double {
            get { value }
            set { value = min(max(0.0, newValue), 1.0) }
        }
    }
    
    class SliderHandle: ObservableObject {
        
        //Slider Size
        let sliderWidth: CGFloat
        let sliderHeight: CGFloat
        
        //Slider Range
        let sliderValueStart: Double
        let sliderValueRange: Double
        
        //Slider Handle
        var diameter: CGFloat = 40
        var startLocation: CGPoint
        
        //Current Value
        @Published var currentPercentage: SliderValue
        
        //Slider Button Location
        @Published var onDrag: Bool
        @Published var currentLocation: CGPoint
            
        init(sliderWidth: CGFloat, sliderHeight: CGFloat, sliderValueStart: Double, sliderValueEnd: Double, startPercentage: SliderValue) {
            self.sliderWidth = sliderWidth
            self.sliderHeight = sliderHeight
            
            self.sliderValueStart = sliderValueStart
            self.sliderValueRange = sliderValueEnd - sliderValueStart
            
            let startLocation = CGPoint(x: (CGFloat(startPercentage.wrappedValue)/1.0)*sliderWidth, y: sliderHeight/2)
            
            self.startLocation = startLocation
            self.currentLocation = startLocation
            self.currentPercentage = startPercentage
            
            self.onDrag = false
        }
        
        lazy var sliderDragGesture: _EndedGesture<_ChangedGesture<DragGesture>>  = DragGesture()
            .onChanged { value in
                self.onDrag = true
                
                let dragLocation = value.location
                
                //Restrict possible drag area
                self.restrictSliderBtnLocation(dragLocation)
                
                //Get current value
                self.currentPercentage.wrappedValue = Double(self.currentLocation.x / self.sliderWidth)
                
            }.onEnded { _ in
                self.onDrag = false
            }
        
        private func restrictSliderBtnLocation(_ dragLocation: CGPoint) {
            //On Slider Width
            if dragLocation.x > CGPoint.zero.x && dragLocation.x < sliderWidth {
                calcSliderBtnLocation(dragLocation)
            }
        }
        
        private func calcSliderBtnLocation(_ dragLocation: CGPoint) {
            if dragLocation.y != sliderHeight/2 {
                currentLocation = CGPoint(x: dragLocation.x, y: sliderHeight/2)
            } else {
                currentLocation = dragLocation
            }
        }
        
        //Current Value
        var currentValue: Double {
            return sliderValueStart + currentPercentage.wrappedValue * sliderValueRange
        }
    }
    
    class CustomSlider: ObservableObject {
        
        //Slider Size
        final let width: CGFloat = 300
        final let lineWidth: CGFloat = 8
        
        //Slider value range from valueStart to valueEnd
        final let valueStart: Double
        final let valueEnd: Double
        
        //Slider Handle
        @Published var highHandle: SliderHandle
        @Published var lowHandle: SliderHandle
        
        //Handle start percentage (also for starting point)
        @SliderValue var highHandleStartPercentage = 1.0
        @SliderValue var lowHandleStartPercentage = 0.0
    
        final var anyCancellableHigh: AnyCancellable?
        final var anyCancellableLow: AnyCancellable?
        
        init(start: Double, end: Double) {
            valueStart = start
            valueEnd = end
            
            highHandle = SliderHandle(sliderWidth: width,
                                      sliderHeight: lineWidth,
                                      sliderValueStart: valueStart,
                                      sliderValueEnd: valueEnd,
                                      startPercentage: _highHandleStartPercentage
                                    )
            
            lowHandle = SliderHandle(sliderWidth: width,
                                      sliderHeight: lineWidth,
                                      sliderValueStart: valueStart,
                                      sliderValueEnd: valueEnd,
                                      startPercentage: _lowHandleStartPercentage
                                    )
            
            anyCancellableHigh = highHandle.objectWillChange.sink { _ in
                self.objectWillChange.send()
            }
            anyCancellableLow = lowHandle.objectWillChange.sink { _ in
                self.objectWillChange.send()
            }
        }
        
        //Percentages between high and low handle
        var percentagesBetween: String {
            return String(format: "%.2f", highHandle.currentPercentage.wrappedValue - lowHandle.currentPercentage.wrappedValue)
        }
        
        //Value between high and low handle
        var valueBetween: String {
            return String(format: "%.2f", highHandle.currentValue - lowHandle.currentValue)
        }
    }
    

    滑块实现:

    import SwiftUI
    
    struct ContentView: View {
        @ObservedObject var slider = CustomSlider(start: 10, end: 100)
        
        var body: some View {
            VStack {
                Text("Value: " + slider.valueBetween)
                Text("Percentages: " + slider.percentagesBetween)
                
                Text("High Value: \(slider.highHandle.currentValue)")
                Text("Low Value: \(slider.lowHandle.currentValue)")
                
                //Slider
                SliderView(slider: slider)
            }
        }
    }
    
    struct SliderView: View {
        @ObservedObject var slider: CustomSlider
        
        var body: some View {
            RoundedRectangle(cornerRadius: slider.lineWidth)
                .fill(Color.gray.opacity(0.2))
                .frame(width: slider.width, height: slider.lineWidth)
                .overlay(
                    ZStack {
                        //Path between both handles
                        SliderPathBetweenView(slider: slider)
                        
                        //Low Handle
                        SliderHandleView(handle: slider.lowHandle)
                            .highPriorityGesture(slider.lowHandle.sliderDragGesture)
                        
                        //High Handle
                        SliderHandleView(handle: slider.highHandle)
                            .highPriorityGesture(slider.highHandle.sliderDragGesture)
                    }
                )
        }
    }
    
    struct SliderHandleView: View {
        @ObservedObject var handle: SliderHandle
        
        var body: some View {
            Circle()
                .frame(width: handle.diameter, height: handle.diameter)
                .foregroundColor(.white)
                .shadow(color: Color.black.opacity(0.15), radius: 8, x: 0, y: 0)
                .scaleEffect(handle.onDrag ? 1.3 : 1)
                .contentShape(Rectangle())
                .position(x: handle.currentLocation.x, y: handle.currentLocation.y)
        }
    }
    
    struct SliderPathBetweenView: View {
        @ObservedObject var slider: CustomSlider
        
        var body: some View {
            Path { path in
                path.move(to: slider.lowHandle.currentLocation)
                path.addLine(to: slider.highHandle.currentLocation)
            }
            .stroke(Color.green, lineWidth: slider.lineWidth)
        }
    }
    

    【讨论】:

    • 哇,太棒了。很容易实现。真的谢谢你。这完全是你自己写的吗?
    • 欢迎您!是的,我曾经使用 SwiftUI 编写了一个色轮,我还必须制作一个自定义滑块。所以很高兴用它制作一个范围滑块。 ;)
    • 有没有办法改变滑块的步长,让它只返回整数?
    • 只是想对如此干净的代码表示感谢。我在restrictSliderBtnLocation 函数中注意到一个错误,您应该像这样使用minmaxlet xOffset = min(max(0, dragLocation.x), sliderWidth) calcSliderBtnLocation(CGPoint(x: xOffset, y: dragLocation.y)) 这将允许滑块一直到达最小值和最大值,并且在您拖动到之前不会停止结束。但除此之外太棒了!
    • 嗨,我在使用视图的整个宽度时遇到问题,我已经尝试让它与 GeometryReader 一起使用,但仍然有问题,有没有人设法让它工作,谢谢对于 cleanCode 和解决方案
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-22
    • 1970-01-01
    • 2018-08-14
    • 1970-01-01
    • 2020-04-30
    相关资源
    最近更新 更多