簡體   English   中英

在 Swift 中使用 reduce 更改結構

[英]Using reduce to change struct in 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(即下一個計時器)無法更改,因為它是 reduce 聲明中的 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 Algorithms ,它會出奇的簡單。 這給出了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 上使用模數運算符 ( % ),我們可以在不壓縮的情況下從最后一個循環到第一個。

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
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM