【SwiftUI】NavigationSplitView と TabView の切り替え判定 🔀

マルチプラットフォームアプリで、

Mac/iPad と iPhone/AppleTV でグループ分けしてそれなりに切り替える。


if prefersTabNavigation { // *
//if UIDevice.current.userInterfaceIdiom == .phone { // NG on macOS
//if UserInterface.prefersTabNavigation {
  TabView(selection: $screen) {
    ForEach(Screen.allCases) { screen in
      screen.destination
        .tag(screen as Screen?)
        .tabItem { screen.label }
    }
  }
} else {
  NavigationSplitView { 
    SidebarList(screen: $screen)
  } detail: {
    DetailColumn(screen: screen)
  }
}

判定の条件 (Bool) をどう書くか。

 

🔀 Apple 公式サンプル

WWDC23 でアナウンスの新機能ですか。

👉 Unleash the UIKit trait system - WWDC23 - Videos - Apple Developer hatena-bookmark

便利な SwiftUI Environment に UIKit UITrait をブリッジするという UITraitBridgedEnvironmentKey を使った方法。


// Create custom Environment
struct PrefersTabNavigationEnvironmentKey: EnvironmentKey {
  static var defaultValue: Bool = false
}

extension EnvironmentValues {
  var prefersTabNavigation: Bool {
    get { self[PrefersTabNavigationEnvironmentKey.self] }
    set { self[PrefersTabNavigationEnvironmentKey.self] = newValue }
  }
}

// Bridge UITrait read only
#if os(iOS)
extension PrefersTabNavigationEnvironmentKey: UITraitBridgedEnvironmentKey {
  static func read(from traitCollection: UITraitCollection) -> Bool {
    return traitCollection.userInterfaceIdiom == .phone || traitCollection.userInterfaceIdiom == .tv
  }

  static func write(to mutableTraits: inout UIMutableTraits, value: Bool) {
    // Do not write
  }
}
#endif

👉 sample-backyard-birds/Multiplatform/General/PrefersTabNavigationEnvironmentKey.swift at 1843d5655bf884b501e2889ad9862ec58978fdbe · apple/sample-backyard-birds hatena-bookmark


@Environment(\.prefersTabNavigation) private var prefersTabNavigation

👉 sample-backyard-birds/Multiplatform/ContentView.swift at 1843d5655bf884b501e2889ad9862ec58978fdbe · apple/sample-backyard-birds hatena-bookmark

高級な感じで相互読み書き可能な状態にできるのはいいけども。

 

🔀 その他

なんとなく試しながら書いてみました。

Constants な感じで。書き方はいくつかあるようです。

enum を使う。意見はいろいろありそう。


enum UserInterface { // as namespace
  static var prefersTabNavigation: Bool {
    #if os(iOS) //canImport(UIKit)
    let idiom = UIDevice.current.userInterfaceIdiom
    return idiom == .phone || idiom == .tv
    #else
    return false
    #endif
  }
}

フツーに struct で書く。


struct UserInterface {
  static var prefersTabNavigation: Bool {
    #if os(iOS) //canImport(UIKit)
    let idiom = UIDevice.current.userInterfaceIdiom
    return idiom == .phone || idiom == .tv
    #else
    return false
    #endif
  }
}

👉 `static let` in enum vs struct? - Using Swift - Swift Forums hatena-bookmark

呼び出し側。


if UserInterface.prefersTabNavigation {

 

🔀 まとめ


#if os(iOS)

ての、なんとなく

なるべくは使いたくないです。

Apple 公式サンプルは、新機能を強引に Read Only でサンプルコードに利用した感じに見えるけどもー。




【SwiftUI / SwiftData】Using ViewModifier for setting ModelContainer 🔌

ViewModifiers can be used in nested parent-child Views for each respective #Preview, making it convenient. This also enhances clarity even further.

 

🔌 Using as an extension of View

Create a custom ViewModifier.


struct DogDataContainerViewModifier: ViewModifier {
  func body(content: Content) -> some View {
    content
      .modelContainer(try! ModelContainer(for: Dog.self))
  }
}

All ViewModifiers will be turned into extensions.


extension View {
  func dogDataContainer() -> some View {
    modifier(DogDataContainerViewModifier())
  }
}

It is used in the implementation of the parent as well as in the #Preview of the child.


struct DogView: View {
  // ...
}

#Preview {
  DogView()
    .dogDataContainer()
}

 

🔌 Initializing or creating data

Additionally, as there are often data initialization or creation tasks, I'll add those.

This will be in the part with View.onAppear().

We'll use ModelContext to manipulate the data.


struct GenerateDataViewModifier: ViewModifier {
  @Environment(\.modelContext) private var modelContext
    
  func body(content: Content) -> some View {
    content.onAppear {
      DataGeneration.generateAllData(modelContext: modelContext)
    }
  }
}

This will also be made into an extension.


extension View {
  func generateData() -> some View {
    modifier(GenerateDataViewModifier())
  }
}

Let's add this to the initial code.


struct DogDataContainerViewModifier: ViewModifier {
  func body(content: Content) -> some View {
    content
      .generateData()
      .modelContainer(try! ModelContainer(for: Dog.self))
  }
}

 

🔌 Conclusion

I'll summarize it.


struct DogDataContainerViewModifier: ViewModifier {
  func body(content: Content) -> some View {
    content
      .generateData()
      .modelContainer(try! ModelContainer(for: Dog.self))
  }
}

struct GenerateDataViewModifier: ViewModifier {
  @Environment(\.modelContext) private var modelContext
    
  func body(content: Content) -> some View {
    content.onAppear {
      DataGeneration.generateAllData(modelContext: modelContext)
    }
  }
}

extension View {
  func dogDataContainer() -> some View {
    modifier(DogDataContainerViewModifier())
  }
}

fileprivate extension View {
  func generateData() -> some View {
    modifier(GenerateDataViewModifier())
  }
}

When using, only basic public extensions are used.


struct DogView: View {
  // ...
}

#Preview {
  DogView()
    .dogDataContainer()
}

It can also be used on the implementation side.

For reference, below is Apple's official sample code.

👉 sample-backyard-birds/BackyardBirdsData/General/BackyardBirdsDataContainer.swift at 1843d5655bf884b501e2889ad9862ec58978fdbe · apple/sample-backyard-birds hatena-bookmark


Swift で Singleton

前回、Apple サンプルコードを見ながら、生存期間が謎だったのですが。

👉 【Swift】この ModelActor ってなぜ生きてるの? hatena-bookmark

シンプルなクラスにするとこういうことですね。


class Cat {
  private(set) static var shared: Cat!

  private init() {}

  static func createInstance() {
    shared = Cat()
  }

  func show() {
    print("ねこ")
  }
}

Cat.createInstance()
Cat.shared.show() // ねこ

一度インスタンスを生成すると、

全スコープから参照可能な状態で、アプリが死ぬまで生きてるんですね。


👉 Singleton pattern - Wikipedia hatena-bookmark

どう書いてますか Singleton。

少し Xcode PlayGround で試してみます。

ざっくりこのようなイメージでいたけども。


class Monkey {
  private(set) static var instance: Monkey?

  private init() {}

  static func createInstance() -> Monkey {
    if (instance == nil) {
      instance = Monkey()
    }
    return instance!
  }

  func show() {
    print("さる")
  }
}

var monkey: Monkey? = Monkey.createInstance()
monkey?.show() // "さる"
monkey = nil
monkey?.show() // not show

公式ドキュメントを見ると以下のような感じになってます。

👉 Managing a Shared Resource Using a Singleton | Apple Developer Documentation hatena-bookmark


class Dog {
  static let shared = Dog()

  func show() {
    print("いぬ")
  }
}

Dog.shared.show() // いぬ


class Kiji {
  static let shared: Kiji = {
    let instance = Kiji()
    return instance
  }()

  func show() {
    print("きじ")
  }
}

Kiji.shared.show() // きじ

var shared: Kiji? = Kiji.shared
shared?.show() // きじ
shared = nil
shared?.show() // not show

本当に死んでるのか?

Swift では、結構嫌がられてますよね Singleton。

使いすぎて収集つかずに太っちゃうやつですかね。

👉 Automatic Reference Counting | Documentation hatena-bookmark
👉 Kotlin で書きたい「正しいシングルトン(Singleton)」 hatena-bookmark