タイトル02

SwiftUIの画面レイアウト 後編

SwiftUIの画面レイアウト 前編 の続きです。
1  HStackとVStack、2 ZStack は前編をご覧ください。

SwiftUIで複数の要素を組み合わせた場合の関係や効果をサンプルで解説します。

※この記事ではSwift言語(最新の5.1)の基本的な知識を前提にしています。コード内のキーワードや書式などの不明点は Swift5初級ガイドなどを参照してください。

※SwiftUIについて全体的なことは『SwiftUI最初の一歩』、コードの基本とビューについては『SwiftUIの文法 その1 View』を参照してください

毎月札幌でiOSアプリ作りをアシストするセミナーをやっています。1時間にわたるセミナーの全内容を、物理的に参加できない方のためにnote上で公開します。

お知らせ
電子書籍『Swift5初級ガイド』をAppleのブックストアから出しました。サンプルは無料です。MacでもiPadでもiPhoneでも読めます。
WWDC2020で発表されたSwift 5.3に対応した第6版がダウンロード可能です。(ご購入済みの場合は無料アップデートです)
ブックストアから一度購入すると今後のアップデートは無料で読めます。

6宣伝store

iOSアプリ作りをアシストするセミナーは今後も月一回のペースで続ける予定です。(2020年3月以降COVID-19感染拡大防止のため休止しています)
詳細は connpass.com の 札幌Swift でご確認ください。そして機会があればぜひ参加してください。
アプリ作りやプログラミング教育に関連する話題は 札幌Swift のfacebookページ で発信しています。

・画像クリックで拡大表示できます
・画像を拡大表示中は画像の左右をクリックで画像だけを順に表示できます
・ソースコード部分は左右にスクロールできます
Xcode は 11.3.1 を使っていています。
サンプルはすべてXcodeのiOS用プレイグラウンド書類用コードです。(iPadのPlaygrounds 3.2でも直接入力し実行できます)
この記事の最後の有料部分にあるリンクから完全なサンプルをダウンロードできます。


3 ScrollView

ScrollViewは配置したビューをスクロール可能にします
(VStackビュー・HStackビューだけではスクロールしません)
イニシャライザはひとつだけです。

// ScrollViewのイニシャライザ
init(_ axes: Axis.Set = .vertical, 
showsIndicators: Bool = true, 
@ViewBuilder content: () -> Content)

三つの引数を持ちます。
最初の引数はスクロール方向の指定です。
デフォルトは .vertical で縦方向にスクロールします。
.horizontal を指定すると横方向にスクロールします。
両方向を指定することもできます。

二つ目の引数showsIndicatorsスクロール中のインジケータ表示です。
実行するプラットフォームにより表示がかわります。
デフォルトでインジケータ表示です。

スクロールインジケータはスクロールする全体量と現在の表示位置を示す重要な役割を持ちます。基本的に表示すべきものです。

三つ目の引数は配置するビューの指定です。
@ViewBuilder属性なので10個までのビューを指定できます。
通常はトレイリングクロージャで引数のカッコの直後に出します。

トレイリングクロージャの書き方を使いaxesとshowsIndicatorsをデフォルトにする場合はカッコも省略し ScrollView { ... } と書くことができるのもHStackなどと同じです。

サンプルは二つのスクロールビューを表示します。
この記事のサンプルはすべてプレイグラウンド用です。(iPadのPlaygroundsアプリでも実行できます)

import SwiftUI
import PlaygroundSupport

// 02-17 ScrollViewサンプル
struct Sample01View: View {
  var body: some View {
     HStack {
        // 縦スクロール
        ScrollView {
           VStack(spacing: 8) {
              ForEach(0..<60) {
                 Text("番号 \($0)").font(.title)
              }
           }
        }
        
        // 横スクロール
        ScrollView(.horizontal) {
           HStack(spacing: 20) {
              ForEach(0..<30) {
                 Text(" \(30 - $0)").font(.largeTitle)
                    .background(Color.purple)
                    .foregroundColor(.white)
                 
              }
           }
        }
        
     }
  }
}

// playgroundで実行する場合に必要なコード
PlaygroundPage.current.setLiveView(Sample01View())

1行目と2行目の import 文と最後の文はすべてのサンプルで必要です。

画像1

左側の縦スクロールは60行のTextを表示します。
右側の横スクロールは30項目のテキストを表示します。
それぞれスクロール操作は独立しています。

縦スクロールは引数なしの ScrollView です。
60行表示するため ForEach で範囲をゼロから60までと指定し、Textで短縮型引数名$0を"番号 "に続けて表示する行を繰り返しています。
行の間隔を VStack(spacing: 8) { の部分で指定しています。

横スクロールは ScrollView(.horizontal) { と引数で指定します。
範囲指定の ForEach を使うところは同じですが、直接短縮型引数名$0を表示するのではなく、30から引いた結果を表示するので右方向へ降順で数字を並べます
こちらの間隔は HStack(spacing: 20) { と少し大きめにしています。

横スクロールで表示しているTextは.font(.largeTitle)で大きな文字を .background(Color.purple) と .foregroundColor(.white)で背景色と文字色を指定しています。

スクロール操作をするとインジケータを自動表示します。(デフォルトの動きです)

このサンプルを操作すると確認できますが、ScrollViewのサイズは縦スクロールならば内容のVStackの幅、横スクロールならHStackの高さになっています。
それ以外はスクロールの操作に反応しません。


4 Spacerビュー

Spacer型は少し特殊なビューです。
公式ドキュメントでは『含まれるスタックレイアウトの主軸に沿って、またはスタックに含まれていない場合は両方の軸に沿って拡張する柔軟なスペース。』と説明されています。

具体的にはVStack内では縦方向、HStack内では横方向に伸び縮みするスペースとして働きます。
Spacer() も一つのビューとカウントされるのでViewBuilderの最大10件の制限を受けます

SpacerがVStackまたはHStack内で一つだけの場合はバネのように広がる効果があります。

複数のSpacerを一つのHStack(またはVStack)に配置すると同じスペースとなるように働きます。(これはドキュメントには書かれていません)

// Spacerのイニシャライザ
init(minLength: CGFloat? = nil)

引数はひとつだけです。
minLengthは1つまたは複数の拡張軸に沿って、このスペーサーを縮小できる最小の長さです。
nilの場合、ビュー間のシステムのデフォルトの間隔が使用されると、ドキュメントではなく Jump to Definition で表示される(ヘッダーの)コメントに書かれています。

4-1 VStackでのSpacerビュー

シンプルにVStack内のSpacerビューのはたらきを確認しましょう。

// 02-18 Spacerの効果
struct Sample02View: View {

  var body: some View {
     VStack{
        Text("テキスト文字列")
        Spacer()
        Text("札幌Swift")
     }
     .font(.title)
  }
  
}

二つのTextの間にSpacerをレイアウトした場合。

画像14

このように上下に接するまで離れてレイアウトされます。

4-2 HStackでのSpacerビュー

次にHStackでの効果を確認しましょう。

// 02-19 Spacerの効果
struct Sample02View: View {
  
  var body: some View {
     HStack{
        Text("テキスト文字列")
        Spacer()
        Text("札幌Swift")
     }
     .font(.title)
  }
  
}

HStack内ではSpacerは左右に広がります。

画像15

左右に押し付けるような効果となります。

4-3 複雑な場合のSpacer

『1-4 複数のSpacerの効果』02-04のサンプルで確認できるように一つのスタック内の複数の Spacer() は同じ幅または高さとなります

しかしレイアウトが複雑になるとSpacerの効果は予想しにくい結果となりました。

// 02-20 Spacerの効果
struct Sample02View: View {
  
  var body: some View {
     VStack{
        Text("A flexible space that expands along the major axis of its containing stack layout, or on both axes if not contained in a stack.")
        Spacer()//minLength:100)
        Text("札幌Swift").font(.title)
        Spacer()

        HStack{
           Text("テキスト文字列")
           Spacer()
           Text("SwiftUI").font(.title)
           
           VStack{
              Text("A flexible space that expands along the major axis of its containing stack layout, or on both axes if not contained in a stack.")
              Spacer()
              Text("Spacerサンプル").font(.title)
           }
        }

     }
  }
  
}

内側(HStack内のVStack)から高さを決めて行くためにこのようになると考えられます。

画像16

Spacer()//minLength:100) の部分を
Spacer(minLength:100) に変更して実行すると100ポイントのスペーサーが確保されることを確認できます。


5 Dividerビュー

Divider型は他のコンテンツを分離するために使用できる視覚要素です。
具体的にはVStack内では横線、HStack内では縦線を表示します。
(表示は実行するプラットフォームで変わります)

イニシャライザはひとつだけで引数はありません。

Divider() も一つのビューなのでViewBuilderの10件の制限を受けます。

VStackでのDividerビューを確認するサンプルです。

// 02-21 Dividerの効果
struct Sample02View: View {

  var body: some View {
     VStack{
        Text("テキスト文字列")
        Divider()
        Text("札幌Swift")
     }
  }

}

VStack内のDividerは横線です。

画像17

次にHStackでのDividerビューです。

// 02-22 Dividerの効果
struct Sample02View: View {

  var body: some View {
     HStack{
        Text("テキスト文字列")
        Divider()
        Text("札幌Swift")
     }
  }

}

HStack内のDividerは縦線です。

画像18


5-1 Dividerによるレイアウトへの影響

Dividerによりレイアウトががらっと変わってしまう場合があります
まずHStack内のDivider()をコメントにして実行するとこのようになります。

画像2

ソースコードはこれだけです。

// 02-23 Dividerの効果
struct Sample02View: View {

  var body: some View {
     VStack {
        Text("Dividerの有無のレイアウトの違い")
        Divider()
        HStack{
           Text("テキスト文字列")
           //Divider()
           Text("札幌Swift")
        }
     }
  }

}

次にDivider()のコメントをはずした場合はこのようになります。

画像3

Divider()によりVStack内のHStackの高さが最大になりました

この効果はForm内の行では見られません。
まったく同じコードをForm内に配置し実行した例です。

// 02-24 Dividerの効果 Form内の場合
struct Sample02View: View {

  var body: some View {
     Form {
        VStack {
           Text("Dividerの有無のレイアウトの違い")
           Divider()
           HStack{
              Text("テキスト文字列")
              Divider()
              Text("札幌Swift")
           }
        }
     }
  }

}

Form内の行の場合はDividerを配置してもレイアウトは変わりません。

画像4


6 frameメソッド

frameメソッドはViewプロトコルのインスタンスメソッドです。
Viewプロトコルのメソッドは『モディファイア』と呼ぶ場合もあります。
引数の違いで二種類のメソッドがあります。

SwiftUIではビューのframeは幅と高さを持ちます。
ビューの位置に対応する座標はframeメソッド(の設定)では考えません。
ビューの表示位置は基本的にほかのビューとの関係で決まります。
このため公式ドキュメントでもframeメソッドは Sizing a View の分類にあります。

これはCocoa touch と Cococa でのビューの(原点座標を持つ)frameとは全く違った考え方です。
Cocoa touch と Cococa ではframeでビューの画面上の位置を決めてしまいます。
SwiftUIではビュー同士の関係でレイアウトが決まります。
この違いがSwiftUIが柔軟で時計からテレビまで色々なプラットフォームで利用可能なポイントのひとつです。

サンプルではTextビューに対してframeを設定し効果を確認しています。

ただしTextビューのサイズは特殊です
Textビューのサイズは、文字数や文字サイズ、行数制限、幅制限・高さ制限などでサイズが変化しますが、基本的に全ての文字列を表示できるサイズがデフォルトです。
サイズの制限があると改行したり、...で省略したりします。
幅が必要な幅よりも広い場合でも文字間隔が広がったりはしません。

そしてTextビューの場合、実行時にダイナミックタイプで文字サイズを利用者が決めることを忘れてはいけません。
ダイナミック対応のためにはframeでレイアウトするよりも行数制限などの機能を検討してください

このように特殊ですが、Textビューは最も重要なUI要素で、レイアウトの調整もしばしば必要になるので今回frameのサンプルはTextビューを使っています。

ここから先は

9,527字 / 9画像 / 1ファイル
この記事のみ ¥ 500

今後も記事を増やすつもりです。 サポートしていただけると大変はげみになります。