【SwiftUI】ストップウォッチ つくってみる ⏱

非同期処理 や Observable クラスの確認です。



Timer クラスというのがあるようなので

それもやって比較してみました。



 

⏱ @Obserevable ってシンプル

マクロ @Observable ひとつだけで書けるようになったんですね !

👉 【SwiftUI】今どきの データモデル (Model data) のマクロ記述 📝 hatena-bookmark
👉 Migrating from the Observable Object protocol to the Observable macro | Apple Developer Documentation hatena-bookmark

 

⏱ 謎な部分

macOS で ビルトインの Timer クラスが30秒につき7秒ぐらいも遅れる。

なんでなんやろ ?

👉 Timer | Apple Developer Documentation hatena-bookmark


【Swift】プロトコル と 型 の難しさを Array のextension 化で感じた初心者

以前、so でみつけたこういうコード。


extension Array where Element: Collection { 
  func toD1() -> [Element.Element] {
    return self.flatMap { $0 } 
  }
}

👉 【 Swift 】2次元配列と1次元配列の相互変換 🤔 hatena-bookmark

なんとなく「Collection」 の部分が気持ち悪かったのであれこれやってみます。

 

■ お題

2次元配列を平坦化します。


let data = [
  ["あ", "い", "う"],
  ["え", "お"]
]

print(data.flatMap { $0 })
// ["あ", "い", "う", "え", "お"]

print(Array(data.joined()))
// ["あ", "い", "う", "え", "お"]

これを extension 化しようとして、


extension Array {
  var flattened: [String] {
    flatMap { $0 }
  }
}


Instance method 'flatMap' requires that 'Element' conform to 'Sequence'

とエラーがでるとこからスタートです。

 

■ やってみた

ドキュメントや先人の記事を読んでおきます。

👉 Element | Apple Developer Documentation hatena-bookmark
👉 Array | Apple Developer Documentation hatena-bookmark
👉 [Swift]Array型 #Swift - Qiita hatena-bookmark

型や制約の記述を確認しながら進みます。


extension Array where Element == Array<String> {
  var flattened: [String] {
    // Array(joined()) // Cannot convert return expression of type 'Array<Array<String>>' to return type '[String]'
    // joined().map { $0 } // OK
    // Array<String>(joined()) // OK
    // [Element.Element](joined()) // OK
    // [String](joined()) // OK
    flatMap { $0 } // OK
}


extension [[String]] where Element == [String] {
  var flattened: [String] {
    flatMap { $0 }
  }
}

// ["あ", "い", "う", "え", "お"]


extension Array where Element == [String], Element.Element == String {
  var flattened: [Element.Element] {
    flatMap { $0 }
  }
}

// ["あ", "い", "う", "え", "お"]

ここで、制約を先祖のプロトコルのみにします。


extension Array where Element: Collection {
  var flattened: [Element.Element] {
    flatMap { $0 }
  }
}

// ["あ", "い", "う", "え", "お"]

揃えたほうが良さげです。


extension Collection where Element: Collection {
  var flattened: [Element.Element] {
    flatMap { $0 }
  }
}

// ["あ", "い", "う", "え", "お"]

スッキリしました !

Sequence でもいけるんですね。


extension Sequence where Element: Sequence  {
  var flattened: [Element.Element] {
    flatMap { $0 }
  }
}

// ["あ", "い", "う", "え", "お"]

👉 誰もが知りたいSequenceとCollectionのすべて hatena-bookmark

ちなみに、元の配列に nil を混ぜると、


let data = [
  ["あ", nil, "う"],
  ["え", "お"]
]

上手に処理してくれてますね!


// [Optional("あ"), nil, Optional("う"), Optional("え"), Optional("お")]

 

■ まとめ

プロトコルと型。

よく分かってませんが

なんだか深そうです。

そういえば、どこかに書いてました、

プロトコルを使用することによって得られる柔軟性と、特定の型のメソッドの作成の容易さのバランス

というようなフレーズ。

ありがとうございます。

どこか継承グラフみれるとことかあるのでしょうか ? 独自に書き出さずに。

👉 Dash for macOS - API Documentation Browser, Snippet Manager - Kapeli hatena-bookmark
👉 realm/jazzy: Soulful docs for Swift & Objective-C hatena-bookmark


【SwiftUI】 今日のなんでやねん - @ViewBuilder


@ViewBuilder

よく見かけますが、なんのために使うのか。

 

■ 公式ドキュメント - ViewBuilder

A custom parameter attribute that constructs views from closures.

クロージャから View を構築するカスタムパラメータ属性。

You typically use ViewBuilder as a parameter attribute for child view-producing closure parameters, allowing those closures to provide multiple child views.

通常、ViewBuilderを子 View 生成クロージャパラメータのパラメータ属性として使用し、それらのクロージャが複数の子 View を提供できるようにします。


func contextMenu<MenuItems: View>(
  @ViewBuilder menuItems: () -> MenuItems
) -> some View

myView.contextMenu {
  Text("Cut")
  Text("Copy")
  Text("Paste")
  if isSymbol {
    Text("Jump to Definition")
  }
}

👉 ViewBuilder | Apple Developer Documentation hatena-bookmark

並列のまま、持ち運びできて、

記述が一層不要になるってことか。

 

■ if も使えるようになる


// NG
var body: some View {
    if imageName.isEmpty {
        return Text("no image")
    } else {
        return Text(imageName)
    }
}

@ViewBuilder
var body: some View {
    if imageName.isEmpty {
        Text("no image")
    } else {
        Image(imageName)
    }
}

var body: some View {
    Group {
        if imageName.isEmpty {
            Text("no image")
        } else {
            Image(imageName)
        }
    }
}

👉 SwiftUIのViewで条件によってViewを出し分ける方法 - The Pragmatic Ball boy hatena-bookmark

そういえば、

公式ドキュメントのサンプルコードにも、

しれっと if が入っていた。

なるほど、

Group / VStack / HStack がなければ使えなかった if を使えるようにしてくれるのか。

便利そう !

 

■ やってみた

こういうのがあったとして、


VStack {
  Text("top")
  Text("bottom")
}
.font(.title)
.foregroundColor(.red)

こう書ける。


TitleTextFormatView1 {
  Text("top")
  Text("bottom")
}

struct TitleTextFormatView1<Content: View>: View {
  @ViewBuilder var content: Content

  var body: some View {
    content
      .font(.title)
      .foregroundColor(.red)
  }
}

続いて、@ViewBuilder を消してみます。


TitleTextFormatView2 {
  Text("top")
  Text("bottom")
}

struct TitleTextFormatView2<Content: View>: View {

  // Type '() -> ()' cannot conform to 'View'

  var content: Content // *

  var body: some View {
    content
      .font(.title)
      .foregroundColor(.red)
  }
}

しかし、これは NG ですね、分かります。

勉強しましたから !

続きましては、

渡す View を VStack を使って、1つにまとめます。

他は変更ありません。

結果は、OK なはずです。


TitleTextFormatView3 {
  VStack { // *
    Text("top")
    Text("bottom")
  }
}

struct TitleTextFormatView3<Content: View>: View {
  var content: Content

  var body: some View {
    content
      .font(.title)
      .foregroundColor(.red)
  }
}

...

なんでや !?

 

■ まとめ

私はこれまで、

まず VStack や HStack を常に書いていたので

@ViewBuilder の必要性を感じなかった

ことが分かりました。

しかし、今回の結果は残念です。

謎です。

以下の基本的なキーワード、


「TupleView」
「Content」
「some View」
「any View」
「View | Stack | Layout」

分かりづらくないですか。

直感的にふんわりしてません ?

長い説明が必要ですよね ?

あなたの感想ですよね ?