SwiftUIMiddleExperience

Какие ошибки делают команды, когда строят приложение на 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 != viewDidLoadonAppear вызывается при каждом появлении 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

Related topics