Топ ошибок начинающих Swift разработчиков: продвинутый уровень
Когда вы переходите от написания простых экранов на SwiftUI к созданию сложных многопоточных приложений с кастомной архитектурой, количество подводных камней резко возрастает. Многие разработчики, освоив базовый синтаксис Swift, считают себя готовыми к продакшену, но допускают критические ошибки, которые приводят к утечкам памяти, нестабильной работе и трудноуловимым багам. В этой статье мы разберем четыре категории ошибок, которые чаще всего встречаются у разработчиков среднего уровня.
1. Утечки памяти в замыканиях: не только [weak self]
Самой популярной ошибкой является использование [weak self] без понимания того, когда это действительно нужно. Многие разработчики ставят weak self везде, где есть замыкание, что приводит к преждевременному уничтожению объектов. Или, наоборот, забывают про сильные ссылочные циклы в неочевидных местах.
Типичная ошибка: создание сильной ссылки на объект внутри замыкания, которое этот объект хранит.
class NetworkManager {
var onCompletion: (() -> Void)?
func fetchData() {
// ОШИБКА: замыкание захватывает self сильно
onCompletion = {
self.updateUI()
}
}
func updateUI() {
print("UI обновлен")
}
}
Правильное решение: использовать [weak self] или [unowned self] в зависимости от времени жизни объектов.
class NetworkManager {
var onCompletion: (() -> Void)?
func fetchData() {
onCompletion = { [weak self] in
guard let self = self else { return }
self.updateUI()
}
}
func updateUI() {
print("UI обновлен")
}
}
Продвинутый нюанс: будьте осторожны с [unowned self]. Если объект будет освобожден, а замыкание вызовется, приложение упадет с крашем. Используйте unowned только когда вы на 100% уверены, что объект переживет замыкание.
2. Неправильная работа с async/await и акторами
С появлением Structured Concurrency в Swift 5.5 многие разработчики начали активно использовать async/await, но забывают про MainActor и Sendable. Это приводит к race conditions и обновлению UI из фонового потока.
Типичная ошибка: вызов асинхронной функции, которая должна выполняться на главном потоке, без явного указания @MainActor.
class ViewModel: ObservableObject {
@Published var data: [String] = []
// ОШИБКА: функция не помечена @MainActor
func loadData() async {
let result = await fetchFromNetwork()
// Это может выполниться на фоновом потоке!
self.data = result
}
func fetchFromNetwork() async -> [String] {
try? await Task.sleep(nanoseconds: 1_000_000_000)
return ["Item1", "Item2"]
}
}
Правильное решение: использовать @MainActor для функций, которые обновляют UI.
class ViewModel: ObservableObject {
@Published var data: [String] = []
@MainActor // Теперь гарантированно выполняется на главном потоке
func loadData() async {
let result = await fetchFromNetwork()
self.data = result
}
func fetchFromNetwork() async -> [String] {
try? await Task.sleep(nanoseconds: 1_000_000_000)
return ["Item1", "Item2"]
}
}
Продвинутый нюанс: не забывайте про nonisolated и actor. Если вы используете акторы, все публичные методы по умолчанию асинхронны. Это может привести к неожиданным задержкам, если вы не продумаете изоляцию.
3. Race Conditions в Combine и SwiftUI
Combine — мощный инструмент, но он легко порождает race conditions, когда несколько подписчиков одновременно изменяют одно и то же состояние. Особенно это критично в SwiftUI, где изменения @Published свойств триггерят перерисовку.
Типичная ошибка: подписка на издателя внутри onAppear без отмены подписки в onDisappear.
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
Text("Count: \(viewModel.count)")
.onAppear {
// ОШИБКА: новая подписка каждый раз без отмены старой
Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { _ in
viewModel.count += 1
}
}
}
}
class ViewModel: ObservableObject {
@Published var count = 0
}
Правильное решение: использовать onReceive или хранить AnyCancellable с очисткой.
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
@State private var cancellable: AnyCancellable?
var body: some View {
Text("Count: \(viewModel.count)")
.onAppear {
cancellable = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { _ in
viewModel.count += 1
}
}
.onDisappear {
cancellable?.cancel()
}
}
}
Продвинутый нюанс: если вы используете @Published в классе, который не является ObservableObject, Combine не будет работать корректно. Всегда проверяйте, что ваш класс наследуется от ObservableObject.
4. Ошибки с Generics и Protocol Associated Types
Generics в Swift — мощный инструмент, но их неправильное использование может привести к падению производительности или невозможности компиляции. Самая частая ошибка — использование Any вместо конкретного типа или неправильная работа с PAT (Protocol with Associated Types).
Типичная ошибка: попытка использовать протокол с associated type в качестве типа массива.
protocol DataProvider {
associatedtype DataType
func fetch() -> DataType
}
// ОШИБКА: протокол с associated type нельзя использовать как тип
var providers: [DataProvider] = [] // Ошибка компиляции
Правильное решение: использовать type erasure (стирание типа) или явно указывать тип.
Блоги
Рекомендаций по книгам
Читайте также
Обработка ошибок в PHP: try/catch и продвинутые техники
Почему Android навигация ломается: IllegalArgumentException и NavController
Деплой C# приложения на VPS: пошаговое руководство
Как отправить POST-запрос в PHP: 4 проверенных способа
Работа с датами и временем в JavaScript: продвинутые методы и библиотеки