【SwiftUI】View プロパティの記述

これ、みんなフツーに書いてるけど。

これだけ。


struct ToggleView: View {
  @State var isOn = true

  var body: some View {
    Toggle(isOn ? "ON" : "OFF", isOn: $isOn)
  }
}

スゴイよね、SwiftUI。

 

📱 プロパティの記述

これよ。


@State var isOn = true

プロパティの記述をざっくり考えると、


[@State] [public|private] {let|var} isOn[: Bool] [= true]

となり、数十通りあるのに!

いくつか書き出して試してみます。


let isOn: Bool
let isOn: Bool = true
var isOn: Bool 
var isOn: Bool = true
private let isOn: Bool
private let isOn: Bool = true
private var isOn: Bool 
private var isOn: Bool = true
@State let isOn: Bool
@State let isOn: Bool = true
@State var isOn: Bool 
@State var isOn: Bool = true 
@State private let isOn: Bool
@State private let isOn: Bool = true
@State private var isOn: Bool 
@State private var isOn: Bool = true

 

📱 結果


// Cannot find '$isOn' in scope

let isOn: Bool
let isOn: Bool = true
var isOn: Bool 
var isOn: Bool = true
private let isOn: Bool
private let isOn: Bool = true
private var isOn: Bool 
private var isOn: Bool = true

→ Toggle() の2番目の引数 $isOn が見つからない。


// Property wrapper can only be applied to a 'var'

@State let isOn: Bool
@State let isOn: Bool = true
@State private let isOn: Bool
@State private let isOn: Bool = true

→ @State には var しか使えない。


// Missing argument for parameter 'isOn' in call

@State var isOn: Bool

→ isOn の中身がない。


// Missing argument for parameter 'isOn' in call
// 'ToggleView' initializer is inaccessible due to 'private' protection level

@State private var isOn: Bool

→ isOn の中身がない。
→ private なのでイニシャライザーがアクセスできません。


// OK

@State var isOn: Bool = true
@State private var isOn: Bool = true

→ OK

 

📱 まとめ

Property wrapper は var 。

中身が変わるからかな。



Jetpack Compose から SwiftUI に来ましたが今の謎をどうにかしたい 😩

Swift 初心者です。Kotlin からきました。

Apple 公式サンプルコードを3日間 ROM ってました。

👉 sample-backyard-birds/Multiplatform/Birds/BirdsSearchResults.swift at main · apple/sample-backyard-birds hatena-bookmark

どうも納得ができないので書き換えてみました。

動かしてみると3つとも特に問題ないような感じに見えました。

ネットで調べていると、どうもこちらも変化が激しいようで、どの記事を信じたらいいのか分かりません。

コードを見比べながら分からないこと、今後調べたいこと、を洗い出してみます。

 

😩 init()

ここで必要なのですか。なくても引数は同じ。


init(searchText: Binding<String>, @ViewBuilder content: @escaping (Bird) -> Content) {

パフォーマンス的な何か、なのでしょうか。

 

😩 KeyPath


_birds = Query(sort: \.creationDate)

すべて、Xcode 任せなのですが、省略できないんです。Bird が。

「Path」というぐらいなのでどこかに通せばいいと思っているのですが。

 

😩 Property Wrapper

いきなりでてくる _(アンダースコア)付きのこれ ROM 勢としては驚きました。

どこにもないのにいきなり登場してくる。

どこかに何か隠れてますか。


struct BirdsSearchResults<Content: View>: View {
  @Binding var searchText: String

  init(searchText: Binding<String>, ...) {
    _searchText = searchText

以下すべて挙動が同じに見えます。


struct BirdsSearchResults<Content: View>: View {
  @Binding var searchText: String


struct BirdsSearchResults<Content: View>: View {
  private var searchText: String

  init(searchText: Binding<String>, ...) {
    self.searchText = searchText.wrappedValue


struct BirdsSearchResults<Content: View>: View {
  private var searchText: Binding<String>

  init(searchText: Binding<String>, ...) {
    self.searchText = searchText

  var body: some View {
    let searchText = self.searchText.wrappedValue

ここの部分。


_searchText = searchText
_birds = Query(sort: \.creationDate)

以下記事で勉強したのですが。

👉 SwiftUI Property WrappersクラスのwrappedValue・projectedValue一覧表 #Swift - Qiita hatena-bookmark

隠しているものを丸出しに露出させることなどできるのでしょうか。

 

😩 $0

この記述よく見かけます。


birds.filter {
  $0.speciesName.contains(

分かりづらい感じがしますが、なぜ具体的なものに置き換えないのでしょうか。

Kotlin でも同様な記述があり、よく怒られていました。

 

😩 まとめ

対象の Apple サンプルコードは WWDC2023 のものなので約1年前ぐらいで、そんなに古くはないと思っています。

どうかどうかよろしくおねがいします。



【Xcode】自動補完 (Auto Completion) の使い方 - trailing closure

ぶっちゃけ、この状態でつらい。

Xcode auto complete trailing closure


公式ドキュメントが見つからなかったので、少しやってみました。

そもそもは、クロージャーをスムーズにに展開したかったので。

 

🎯 結果

いきなり結果です。

説明しづらいので動画です。



一時停止や再生速度を落とすといいかもしれません。

 

🎯 覚えておくべきキー操作

以下、5つだけです。

一般的な操作の順序です。

動画を見ながら確認するといいと思います。


1. 関数の選択 - ↑ ( up ) / ↓ ( down ) 
2. 関数グループの開閉 - → ( right ) /  ← ( left ) 
3. 関数の決定 - ↩ ( return ) 
4. フォーカスの移動 - ⇥ ( tab ) 
5. クロージャーの展開 - ↩ ( return )

あくまで、


- 上下左右のカーソルキー
- タブキー
- リターンキー(エンターキー)

だけで進んでいきます。

その他のキーを押すと、Auto-Completion のモードから外れます。

注意しておくのは、

「青いフォーカス」を意識して選択決定のリターンキー

を押すことだけです。

 

🎯 まとめ

初心者の、

機能が豊富なIDEの操作の習得はつらい。

👉 [Accepted] SE-0279: Multiple Trailing Closures - Evolution / Announcements - Swift Forums hatena-bookmark
👉 xcode - Is there a way to simplify typing Swift trailing closure? - Stack Overflow hatena-bookmark



【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