Я только что обновился до Xcode 13.3 и вижу несколько экземпляров нового предупреждения, которого не видел в предыдущих версиях Xcode. В качестве примера у меня есть простая ячейка табличного представления с именем LabelAndSwitchTableViewCell, которая выглядит так:
import UIKit
class LabelAndSwitchTableViewCell: UITableViewCell {
private let label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let _switch: UISwitch = {
let _switch = UISwitch()
_switch.translatesAutoresizingMaskIntoConstraints = false
_switch.addTarget(self, action: #selector(didToggleSwitch), for: .valueChanged)
return _switch
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(label)
contentView.addSubview(_switch)
// layout constraints removed for brevity
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func didToggleSwitch() {
print("Switch was toggled...")
}
}
Как видите, я добавляю цель к переключателю, который я хочу вызывать при изменении значения переключателей:
_switch.addTarget(self, action: #selector(didToggleSwitch), for: .valueChanged)
После обновления до Xcode 13.3 я вижу новое предупреждение в этой строке:
'self' refers to the method 'LabelAndSwitchTableViewCell.self', which may be unexpected
Предложение Xcode отключить это предупреждение состоит в том, чтобы заменить:
_switch.addTarget(self, action: #selector(didToggleSwitch), for: .valueChanged)
...с...
_switch.addTarget(LabelAndSwitchTableViewCell.self, action: #selector(didToggleSwitch), for: .valueChanged)
Внесение этого изменения отключает предупреждение, но также вызывает сбой приложения (нераспознанный селектор), когда я переключаю переключатель. Вот дамп этого краша:
[app_mockup.LabelAndSwitchTableViewCell didToggleSwitch]: unrecognized selector sent to class 0x1043d86e8
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[app_mockup.LabelAndSwitchTableViewCell didToggleSwitch]: unrecognized selector sent to class 0x1043d86e8'
Создание метода didToggleSwitch()
static
предотвратит сбой, но я не уверен, зачем мне это нужно. Я, очевидно, могу отменить изменение (с LabelAndSwitchTableViewCell.self
обратно на просто self
), но мне интересно, есть ли что-то еще, что я должен сделать, чтобы решить эту проблему?
Вы можете исправить это, изменив let на lazy var.
private lazy var _switch2: UISwitch = {
let _switch = UISwitch()
_switch.translatesAutoresizingMaskIntoConstraints = false
_switch.addTarget(self, action: #selector(didToggleSwitch), for: .valueChanged)
return _switch
}()
Предложение по исправлению Xcode просто неверно.
Я не знаю, является ли это ошибкой в 13.3 или есть какая-то реальная проблема, связанная с тем, что self не гарантируется, что он находится рядом, инициируется и может быть захвачен в рамках автоматического оценивания замыкания.
Я думаю, что в некоторых случаях это может быть ошибкой кодирования, но до сих пор не было предупреждений. (и предупреждение и исправление на данный момент сбивают с толку) self определенно недоступен, когда цель добавляется в это замыкание, но в большинстве случаев это также не требуется, чтобы быть готовым. Может случиться так, что событие селектора, например, .allEvents, и какое-то событие произойдет до самоподготовки + вам нужно обработать это событие. Я считаю, что это очень маловероятный сценарий, когда вам нужно событие перед собой для элементов управления. ленивый просто сделайте так, чтобы этот var был инициализирован после себя.
Причина в том, что self
еще не готов к фазе 1 инициализации объекта. Фаза 1 — установить все сохраненные свойства, и только на фазе 2 вы можете получить доступ к self
.
Чтобы исправить свой код, вы можете использовать свойство lazy
, где фаза инициализации 1 завершена.
Вот ссылка:
https://docs.swift.org/swift-book/LanguageGuide/Initialization.html
Для этого предупреждения:
NSObject
, self
является ошибкой при непосредственном использовании в хранимом свойстве.NSObject
, self
компилируется в автозакрытие (ClassXXX) -> () -> ClassXXX
. Таким образом, во время выполнения self будет использоваться как текущий экземпляр. Но, поскольку Swift 5.6 предупреждает нас об этом, я думаю, что ссылка на себя в сохраненном свойстве подкласса NSObject может быть запрещена в будущих версиях Swift.Пример 1: ошибка при отсутствии суперкласса
Пример 2: скомпилированное self в AST
Для этого кода:
import Foundation
class MyTest: NSObject {
var myself = self
}
Вот часть скомпилированного AST:
Почему же тогда целевое действие работает во время выполнения и правильно нацеливается на сам экземпляр, а не на ClassXXX.self?
На данный момент, если класс является подклассом NSObject
, компилятор Swift скомпилирует self
в автоматическое замыкание: (ClassXXX) -> () -> ClassXXX
(я проверил компилируемые языки AST и SIL, которые могут подтвердить автоматическое замыкание self
). Таким образом, во время выполнения self
будет использоваться как текущий экземпляр. Но, поскольку Swift 5.6 предупреждает нас об этом, я думаю, что ссылка на self
в сохраненном свойстве подкласса NSObject
может быть запрещена в будущих версиях Swift.
Ответ отредактирован, чтобы включить пример AST
Спасибо за дополнительное пояснение!
LabelAndSwitchTableViewCell.self, как было предложено, в большинстве случаев не работает. Используйте nil и выполните поиск в цепочке респондентов.
Как сейчас написано, ваш ответ неясен. Пожалуйста, редактировать, чтобы добавить дополнительную информацию, которая поможет другим понять, как это относится к заданному вопросу. Дополнительную информацию о том, как писать хорошие ответы, можно найти в справочном центре.
Не пробовал делать мои реквизиты ленивыми варами, но вы абсолютно правы - это предотвращает предупреждение! Не уверен, что я полностью понимаю, почему ленивая инициализация свойства переключателя имеет значение в этом конкретном случае, но я, конечно, не могу придумать недостаток, поэтому я соответствующим образом обновлю свой код. Спасибо за предложение!