SwiftUI 自定义步进按钮
SwiftUI custom stepper button
我正在 SwiftUI 中创建一个自定义步进控件,我正在尝试复制内置控件的加速值更改行为。在 SwiftUI Stepper
中,长按“+”或“-”将保持 increasing/decreasing 值,按住按钮的时间越长变化速度越快。
我可以通过以下方式创建按住按钮的视觉效果:
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
上生成错误:
Cannot convert value of type 'GestureState' to expected argument type '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
}
}
我正在 SwiftUI 中创建一个自定义步进控件,我正在尝试复制内置控件的加速值更改行为。在 SwiftUI Stepper
中,长按“+”或“-”将保持 increasing/decreasing 值,按住按钮的时间越长变化速度越快。
我可以通过以下方式创建按住按钮的视觉效果:
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
上生成错误:
Cannot convert value of type 'GestureState' to expected argument type '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
}
}