繁体   English   中英

SwiftUI自定义步进按钮

[英]SwiftUI custom stepper button

我在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?

您将在视图上需要一个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

让我知道我是否得到了您想要的东西,或者我是否可以改善答案。

对于任何尝试类似尝试的人,这与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
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM