Использование reduce для изменения структуры в Swift

#ios #swift #swiftui

Вопрос:

Я пытаюсь понять, как изменить структуру (или класс?) в массиве с помощью 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 в объявлении reduce . Я не знаю, как это изменить (или, если это даже актуально, потому что, по-видимому, это копия копии таймера, а не та, которую я на самом деле хочу изменить).

Я подхожу к этому так, что это идиоматически неправильно для Swift? Как я должен изменять исходные таймеры?

Комментарии:

1. В SwiftUI важно правильно отделить вашу модель от вашего представления. Процедурный код принадлежит вашей модели. Ваш просмотр просто отображает модель и вызывает методы в модели в ответ на взаимодействие с пользователем. Вероятно, вы хотите, чтобы ваши объекты timer были ссылочными типами (классами, а не структурами), поскольку вы хотите, чтобы они были изменяемыми.

Ответ №1:

Я согласен с Полом в том, что, вероятно, все это должно быть извлечено в объект observable model. Я бы сделал так, чтобы эта модель содержала произвольный список таймеров и индекс текущего активного таймера. (Я приведу пример этого в конце.)

Но все же стоит изучить, как вы могли бы заставить это работать.

Во-первых, представления 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() заменяет ваше сокращение.

Комментарии:

1. Спасибо, Роб, это фантастический ответ! Ценю, что вы нашли время, чтобы объяснить это мне.

Ответ №2:

Используя оператор модуля ( % ) для индекса, мы могли бы циклически переходить от последнего к первому без архивирования.

 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
}