Объекты окружения и стили SwiftUI

Туториалы

Объекты окружения и стили SwiftUI

Окружение и стили SwiftUI являются двумя столпами официального фреймворка (декларативной структуры) от Apple. Не смотря на это, при первом запуске SwiftUI их совместное использование приводило к гарантированному падению приложения.

В частности, сбой происходил, когда мы использовали @EnvironmentObject внутри нашего определения стилей: когда безопасно использовать их вместе? Давайте выясним.

Заметка

Для нетерпеливых результаты в конце статьи.

Пример

Познакомьтесь с FSStyle, стилем кнопки, ожидающим объект среды (FSEnvironmentObject):

class FSEnvironmentObject: ObservableObject {
  @Published var title = "tap me"
}

struct FSStyle: ButtonStyle {
  @EnvironmentObject var object: FSEnvironmentObject

  func makeBody(configuration: Configuration) -> some View {
    Button(object.title) { }
  }
}

Исходя из определения, мы ожидаем, что все будет работать, пока мы вводим объект окружения в какой-то момент до того, как применить стиль. Например:

struct ContentView: View {
  @StateObject var object = FSEnvironmentObject()

  var body: some View {
    Button("tap me") {
    }
    .buttonStyle(FSStyle())
    .environmentObject(object)
  }
}

..и все же, если мы запускали этот код в любой версии iOS до 14.5, он гарантированно давал сбой в 100% случаев с Fatal error: No ObservableObject of type FSEnvironmentObject found..

Сбой происходит, как только объект окружения используется внутри метода makeBody (configuration: ), и определение объекта, и исполнение makeBody (configuration: ) не имеют значения.

Есть два основных способа обойти ошибку: или привести стиль в соответствие с DynamicProperty (большое спасибо Линь Цин Мо за подсказку!), или вернуть View в методе makeBody (configuration: ) и заставить этот View читать объект окружения.

Теперь, когда мы увидели, в чем проблема, давайте узнаем, в каких версиях iOS и стилях встречается эта ошибка.

 

Тестовая установка

Мы хотим выяснить, в каких версиях iOS безопасно использовать все возможные стили (не только стили кнопок). Мы можем создать небольшое тестовое приложение и запустить его во всех версиях iOS, поддерживающих SwiftUI, и получить результат.

Продолжая на примере с ButtonStyle, вот полное приложение:

import UIKit
import SwiftUI

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { }

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?
  var object: FSEnvironmentObject = FSEnvironmentObject()

  func scene(
    _ scene: UIScene, 
    willConnectTo session: UISceneSession, 
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    if let windowScene = scene as? UIWindowScene {
      let window = UIWindow(windowScene: windowScene)
      let contentView = ContentView().environmentObject(object)
      window.rootViewController = UIHostingController(rootView: contentView)
      self.window = window
      window.makeKeyAndVisible()
    }
  }
}

struct ContentView: View {
  var body: some View {
    Button("tap me") {
    }
    .buttonStyle(FSStyle())
  }
}

struct FSStyle: ButtonStyle {
  @EnvironmentObject var object: FSEnvironmentObject

  func makeBody(configuration: Configuration) -> some View {
    Button(object.title) { }
  }
}

class FSEnvironmentObject: ObservableObject {
  @Published var title = "tap me"
}

Приложение состоит из одного экрана, на котором находится наш тестовый компонент/стиль.

Несколько замечаний:

  • мы используем жизненный цикл UIKit, потому что мы хотим запускать тесты также и на iOS 13
  • мы не используем @StateObject для объекта окружения, потому что данная обёртка свойств только для iOS 14+
  • единственная разница между тестированием ButtonStyle и другими стилями заключается в определении тела FSStyle и ContentView, все остальное остается прежним

 

Настройка CI/CD

Мы будем тестировать двенадцать версий iOS, от iOS 13.0 до iOS 14.5, и все восемь стилей, поддерживающих настройку.

Тестировать каждую комбинацию вручную было бы довольно сложно, вместо этого мы можем позволить провайдеру CI/CD сделать всю тяжелую работу за нас. Подойдет любая установка CI/CD, вот как различные версии Xcode/iOS были распределены для этого исследования:

  • macOS 10.14
    • iOS 13.0, Xcode 11.0
    • iOS 13.1, Xcode 11.1
    • iOS 13.2, Xcode 11.2
  • macOS 10.15
    • iOS 13.3, Xcode 11.3.1
    • iOS 13.4, Xcode 11.4.1
    • iOS 13.5, Xcode 11.5
    • iOS 13.6, Xcode 11.6
    • iOS 13.7, Xcode 11.7
    • iOS 14.0, Xcode 12.0.1
    • iOS 14.1, Xcode 12.1
    • iOS 14.2, Xcode 12.2
    • iOS 14.3, Xcode 12.3
  • macOS 11.4:
    • iOS 14.4, Xcode 12.4
    • iOS 14.5, Xcode 12.5

Результаты

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

Style 👇🏻 / iOS 👉🏻

13.0

13.1

13.2

13.3

13.4

13.5

13.6

13.7

14.0

14.1

14.2

14.3

14.4

14.5

ButtonStyle

💥

💥

💥

💥

💥

💥

💥

💥

💥

💥

💥

💥

💥

GroupBoxStyle*

LabelStyle*

MenuStyle*

PrimitiveButtonStyle

💥

💥

💥

💥

💥

💥

💥

💥

ProgressViewStyle*

TextFieldStyle

💥

💥

💥

💥

💥

💥

💥

💥

ToggleStyle

💥

💥

💥

💥

💥

💥

💥

💥

💥 = вылет, = пройдено. * Стиль доступен с iOS 14.

Резюмируем:

  • все стили, кроме ButtonStyle, поддерживают @EnvironmentObject с iOS 14.0
  • начиная с iOS 14.5, все стили, включая ButtonStyle, поддерживают @EnvironmentObject

Выводы 

Причина, по которой комбинация styles + @EnvironmentObject не поддерживалась с самого начала, вероятно, останется внутри команды SwiftUI, однако это могло быть намеренно:

Глядя на то, как применяются стандартные стили SwiftUI, помимо нескольких параметров, передаваемых через Configuration, бОльшая часть изменяемых компонентов приходит из EnvironmentValues, например @Environment (\.IsEnabled), @Environment (\.font) и @Environment (\. controlProminence).

В отличие от @EnvironmentObject, EnvironmentValues ​​поддерживается (без вылета!) с iOS 13.0, поэтому я рекомендую их при добавлении динамики в наши пользовательские стили.

Невзирая на то, было ли это ошибкой или намеренно, как поставщик SDK, нам важно устранить неоднозначность и прояснить такие сценарии для разработчиков.

Насколько мне известно, это нигде не было задокументировано и не рассматривалось ни в одном примечании к выпуску: если разработчики это допустили, это находится во фреймворке.

Еще одно место, где это могло быть проблемой - это модификаторы View, однако и EnvironmentValues,  и @EnvironmentObject поддерживаются (без 💥) из iOS 13.0.

Оригинал статьи

Комментарии

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: