November 1, 2024

91. Создаем вьюху протоколом

Хочется закрыть модуль протоколом, чтобы не тащить имплементацию во все места, но непонятно как обойтись без AnyView? Эта статья для тебя.

План

  1. Изучим вводные данные
  2. Посмотрим на "проблемы" some View в протоколах
  3. Напишем протокол, который позволит использовать some View без проблем
  4. ???
  5. Профит.

Вводные данные

Есть какой-то Swift Package, в котором находится твоя фича: там и сервисы, и вьюхи. И вот тимлид/кто-то другой из команды сказал, что нужно изолировать имплементацию от интерфейса.

Немного погуглив, ну или сразу сходу ты делаешь два "продукта" в пакете, т.е. две папки, где в одной будет внутренняя реализация (имплементация) фичи, без публичных штук, а в другой - публичные протоколы (интерфейс).

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

Проблемы

AnyView

Немного погуглив (совсем немного), можно сделать что-то вроде такого:

// MARK: - Где-то в другом пакете
public protocol BadViewProvider {
  func makeView() -> AnyView
}

public struct BadExampleViewProvider: BadViewProvider {
  public func makeView() -> AnyView {
    AnyView(
      Text("Твоя вьюха из имплементации")
        .font(.title.bold())
    )
  }
}

// MARK: - В основном таргете приложения
struct BadContentView: View {
  let viewProvider: BadViewProvider
   
  var body: some View {
    VStack(spacing: 20) {
      Text("Снизу вьюха, которую мы получили из протокола")
      viewProvider.makeView()
    }
    .multilineTextAlignment(.center)
    .padding()
  }
}
  1. Проект собирается? Да!
  2. Вьюха отображается? Да!
  3. Приложение работает? Да!

Трижды "да"!

А что если я скажу, что можно сделать без AnyView, и вообще AnyView тут неправильное решение?

Ты можешь спросить, почему это неправильно и чем это плохо.

На это я отвечу, что статью об этом пока не успел написать, но она на очереди. Если совсем кратко, то AnyView сильно хуже, чем some View, и на английском языке об этом есть уже много статей - можешь загуглить Avoiding SwiftUI’s AnyView.

any View

Если сильно постараться, то можно сделать что-то вроде такого:

func makeView2() -> any View

Обновленный пример:

// MARK: - Где-то в другом пакете
public protocol BadViewProvider {
  func makeView() -> AnyView // <- первый пример
  func makeView2() -> any View // <- второй пример
}

public struct BadExampleViewProvider: BadViewProvider {
  public func makeView() -> AnyView {
    AnyView(
      Text("Твоя вьюха из имплементации")
        .font(.title.bold())
    )
  }
   
  public func makeView2() -> any View {
    AnyView(
      Text("Вторая вьюха из имплементации")
        .font(.title.bold())
    )
  }
}

// MARK: - В основном таргете приложения
struct BadContentView: View {
  let viewProvider: BadViewProvider
   
  var body: some View {
    VStack(spacing: 20) {
      Text("Снизу вьюха, которую мы получили из протокола")
      viewProvider.makeView()
      AnyView(viewProvider.makeView2()) // <- без AnyView уже не работает совсем
    }
    .multilineTextAlignment(.center)
    .padding()
  }
}

Как видим, второй вариант вовсе не работает без AnyView.

Вариантов неправильного решения очень много, теперь посмотрим на правильное.

Правильное решение

Чтобы не гуглить, и сходу применить правильное решение, заглянем прямо в Xcode в протокол View, который нам предоставляет Apple:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@MainActor @preconcurrency public protocol View {
  associatedtype Body : View
  @ViewBuilder @MainActor @preconcurrency var body: Self.Body { get }
}

Заглянули, и сделали по аналогии:

// MARK: - Где-то в другом пакете
@MainActor
public protocol DemoViewProvider {
  associatedtype Content: View
  @ViewBuilder @MainActor func makeDemoView() -> Content
}

struct ExampleDemoViewProvider: DemoViewProvider {
  func makeDemoView() -> some View {
    Text("Вьюха из другого пакета, полученная из метода")
      .font(.title.bold())
  }
}

// MARK: - В основном таргете приложения
// Вьюха использует дженерик, без которого ничего не заведется
struct MainContentView<Provider: DemoViewProvider>: View {
  let viewProvider: Provider
   
  var body: some View {
    VStack(spacing: 20) {
      Text("Снизу вьюха, которую мы получили из протокола")
        .multilineTextAlignment(.center)
      viewProvider.makeDemoView()
    }
    .padding()
  }
}

Профит

  1. Мы не изобрели велосипед и не ставим палки в колеса SwiftUI
  2. Все работает как и должно

Заключение

Теперь ты знаешь как закрыть протоколом свою вьюху, а на собесах это может сильно удивить интервьюеров 😉

Код для этой статьи можно посмотреть тут, другие статьи по разработке - тут, а про инвестиции - тут.