#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
}
Это не работает, я вижу две ошибки
- таймеры в массиве являются копиями таймеров, а не фактическим таймером, поэтому их изменение фактически не изменяет таймеры, отображаемые на экране.
- 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
}