Вопрос или проблема
(Для контекста, я знаком со Swift, но довольно нов в параллельном программировании на Swift; однако я понимаю основные концепции) Я пытаюсь реализовать функциональность отмены/повтора с помощью UndoManager
, но столкнулся с предупреждением компилятора, когда добавил реализацию повтора:
Захват 'undoManager' с несоответствующим типом 'UndoManager?' в замыкании `@Sendable`; это ошибка в языке Swift 6
Вот упрощенная реализация:
class ViewModel: ObservableObject {
@Published var model: MyModel = .init()
func exampleIntent(with undoManager: UndoManager? = nil) {
undoablyPerform(with: undoManager) {
model.changeSomething()
}
}
private func undoablyPerform(with undoManager: UndoManager? = nil, doit: () -> Void) {
let oldModel = model
doit()
undoManager?.registerUndo(withTarget: self) { targetSelf in
self.undoablyPerform(with: undoManager) { // Реализует повтор, но захватывает UndoManager внутри замыкания @Sendable
targetSelf.model = oldModel
}
}
}
}
Я регистрирую действие отмены внутри функции registerRedo()
, чтобы реализовать повтор, и это кажется мне общепринятым подходом к реализации повтора. Я понимаю, почему компилятор выдает это предупреждение (потому что UndoManager не является Sendable, не говоря уже о типе Optional UndoManager?), но не знаю правильного подхода для реализации функции повтора.
Наверняка, я не единственный, кто сталкивается с этой проблемой… Есть ли какой-то другой способ реализовать функциональность повтора, чтобы избежать этого предупреждения? Буду признателен за любые идеи или советы!
Ответ или решение
Для реализации функциональности отмены/возврата с использованием UndoManager
в SwiftUI и избежания предупреждений о Sendable
замыканиях, вы правы в своей оценке проблемы. UndoManager
не является типом, который может быть передан в @Sendable
замыкания, поэтому необходимо найти обходные пути для обеспечения необходимого поведения.
Вот как вы можете модифицировать ваш код, чтобы избежать этой проблемы:
import SwiftUI
class ViewModel: ObservableObject {
@Published var model: MyModel = .init()
func exampleIntent(with undoManager: UndoManager? = nil) {
undoablyPerform(with: undoManager) {
model.changeSomething()
}
}
private func undoablyPerform(with undoManager: UndoManager? = nil, doit: @escaping () -> Void) {
// Сохраняем текущее состояние модели перед изменением
let oldModel = model
// Выполнение запроса на изменение
doit()
// Регистрация действия отмены
undoManager?.registerUndo(withTarget: self) { [weak self] targetSelf in
// Здесь мы используем weak self, чтобы избежать сильной ссылки
guard let self = targetSelf else { return }
self.undoablyPerform(with: undoManager) {
targetSelf.model = oldModel
}
}
// Регистрация действия возврата
undoManager?.registerUndo(withTarget: self) { [weak self] targetSelf in
guard let self = targetSelf else { return }
// Здесь нужно будет определить действие, которое должно быть выполнено для возврата
// Например, вы можете сохранить состояние в другой переменной, если это уместно
}
}
}
Объяснение модификаций:
-
Использование
@escaping
: Добавлено@escaping
к замыканиюdoit
, позволяя ему захватывать параметры на протяжении времени, что необходимо для поддержки отмены/возврата. -
Слабая ссылка на
self
: Использование[weak self]
в замыкании помогает предотвратить потенциальные циклы сильных ссылок и повышает безопасность кода, избегая утечек памяти. -
Обработка действия возврата: Необходимо учесть, что для полного функционала возврата вам нужно будет определить, какие действия вы хотите зарегистрировать. Это можно сделать, сохраняя предыдущее состояние или используя другие подходы, основываясь на вашей логике приложения.
Заключительные замечания:
Обратите внимание, что абстракция над UndoManager
может быть сложной, поэтому может потребоваться доработка, учитывающая все контексты использования вашей модели. Основной идеей является избегать захвата не-Sendable типов в однопоточных контекстах.
Если у вас возникнут дополнительные вопросы или сложности, не стесняйтесь задавать их!