iOSアプリ開発：タイマーアプリ（6.設定画面の作成）

![スクリーンショット 2020-10-28 11.14.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570497/9de1627a-e6ee-ce0d-bd51-35fd7cded359.png)



##記事
タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、アラーム音のオン/オフやプログレスバーのオン/オフを含む設定画面の作成方法について掲載します。



##環境

- OS: macOS 10.15.7 (Catalina)
- エディタ： Xcode 12.1
- 言語: Swift
- 主な使用ライブラリ: SwiftUI


##Gitリポジトリ
以下のGitリポジトリのURLからサンプルコードをご覧いただけます。
https://github.com/msnsk/Qiita_Timer.git


##手順


1. 設定項目をリストアップする
2. SettingView を作成する
3. SettingView にリストを作成する
4. SettingView のリストに設定項目を追加する
5. TimeManager に各種設定に必要なプロパティを追加する
6. SettingView の設定項目を TimeManager のプロパティと関連づける
7. SettingView をモーダルにする
8. MainView に設定ボタンを追加し、設定画面をモーダルで表示する


##手順詳細
###1. 設定項目をリストアップする

設定画面を作成していきます。今まで作成してきた PickerView や TimerView、ButtonsView はすべて MainView に配置され、タイマーアプリには MainView 以外の画面がない状態でした。

今回はMainViewとは別に、設定画面を1つ作成していきます。
この設定画面には以下の項目を表示することにします。

- アラーム音を鳴らすかどうかのトグルスイッチ
- バイブレーションを有効にするかどうかのトグルスイッチ
- アラーム音をリストから選択
- プログレスバーを表示するかどうかのトグルスイッチ
- エフェクトアニメーションを表示するかどうかのトグルスイッチ
- 設定画面を閉じるボタン


###2. SettingViewを作成する

SwiftUI テンプレートから、SettingView という名前で新しくファイルを作成します。

ここでも、最終的に TimeManager クラスのプロパティに設定情報を反映することになるので、先に@EnvironmentObject プロパティラッパーをつけて、TimeManager のインスタンスを作成します。

```swift:SettingView.swift
import SwiftUI

struct SettingView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        Text("Hello, World!")
    }
}
```




###3. SettingViewにリストを作成する

SwiftUI で主にリスト表示に使われるコンポーネントは2種類あります。1つは List、もう一つは Form です。

一般的な iOS アプリの設定画面は Form が用いられていますので、今回は Form を利用します。

ちなみに、List はいくつかのセクションをタイトルをつけて区切っても、そのセクション区切りを含めてすべて罫線で仕切られた表示になるため、どちらかというとリマインダーなどのアイテムをずらりと並べるのに向いています。

アラーム音の選択の項目については、設定画面からさらにサウンド選択画面に遷移させたいので、Form コンポーネントを NavigationView の中に入れます。こうすることで、Form 内からさらに別の List や Form へ遷移するような構成にすることができます。


```swift:SettingView.swift
struct SettingView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        NavigationView {
            Form {
            }
        }
    }
}
```




###4. SettingView のリストに設定項目を追加する

画面構成としては、以下のように想定しています。

<strong>セクション1：アラーム関連</strong>
- アラーム音オン/オフのトグルスイッチ
- バイブレーションオン/オフのトグルスイッチ
- アラーム音選択

<strong>セクション2：アニメーション関連</strong>
- プログレスバー表示オン/オフのトグルスイッチ
- エフェクトアニメーションオン/オフのトグルスイッチ
※セクション2の実際の実装は設定画面作成ができてからやっていきます。

<strong>セクション3：設定画面を閉じる</strong>
- 設定画面を閉じるボタン

Form の中に Section を3つ入れていきます。
1 つ目の Section には、Toggle 2つと NavigationLink を1つ入れます。
二 つ目の Section には、Toggle を1つ入れます。
三 つ目の Section には、Button を1つ入れます。

Section の引数headerには、そのセクションのタイトルを Textで入れておくと何のセクションかわかりやすくなります。

```swift:SettingView.swift
struct SettingView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Alarm:")) {
                    Toggle(isOn: ) {
                    }
                    Toggle(isOn: ) {
                    }
                    NavigationLink(destination: )) {
                    }
                }
                Section(header: Text("Animation:")) {
                    Toggle(isOn: ) {
                    }
                    Toggle(isOn: ) {
                    }
                }
                Section(header: Text("Save:")) {
                    Button(action: ) { 
                    }
                }
            }
        }
    }
}
```

さて、ここで Toggle コンポーネントのオン/オフを反映するためのプロパティが必要になります。そのプロパティは、設定画面から MainView の各コンポーネントに反映される必要があります。例えば、設定画面でアラームをオンにしたら、その設定が実際に発動するのは MainView のほうです。

ですので、各設定情報を格納するプロパティは、TimeManager クラスに @Published のプロパティラッパーをつけて追加する必要があります。




###5. TimeManagerに各種設定に必要なプロパティを追加する

TimeManager にトグルスイッチの設定情報を格納するためのプロパティを4つ追加します。追加するのは以下の4つになります。

- アラーム音オン/オフの設定
- バイブレーションオン/オフの設定
- プログレスバー表示オン/オフの設定
- エフェクトアニメーション表示オン/オフの設定


```swift:TimeManager.swift
class TimeManager: ObservableObject {
    //（他のプロパティ省略）

    //soundIDプロパティの値に対応するサウンド名を格納
    @Published var soundName: String = "Beat"

    //アラーム音オン/オフの設定
    @Published var isAlarmOn: Bool = true
    //バイブレーションオン/オフの設定
    @Published var isVibrationOn: Bool = true
    //プログレスバー表示オン/オフの設定
    @Published var isProgressBarOn: Bool = true
    //エフェクトアニメーション表示オン/オフの設定
    @Published var isEffectAnimationOn: Bool = true

    //1秒ごとに発動するTimerクラスのpublishメソッド
    var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    //(メソッド省略)
}
```



###6. SettingView の設定項目を TimeManager のプロパティと関連づける

Toggle メソッドの引数 isOn に設定を格納したい TimeManager のプロパティを指定します。このとき$記号を頭につける必要があります。また、クロージャ内には Form 内の設定項目として表示したい名前を Text で入れます。

```swift
Toggle(isOn: $timeManager.isAlarmOn) {
         Text("Alarm Sound")
 }
```

このようにして、ひとまずすべての Toggle の引数、クロージャを記述していきます。

アラーム音選択の項目については、選択画面がまだないので、いったんコメントアウトしておきます。

最後の保存ボタンのラベルは、テキストとチェックマークアイコンで用意します。水平方向の中央に配置したいので、HStack で囲み、左右から Spacer を配置することで中央にもってきます。ボタンをタップしたときのアクションはまだ空白です。

```swift:SettingView.swift
struct SettingView: View {
    @EnvironmentObject var timeManager: TimeManager
    
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Alarm:")) {
                    Toggle(isOn: $timeManager.isAlarmOn) {
                        Text("Alarm Sound")
                    }
                    Toggle(isOn: $timeManager.isVibrationOn) {
                        Text("Vibration")
                    }
//                    NavigationLink(destination: ) {
//
//                    }
                }
                Section(header: Text("Animation:")) {
                    Toggle(isOn: $timeManager.isProgressBarOn) {
                        Text("Progress Bar")
                    }
                    Toggle(isOn: $timeManager.isEffectAnimationOn) {
                        Text("Effect Animation")
                    }
                }
                Section(header: Text("Save:")) {
                    Button(action: ) {
                        HStack {
                            Spacer()
                            Text("Done")
                            Image(systemName: "checkmark.circle")
                            Spacer()
                        }
                    }
                }
            }
        }
    }
}
```
Canvas で SettingView の表示を確認します。以下はプレビュー用のコードです。

```swift
struct SettingView_Previews: PreviewProvider {
    static var previews: some View {
        SettingView().environmentObject(TimeManager())
    }
}
```

下の画像のようになっているはずです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570497/5f510fd0-e388-886a-c6bb-37a52d2dc9aa.png" alt="スクリーンショット 2020-10-28 11.14.35.png" width="300">



###7. SettingView をモーダルにする

SettingView をモーダルで表示させます。モーダルというのは、モーダルウインドウを縮めた名前で、そのウインドウが開いている間は他の操作が不可になるようなものです。

SettingView に @Environment(\.presentationMode) というプロパティラッパーをつけた変数を用意します。

次に設定画面を閉じるときにタップする Save ボタンの　action 引数のクロージャ内にモーダルを閉じるためのコードを記述します。

>self.presentationMode.wrappedValue.dismiss()

```swift:SettingView.swift
struct SettingView: View {
    //モーダルシートを利用するためのプロパティ
    @Environment(\.presentationMode) var presentationMode
    @EnvironmentObject var timeManager: TimeManager
    
    var body: some View {
        NavigationView {
            Form {
                //（他のSection省略）

                Section(header: Text("Save:")) {
                    Button(action: { 
                        //タップしたらモーダルを閉じる
                        self.presentationMode.wrappedValue.dismiss()
                    }) {
                        HStack {
                            Spacer()
                            Text("Done")
                            Image(systemName: "checkmark.circle")
                            Spacer()
                        }
                    }
                }
            }
        }
    }
}
```

今度は反対に設定画面を開くためのボタンを MainView に用意していきます。ボタン一つですが、わかりやすく View を作成します。では、新規に SettingButton という名前の新規 SwiftUI ファイルを作成します。

```swift:SettingButtonView.swift
import SwiftUI

struct SettingButtonView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
    }
}
```

ボタンアイコンは SF Symbols から "ellipsis.circle.fill" を使います。

```swift:SettingButtonView.swift
import SwiftUI

struct SettingButtonView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        Image(systemName: "ellipsis.circle.fill")
    }
}
```

設定ボタンは、スタート/一時停止ボタンやリセットボタンより少し小さめのサイズにするので、frame モディファイアで縦、横のサイズを入れておきます。

```swift:SettingButtonView.swift
import SwiftUI

struct SettingButtonView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        Image(systemName: "ellipsis.circle.fill")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 40, height: 40)
    }
}
```

ここで、モーダルを表示/非表示を示す、Bool 型のプロパティを TimeManager クラスに作成しておきます。名前は isSetting としておきます。

```swift:TimeManager.swift
class TimeManager: ObservableObject {
    //（他のプロパティ省略）

    //設定画面の表示/非表示
    @Published var isSetting: Bool = false

    //（メソッド省略）
}
```

そして最後に SettingButtonView に戻り、.onTapGesture を追加して、クロージャ{}内に TimeManager の isSetting プロパティが trueになるようにします。

```swift:SettingButtonView.swift
import SwiftUI

struct SettingButtonView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        Image(systemName: "ellipsis.circle.fill")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 40, height: 40)
            .onTapGesture {
                self.timeManager.isSetting = true
    }
}
```

Canvas で画面を確認してみます。以下はプレビュー用のコードです。

```swift
struct SettingButton_Previews: PreviewProvider {
    static var previews: some View {
        SettingButtonView()
            .environmentObject(TimeManager())
            .previewLayout(.sizeThatFits)
    }
}
```
下の画像のようになります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570497/ac4571fa-d42c-84e8-46d9-9933aa7e28e2.png" alt="スクリーンショット 2020-10-28 10.56.37.png" width="300">



###8. MainViewに設定ボタンを追加し、設定画面をモーダルで表示する


それでは ManiView に SettingButtonView を追加していきます。

追加する箇所は、PickerView や TimerView より下の、スタート/一時停止、リセットボタンと同じ画面下端です。そのため、ZStack{} で ButtonsView と SettingButtonView　をレイヤー状に前後に重ねます。どちらが前、後ろでも構いません。

垂直方向の位置が全てのボタンで揃ったほうが美しいので、SettingBottonView も ButtonsView 同様、padding(.bottom) モディファイアを追加します。

そして、モーダルウインドウを表示するための .sheet() モディファイアを追加します。 isPresented 引数には、true のときにモーダルが表示される Bool 型プロパティを指定します。つまり、先に用意しておいた TimeManager クラスの isSetting プロパティです。

クロージャ {} 内にはモーダルで表示したい View を記述します。ここでは SettingView() です。このとき必ずプロパティの .environmentObject(self.timeManager) も記述しておきます。

```swift:MainView.swift
struct MainView: View {
    @EnvironmentObject var timeManager: TimeManager
    
    var body: some View {
        ZStack {
            if timeManager.timerStatus == .stopped {
                PickerView()
            } else {
                TimerView()
            }
            
            VStack {
                Spacer()
                //ButtonsViewとSettingButtonViewをレイヤー状に重ねる
                ZStack {
                    ButtonsView()
                        .padding(.bottom)
                    //設定ボタンを追加
                    SettingButtonView()
                        .padding(.bottom)
                        //isSettingプロパティがtrueになったらSettingViewをモーダルで表示
                        .sheet(isPresented: $timeManager.isSetting) {
                            SettingView()
                                            .environmentObject(self.timeManager)
                        }
                }
            }
        }
        //（.osReceiveモディファイア部分省略）
    }
}
```

アラーム音やバイブレーションの設定項目をもうけましたので、MainView の onReceive モディファイアのクロージャ内のタイマーが 0 になるとアラーム音を鳴らす、バイブレーションを発動させる記述をそれぞれ、設定がONだったらという形にします。これはシンプルに TimeManager クラスの isAlarmOn プロパティと、isVibrationOn プロパティの値によって if 文で条件分岐させるだけです。

```swift:MainView.swift
struct MainView: View {
    @EnvironmentObject var timeManager: TimeManager
    
    var body: some View {
        ZStack {
            //(Viewの記述省略)
        }
        //指定した時間（1秒）ごとに発動するtimerをトリガーにしてクロージャ内のコードを実行
        .onReceive(timeManager.timer) { _ in
            //タイマーステータスが.running以外の場合何も実行しない
            guard self.timeManager.timerStatus == .running else { return }
            //残り時間が0より大きい場合
            if self.timeManager.duration > 0 {
                //残り時間から -0.05 する
                self.timeManager.duration -= 0.05
                //残り時間が0以下の場合
            } else {
                //タイマーステータスを.stoppedに変更する
                self.timeManager.timerStatus = .stopped
                //アラーム音を鳴らす
                if timeManager.isAlarmOn {
                    AudioServicesPlayAlertSoundWithCompletion(self.timeManager.soundID, nil)
                }
                //バイブレーションを作動させる
                if timeManager.isVibrationOn {
                    AudioServicesPlayAlertSoundWithCompletion(SystemSoundID(kSystemSoundID_Vibrate)) {}
                }
            }
        }
    }
}
```


MainView を Canvas で確認してみます。以下はプレビュー用のコードです。

```swift
struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            MainView().environmentObject(TimeManager())
        }
    }
}
```

下の画像のようになります。設定ボタンがスタートボタンとリセットボタンの間に高さも揃った状態で配置されています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570497/e9d4884e-6a76-9b53-094c-d1f23e6b6afd.png" alt="スクリーンショット 2020-10-28 10.57.44.png" width="300">

これで、設定ボタンをタップすると、モーダルで設定画面が表示されるようになりました。Canvas で実際に確認してみてください。


今回は設定ボタンだけの View を SettingButtonView として作成しましたが、先に作成した ButtonsView にまとめてしまっても良いかと思います。