在Swift中使用reduce来改变结构



我试图了解如何通过使用减少来改变数组中的结构(或类?)。创建4个倒计时计时器,点击暂停当前计时器并开始下一个。所以我尝试这样做:

var timer1 = CountdownTimer()
// var timer2 = CountdownTimer() etc.
.onTapGesture(perform: {
var timers: [CountdownTimer] = [timer1, timer2, timer3, timer4]
var last = timers.reduce (false) {
(setActive: Bool, nextValue: CountdownTimer) -> Bool in
if (nextValue.isActive) {
nextValue.isActive = false;
return true
} else {
return false
}
}
if (last) {
var timer = timers[0]
timer.isActive = true
}
})

############# CountdownTimer is a struct ######
struct CountdownTimer {
var timeRemaining: CGFloat = 1000
var isActive: Bool = false
}

这个不工作,我看到两个错误

  1. 数组中的计时器是计时器的副本,而不是实际计时器,因此更改它们实际上并不会改变屏幕上显示的计时器。
  2. nextValue(即下一个定时器)不能被改变,因为它是一个let变量在减少声明。我不知道如何改变这个(或者如果它甚至是相关的,因为它可能是计时器的副本,而不是我真正想要改变的那个)。

我是不是用了一种习惯上不对Swift使用的方式?我该如何改变原来的计时器?

我同意Paul的观点,这些都应该放到一个可观察的模型对象中。我会让这个模型保存一个任意的计时器列表,以及当前活动计时器的索引。(我将在最后给出一个例子。)

但是你如何使它工作还是值得探索的。

首先,SwiftUI视图不是像UIKit那样在屏幕上的实际视图。它们是屏幕上视图的描述。他们的数据。它们可以随时被复制和销毁。所以它们是只读对象。跟踪它们的可写状态的方法是通过@State属性(或类似的东西,如@Binding,@StateObject,@ObservedObject等)。所以你的属性需要标记为@State

@State var timer1 = CountdownTimer()
@State var timer2 = CountdownTimer()
@State var timer3 = CountdownTimer()
@State var timer4 = CountdownTimer()

正如您所发现的,这种代码不起作用:

var timer = timer1
timer.isActive = true

生成timer1副本并修改副本。相反,您希望WriteableKeyPath访问属性本身。例如:

let timer = Self.timer1  // Note capital Self
self[keyPath: timer].isActive = true

最后,reduce是一个错误的工具。reduce的目的是将序列简化为单个值。它不应该有副作用,比如修改值。相反,您只需要找到正确的元素,然后更改它们。

要做到这一点,最好能够很容易地跟踪这个元素和下一个元素,最后一个元素后面跟着第一个。"这看起来很复杂,但如果你包括Swift算法,它就会非常简单。这就得到了cycled(),它返回一个永远重复其输入的序列。为什么这有用呢?因为你可以这样做:

zip(timers, timers.cycled().dropFirst())

这返回

(value1, value2)
(value2, value3)
(value3, value4)
(value4, value1)

完美。这样,我就可以获取第一个活动计时器(keypath)及其后续计时器,并更新它们:

let timers = [Self.timer1, .timer2, .timer3, .timer4]
if let (current, next) = zip(timers, timers.cycled().dropFirst())
.first(where: { self[keyPath: $0.0].isActive })
{
self[keyPath: current].isActive = false
self[keyPath: next].isActive = true
}

也就是说,我不会那样做。这里有一些微妙的需求应该在类型中被捕获。特别是,您假设只有一个活动计时器,但没有强制这样做。如果这就是你的意思,你应该做一个这样的类型。例如:

class TimerBank: ObservableObject {
@Published private(set) var timers: [CGFloat] = []
@Published private(set) var active: Int?
var count: Int { timers.count }
init(timers: [CGFloat]) {
self.timers = timers
self.active = timers.startIndex
}
func addTimer(timeRemaining: CGFloat = 1000) {
timers.append(timeRemaining)
}
func start(index: Int? = nil) {
if let index = index {
active = index
} else {
active = timers.startIndex
}
}
func stop() {
active = nil
}
func cycle() {
if let currentActive = active {
active = (currentActive + 1) % timers.count
print("active = (active)")
} else {
active = timers.startIndex
print("(init) active = (active)")
}
}
}

有了这个,timerBank.cycle()取代了你的reduce。

通过对Index使用模数运算符(%),我们可以在不压缩的情况下从last到first循环。

let timers = [Self.timer1, .timer2, .timer3, .timer4]
if let onIndex = timers.firstIndex(where: { self[keyPath: $0].isActive }) {
self[keyPath: timers[onIndex]].isActive = false
let nextIndex = (onIndex + 1) % 4 // instead of 4, could use timers.count
self[keyPath: timers[nextIndex]].isActive = true
}