blog

SWiftUIのレイアウトの基本

SwiftUIのレイアウトのアイデアでは、レイアウトのUIKitで少し全く同じではありませんが、この記事では主にSwiftUIで最も一般的なレイアウトの遊びのいくつかを説明し、これらのレイアウト関連の...

Mar 16, 2020 · 14 min. read
シェア

SwiftUIのレイアウトのアイデアでは、UIKitのレイアウトが同じよりも少し少ないですが、この記事では、SwiftUIのプレイで最も一般的なレイアウトのいくつかを説明することに焦点を当て、これらのレイアウト関連のルールは非常に基本的ですが、これらのテクニックを理解し、非常に必要です。

この記事では、いくつかのポイントを取り上げます:

  • frame
  • GeometryReader
  • Alignment Guide
  • Preference
  • Stacks
  • Spacer
  • layoutPriority

レイアウトの法則

次の3つの法則はSwiftUIのレイアウトの最も基本的な法則です。どのようなレイアウトに直面するときでも、これらの3つの法則を考えるだけで、なぜUIがそのような効果になるのか理解できます:

  1. 親ビューは子ビューの推奨サイズを提供します。
  2. サブビューは、それ自身の特性に従ってサイズを返します。
  3. 親ビューは、子ビューから返されたサイズに基づいて、子ビューのレイアウトを作成します。

単純にを取ります:

struct ContentView: View {
 var body: some View {
 Text("Hello, world")
 .border(Color.green)
 }
}

ContentViewはTextの親ビューとして、Textのための推奨サイズを提供し、それはこの場合、フルスクリーンサイズです。SwiftUIでは、コンテナのデフォルトのレイアウトは中央揃えです。

上記の3つの基本的なレイアウト・ルールは、プレゼンテーションの残りの部分で何度も繰り返し使用されます。

frame

UIKitでは、フレームはある種の絶対的なレイアウトで、その位置は親ビューの左上隅からの相対的な絶対座標です。しかしSwiftUIでは、修飾子としてのフレームの概念は完全に異なります。

まずはを見てください:

struct ContentView: View {
 var body: some View {
 Text("Hello, world")
 .background(Color.green)
 .frame(width: 200, height: 50)
 }
}

理想的なディスプレイはこんな感じ:

しかし、実際の効果はこうです:

上記のコードでは、.backgroundは元のテキストを直接修正せず、代わりにテキストレイヤーの下に新しいビューを作成します。

あなたはレイアウトの3つの法則からこの問題を考慮した場合、それは非常に単純になります、.frameの役割は、このケースでは、背景のためのフレームがサイズを提供し、背景も、その子に依頼する必要がある、つまり、テキスト、テキストは、独自のニーズのサイズを返したので、背景もテキストと同じサイズで緑の背景の効果を引き起こしたテキストの実際のサイズを返しました。Textは自分の必要なサイズを返すので、backgroundもTextの実際のサイズを返し、緑色の背景がテキストと同じサイズであるという効果が生じます。

このレイアウトのプロセスを知ると、望みの効果を得るためには、上のコードを少し修正する必要があることがわかります:

struct ContentView: View {
 var body: some View {
 Text("Hello, world")
 .background(Color.green)
 .frame(width: 200, height: 50)
 }
}

ただ、この機能を達成するために、フレームと背景の順序を調整し、慎重に考えてください、これはなぜですか?スペースを節約するために、あまりにも多くの説明を行うことはありませんが、それはそのようなテキストなどの各ビューの異なる特性は、独自のサイズに戻り、そのような形状として、親ビューの提案サイズに戻り、実際のレイアウトでは、これらの異なる特性の影響を考慮する必要があることは注目に値します。

SwiftUIはフレームの2つの定義を持っています:

func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center) -> some View

widthとheightはnilにすることができ、nilの場合、直接親ビューのサイズを使用します。まず、以下のコードを見てください:

struct ContentView: View {
 var body: some View {
 HStack {
 Text("Good job.")
 .background(Color.orange)
 }
 .frame(width: 300, height: 200, alignment: .topLeading)
 .border(Color.green)
 }
}

通常の開発で、フレーム内でアライメントを設定してもうまくいかない場合、HStackやVStackなどの外側のコンテナのサイズが、ちょうど子ビューのサイズと等しいことが主な原因ですが、このような場合もアライメントの効果は同じです。この場合の整列の効果は同じです。

すると、フレームの2つ目の定義は次のようになります:

 public func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .center) -> some View

この関数にはさらに多くのパラメーターがありますが、一般的に3つのカテゴリーに分類されます:

  • minWidth,idealWidth,maxWidth
  • minHeight,idealHeight,maxHeight
  • alignment

minとmaxの内容については、以下の関係図を見ていただければと思います:

idealWidthとidealHeightについて簡単に説明しましょう。 文字通り、idealは理想的という意味です。では、ビューにidealWidthを設定するとどうなるのでしょうか?

struct ContentView: View {
 var body: some View {
 Text("Good job.")
 .frame(idealWidth: 200, idealHeight: 100)
 .border(Color.green)
 }
}

実行した結果、Textは指定された理想的なサイズを使用しないことがわかりました:

.fixedSize(horizontal: true, vertical: true)実際、この理想を実現するためには、この理想を併用しなければなりません:

  • horizontal: 固定の水平方向、つまりidealWidthを示します。
  • vertical: 固定の垂直方向、つまりidealHeightを示します。

もう一度確認してください:

struct ContentView: View {
 var body: some View {
 HStack {
 Text("horizontal")
 .frame(idealWidth: 200, idealHeight: 100)
 .fixedSize(horizontal: true, vertical: false)
 .border(Color.green)
 
 Text("vertical")
 .frame(idealWidth: 200, idealHeight: 100)
 .fixedSize(horizontal: false, vertical: true)
 .border(Color.green)
 
 Text("horizontal & vertical")
 .frame(idealWidth: 200, idealHeight: 100)
 .fixedSize(horizontal: true, vertical: true)
 .border(Color.green)
 }
 }
}

実際の開発で役立つこのテクニックは、外部条件によってビューのサイズを変更することなく、ビューのサイズを直接固定することができます。フレームの詳細について知りたければ、SwiftUISwiftUIコレクション。詳細をご覧ください。

GeometryReader

フレームを変更することは、親ビューが提案するサイズを変更することと等価であることはすでに知られており、子ビューはこのサイズに基づいて何かを行うことが非常にスマートになりますが、このサイズは今のところまだ暗黙的であり、いわゆる暗黙的とは、このサイズを明示的に取得できないという意味です。

この推奨サイズを明示的に取得したい場合は、GeometryReaderを使用する必要があります:

struct ContentView: View {
 @State private var w: CGFloat = 100
 @State private var h: CGFloat = 100
 
 var body: some View {
 VStack {
 GeometryReader { geo in
 Text("w: \(geo.size.width, specifier: "%.1f") 
 h: \(geo.size.height, specifier: "%.1f")")
 }
 .frame(width: w, height: h)
 .border(Color.green)
 
 Slider(value: self.$w, in: 10...300)
 .padding(.horizontal, 30)
 }
 
 }
}

親ビューの幅を動的に変更する場合、Text は GeometryReader でサイズを取得できます。

struct ContentView: View {
 var body: some View {
 HStack() {
 Spacer()
 
 MyProgress()
 .frame(width: 100, height: 100)
 
 Spacer()
 
 MyProgress()
 .frame(width: 150, height: 150)
 
 Spacer()
 
 MyProgress()
 .frame(width: 300, height: 300)
 
 Spacer()
 }
 }
}
struct MyProgress: View {
 var body: some View {
 GeometryReader { geo in
 Circle()
 .stroke(Color.green, lineWidth: min(geo.size.width, geo.size.height) * 0.2)
 
 }
 }
}

この例では、Progressの幅を親ビューの幅に基づいて計算する必要がありますが、これはGeometryReaderの単純な応用にすぎません。

GeometryReaderのもう一つの強力な機能は、特定の座標空間に対するビューの境界を取得できるframe(in)です。

struct ContentView: View {
 var body: some View {
 VStack {
 Spacer()
 
 ForEach(0..<5) { _ in
 GeometryReader { geo in
 Text("coordinateSpace: \(geo.frame(in: .named("MyVStack")).minY) global: \(geo.frame(in: .global).minY)")
 }
 .frame(height: 20)
 .background(Color.green)
 }
 
 Spacer()
 }
 .frame(height: 300)
 .border(Color.green)
 .coordinateSpace(name: "MyVStack")
 }
}

このように、.named("MyVStack")と.globalではminYの値が異なるため、GeometryReaderの関数も実際には非常に強力で、例えば次のような効果を得ることができます:

GeometryReader の詳細については、SwiftUIジオメトリリーダー を参照してください。

Alignment Guide

コードの多くの場所で.alignを使用していると思いますが、SwiftUIでalignmentが使用される場所の総数は次のとおりです:

このイメージはアライメントを使用することができるすべての方法をカバーしており、今は途方に暮れているかもしれませんが、記事の残りの部分を読み、このイメージを見返した後、まさに古典的であることに気づくでしょう。

上記のコンセプトのいくつかを簡単にご紹介します:

  • コンテナのアライメント: コンテナのアライメントには、主に2つの目的があります。まず、内部ビューの暗黙的なアライメントを定義します。)ビューを使用する内部ビューを定義します。コンテナは、引数がコンテナの alignment パラメータと同じ場合にのみ、戻り値に基づいてレイアウトを計算します。
  • アライメントガイド:値がコンテナアライメントのパラメータと一致しない場合、その値は有効になりません。
  • 暗黙のアライメント値:一般的に、暗黙のアライメントに使用される値はデフォルト値であり、システムは通常、アライメントパラメータに一致する値を使用します。
  • 明示的アライメント値:明示的アライメントとは、暗黙的アライメントの逆で、プログラム自身が明示的に与える戻り値のことです。
  • Frame Alignment:コンテナ内のビューの配置を示し、ビューを全体として扱い、全体を左、中央、または右に配置します。
  • テキストの整列:複数行のテキストの整列を制御します。

もし興味があれば、SwiftUI整列ガイドご覧ください。簡単な例を通して.alignmentGuideの使い方を説明しています。

仮に、以下のような効果を達成する必要があるとします:

コードは次のようになります:

struct ContentView: View {
 var body: some View {
 Image(systemName: "cloud.bolt.fill")
 .resizable()
 .frame(width: 50, height: 50)
 .padding(10)
 .foregroundColor(.white)
 .background(RoundedRectangle(cornerRadius: 5).foregroundColor(Color.green.opacity(0.8)))
 .addVerifiedBadge(true)
 }
}
extension View {
 func addVerifiedBadge(_ isVerified: Bool) -> some View {
 ZStack(alignment: .topTrailing) {
 self
 if isVerified {
 Image(systemName: "circle.fill")
 .foregroundColor(.red)
 .offset(x: 10, y: -10)
 }
 }
 }
}

addVerifiedBadgeでは、小さな赤い点の位置のオフセットはoffsetを使用して実装されており、同様に.alignmentGuideを使用して同じ効果を得ることができます。

extension View {
 func addVerifiedBadge(_ isVerified: Bool) -> some View {
 ZStack(alignment: .topTrailing) {
 self
 if isVerified {
 Image(systemName: "circle.fill")
 .foregroundColor(.red)
 .alignmentGuide(.top) { (d) -> CGFloat in
 d[VerticalAlignment.center]
 }
 .alignmentGuide(.trailing) { (d) -> CGFloat in
 d[HorizontalAlignment.center]
 }
 }
 }
 }
}

.alignmentGuideを使用する最大の利点の1つは、ビューの寸法に関する情報を取得できることです。例えば、上のコードではパラメータdを使用しています。

alignmentGuideはとても強力な技術で、特にSwiftUI整列ガイドみることをお勧めします。 一言で言えば、alignmentGuideの核となるアイデアの1つはアライメントを設定することです。

Preference

上記で説明したレイアウトのアイデアは、基本的に子ビューに関連するものです。実際の開発シナリオでは、親ビューが子ビューの内部情報を知る必要があることがよくあります。

まず、このタイプの問題がどのように機能するか見てみましょう。

この例では、緑色の円は、一番上の番号をクリックして移動し、実装の原理は非常に簡単ですが、限り、あなたは情報のこれらの番号の境界を知って、あなたは上記の機能を実現することができ、ここで嗜好関連の知識の使用については、コアのアイデアは、次の2点を持っています:

  • 親ビューは、PreferenceKeyに基づいてすべての子ビューのPreferenceDataを取得します。

PreferenceKeyとPreferenceDataを設定するには?基本的には以下のコードで解決します:

struct NumberPreferenceValue: Equatable {
 let viewIdx: Int
 let rect: CGRect
}
struct NumberPreferenceKey: PreferenceKey {
 typealias Value = [NumberPreferenceValue]
 static var defaultValue: [NumberPreferenceValue] = []
 static func reduce(value: inout [NumberPreferenceValue], nextValue: () -> [NumberPreferenceValue]) {
 value.append(contentsOf: nextValue())
 }
}

親ビューはどのようにしてこのデータにアクセスするのでしょうか?.onPreferenceChange

var body: some View {
 ZStack(alignment: .topLeading) {
 ...
 VStack {
 ...
 }
 }
 .onPreferenceChange(NumberPreferenceKey.self) { preferences in
 for pre in preferences {
 self.rects[pre.viewIdx] = pre.rect
 }
 }
 .coordinateSpace(name: "ZStackSpace")

正直なところ、Preferenceのテクニックは学ぶのがとても簡単で、anchorPreferenceのようにサブビューのアンカーを直接取得することができます:

Preferenceの核となる考え方は、親ビューは内部の子ビューにバインドされた情報にアクセスできるというものです。

私が書いた記事では、他にも3つの使い方が紹介されています:

  • サブビューをリアルタイムで聴くことができます:

  • バイナリツリーの描画

  • ドロップダウン リフレッシュ

もっと詳しく知りたい方は、以下の記事をご覧ください;

SwiftUIツリーを見る

SwiftUIツリーを見る

SwiftUIビューツリー実践1

SwiftUIビューツリー実践2

SwiftUIのビューツリーの練習3

Stacks

SwiftUI 2.0の新しいスタックについては、記事の後半で説明します。

VStackは垂直方向にレイアウトされたコンテナであり、他の制約がない場合、そのレイアウト特性は次のように表されます:サブビューのレイアウト要件を満たそうとし、それ自身の最終的なレイアウトサイズはサブビューのサイズに依存します。

 var body: some View {
 VStack(spacing: 10) {
 Text("Hello, World!")
 .background(Color.orange)
 
 Text("Hello, World!")
 .background(Color.red)
 }
 .border(Color.green)
 
 }

HStackは水平にレイアウトされたコンテナで、HStackと同じ特徴を持っています:

 var body: some View {
 HStack(spacing: 10) {
 Text("Hello, World!")
 .background(Color.orange)
 
 Text("Hello, World!")
 .background(Color.red)
 }
 .border(Color.green)
 }

ZStackは階層的にレイアウトされたコンテナで、後から追加されたビューはその前のビューの上位にあり、HStackやVStackと同じ特徴を持っています:

 var body: some View {
 ZStack {
 Color.orange
 .frame(width: 100, height: 50)
 
 Text("Hello, World!")
 .border(Color.red)
 }
 .border(Color.green)
 }

これらの3つのコンテナは、開発で最も頻繁に使用されますが、コアのアイデアは非常にシンプルですが、レイアウトのための独自のルールに従って、ビューのコンテナであり、特定の間隔と整列を設定することができます。

Spacer

スペーサーは、より多くのスタックと組み合わせて使用する必要があります。 スペーサーの性質は、特定の方向にできるだけ多くのスペースを取ることです。 ここには方向の概念があり、例えばVスタックでスペーサーを使用すると垂直方向に多くのスペースを取ることになり、逆にHスタックでは水平方向に多くのスペースを取ることになります。

 var body: some View {
 VStack {
 Color.orange
 .frame(width: 100, height: 50)
 Text("Hello, World!")
 .border(Color.red)
 Spacer()
 }
 .border(Color.green)
 }

ご覧のように、この時点でVStackの高さはスクリーンのセーフエリア全体の高さになり、Spacerは縦方向に残りのスペースをすべて占めますが、横方向の幅は変わりません。

layoutPriority

コンテナ内にレイアウトされた個々のビューと、その優先順位について疑問に思ったことはありませんか? HStackを例にとると、同じ優先順位を持つ2つのビューが内部にある場合、VStackのスペースを等しく共有することになります:

 var body: some View {
 HStack {
 Color.orange
 Text("窓の前の月の光は、地面の霜の疑いがある")
 .border(Color.red)
 }
 .frame(width: 200, height: 100)
 .border(Color.green)
 }

見ての通り、Textの優先順位は高くなりません。layoutPriorityを使って、例えばTextの優先順位を少し高くするなど、ビューの優先順位を変更することができます:

 var body: some View {
 HStack {
 Color.orange
 Text("窓の前の月の光は、地面の霜の疑いがある")
 .border(Color.red)
 .layoutPriority(1)
 }
 .frame(width: 200, height: 100)
 .border(Color.green)
 }

200の幅が唯一のテキストを収容することができるように、したがって、色の左側を表示することはできません、テキストの優先順位を1に設定することができ、非常に大きな値を設定する必要はありませんが、デフォルトでは、優先順位0のビュー。

まとめ

この記事はSwiftUIレイアウトの入門的な内容で、私が書く記事のいくつかは入門的なものではありません。

SwiftUIコレクション:FuckingSwiftUI

Read next

JavaScriptにおける "データ構造 "ヒープの実装

ヒープとは、コンピュータサイエンスにおけるツリー状のデータ構造の特殊な種類です。ヒープ内の任意のノードPとCが与えられたとき、PがCの親ノードであれば、Pの値はCの値以下になる」という性質を満たす場合にヒープと呼ばれます。 逆に、親ノードの値が常に子ノードの値以上である場合、このヒープは最大ヒープと呼ばれます。 ヒープの最上位にあるノード...

Mar 16, 2020 · 6 min read