[SwiftUI]ForEachについて
何気なく使っているForEatch
について、調べたい。
以下を読む。
struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable
ForEach を使用して、何らかのデータ型の RandomAccessCollection に基づくビューを提供します。コレクションの要素が Identifiable に準拠しているか、ForEach 初期化子に id パラメータを指定する必要があります。
private struct NamedFont: Identifiable {
let name: String
let font: Font
var id: String { name }
}
private let namedFonts: [NamedFont] = [
NamedFont(name: "Large Title", font: .largeTitle),
NamedFont(name: "Title", font: .title),
NamedFont(name: "Headline", font: .headline),
NamedFont(name: "Body", font: .body),
NamedFont(name: "Caption", font: .caption)
]
var body: some View {
ForEach(namedFonts) { namedFont in
Text(namedFont.name)
.font(namedFont.font)
}
}
RandomAccessCollection
について知りたい。
まずは、Collection
について学ぶ。
Collectionとは
・複数の要素を、非破壊的に走査することができ、インデックスでもアクセスできる。
Collection Protocol
・Array、Dictionary、Setなどの継承されている。
・ここで添付されていた、継承図がわかりやすかった。
Protocolを継承するとできること
・for inを使える。
・map、filterなどの関数が使える。
・定められた添え字にアクセスできる。
・要素の数を取得できる(count)。
Collectionを自作する
・Collectionプロトコルを継承すると、必須で定義しなければならないものがある。
以下は例として載っていたコード。
struct FizzBuzzCollection: Collection {
typealias _Element = String
typealias Index = Int
var startIndex: Index { return 0 }
var endIndex: Index { return limit }
let limit: Int
subscript (position: Index) -> _Element {
precondition((startIndex..<endIndex) ~= position, "Index out of bounds.")
let num = position + 1
switch (num % 3, num % 5) {
case (0, 0): return "Fizz Buzz"
case (0, _): return "Fizz"
case (_, 0): return "Buzz"
default: return "\(num)"
}
}
func index(after i: Index) -> Index {
precondition(i < endIndex, "Can't advance beyond endIndex")
return i + 1
}
}
let fizzbuzzCollection = FizzBuzzCollection(limit: 100)
fizzbuzzCollection.forEach { print($0) }
//1
//2
//Fizz
//4
//Buzz
//Fizz
//...
//98
//Fizz
//Buzz
print(fizzbuzzCollection[4]) // -> "Buzz"
print(fizzbuzzCollection.first as Any) // -> Optional("1")
lastを使えるようにする
・Collectionを継承するだけでは、first
は使えてもlast
の使用ができない。(エラーになる)
・その時は、BidirectionalCollection
を継承するようにする。
・BidirectionalCollectionは、Collection
と、BiDirectionalIndexable
を継承したもの。
・ここでも継承図が載っていたので、みると良いかも。
beforメソッドが必須らしいので、実装する。
struct FizzBuzzCollection: BidirectionalCollection {
func index(before i: Int) -> Int {
precondition(startIndex <= i, "Can't back beyond startIndex")
return i - 1
}
}
RandomAccessCollectionと、BidirectionalCollection
読み進めていくと、以下の内容が書いてあった。
もし、要素へのランダムアクセスに関して適切な計算量で行える実装ができる場合には、 RandomAccessCollection として実装してみてもよいかもしれません。
BidirectionalCollection と比べて必須の実装は増えませんが、
index(_:offsetBy:), distance(from: to:) といったメソッドを計算量 O(1) で実装できるのが望ましいようです。
理解できなかったので、違いについて調べてみる。
BidirectionalCollection
・コレクションの要素を双方向(前後)にトラバースできることを保証するプロトコル
以下のメソッドを定義する必要がある。
・index(before:): 指定したインデックスの前のインデックスを返す。
・index(after:): 指定したインデックスの次のインデックスを返す。
定数時間(O(1))
RandomAccessCollectionを調べていくと、よく出てくる用語なので調べる。
以下はChatGPTからの引用
計算量とは、アルゴリズムがどれだけのリソース(時間やメモリ)を消費するかを示す指標です。これを表現するために「ビッグオー記法(Big O notation)」が使われます。ビッグオー記法では、入力サイズに対するアルゴリズムの実行時間や空間使用量の最悪ケースを表現します。
定数時間(O(1)とは、アルゴリズムの実行時間が入力サイズに依存せず、一定であることを意味します。入力のサイズがどれだけ大きくなっても、処理にかかる時間が変わらない場合、その処理は定数時間で行われていると言えます。
- 配列の要素アクセス:
配列(例えば、Array)の要素にインデックスを指定してアクセスする操作は定数時間で行われます。例えば、array[5] といった操作は、配列のサイズに関係なく同じ時間で完了します。- ハッシュテーブルの操作:
ハッシュテーブル(例えば、Dictionary)において、キーを使って値を取得する操作も通常は定数時間です。例えば、dictionary["key"] で値を取得する操作は、ハッシュ関数の計算にかかる時間が一定であるため、入力サイズに関係なく一定時間で行われます。定数時間の操作は非常に効率的で、特に大規模なデータセットを扱う場合には重要です。定数時間のアルゴリズムは、入力サイズが大きくなっても実行時間が増加しないため、パフォーマンスが安定しています。
入力サイズに関係なく、一定時間で終わることが重要なポイントらしい。
その代表例として、配列操作や、ハッシュ操作があるとのこと。
RandomAccessCollection
・BidirectionalCollection に加えて、ランダムアクセスを保証するプロトコル。
・要素へのアクセスが定数時間(O(1))で行えることが期待される。
・コレクションの効率性が向上し、特定の操作が高速になる。
必須のメソッドは無いが、O(1)で終わらすためのindexメソッドと、
distanceメソッドが必要になるらしい。
これを指定して、距離を調べられるようになると、
要素まで一回の操作でたどり着けるみたな感じかな?
これが、BidirectionalCollectionには無いメソッドだから、
要素にアクセスするためには、順々に先頭から見ていく必要があったりして、
要素へのアクセスが必ずしも一定時間(O(1))で終わるわけではなくなる。(O(n) )
struct RandomAccessArray<Element>: RandomAccessCollection {
private var elements: [Element]
init(_ elements: [Element]) {
self.elements = elements
}
var startIndex: Int { elements.startIndex }
var endIndex: Int { elements.endIndex }
subscript(position: Int) -> Element {
return elements[position]
}
// 指定したインデックスからのオフセット位置にあるインデックスを返す
func index(_ i: Int, offsetBy distance: Int) -> Int {
return i + distance
}
// 2つのインデックス間の距離を返す
func distance(from start: Int, to end: Int) -> Int {
return end - start
}
}
ForEach+Range
public init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content)
ForEach+Binding
public init<C>(
_ data: Binding<C>,
id: KeyPath<C.Element, ID>,
@ViewBuilder content: @escaping (Binding<C.Element>)
-> Content) where Data == LazyMapSequence<C.Indices, (C.Index, ID)>, C : MutableCollection, C : RandomAccessCollection, C.Index : Hashable
Contentの中で、@ViewBuilderを指定したクロージャを持ち、Viewを生成している。
面白そうなので、以下を読む。
StackViewとLazyStackView
3 つの標準スタック ビュー (HStack、VStack、ZStack) はすべて、表示時にそのビュー階層をロードします。大量のビューを一度にロードすると、実行時のパフォーマンスが低下する可能性があります。
スタック内のビューの数が増えたら、HStack と VStack の代わりに LazyHStackと**LazyVStack **の使用を検討してください。Lazy スタックは、サブビューをオンデマンドでロードしてレンダリングするため、大量のサブビューをロードするときにパフォーマンスが大幅に向上します。
スタック ビューは子ビューを一度にすべてロードするため、システムはロード時にすべてのサブビューのサイズと形状を把握しているため、レイアウトが高速かつ信頼性が高くなります。遅延スタックでは、システムはサブビューが表示されるときにのみサブビューのジオメトリを計算するため、ある程度のレイアウトの正確さをパフォーマンスと引き換えます。
使用するスタック ビューの種類を選択するときは、常に標準スタック ビューから開始し、コードのプロファイリングでパフォーマンスの向上が見込める場合にのみ遅延スタックに切り替えます。
・スタックビューと遅延スタックを使う場所を考えること。
・レイアウトの信頼性と描画速度(大きなロードでない)を大切にする場合は、
スタックビューを使用する。
・データが大量になってきて、読み込みに時間がかかるようになった時にLazyスタックを検討する
パフォーマンス問題点を発見する
アプリの高速化を行うために、パフォーマンスの問題点を見つける。
表示の仕方は以下
・Instruments>SwiftUI>View Body
・View Body: Count(作成された回数)、Avg Duration(かかった時間)は大事。
・View Properties: 現在の値と以前のすべての値を含む、すべてのViewのプロパティが見れる。
詳細は以下
積極的に使っていきたいな。