December 23, 2023

46. Чиним SwiftUI Preview

Одно из преимуществ верстки на SwiftUI - превью, которые во многих ситуациях можно использовать вместо запуска симулятора. Разберемся как поправить самые часты ошибки, ломающие превью.

Ошибка билда

Превью не будет работать, если проект не удается собрать (command + B). Поэтому если превью не запускается, но и ошибку не выводит, то нужно попробовать сбилдить проект. Если найдется ошибка - нужно ее поправить и превью должен завестись.

EnvironmentObject

Эту штуку можно использовать для передачи зависимостей вглубь через несколько экранов/вьюшек. Есть нюанс - на момент публикации этой статьи Xcode не умеет проверять, была ли на самом деле передана зависимость во вьюшку, или нет.

Возьмем для примера такой код:

struct RootView: View {
  @EnvironmentObject private var viewModel: TabViewModel
  @EnvironmentObject private var defaults: DefaultsService

  var body: some View {
    TabView(selection: $viewModel.selectedTab) {
      ForEach(TabViewModel.Tab.allCases, id: \.rawValue) { tab in
        tab.screen
          .tabItem { tab.tabItemLabel }
          .tag(tab)
      }
    }
    .navigationViewStyle(.stack)
    .animation(.spring(), value: defaults.isAuthorized)
  }
}

#if DEBUG
#Preview {
  RootView()
}
#endif

В данном случае превью работать не будет, и на первый взгляд это можно легко поправить таким образом:

struct RootView: View {
  @EnvironmentObject private var viewModel: TabViewModel
  @EnvironmentObject private var defaults: DefaultsService

  var body: some View {
  // верстка
  }
}

#if DEBUG
#Preview {
  RootView()
    .environmentObject(DefaultsService())  // <- добавили
    .environmentObject(TabViewModel()) // <- добавили
}
#endif

Но даже сейчас превью не работает, видим алерт:

Алерт с информацией о поломке

Смело жмем на кнопку "Отчет..." и смотрим, что привело к ошибке:

Отчет об ошибке

Видим, что какая-то вьюха обращается к какому-то свойству и возникает ошибка. Переходим в эту вьюху в коде и видим еще два @EnvironmentObject:

struct SportsGroundsMapView: View {
  @EnvironmentObject private var network: NetworkStatus
  @EnvironmentObject private var defaults: DefaultsService
  @EnvironmentObject private var groundsManager: SportsGroundsManager
  // другой код
}

Теперь добавляем две оставшиеся зависимости и превью работает:

struct RootView: View {
  @EnvironmentObject private var viewModel: TabViewModel
  @EnvironmentObject private var defaults: DefaultsService

  var body: some View {
  // верстка
  }
}

#if DEBUG
#Preview {
  RootView()
    .environmentObject(DefaultsService())
    .environmentObject(TabViewModel())
    .environmentObject(SportsGroundsManager())
    .environmentObject(NetworkStatus())
}
#endif

Ошибка Xcode 15

В первой версии Xcode 15 превью могли не работать на всех симуляторах кроме одного - iPhone 15 pro. Нужно было выбрать его, чтобы починить превью - такие моменты в iOS-разработке тоже бывают, от них не спастись 💁‍♂️

Ошибка сторонних зависимостей

Бывает, что какой-то инструмент отказывается работать в превью. Самый простой пример - Firebase: достаточно подключить его к проекту, чтобы превью отвалились на всех экранах, где есть обращение к Firebase.

Решить такую проблему можно при помощи проверки:

if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
    // не обращаемся и не инициализируем инструменты, ломающие превью
} else {
    // выполняем обычный код со всеми нужными инструментами
}

Для удобства можно сделать обертку, например:

enum AppEnvironment {
  static let isRunningPreview = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
}
// где-то в коде
if AppEnvironment.isRunningPreview { ... } else { ... }

Можно дружно поблагодарить ребят за помощь на SO - именно там я впервые узнал о таком решении 🙂

Другие статьи можно посмотреть тут.