[Memo] VStackに大量のViewを表示するアプローチ

image

ViewBuilderは10個のViewまでコンパイル可能

VStackHStackViewBuilder によって複数のViewを受け付けることができます ViewBuilderはFunction buildersの実装です。(ここでは割愛)

VStack {
  Text("")
	Text("")
}

2つのTextは実際には TupleView によって包まれます。 TupleViewの特徴は要素の数と要素の型がコンパイル時に分かることです。 ですが TupleViewには個数の制限が存在します。

ViewBuilderの実装を見ると

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

    public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View
}

C0 から C9までなので、VStackの中には10個までViewが書けるということになりそうです。

実際にVStackに10個のTextを入れてみると、コンパイルは通ります。

image

そして、11個のTextを書いてみるとエラーになります。 ViewBuilderの実装が11個のViewをサポートしていないということを意味します。

image

ForEachを使って対応

無限に書ければ嬉しいのですが、今のところそのサポートは無いようです。 現実問題としてもTupleViewで無限の表現は無理で、Tupleの定義を10個から100個にしても限界は来るし、定義もものすごいことになってしまいます。

そこでリストとしてレンダリングを行う ForEach というViewを使うことを検討してみます。

ForEach はデータソースに Identifiable protocolの実装を求めるので、適当に作ります。

struct IdentifiedView: View, Identifiable {
      
  let id = UUID() // this is just for example
  private let content: AnyView
  
  init<Content>(@ViewBuilder _ content: () -> Content) where Content: View {
    self.content = AnyView(content())
  }
  
  var body: some View {
    content
  }
  
}

使い方

IdentifiedView { Text("Hello") }

極端ですが、これでいくらでも書けます。

その代わりTupleViewと違い、Contentの型は不明瞭になります。

image

image

こんなことを調べていたのは、UIScrollViewを使ってカスタムUIなFormを作りたかったから。

image
import SwiftUI

struct DevelopmentMenuView: View {
  
  var body: some View {
               
    ScrollView(.vertical, showsIndicators: true) {
      ZStack {
        Spacer() // In order to expand view to full-screen
        VStack(spacing: CGFloat(0.0)) {                    
          ForEach([
            section(title: "Options"),
            separator(),
            row(title: "A"),
            separator(),
            row(title: "B"),
            separator(),
            row(title: "C"),
            separator(),
            row(title: "D"),
            separator(),
            row(title: "E"),
            separator(),
            row(title: "F"),
          ], content: { $0 })
          
        }
        .padding(.init(top: 0, leading: 20, bottom: 0, trailing: 20))
      }
      
    }
    
  }
}

struct IdentifiedView: View, Identifiable {
      
  let id = UUID()
  private let content: AnyView
  
  init<Content>(@ViewBuilder _ content: () -> Content) where Content: View {
    self.content = AnyView(content())
  }
  
  var body: some View {
    content
  }
  
}

private func section(title: String) -> IdentifiedView {
  
  IdentifiedView {
    HStack() {
      Text(title)
        .multilineTextAlignment(.leading)
        .font(.system(.largeTitle, design: .default))
        .foregroundColor(.black)
      Spacer()
    }
    .padding(.init(top: 16, leading: 0, bottom: 16, trailing: 0))
  }

}


private func row(title: String) -> IdentifiedView {
  
  IdentifiedView {
    HStack() {
      Text(title)
        .multilineTextAlignment(.leading)
        .font(.system(.body, design: .default))
        .foregroundColor(.black)
      Spacer()
    }
    .padding(.init(top: 16, leading: 0, bottom: 16, trailing: 0))
  }

}

private func separator() -> IdentifiedView {
  
  IdentifiedView {
    Color(white: 0.95)
      .frame(height: 1)
  }
}