見出し画像

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


今回の内容: SwiftUIでボタンを作成して表示を変える

前回はContentViewに最初から書いてあるウェルカムコード?を編集して画面を変
えてみました。
今回はそこにボタンを配置して、押すごとに数値が増える画面を追加します。
下記を読みながら進めますが、途中でうまくいかなくなった場合は最後にあるコードをそのまま写して、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()
}

表示させる数字を準備する

まずは数字を表示させたいのでContentViewに2つ目の変数をセットします。
ひとつ目の変数は? それは var body: some View { } のbodyです。

これはsome View型の変数で、View プロパティを適用するには必ずこの変数をセットするのでしたね。
そしてbodyにはイコール= の記号はなく { } で囲われた範囲が続いていますが、これはcomputed プロパティだからでしたね。
そして、この中にsome Viewに適合するTextやImageなどのView型を配置していくのでしたね。
しつこくてすいません!

var score: Int = 0 としてInt型のプロパティ scoreをbodyの上に記入してみましょう。
もちろん var score = 0 としてIntを省略してもSwiftはInt型と設定してくれます。

 10 struct ContentView: View {
 11     var score: Int = 0
 12     
 13     var body: some View {

変数scoreを記入しましたが、Canvas上では何の変化も起こりません。
画面の表示するのはbodyの中のText型やImage型のコードだけでしたね。
ではこの変数を画面に反映させてみましょう。

 15             Text("Score: \(score)")
 16                 .font(.title)

Text("Score: ") に \(score) を挿入して Text("Score: \(score)") としました。
これでCanvas画面にもScore: 0とでました。
※ \() のような書き方をエスケープシーケンスと言います。この他にも、文字列の間に \n :改行 ,  \":ダブルクオート などのエスケープシーケンスがあります。
私はいまだにエスケープシーケンスという言葉はピンと来ないのですが。


ボタンを配置する

ではボタンを配置してみます。
今までText型とImage型のviewを扱ってきましたが、今度はButton型のviewを配置します。

ボタンはButton型の構造体を使用します。
Button型の役割は2つあって、①数値を増やすなどの仕事をする ②ボタンの形を表示する(自身の見栄えを整える)こと、この2つの役割のために、コードもそのような書き方になります。(いろいろな記入方法があるので今後取り上げます
Button { 仕事 } label: { 表示 }   このような記載方法ですが、これを改行して下記のようにします。

Button { 
仕事
} label: {
表示
}

まずは下記のようにボタンの仕事は空白にしておいて、label: の表示部分はすでに記入してあるHStack の内容(三角印とLet's Play!の文字)を入れ込みます。
下の例ではlabelは18行目から27行目となります。

 13     var body: some View {
 14         VStack {
 15             Text("Score: \(score)")
 16                 .font(.title)
 17             Button {    
 18             } label: {    
 19                 HStack {
 20                     Image(systemName: "play")
 21                         .font(.largeTitle)
 22                         .foregroundStyle(.tint)
 23                     Text("Let's Play!")
 24                         .font(.title)
 25                 }
 26                 .padding()
 27             }   
 28         }
 29     }

これでHStack の内容(三角印とLet's Play!の文字)がボタンとしての機能を持ちました。
このHStackの部分をクリックしてみると、押された反応をチカチカと返してくれるはずです。
まだ仕事は与えていないものの、ボタン型の機能(押されたときに反応する)は備えています。

変数に数値を加える仕事を設定する
今度はボタンにさせる仕事のコードを記入します。
作成したボタンを押すたびに数値が1づつ大きくなるように計算式をつくります。この式は score = score + 1  となります。
数学のイコール記号とは違ってプログラムの世界でのイコール記号は
「右にあるscore + 1 を最新のscoreとしてね」(つまり右辺の結果を左辺に代入してね)という意味となります。
そして1づつ増やすことを increment インクリメント と言ってプログラミングでは処理方法の一部として頻繁に使われます。
またSwiftではこれを score += 1 のように記入する方法がよくとられます。
下記のように他の演算式でも同様のことができます。

        var yokin: Double = 1200
        yokin *= 1.05
        print(yokin)      // 1260.0 yokin が 5%増えた!
        
        var shakkin: Double = 10_000
        shakkin -= 500
        print(shakkin)    // 9500.0  shakkin が500 減った!
        
        var egg: Int = 12
        egg /= 3
        print(egg)        // 4 egg が割れた?!


ではscore += 1 を先ほどのButtonの仕事を記述するところに書いて見ましょう。

 10 struct ContentView: View {
 11     var score: Int = 0
 12     
 13     var body: some View {
 14         VStack {
 15             Text("Score: \(score)")
 16                 .font(.title)
 17             Button { score += 1    
 18             } label: {    
 19                 HStack {
 20                     Image(systemName: "play")
 21                         .font(.largeTitle)
 22                         .foregroundStyle(.tint)
 23                     Text("Let's Play!")
 24                         .font(.title)
 25                 }
 26                 .padding()
 27             }   
 28         }
 29     }

そうするとどうでしょうか。エラーメッセージがでてきてしまいました。

Button に score + 1 の仕事をさせたらエラーとなった

エラーの内容は「数値を変更する記号が左にありますが、selfは変更不可ですよ」とでています。「selfは自分自身ということだから var score なので変更できるのでは?」 と考えたくなりますが、構造体( struct ContentView)は immutableで、プロパティを変更することは構造体を変更することでもあり通常の方法ではできないのです。(クラスとの基本的な違いのひとつです
最初に変数score = 0 を設定しましたが、そのままでは構造体のプロパティを内部から変えることはできないということです。
ではどうするか? 
@State をつけて @State var score = 0 とします。
この@Stateをつける(ラップする(包む)と言います)ことにより、struct 本人?には気づかれずにプロパティを変えることができるのです。
 ※ プロパティを包んで(くっついて)機能を与えるものをプロパティ・ラッパー(property wrapper) といいます。
@State はプロパティ・ラッパーの一つです。イエ〜い

参考
以下は特に意識するものではありませんが。参考としてなんとなく読んでいただければ結構です。
SwiftUI では struct でview を表示しますが、struct はimmutable のためプロパティラッパーを使用してstruct 自身のプロパティを変更していきます。

なお第6回 Swift基礎では struct 自身のプロパティは mutating func で変更するという記載をしています。

SwiftUIでviewを扱う際にはmutating func を使うことはありませんが、Swift では mutating func が使われています
例えば、Array struct(構造体)につく .append()、.remove() や .sort() などもQuick Helpを見てみると mutating func を使用したメソッドであることがわかります。
これらはSwiftUI とともにSwift を使っていく際に必要なので無意識にmutating func を使用していくことになります。

Swift では配列などの取り扱いがシンプルに直感的に行えるような工夫がされていると感じます。

では、score に@State をつけて見ましょう。

 10 struct ContentView: View {
 11     @State var score: Int = 0
 12     
 13     var body: some View {

今度はCanvasにエラーが出ずに画面が表示されたはずです。
そしてPlayボタンを押すごとにScoreが増えていきます
左下にある青丸三角印を押せば、初期化されて最初にもどります。

今度は簡単なクリアボタンを作成しましょう。
まずはButtonの範囲(例では17行目から27行目)をコピーして、直後に貼り付けます。そうすると下のコードとなりますが、Canvasでも縦にボタンが並びます。

 17             Button { score += 1    
 18             } label: {    
 19                 HStack {
 20                     Image(systemName: "play")
 21                         .font(.largeTitle)
 22                         .foregroundStyle(.tint)
 23                     Text("Let's Play!")
 24                         .font(.title)
 25                 }
 26                 .padding()
 27             }   
 28             Button { score += 1    
 29             } label: {    
 30                 HStack {
 31                     Image(systemName: "play")
 32                         .font(.largeTitle)
 33                         .foregroundStyle(.tint)
 34                     Text("Let's Play!")
 35                         .font(.title)
 36                 }
 37                 .padding()
 38             }   

続けて貼り付けた28行目のButtonの仕事を  score = 0 にします。
つまり scoreに0を代入するということですね。
またImageのsystemName  は"stop" にします。
"stop" はSF Symbolsの四角いボタンです。
Imageを修飾している.foregroundStyleは .red にします。もともとは.tintでしたが、.tintは「ちょこっと色をつける」という意味で「初期設定で用意してある色をつけて」くださいということです。今回は、これを赤に変更しました。


参考:初期設定の色を変更することもできます。 VStackなどのコンテナに .accentColor(.purple) をつければその範囲の初期設定の色を変更できます。
また他の方法としてNavigator から Assets.xcassets を開きAccent Color 枠を変える方法も使用されます。)



最後にText ("Clear") とします。
そうすると全体のコードは下記となります。

import SwiftUI

struct ContentView: View {
    @State var score: Int = 0
    
    var body: some View {
        VStack {
            Text("Score: \(score)")
                .font(.title)
            Button { score += 1
            } label: {
                HStack {
                    Image(systemName: "play")
                        .font(.largeTitle)
                        .foregroundStyle(.tint)
                    Text("Let's Play!")
                        .font(.title)
                }       
                .padding()
            }   
            Button { score = 0
            } label: {
                HStack {
                    Image(systemName: "stop")
                        .font(.largeTitle)
                        .foregroundStyle(.red)
                    Text("Clear")
                        .font(.title)
                }       
                .padding()
            }   
        }   
    }   
}   

#Preview {
    ContentView()
}
Playボタンで Score +1 Up,  Clearボタンで Score 0

Shape を追加する

ここに別のあるShape() を加えていきましょう。
次のコードを書き写すかコピペするかして、今あるコードのトップレベルどこでもよいですので(例では 一番上に記入を進めていきます)セットしてください。

//これは(番号が表記してあるので)書き写す用です。コピペの場合は下のコピペ用を使用してください。
  8 import SwiftUI
  9 
 10 struct StackPapers: View {
 11     
 12     let colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple]
 13     
 14     var body: some View {
 15         ZStack {
 16             ForEach(0..<colors.count, id: \.self) { i in
 17                 Rectangle()
 18                     .fill(colors[i])
 19                     .frame(width: 100, height: 100)
 20                     .offset(x: CGFloat(i) * 10.0,
 21                             y: CGFloat(i) * 10.0)
 22             }
 23         }
 24     }
 25 }
 26  
 27 struct ContentView: View {
//コピペ用
struct StackPapers: View {
    
    let colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple]

    var body: some View {
        ZStack {
            ForEach(0..<colors.count, id: \.self) { i in
                Rectangle()
                    .fill(colors[i])
                    .frame(width: 100, height: 100)
                    .offset(x: CGFloat(i) * 10.0,
                            y: CGFloat(i) * 10.0)
            }
        }
    }
}

上の行番号がある例の11行目から24行目(struct の内部)はZStack のQuick Help からほとんどそのまま移したものです。
ZStackはview を下から重ねていくことが色からわかるようになっています。
なお上の例では ForEach() { i in  } という基本的形で書きましたが、Quick Help
では ForEach() {  $0 } というクロージャ省略形で書かれています。
※ 第7回クロージャに省略形の詳細を記載しています。
また ForEach(〜, id: \.self)  とありますが、 「内容は重複していませんよ」という印が id: \.self です。もし内容に重複がある場合は検索などで問題が出る可能性があります。詳しくは今後取り上げます。

ContentView()の中で StackPapers() としてイニシャライズ(初期化、使う)します。
位置決めについては、このあとアニメーションの動きをさせたいので .offset() を使用しました。
きちんとした表示をさせたい(動きのない)場合は .padding() やSpacer()などの使用を検討します。

struct ContentView: View {
    @State var score: Int = 0
    
    var body: some View {
        VStack {
            StackPapers()
                .offset(x: -25, y: -100)
            
            Text("Score: \(score)")
                .font(.title)
            Button { score += 1

Canvas かシミュレーターに上のような画面が表示されたでしょうか?
このコードはZStack のQuick Help に載っていたものをstruct にしたものです。


動きを加える

今度は動きを持たせてみましょう。
スコアが増えると色紙が順番に上に引き出されるような動きをしてみます。

まずは struct ContentView() から struct StackPapers() に score の情報を教えてあげることを考えます。
その為にStackPapers にscore プロパティをセットします。
同じ名前でなくても良いですが、都合が良いことも多いので同じにします。
こちらのscore は数値を貰う方なので@State はつけず var score でOKです。
なお@State をつけても問題ありません。

struct StackPapers: View {
    
    var score: Int = 0
    let colors: [Color] =
        [.red, .orange, .yellow, .green, .blue, .purple]

ContentView では StackPapers(score: score) として「スコアを送るね」、
としてイニシャライズします。
StackPapers(score: score) の左のscore はStackPapers()の持つプロパティ名、右はContentView() が送り込むscore値 です。カーソルを score の上に置いて背景色が変わるのを確認してください。

// ContentView()において
            StackPapers(score: score)
                .offset(x: -25, y: -100)

スコアを送り込んだだけではview は何も変化を起こしてくれません。
「view を持ち上げる?」「一つづつ?」………… 
私は考え込んでしまいました。

そうだ! こういう時は基本に立ち戻って考えてみるんだった!
プログラムとは?。。。プログラムとは!!。。。(音楽)🎵

〜〜〜〜〜〜 編集済み 〜〜〜〜〜


え〜、view を表示させるプログラムは
1. 変数を設定する:  初期view の表示
2. 関数で計算処理をして変数を更新する
3. 更新された変数を受け取る:view の更新
4. 2と3を繰り返す
 ごくごく単純に言えばこのようにできています。

というように考えれば、まずは変数とview を結びつけてやることが必要だとわかります。



そこで StackPapers() に何番が選ばれたかを決めるcomputed property を設置します。
var pickNumber: Int  { return score % colors.count }  としました。
これは score % 6 (colors.count は6つある) として余りを求めることで 0 から5 の数値が順に返されるようにしたものです。
(return は省略してもOKです)

// StackPapers() において
    var score: Int
    var pickNumber: Int {
        return score % colors.count
    }

あとはこの変数によって view の状態が変化できるようにします。
どのようにするのかというと、view の位置を決めるメソッドに変数をセットします。
記入方法が少々見にくいかもしれませんが、
 .offset(y: i == pickNumber ? -50 : 0)   としました。
この1行を最後にセットするだけでview と変数をつなぐことができました。

            ForEach(0..<colors.count, id: \.self) { i in
                Rectangle()
                    .fill(colors[i])
                    .frame(width: 100, height: 100)
                    .offset(x: CGFloat(i) * 10.0,
                            y: CGFloat(i) * 10.0)
                    .offset(y: i == pickNumber ? -50 : 0)
            }

上記の.offset の内容を確認してみます。
実行したいことは
 条件  i == pickNumber 
 自分(Rectangle())が選ばれたら.offset(y: -50)   上に50移動する
 自分(Rectangle())が選ばれていない.offset(y: 0)   移動しない
これを組み合わせると .offset(y: i == pickNumber ? -50 : 0)  となります。

この条件によってForEach() で表現される色紙のセットが一瞬で切り替えられます。つまりボタンを押すたびに新たなstruct が作成され表示されます。
できましたでしょうか? ①

なんとなく楽しめますね。
ただ最初から赤い紙が上に上がっているのは気になります。
そこで三項演算子の条件を修正(条件を増やす)してみます。

                    .offset(y: score <= 0 ? 0 : i == pickNumber ? -50 : 0)

これでscore が0の時はみんな下がっていて②、ボタンを押すと順番に上がっていくものが作れました。
条件1    score <=0              スコアが0以下だったなら(score == 0 でもOKです)
条件2 i == pickNumber      i とpickNumber が等しければ

このように三項演算子はどんどん追加していくこともできますが、何をやっているのか読みにくい場合は、関数にすることも検討します。

下はここまでのコードです。

struct StackPapers: View {
    
    var score: Int = 0
    var pickNumber: Int {
            return score % colors.count
        }
    let colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple]
    
    var body: some View {
        ZStack {
            ForEach(0..<colors.count, id: \.self) { i in
                Rectangle()
                    .fill(colors[i])
                    .frame(width: 100, height: 100)
                    .offset(x: CGFloat(i) * 10.0,
                            y: CGFloat(i) * 10.0)
                    .offset(y: score <= 0 ? 0 : i == pickNumber ? -50 : 0)
            }
        }
    }
}

struct ContentView: View {
    @State var score: Int = 0
    
    var body: some View {
        VStack {
            StackPapers(score: score)
                .offset(x: -25, y: -100)
            
            Text("Score: \(score)")
                .font(.title)
            Button { score += 1
            } label: {
                HStack {
                    Image(systemName: "play")
                        .font(.largeTitle)
                        .foregroundStyle(.tint)
                    Text("Let's Play!")
                        .font(.title)
                }
                .padding()
            }
            Button { score = 0
            } label: {
                HStack {
                    Image(systemName: "stop")
                        .font(.largeTitle)
                        .foregroundStyle(.red)
                    Text("Clear")
                        .font(.title)
                }
                .padding()
            }
        }
    }
}

今度はランダムにピックアップするのに挑戦しましょう。
先ほどはscore が送られたタイミングで pickNumber を働かせることができました。つまりscore が変化することで pickNumber の中の計算式のscore が変わって計算式が行われる。計算が終了すると、それを欲しい人(view についているoffset())が貰うことができるようになって位置が変わる、という流れでした。
しかしランダムにするとscore と関わらなくなってしまい、計算のタイミングがわからなくなってしまいます。
そこでクロージャを使用してみます。
先ほどのcomputed プロパティと少し違うのは、
var pickNumber =  { return Int.random(in: 0..<6) } ()
のように =()  「イコールと最後のカッコ」がついてきました。これはクロージャでこのpickNumber が呼ばれるときには () イニシャライザが働いて計算して返すので正しく結果が求められます。
なお、これは関数にすることでも同様の処理を行うことができます。
 ※ランダムなので同じ数値が出る場合も多々あります。

    var pickNumber: Int = {
        return Int.random(in: 0..<6)
        } ()

最後に動きを滑らかにするためにAnimation アニメーションをつけてみます。
「アニメーション?」「滑らかに?」………… 
私は考え込んでしまいました。
そうだ!
---
あー失礼しました。
アニメーションを考える際は動きのきっかけになる変数(動きを誘発する変数)の場所を確認します。
この場合は
ボタンを押す -> score がインクリメントされる -> score がStackPapers に送られイニシャライズされる -> pickNumber変数がクロージャで算出される
-> offset がそのpickNumber を読み取って移動する

という仕組みになっていていますが、 score のインクリメントがきっかけとなっているのでここにアニメーションをつけてみます。

Text("Score: \(score)")
                .font(.title)
            Button {
                withAnimation(.spring(duration: 0.5, bounce: 0.75)) {
                    score += 1
                }
            } label: {

上のwithAnimation(.sprint(duration: 0.5, bounce: 0.75)) { score += 1 } のところを数値を変えてみたり他のアニメーション設定すればいろいろな動きをみることができます。
またoffset の数値を大きくすれば画面から飛び出させることもできます。
※アニメーションについては、今後詳しく取り上げる回を設ける予定です。

※今回のアニメーションで気づいたことは、同じ値がでたときに動きがないのがつらいですね。

必ず違う値がでるようにするためには pickNumber をcomputed プロパティやクロージャを使わずに通常のプロパティ(stored property といいます)にしておき、ContentView での数値を管理、メソッドでのプロパティの数値を確認すること、でできました。
またwithAnimationの中には、score += 1 と setPickNumber() というランダムな数を出すためのメソッドを入れました。
参考までにコードを載せておきます。

import SwiftUI

struct StackPapers: View {
    
    var score: Int = 0
    var pickNumber: Int = 0
    let colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple]
    
    var body: some View {
        ZStack {
            ForEach(0..<colors.count, id: \.self) { i in
                Rectangle()
                    .fill(colors[i])
                    .frame(width: 100, height: 100)
                    .offset(x: CGFloat(i) * 10.0,
                            y: CGFloat(i) * 10.0)
                    .offset(y: score <= 0 ? 0 : i == pickNumber ? -50 : 0)
            }
        }
    }
}

struct ContentView: View {
    @State var score: Int = 0
    @State var pickNumber = 0
    var body: some View {
        VStack {
            StackPapers(score: score, pickNumber: pickNumber)
                .offset(x: -25, y: -100)
            
            Text("Score: \(score)")
                .font(.title)
            Button {
                withAnimation(.spring(duration: 0.5, bounce: 0.75)) {
                    score += 1
                    setPickNumber()
                }
            } label: {
                HStack {
                    Image(systemName: "play")
                        .font(.largeTitle)
                        .foregroundStyle(.tint)
                    Text("Let's Play!")
                        .font(.title)
                }
                .padding()
            }
            Button { score = 0
            } label: {
                HStack {
                    Image(systemName: "stop")
                        .font(.largeTitle)
                        .foregroundStyle(.red)
                    Text("Clear")
                        .font(.title)
                }
                .padding()
            }
        }
    }
    func setPickNumber() {
        var newNumber = Int.random(in: 0..<6)
        while newNumber == pickNumber {
            newNumber = Int.random(in: 0..<6)
        }
        pickNumber = newNumber
    }
}

#Preview {
    ContentView()
}


今回はこれだけのことですが、 例えばここから飛び出すおみくじを作ってみるとか、カードをドラッグしてメモができるようにするとか、めくると写真がでてくるとか、いろいろ追加して遊ぶことも可能です。
回を追うごとに自分なりに改良していけるようになるので、今後とも是非!よろしくお願いします。


まとめ

 いかがでしたでしょうか?
 自分のコードに合わせて画面が変化していくのは面白いですね!
 最初のうちは、うまくいかなくて当たり前、うまくいったらもうけもの、ぐらい
 でOKです。
 見て、書いて、エラーを見て、考えて、の繰り返しでたくさんのことが学べま
 す。
 次回も今回のコードを使いますので、下のコードまで戻しておいてください。

 次回第12回内容  コードの整理整頓でわかりやすくする、です。
          よろしくお願いします。


ベースコード

import SwiftUI

struct ContentView: View {
    @State var score: Int = 0
    
    var body: some View {
        VStack {
            Text("Score: \(score)")
                .font(.title)
            Button { score += 1
            } label: {
                HStack {
                    Image(systemName: "play")
                        .font(.largeTitle)
                        .foregroundStyle(.tint)
                    Text("Let's Play!")
                        .font(.title)
                }       
                .padding()
            }   
            Button { score = 0
            } label: {
                HStack {
                    Image(systemName: "stop")
                        .font(.largeTitle)
                        .foregroundStyle(.red)
                    Text("Clear")
                        .font(.title)
                }       
                .padding()
            }   
        }   
    }   
}   

#Preview {
    ContentView()
}

       

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