タイトル02

SwiftUIの画面レイアウト 前編

今回はSwiftUIの画面レイアウトの基本です。
レイアウト用ビューの使い方と、複数のビューを組み合わせた場合の関係や効果をサンプルで解説します。

基本的な 1 HStackとVStack、2 ZStackだけでかなりのボリュームになってしまったので前編後編の二つに分けます。

※この記事では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アプリ作りをアシストするセミナーは今後も月一回のペースで続ける予定です。
詳細は connpass.com の 札幌Swift でご確認ください。そして機会があればぜひ参加してください。
アプリ作りやプログラミング教育に関連する話題は 札幌Swift のfacebookページ で発信しています。

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


はじめに

SwiftUIの公式ドキュメントView Layout and Presentationは次のようにグループ分けされています。
Stacks
Lists and Scroll Views
Container Views
Spacers and Dividers
Architectural Views
Presentations
Conditionally Visible Items
Infrequently Used Views

今回は Stacks と Spacers and Dividers を中心に解説します。
Lists and Scroll ViewsとContainer Viewsは『SwiftUI でリスト表示』、Architectural Viewsについては『SwiftUIの画面切替』を参照してください。
(ScrollViewは後半のサンプルで説明します)

ⅰ SwiftUIのビュー

まず最初にSwiftUIのビューについて確認しましょう。

SwiftUIではViewプロトコルに準拠したカスタムビューを使ってビューをレイアウトします。
ひとつのビューは画面の中央に表示されます。
デバイスの回転などで画面縦横の寸法が変化した場合も中央に再表示されます。

ビューの表示パターンにはいくつかの種類があります。
❶表示する内容でビューのサイズが決まるもの
❷画面の縦横を目一杯に使いできるだけ大きく表示しようとするもの
などがあります。
❶にはText(表示する文字列でビューのサイズが決まる)やStack類があります。
❷にはShapes類(四角形や円など)があります。

複数のビューを並べるためのビュー(これから説明するHStackやVStack)や、大きなビューをスクロールして全体を表示するためのビュー(ScrollView)もあります。

もう一つ、SwiftUIはアクセシビリティの『ダイナミックタイプ』機能に対応しています。
実行時に文字サイズが決まるためTextビューのサイズも変化する前提でレイアウトしなければなりません

ⅱ ビューの範囲を確認

ビューの範囲は表示できる内容や複数のビューのレイアウトに最も深くかかわります。

ビューの範囲はCocoa touchではframeプロパティで確認できました。
SwiftUIにもframeメソッドがあり幅や高さを設定できます。
ここでは『ビューの範囲』を『frame』と書いています。

通常はビューの範囲は(透明で)確認できません。
今回のサンプルではビューの範囲を確認するために背景に色を付ける方法と、枠線を描く方法を使っています。

背景に色を付けるにはViewプロトコルのbackgroundメソッドを使っています。
引数は『SwiftUIの画面レイアウト 後編』8 backgroundメソッド を参照してください。

// 背景を黄色にする
.background(Color.yellow)

各ビューの直後にこのメソッドを続けます。
通常はframe(ビューの範囲)の内側を指定色で塗りつぶします。

塗りつぶした後でframeを変形しても、既に塗りつぶした範囲は影響を受けません
塗りつぶした後にframeを小さくすると、レイアウトには小さいframeを使い位置を計算しますが、実際にはframeの外側も塗りつぶした状態にできます。(隣のビューを覆い隠す場合あり)

ビューの枠線を描くにはViewプロトコルのborderメソッドを使っています。
引数は『SwiftUIの画面レイアウト 後編』9 borderメソッド を参照してください。

// 赤い枠を描く
.border(Color.red)

枠線も塗りつぶしと同様に後からframeを変更しても影響を受けません。
このためframeの外側に描く場合もあります。

ⅲ ビューframeの大きさを変える

frame(ビューの範囲)を指定した幅や高さにするにはViewプロトコルframeメソッドを使います。
frameメソッドについて詳しくは『SwiftUIの画面レイアウト後編』を参照してください。

// frameの幅や高さを数値で指定する
frame(width:40, height:40)

frameを適切な余白を付け外に広げるにはViewプロトコルpaddingメソッドを使います。
paddingメソッドについて詳しくは『SwiftUIの画面レイアウト後編』を参照してください。

// frameを拡大する(上下左右にデフォルトの余白ぶん広げる)
padding()

ⅳ フォント指定

フォントを指定するにはViewプロトコルのfontメソッドを使います。
fontメソッドについて詳しくは『SwiftUI UI部品カタログ前編』を参照してください。

// 文字表示のフォントにタイトル(Font.title)を指定する
font(.title)


では複数のビューをまとめるStack類からサンプルで確認しましょう。


1 HStackビューとVStackビュー

HStackはビューを水平方向にレイアウトし、VStack垂直方向にレイアウトします。
どちらも頻繁に利用するビューでこれまでも使ってきましたが、ここでは掘り下げて解説します。

この説明でもHStackのサンプルで複数のHStackをひとつの画面で比較するためにVStackを使うなど、どちらも画面レイアウトの基本です。

1-1 HStackビュー

HStackはビューを横に並べるビューです。
最初のサンプルで確認しましょう。

import SwiftUI
import PlaygroundSupport

// 02-01 HStack
struct Sample02View: View {

  var body: some View {
     HStack {
        Image(systemName: "person.fill")
        Text("なまえ")
     }
     .font(.title)
  }
}

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

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

画像6

計算型プロパティ body ではひとつのViewインスタンスしか返すことができません。
このサンプルでは、中に二つのビューを持つHStackビューをひとつ返しています。

SF Symbolsのアイコン(Image)とTextを並べます。
デフォルトでは画面中央に並びます。

HStackに設定した .font(.title) モディファイアはHStack内の各ビューに対して有効です。
同じ文字サイズなら指定を一箇所で済ませることができます。

1-2 HStackビューとSpacerビュー

次にアイコンとTextの間にSpacerを追加します。

// 02-02 スペーサーの効果1
struct Sample02View: View {
  
  var body: some View {
     HStack {
        Image(systemName: "person.fill")
        Spacer()
        Text("なまえ")
     }
     .font(.title)
  }
}

Spacer()は間隔をあける役割を持つビューです。
大きさは決まっていませんが、水平あるいは垂直にできるだけ広がろうとする性質があります。
このサンプルではアイコンとTextを左右の端まで押しやっています

画像7

Spacer()はHStackでは幅方向に間隔を開けます。

1-3 Spacerビューの位置と効果

Spacerはバネのように間隔を広げる効果があります。
次のサンプルはSpacerの位置による表示の違いを確認できます。

// 02-03 HStack
struct Sample02View: View {
  
  var body: some View {
     VStack {
        HStack {
           Image(systemName: "person.fill")
           Text("なまえ")
        }
        HStack {
           Image(systemName: "person.fill")
           Spacer()
           Text("なまえ")
        }
        HStack {
           Image(systemName: "person.fill")
           Text("なまえ")
           Spacer()
        }
        HStack {
           Spacer()
           Image(systemName: "person.fill")
           Text("なまえ")
        }
     }
     .font(.title)
  }
}

上からSpacerなし、間にSpacer、最後にSpacer、最初にSpacerです。

画像8

1-4 複数のSpacerビューの効果

HStackの中に複数のSpacerを置くと、Spacerは同じ幅でレイアウトされます。

// 02-04 スペーサーの効果3
struct Sample02View: View {

  var body: some View {
     VStack {
        // 3つのView、Spacerなし
        HStack {
           Image(systemName: "person.fill")
              .resizable()
              .frame(width:40, height:40)
           Text("対称1")
           Image(systemName: "tram.fill")
              .resizable()
              .frame(width:40, height:40)
        }

        // 3つのView、間にSpacer
        HStack {
           Image(systemName: "person.fill")
              .resizable()
              .frame(width:40, height:40)
           Spacer()
           Text("対称2")
           Spacer()
           Image(systemName: "tram.fill")
              .resizable()
              .frame(width:40, height:40)
        }

        // 3つのView、両端と間にSpacer
        HStack {
           Spacer()
           Image(systemName: "person.fill")
              .resizable()
              .frame(width:40, height:40)
           Spacer()
           Text("対称3")
           Spacer()
           Image(systemName: "tram.fill")
              .resizable()
              .frame(width:40, height:40)
           Spacer()
        }

        Divider()

        // Spacerの連続
        HStack {
           Spacer()
           Image(systemName: "person.fill")
              .resizable()
              .frame(width:40, height:40)
           Spacer()
           Text("連続1")
           Spacer()
           Spacer()
           Image(systemName: "tram.fill")
              .resizable()
              .frame(width:40, height:40)
        }

        // Spacerの連続
        HStack {
           Image(systemName: "person.fill")
              .resizable()
              .frame(width:40, height:40)
           Spacer()
           Text("連続2")
           Spacer()
           Spacer()
           Image(systemName: "tram.fill")
              .resizable()
              .frame(width:40, height:40)
           Spacer()
        }

        Divider()

        // 4つのView、間にSpacer
        HStack {
           Image(systemName: "person.fill")
              .resizable()
              .frame(width:40, height:40)
           Spacer()
           Text("なまえ")
           Spacer()
           Image(systemName: "tram.fill")
              .resizable()
              .frame(width:40, height:40)
           Spacer()
           Image(systemName: "heart.fill")
              .resizable()
              .frame(width:40, height:40)
        }

        // 4つのView、両端と間にSpacer
        HStack {
           Spacer()
           Image(systemName: "person.fill")
              .resizable()
              .frame(width:40, height:40)
           Spacer()
           Text("なまえ")
           Spacer()
           Image(systemName: "tram.fill")
              .resizable()
              .frame(width:40, height:40)
           Spacer()
           Image(systemName: "heart.fill")
              .resizable()
              .frame(width:40, height:40)
           Spacer()
        }
     }
  }
}

Spacerを除き三つと四つのビューを配置する例です。

画像9

このようにHStack内の各Spacerの間隔は同じになります

1-5 余白が十分でない場合のSpacerビュー

ここまでは幅に対して余裕が十分あるばあいのSpacerの効果を確認しました。
次に幅が十分でない場合を確認します。

// 02-05 スペーサーの効果4 余白が十分でない場合
struct Sample02View: View {
  
  var body: some View {
     HStack {
        Image(systemName: "person.fill")
        Spacer()
        Text("長い文字列 吾輩は猫である。名前はまだ無い。どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。")
        
     }
  }
}

アイコンと長いテキストの間にSpacerを配置した例です。
複数行で折り返して表示しています。
上がSpacerがコメント、下がSpacerありです。

画像11

画像10

この場合Spacerによる余白はかなり小さくなります。
(引数なしのSpacerの最小幅は『SwiftUIの画面レイアウト 後編』に詳しく書きました)
コードのSpacerをコメントをはずして、実行し確認してください。

1-6 alignmentを指定したHStackビュー

ここでHStackビューのイニシャライザを確認しましょう。
三つの引数を持つイニシャライザです。

// HStackのイニシャライザ
init(alignment: VerticalAlignment = .center, 
spacing: CGFloat? = nil, 
@ViewBuilder content: () -> Content)

最初の引数はalignmentの指定です。
省略した場合は.centerになります。
ほかに. bottom、.top、.firstTextBaseline、.lastTextBaseline を指定できます。

二つ目の引数は並べる場合の間隔(spacing)の指定です。
間隔の指定を省略するとデフォルトの間隔になります。
間隔なしにするには0.0を指定します。

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

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


次のサンプルではHStackのalignment引数による効果を確認します。
アイコンを大きく表示し背景に色を表示しています。

// 02-06 alignment指定 その1
struct Sample02View: View {
  
  var body: some View {
     VStack {

        HStack(alignment:.top) {
           Image(systemName: "person.fill")
              .resizable()
              .frame(width:120, height:120)
              .background(Color.yellow)
           Text("top揃え")
           Text("10").font(.largeTitle)
        }

        HStack(alignment:.center) {
           Image(systemName: "person.fill")
              .resizable()
              .frame(width:120, height:120)
              .background(Color.yellow)
           Text("center揃え")
           Text("11").font(.largeTitle)
        }

        HStack(alignment:.bottom) {
           Image(systemName: "person.fill")
              .resizable()
              .frame(width:120, height:120)
              .background(Color.yellow)
           Text("bottom揃え")
           Text("12").font(.largeTitle)
        }
        
     }
  }
}

.topはビューの上端、.centerは上下の中央、.bottomはビューの下端でそろえます。

画像12

alignmentの指定でアイコンよりも高さの小さい10〜12テキストの位置が変わります。

サイズの違うテキストを上端や下端でそろえると不自然に見えます。

ここから先は

11,687字 / 12画像 / 1ファイル
この記事のみ ¥ 500
期間限定!PayPayで支払うと抽選でお得

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