見出し画像

iPhone アプリを自分でつくる 10.



今回の内容: SwiftUIを書いてみる

今回はSwiftUIを少しづつ書いてみます
読みながら進めますが、途中でうまくいかなくなった場合は ctrl + z で戻るか、
一番下にあるコードをコピペして戻るかしてから進めてください。
ただ、自分が間違えたときのエラーはよく見ておいてください。エラーの内容がよくわからなくてもキーワードはこれかな?と予想してください。
新しいエラーはみっけものです。

コードの理解については、今後進めるなかで説明していきますのであまり気にしないで大丈夫です。
まず雰囲気を味わってください。

特に「各view の持つ範囲の確認」の項はつまらないし見るだけでも疲れますが、いざ自分で画面のデザインを作成するときにかかる時間は、ここを理解しているかどうかで全く違ってきますので、「ふ〜ん」で良いので眺めてください。
そして疲れたらゆっくり休んでくださいね。

ContentView.swiftのコードを変えてみる

最初から表示されているコードとCanvasを見比べながら進めていきましょう。

  8 import SwiftUI
  9 
 10 struct ContentView: View {
 11     var body: some View {
 12         VStack {
 13             Image(systemName: "globe")
 14                 .imageScale(.large)
 15                 .foregroundStyle(.tint)
 16             Text("Hello, world!")
 17         }
 18         .padding()
 19     }
 20 }
 21 
 22 #Preview {
 23     ContentView()
 24 }

最初はImageの内容を変えてみましょう。

13             Image(systemName: "play")

行数は目安として残してありますが、改行などで変わっても問題ありません。
Imageの中に書いてある"globe" を"play"にしてみました。
しばらくするとCanvas画面が変わるはずですが、変わらなければ左下の三角印(またはResumeボタン(マル矢印)があればそちらでも)をクリックしてください。
Canvasの表示がどう変わったか確認してください。
このImage(systemName: "xxxx")はAppleが管理しているSF Symbols という一覧にあるパーツを呼び出す方法です。登録されているイメージや名前などはすべてダウンロードできます。ぜひいろいろなイメージを試してください。
このイメージを呼び出す際の文字で "play" を "Play" のように大文字で書いても
エラーは出ませんが イメージも出ません
今回のように1個だけ呼び出して使う場合は当然気づきますが、例えば多くのイメージを扱う中の1個でミスがあったりした場合には非常に気づきにくい状態が生まれます。そのような場合には enum(列挙型)を使用して間違いを防ぐなどの方法も検討します。

14                 .font(.largeTitle)

Imageを修飾している .imageScale(.large)を.font(.largeTitle)に変えてサイズを
大きくしてみましょう。.font(. xxxx)はText型の修飾に使われますが、SF Symbolsを使うImage型にも同様に使用できます。



 16             Text("Let's Play!")

今度は"Hello, world!"を変えてみました。
変わりましたか?



 16             Text("Let's Play!")
 17                 .font(.title)
 18         }       

Textで表示される文字もサイズを大きくしてみましょう。
Imageで行った.font( .xxx )を使用します。Imageで使用した.largeTitleよりすこし小さい.titleを使用してみます。



もしどこかで間違った操作をしてしまったら、command + z で戻りたいところまで戻れば問題ありませんので、失敗を怖がらず進めてくださいね。

12         HStack {

今度はVStack をHStackに変えてみましょう。
HStack はエイチスタックと呼ばれ、Horizontal Stack(ホリゾンタルスタック)の略で水平、横置きに並べるということです。



今度はHStack の上にText("Score: ")と記入して .font(.title)で修飾してください。

 11     var body: some View {
 12         Text("Score: ") 
 13             .font(.title)
 14         HStack {
 15             Image(systemName: "play")
 16                 .font(.largeTitle)
 17                 .foregroundStyle(.tint)
 18             Text("Let's Play!")
 19                 .font(.title)
 20         }       


今度はbodyの中身の一番外側をVStackで囲んでください。例では12行目にVStack {を記入して23行目で } で閉じる事でこの範囲をVStackでカバーしたかたちになります。
例ではインデント(字下げ)が済んだ状態ですが、まずは気にせずに
12行目  VStack {
23行目(改行してから)  }
を記入してみてください。  

 10 struct ContentView: View {
 11     var body: some View {
 12         VStack {
 13             Text("Score: ")
 14                 .font(.title)
 15             HStack {
 16                 Image(systemName: "play")
 17                     .font(.largeTitle)
 18                     .foregroundStyle(.tint)
 19                 Text("Let's Play!")
 20                     .font(.title)
 21             }       
 22             .padding()
 23         }   
 24     }   
 25 } 

インデントについては、通常VStackを記入するとXcodeが自動的に行なってくれますが、自動的に例の状態になったでしょうか?
うまくインデント表示されなかった場合は、全体をcommand + aで選び
control + i (コントロールを押しながらi アイ)を押すとインデントを正してくれます
もしこれでもインデントがおかしな状態になった場合はどこかに{ } の位置の間違いか過不足があるはずなので確認してください。


コードとCanvasとの照らし合わせ方

そしてこのコードの全体の表現とCanvasでの表示を照らし合わせてください。

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Score: ")
                .font(.title)
            HStack {
                Image(systemName: "play")
                    .font(.largeTitle)
                    .foregroundStyle(.tint)
                Text("Let's Play!")
                    .font(.title)
            }
            .padding()
        }
    }
}

#Preview {
    ContentView()
}
コードとCanvasを見比べてみる

var bodyに入れ子で入っている情報をインデントの状態で見ていくと、
1. bodyの中で一番外側にはVStack がコンテナとして全体をカバーしている。
2. VStackの中には以下の2つの子(子ビュー)が縦並びに配置されています。
  (1) Text("Score: ")
       (2) HStack {   }
3. 上記のHStackの中には2つの子(子ビュー)が横並びに配置されています。
  (1) Image(systemName: "play")
       (2) Text("Let's play")

イメージがつかめましたか?
上記がもしうまく理解できなくてもこれから何度も同じようなことを実施していきますので、特に心配せずに先に進んでいってください。
何回も実行するうちに確実に理解できるようになります。

※ 途中で気づいたかたもいるかもしれませんが、今回の例の途中でVStack がなくても同じ縦並びの画面表示となる場面がありました。
これはSwiftUI*が画面のサイズと表示内容を読み取って「この場合はVStackを使用すれば良いな」と判断しているからです。
  *  ViewBuilder ビュービルダーが働きます
ただし設定方法によっては、思うように反映されない場合もあります。
まず基本的な例を体験しながら少しづつ応用を理解していきましょう。


各view の持つ範囲の確認

まず各部品の持つ範囲を調べてみます。
範囲を調べるには、background() で背景に色をつけます
この方法は簡単ですが大変有効ですのでデザインする際の確認作業に使用します。
* background() のかわりに border() もよく使われます
以下のように各view にbackground(.orange)としてみましょう。
これはbackground(Color.orange)  のColor を省略したものです。

        VStack {
            Text("Score: ")
                .font(.title)
                .background(.orange)
            HStack {
                Image(systemName: "play")
                    .font(.largeTitle)
                    .foregroundStyle(.tint)
                    .background(.orange)
                Text("Let's Play!")
                    .font(.title)
                    .background(.orange)
            }
            .padding()
        }

上記は HStack につく padding が中にある Image と Text に影響を与えていることがわかります。(HStack に影響していると考えていただいても問題ありません)
今度はpadding をはずしてみます。
padding の影響がなくなってスペースが狭くなりましたが、それぞれ少しづつスペースがあります。(Score とLet's Playの間も微かに開いています)
これはDefault の状態でもpaddingがかかるようになっているからです。

この残ったスペースを外すには VStack と HStack をそれぞれ
VStack(spacing: 0) {   と  HStack(spacing: 0) { に変更します。
そうすると微妙にあったスペースも削除することができます。


今度はそれぞれのview にpaddingをつけますが、background() の前とします。
各view にpadding が効いてbackground が広くなりました。


今度はそれぞれbackground() の後ろにつけてみます。
そうするとbackground の範囲は元に戻って、空白の部分が広がりました。
これは何を意味しているでしょうか?


Modifier(モディファイア:修飾するメソッド)は後についたものが、そこまでに修飾されたものをラップしてさらに修飾する働きをします。
下の少しやばそうな図を見ていただくと、先にpadding が来て範囲が広げられたあとにbackgroundが決まるか、background が決まった後にpadding されるかで
背景が変わることが色の大きさでわかります。

 図は個人のイメージです。



                Text("Let's Play!")
                    .font(.title)
                    .background {
                        Circle()
                            .fill(.orange)
                            .frame(width: 200, height: 200)
                    }

ただし注意点として、background は Shape型をとることができるため以上のような記入(  background { Circle() }  )なども可能ですが、
この場合には、SwiftUI* は、Circle() と Text() を別のレイヤーとして重ね合わせる作業(ZStack と呼ばれる:後述)をこっそりしていてるため背景(Circle())が他のview の位置に影響されません。
そのためview同士が重なることがあります。
もちろん自由にデザインを行いたいときには活用できます。
*SwiftUI のViewBuilder というものがこの作業を行います。



参考
上で見たようにModifier は修飾する順番で様子が変わってしまいますが、特に大きな変化は offset() で見ることができます。
offset() は offset(x: 50, y:50) とすると x方向(右方向)に50、y方向(下方向)に50 移動させるのですが、下例①にあるように修飾の途中につけると文字だけが動いて、その後のModifier はもともの場所を修飾してしまいます。
offset は後からくるModifier に気づかれずに移動してしまうやつなので、基本的には②のように修飾の最後にセットします。
またoffset() は位置の微調整やアニメーション(view の動き)での移動などでは使用しますが、通常のデザインの位置決めではあまり使わない方が良いと思います。offset を使用すると思わぬところでview 同士が重なってしまったりすることがあるからです。
通常はSwiftUI は各view の位置の把握をしているのですがoffset だと把握できなくなってしまいます。
ただ逆に画面から飛び出す動きなどは得意なのでそのような場合には積極的に使用できます。
(でも本当にそうかどうか試してみることが一番大事です。
 問題なければなんでもありです!)


今度は Shape 型を見てみましょう。
Shape 型は大きく分けると長方形や正方形などの四角:Rectangle(レクタングル)と正円Circle(サークル)や楕円Ellipse(エリプス)などの円:があります。
なお三角や五角形などその他の形は自分で Shape 型をつくることになります。機会があれば取り上げます。((小さな声で)三角形ぐらいあっても良さそうですよね。)


上のほうにCircle().background(.orange) としてみます。
そうするとbackground は四角い形をしていて幅いっぱいまで広がっていることがわかります。
Shape 型は親*が持つエリアの中で自分が持てる最大限の四角いエリア(rect レクト)を受け取ります
* この場合の親というのは、画面自体と捉えていただいてOKです。
Text 文字や systemImage イメージ は自分のサイズを持っていますが、Shape 型はサイズが決まっておらず親から頂けるだけいただく 頂き系イタダキShape ちゃんなのです。
深い…… ですか?


今度は Rectangle() という四角を入れてみます。
Rectangle() もShape型なので大きく広がりましたが Circle() は小さくなりました。
これは Shape 型が複数入ると親は平等にエリアを分配しようとするからです。
今回の場合はエリアを縦としていて、Rectangle() と Circle() 両方に同じ高さを分け与えています。


ただ、親は少し隠し財産を持っていて、上の例で画面一番上のオレンジ色の部分と一番下部分(ここではよく分かりませんが)にあたるのですが、SafeArea セーフエリアと言います。
この部分には通常、時間とか充電状態とか受信強度とか重要な表示やスワイプ操作が必要な場所とかあるので無断では使わせないのです。
今回の場合Rectangle() はこの財産のありかを知っているのでこれをオレンジ色にしているようです。
この財産をもらう許可の方法ですが、.ignoresSafeArea() (イグノアズ・セーフエリア) をすることにより認められます。


ではかけた ignoresSafeArea() を外してしまって、Shape それぞれに .padding() をかけてみましょう。
そうするとRectangle() のbackground() はRectangle() そのものということがわかります。
これぐらいのサイズだとここに文章が入ったり写真が入ったりするのがイメージできますね。 


以下は参考までです
何か言いたいことがあるらしいと思っていただければ結構です。

Color() をRectangle() の上に使ってみます。
先ほどの Rectangle() の上に Color.red と記入してみてください。
Color() に形があるの? と思ってしまいますが、SwiftUI ではColor() もひとつのView型(Viewプロトコルに沿った型)でShapeStyle 型と呼ぶ型です。
今までの Rectangle() を Color.red にしてみても形は同じで色がつきます。
ここが少しややこしいところなのですが、Shape型との違いについてみてみます。

・Color() は最初から色を持っていて(色決めでイニシャライズされ)空きスペースを親から平等にもらえます。
 Color() も四角い場所をもらえるだけもらう頂き系です。
    Color() はiOS16 からグラデーションの機能を持ちましたが、その機能をつけると(Color.blue.gradientなど)gradient というAnyGradientプロトコルに適合したものを返します。これはViewプロトコルには適合しません

・Rectangel() や Circle() など Shape型 は Default では色がない(普通は黒)状態ですが、Color() などのShapeStyle を乗せることができます
また AnyGradient型(Gradient型)なども乗せることができます


次はこのColor() を使ってよく行われる画面背景の色付けをしてみます。
すこしゴチャゴチャしてきたので、VStack で囲んだ Rectangle().padding() と
Circle().padding() のみ残して
あとはコメントアウトするか消去してください。

struct ContentView: View {
    var body: some View {
        
        VStack(spacing: 0) {
            Rectangle()
                .padding()
            Circle()
                .padding()
//                Text("Score: ")
//                    .font(.title)
//                    .padding()
//                    .background(.orange)
//                HStack(spacing: 0) {
//                    Image(systemName: "play")
//                        .font(.largeTitle)
//                        .foregroundStyle(.tint)
//                        .padding()
//                        .background(.orange)
//                    Text("Let's Play!")
//                        .font(.title)
//                        .padding()
//                        .background(.orange)
//                }
//                .padding()
        }
    }
}

そうして下の図のようにZStack { で全体を覆ってZStack 内の一番上にColor.mint をセットします。Color.mint には.ignoresSafeArea() をつけます。
なお、図は15 ~ 36行目まで折りたたんでいます。
(Line number 横に折りたたみバーが表示されていない場合は、Xcode - settings - TextEditing - Code folding ribbon で表示されますので、是非表示させておいてください。)


ZStack はスクリーンのレイヤを重ねていきます
最初に書かれたものが一番下の層になって、次から次へと上に層を重ねていきます。
上記の例では、最初に Color.mint が下地の層になって、その上にVStack { } が1枚の層(そこにはRectangle() と Circle() が書かれている)として乗っています。


view の位置をコントロールする

それぞれのview は自分の範囲を持っていて、そのまま置いただけではくっつかないようになっていますが、実際にはくっつけたり離したりして自分のデザインしたい場所に調整していきます。

写真を表示させます

まず写真を2枚用意しましょう。
Navigation でAssets.xcassets を開けて①、写真を白い範囲までドラッグします。②  
2枚セットしてください。
なお写真は自分が見たく無い写真以外でしたらなんでもOKです。


すると次のように右側の 1x というところに写真がセットされます。①
写真をドラッグして 2x に移動します。②
ctrl + c と ctrl + v で 3x にもセットします。③
写真(Image)を呼び出すときの簡単な名前をセットします。④


それではコードに戻ります。
先ほどのRectangle() と Circle() をそれぞれ Image("自分のつけた名前") にします。
そうするといかがでしょうか?
と、ここで写真が巨大になってしまいました、と言いたいところだったんですが、すでに良い具合に調整してくれるようです。
ただ、通常は必ず下記のように、
resizable() でデータのサイズそのものからサイズ調整できるようにする
scaledToFit() で調整するサイズに写真すべてが入るように小さく調整する
または scaledToFill() で調整するサイズに余白がでないように大きく調整する

                Image("perfectdays")
                    .resizable()
                    .scaledToFit()     // または.scaledToFill           

上記のように行なってフレームを調整します。順番もこの通りです
今回は 上のImage をscaledToFit() で 下のImage をscaledToFill() で行いました。

                Image("perfectdays")
                    .resizable()
                    .scaledToFit()
                    .clipShape(RoundedRectangle(cornerRadius: 10))
                    .frame(width: 320, height: 280)
                    .padding()
                Image("greatbudda")
                    .resizable()
                    .scaledToFill()
                    .frame(width: 240, height: 240)
                    .clipShape(Circle())
                    .padding()


サイズの数値については、今後お話ししていきますが、ご自分でいろいろ試してみるとわかってきます。
上のImage は clipShape(RoundedRectangle(cornerRadius: 10)) として
下のImage は clipShape(Circle()) として、それぞれの形に切り取っています。
これもclipShape() をframe() の前に置くか後ろに置くかで大きく変わる場合があるので試してみてください。

clipShape() は他の形でも多く使っていきます。


今度はこの写真の上に文字を記入してみます。
それぞれのImage() を ZStack { } で囲います。
ZStack  - Image - Text(文字) の順番で書くと文字を上に重ねることができます。
文字が黒いと写真と重なって見えなくなったので色を白にしました。①

        VStack(spacing: 0) {
            ZStack {
                Image("perfectdays")
                    .resizable()
                    .scaledToFit()
                    .clipShape(RoundedRectangle(cornerRadius: 10))
                    .frame(width: 320, height: 280)
                    .padding()
                Text("Perfect days")
                    .font(.title)
                    .foregroundStyle(.white)
                    .shadow(radius: 3)
            }
            ZStack {
                Image("greatbudda")
                    .resizable()
                    .scaledToFill()
                    .frame(width: 240, height: 240)
                    .clipShape(Circle())
                    .padding()
                Text("Kamakura")
                    .font(.title)
                    .foregroundStyle(.white)
                    .shadow(radius: 2)
            }
        }

上の写真は文字を右下にしたいので、ZStack(alignment: .bottomTrailing) { } としました。② (アラインメント・ボトムトレーリング)
これでZStack の右下揃えができます。

                ZStack(alignment: .bottomTrailing) {
                    Image("perfectdays")

しかし、scaledToFit() で写真がフレームサイズより小さくなっているので文字が外にでてしまいました。
そこでText() に offset(x: -30, y: -40) として文字位置をずらしました。
特にoffset による問題は発生しないと判断しました。③

                    Text("Perfect days")
                        .font(.title)
                        .foregroundStyle(.white)
                        .shadow(radius: 3)
                        .offset(x: -30, y: -40)

下の写真には上下に文字を配置したいと思います。
今度は先ほどとは違う方法で行います。
外側をZStack { } が囲んで、最初のImage() が下地になってその上にVStack {  } のレイヤが乗ります。VStack には 上からText() 、Spacer() 、Text() の順に並びます。 
Image() と VStack { } それぞれのサイズを frame() で調整することにより自分の良いと思える位置に仕上げることができました。④

                ZStack {
                    Image("greatbudda")
                        .resizable()
                        .scaledToFill()
                        .frame(width: 240, height: 240)
                        .clipShape(Circle())
                        .padding()
                    VStack {
                        Text("Great Budda")
                            .font(.largeTitle)
                            .foregroundStyle(.white)
                            .shadow(radius: 3)
                        Spacer()
                        Text("Kamakura")
                            .font(.title)
                            .foregroundStyle(.white)
                            .shadow(radius: 2)
                    }
                    .frame(width: 260, height: 260)
                }

Spacer() は透明なスペース頂き系でこの例ではVStack にある2つの Text() の間に入り、2つを上下に押し広げる役割をしています。
HStack の中で2つのview の真ん中にSpacer() をかませれば、左右にview を配置できるし、いくつかの場所にかませれば等分にスペースを配分することもできます。
注意することはScrollView() などでは、全くスペースが働かないことがあることです。
その場合は Color.clear とかRectangle()を透明にしてスペース代わりにするなどいろいろな方法があります。


最後に画面の上下にタイトル、サブタイトルを入れてみましょう。
タイトルを大きな文字で左上に、サブタイトルを少し小さな字で右下に配置してみます。
上のタイトルはHStack { } で囲んで Text() 、Spacer() で位置決めしました。

                HStack {
                    Text("風に触れた")
                        .font(.largeTitle.weight(.bold))
                        .foregroundStyle(.white)
                        .shadow(radius: 5)
                        .padding(.leading, 40)
                    Spacer()
                }

下のサブタイトルは HStack { } で囲んで Spacer() 、Text() の順で位置決めしました。

                HStack {
                    Spacer()
                    Text("声を聴いた")
                        .font(.title.weight(.bold))
                        .foregroundStyle(.white)
                        .shadow(radius: 5)
                        .padding(.top, 30)
                        .padding(.trailing, 40)
                }
import SwiftUI

struct ContentView: View {
    var body: some View {
        ZStack {
            Color.mint
                .ignoresSafeArea()
            VStack(spacing: 0) {
                HStack {
                    Text("風に触れた")
                        .font(.largeTitle.weight(.bold))
                        .foregroundStyle(.white)
                        .shadow(radius: 5)
                        .padding(.leading, 40)
                    Spacer()
                }
                ZStack(alignment: .bottomTrailing) {
                    Image("perfectdays")
                        .resizable()
                        .scaledToFit()
                        .clipShape(RoundedRectangle(cornerRadius: 10))
                        .frame(width: 320, height: 280)
                        .padding()
                    Text("Perfect days")
                        .font(.title)
                        .foregroundStyle(.white)
                        .shadow(radius: 3)
                        .offset(x: -30, y: -40)
                }
                ZStack {
                    Image("greatbudda")
                        .resizable()
                        .scaledToFill()
                        .frame(width: 240, height: 240)
                        .clipShape(Circle())
                        .padding()
                    VStack {
                        Text("Great Budda")
                            .font(.largeTitle)
                            .foregroundStyle(.white)
                            .shadow(radius: 3)
                        Spacer()
                        Text("Kamakura")
                            .font(.title)
                            .foregroundStyle(.white)
                            .shadow(radius: 2)
                    }
                    .frame(width: 260, height: 260)
                }
                HStack {
                    Spacer()
                    Text("声を聴いた")
                        .font(.title.weight(.bold))
                        .foregroundStyle(.white)
                        .shadow(radius: 5)
                        .padding(.top, 30)
                        .padding(.trailing, 40)
                }
            }
        }
    }
}

#Preview {
    ContentView()
}


まとめ

 今回の内容は、SwiftUIを少しでも触ったことがある方であれば簡単すぎる
 し、まったく初めての方にとっては説明が不足していて理解できない、不親
 切だと感じると思います。
 今後理解しやすい説明を心がけていきますが、多くのコードを書いたり見たり
 していくうちに自然に仕組みが腹落ちしていくことも多くあります。
 是非今後もお付き合いくださいね!
 SwiftUI の基礎は10回ほど続く予定ですが、これに懲りずによろしくお願い
 します。
 次回は今回のコード(写真を加える前のコード:下のコードです)に手を加え
 てボタンで表示(Score)を変えます。

 
 次回第11回内容  SwiftUIでボタンを作成して表示を変える、です。
          よろしくお願いします。

            
次回に使用しますので、最後にこの状態までもどしておいてください。

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Score: ")
                .font(.title)
            HStack {
                Image(systemName: "play")
                    .font(.largeTitle)
                    .foregroundStyle(.tint)
                Text("Let's Play!")
                    .font(.title)
            }
            .padding()
        }
    }
}

#Preview {
    ContentView()
}

お疲れ様でした!

この記事が気に入ったらサポートをしてみませんか?