【问题标题】:SwiftUI custom stepper buttonSwiftUI 自定义步进按钮
【发布时间】:2019-08-26 16:41:55
【问题描述】:

我正在 SwiftUI 中创建一个自定义步进控件,并尝试复制内置控件的加速值更改行为。在 SwiftUI Stepper 中,长按“+”或“-”将不断增加/减少值,按住按钮的时间越长,变化速度越快。

我可以通过以下方式创建按住按钮的视觉效果:

struct PressBox: View {
    @GestureState var pressed = false
    @State var value = 0

    var body: some View {
        ZStack {
            Rectangle()
                .fill(pressed ? Color.blue : Color.green)
                .frame(width: 70, height: 50)
                .gesture(LongPressGesture(minimumDuration: .infinity)
                    .updating($pressed) { value, state, transaction in
                        state = value
                    }
                    .onChanged { _ in
                        self.value += 1
                    }
                )
            Text("\(value)")
                .foregroundColor(.white)
        }
    }
}

这只会增加一次值。将计时器发布者添加到 onChanged 修饰符,以实现如下手势:

let timer = Timer.publish(every: 0.5, on: .main, in: .common)
@State var cancellable: AnyCancellable? = nil

...

.onChanged { _ in 
    self.cancellable = self.timer.connect() as? AnyCancellable
}

将复制更改的值,但由于手势永远不会成功完成(永远不会调用onEnded),因此无法停止计时器。手势没有onCancelled 修饰符。

我也尝试使用TapGesture 来执行此操作,这将用于检测手势的结束,但我看不到检测手势开始的方法。这段代码:

.gesture(TapGesture()
    .updating($pressed) { value, state, transaction in
        state = value
    }
)

$pressed 上生成错误:

无法将“GestureState”类型的值转换为预期的参数类型“GestureState<_>”

有没有办法复制行为而不回退到 UIKit?

【问题讨论】:

    标签: ios swiftui


    【解决方案1】:

    您需要视图上的onTouchDown 事件来启动计时器,并需要onTouchUp 事件来停止它。 SwiftUI 目前不提供着陆事件,所以我认为获得你想要的最好的方法是使用 DragGesture 这种方式:

    import SwiftUI
    
    class ViewModel: ObservableObject {
        private static let updateSpeedThresholds = (maxUpdateSpeed: TimeInterval(0.05), minUpdateSpeed: TimeInterval(0.3))
        private static let maxSpeedReachedInNumberOfSeconds = TimeInterval(2.5)
    
        @Published var val: Int = 0
        @Published var started = false
    
        private var timer: Timer?
        private var currentUpdateSpeed = ViewModel.updateSpeedThresholds.minUpdateSpeed
        private var lastValueChangingDate: Date?
        private var startDate: Date?
    
        func start() {
            if !started {
                started = true
                val = 0
                startDate = Date()
                startTimer()
            }
        }
    
        func stop() {
            timer?.invalidate()
            currentUpdateSpeed = Self.updateSpeedThresholds.minUpdateSpeed
            lastValueChangingDate = nil
            started = false
        }
    
        private func startTimer() {
            timer = Timer.scheduledTimer(withTimeInterval: Self.updateSpeedThresholds.maxUpdateSpeed, repeats: false) {[unowned self] _ in
                self.updateVal()
                self.updateSpeed()
                self.startTimer()
            }
        }
    
        private func updateVal() {
            if self.lastValueChangingDate == nil || Date().timeIntervalSince(self.lastValueChangingDate!) >= self.currentUpdateSpeed {
                self.lastValueChangingDate = Date()
                self.val += 1
            }
        }
    
        private func updateSpeed() {
            if self.currentUpdateSpeed < Self.updateSpeedThresholds.maxUpdateSpeed {
                return
            }
            let timePassed = Date().timeIntervalSince(self.startDate!)
            self.currentUpdateSpeed = timePassed * (Self.updateSpeedThresholds.maxUpdateSpeed - Self.updateSpeedThresholds.minUpdateSpeed)/Self.maxSpeedReachedInNumberOfSeconds + Self.updateSpeedThresholds.minUpdateSpeed
        }
    }
    
    struct ContentView: View {
        @ObservedObject var viewModel: ViewModel
    
        var body: some View {
            ZStack {
                Rectangle()
                    .fill(viewModel.started ? Color.blue : Color.green)
                    .frame(width: 70, height: 50)
                    .gesture(DragGesture(minimumDistance: 0)
                        .onChanged { _ in
                            self.viewModel.start()
                        }
                        .onEnded { _ in
                            self.viewModel.stop()
                        }
                )
    
                Text("\(viewModel.val)")
                    .foregroundColor(.white)
            }
        }
    }
    
    
    #if DEBUG
    struct ContentView_Previews: PreviewProvider {
      static var previews: some View {
        ContentView(viewModel: ViewModel())
      }
    }
    #endif
    

    如果我得到了你想要的,或者我是否能以某种方式改进我的答案,请告诉我。

    【讨论】:

    • 没想到要使用拖动手势。在我们在 API 中获得更大的灵活性之前,这是一个很好的方法。谢谢!!
    【解决方案2】:

    对于任何尝试类似事情的人,这里对 superpuccio 的方法略有不同。该类型用户的 api 更简单一些,并且随着速度的提高,它最大限度地减少了计时器触发的次数。

    struct TimerBox: View {
        @Binding var value: Int
        @State private var isRunning = false
        @State private var startDate: Date? = nil
        @State private var timer: Timer? = nil
    
        private static let thresholds = (slow: TimeInterval(0.3), fast: TimeInterval(0.05))
        private static let timeToMax = TimeInterval(2.5)
    
        var body: some View {
            ZStack {
                Rectangle()
                    .fill(isRunning ? Color.blue : Color.green)
                    .frame(width: 70, height: 50)
                    .gesture(DragGesture(minimumDistance: 0)
                        .onChanged { _ in
                            self.startRunning()
                        }
                        .onEnded { _ in
                            self.stopRunning()
                        }
                )
    
                Text("\(value)")
                    .foregroundColor(.white)
            }
        }
    
        private func startRunning() {
            guard isRunning == false else { return }
            isRunning = true
            startDate = Date()
            timer = Timer.scheduledTimer(withTimeInterval: Self.thresholds.slow, repeats: true, block: timerFired)
        }
    
        private func timerFired(timer: Timer) {
            guard let startDate = self.startDate else { return }
            self.value += 1
            let timePassed = Date().timeIntervalSince(startDate)
            let newSpeed = Self.thresholds.slow - timePassed * (Self.thresholds.slow - Self.thresholds.fast)/Self.timeToMax
            let nextFire = Date().advanced(by: max(newSpeed, Self.thresholds.fast))
            self.timer?.fireDate = nextFire
        }
    
        private func stopRunning() {
            timer?.invalidate()
            isRunning = false
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2021-07-04
      • 2020-12-30
      • 1970-01-01
      • 2019-11-09
      • 2020-07-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-13
      相关资源
      最近更新 更多