Какие ошибки делают команды, когда строят приложение на SwiftUI как web/backend-проект без учёта mobile constraints?
Частые ошибки: блокировка Main Thread синхронным I/O, God Object в EnvironmentObject вызывающий лишние перерисовки, неограниченный кэш изображений без NSCache, строковая навигация вместо типизированного NavigationStack, и вызов onAppear без дедупликации запросов.
Ошибка 1: синхронные сетевые запросы и I/O на Main Thread
Web-разработчики привыкли к event loop Node.js или sync I/O в Rails. В iOS Main Thread — это UI thread; любая блокировка вызывает «заморозку» интерфейса и watchdog kill (crash с кодом 0x8badf00d).
// ПЛОХО: блокирует Main Thread
struct BadView: View {
@State private var data: String = ""
var body: some View {
Text(data)
.onAppear {
// URLSession.shared.dataTask синхронно — НИКОГДА так не делать
let url = URL(string: "https://api.example.com/data")!
let d = try! Data(contentsOf: url) // блокирует поток
data = String(data: d, encoding: .utf8) ?? ""
}
}
}
// ХОРОШО: async/await + @MainActor
struct GoodView: View {
@State private var data: String = ""
var body: some View {
Text(data)
.task {
let url = URL(string: "https://api.example.com/data")!
let (d, _) = try! await URLSession.shared.data(from: url)
data = String(data: d, encoding: .utf8) ?? ""
}
}
}
Ошибка 2: хранение всего состояния в @State / @EnvironmentObject
В backend привычен единый глобальный store (Redux, Zustand, DI-контейнер). В SwiftUI это ведёт к God Object в @EnvironmentObject: один объект с 50 @Published свойствами заставляет перерисовываться весь граф View при любом изменении.
// ПЛОХО: один большой EnvironmentObject
@MainActor
final class AppState: ObservableObject {
@Published var user: User?
@Published var posts: [Post] = []
@Published var notifications: [Notification] = []
@Published var settings: Settings = .default
// При изменении settings перерисовываются PostListView, NotificationView и т.д.
}
// ХОРОШО: разделённые объекты
@Observable final class UserStore { var current: User? }
@Observable final class PostStore { var items: [Post] = [] }
// Каждый View подписывается только на нужный store
Ошибка 3: игнорирование memory constraints
На backend можно хранить сотни мегабайт в памяти. iOS агрессивно убивает приложения при memory pressure (iPhone с 3–4 GB RAM, но ограниченный бюджет для foreground app). Типичная ошибка — кэшировать изображения в массиве без использования NSCache.
// ПЛОХО: неограниченный кэш изображений
class ImageCache {
var cache: [URL: UIImage] = [:] // растёт бесконечно
}
// ХОРОШО: NSCache с автоматическим eviction
class ImageCache {
private let cache = NSCache<NSURL, UIImage>()
init() {
cache.countLimit = 100
cache.totalCostLimit = 50 * 1024 * 1024 // 50 MB
}
func image(for url: URL) -> UIImage? {
cache.object(forKey: url as NSURL)
}
}
Ошибка 4: навигация через URLRouter как на web
Web-разработчики часто реализуют роутинг через строковые пути (/profile/123). В SwiftUI NavigationStack работает с типизированными значениями; строковые пути ломают type safety и усложняют deep links.
// ПЛОХО: строковый роутинг
enum Route: String {
case profile = "/profile"
case settings = "/settings"
}
// ХОРОШО: типизированный NavigationStack
enum AppRoute: Hashable {
case profile(userId: UUID)
case settings
case postDetail(postId: UUID)
}
NavigationStack(path: $navigationPath) {
HomeView()
.navigationDestination(for: AppRoute.self) { route in
switch route {
case .profile(let id): ProfileView(userId: id)
case .settings: SettingsView()
case .postDetail(let id): PostDetailView(postId: id)
}
}
}
Ошибка 5: обработка ошибок по-backend-у (исключения)
В Python/Java принято бросать исключения и ловить их где-то выше. Swift не имеет unchecked exceptions; весь error handling — через Result, throws и do-catch. Игнорирование try? везде скрывает реальные ошибки от пользователя.
Подводные камни
- Background thread UI updates — публикация
@Publishedс фонового потока без@MainActorвызывает фиолетовые Xcode предупреждения и потенциальные гонки; в iOS 17 это может стать crash. - ForEach без стабильного id — использование индекса массива как id (
ForEach(items.indices)) вместоitem.idвызывает неправильные анимации и accessibility проблемы. - Lifecycle != viewDidLoad —
onAppearвызывается при каждом появлении View в стеке; сетевые запросы без debounce/deduplication уходят несколько раз при back navigation. - Retain cycles в замыканиях — использование
selfв.onAppear { self.load() }наclass-объекте создаёт retain cycle; нужно[weak self]. - Игнорирование App Backgrounding — незавершённые URLSession tasks при уходе в background получают ограниченное время (30 сек); нужен
URLSession.background(withIdentifier:)для долгих загрузок. - Перенос REST-pagination на iOS без кэша — запрашивать страницы заново при каждом появлении View приводит к мерцанию UI; нужен локальный кэш в CoreData или SwiftData.
What hurts your answer
- Перечислять ошибки без объяснения причин
- Не отличать beginner mistakes от production failure modes
- Не предлагать процесс, который предотвращает повторение ошибок
What they're listening for
- Знает типичные ошибки при работе с SwiftUI
- Понимает причины ошибок
- Предлагает практики prevention и early detection