Swift勉強約1ヶ月でiOSアプリを公開してみました - 実装編 -

## 前回の続き
　[-準備編-](https://qiita.com/soysan/items/466dc2a839e58de2a682)

## Versionなど
 - Swift 5.3
 - Xcode 12.4
 - UIKitでやりました（現場でまだまだこちらを使っていることとSwiftUIだけ知っていてもよくないなと思ったからです）
 - Storyboardを使っておりません。（消しました。）

## Swiftを選んだ理由
　- 私がiPhoneユーザーであること　（後に実機でテストができる）
　- Androidの方が世界的にみてユーザー数が多いし、Javaも触ったことあるのでとっつきやすいと思いましたが実機テストできないことがネックでした。
　- MacBook proをメインで使用している

　このような理由で開発環境が整っているのでスタートしやすいと思い選びました。

## Xcodeのダウンロード
　XcodeとはMacが制作したEditorです。
　
　[公式サイト](https://developer.apple.com/jp/xcode/)

　ここにコードを書いてプログラムを走らせます。無料で使えるので他にもeditorの選択肢はありますが初めはこれを使ってみましょう。
　beta版と安定版がありますがbeta版は新機能を試しにAppleが実装してみたので使ってみてください、でもバグがあるかもしれないので教えてくださいというものなので、使っているうちにバグが出ると面倒です。
　安定版をMacに初めから入っているアプリのApp Store.appからインストールしましょう！
　Xcodeはとても重いのでinstallに少し時間がかかります。

　どんな記事でもいいので使い方をネットで調べて一読しましょう。versionによってUIが違いますが、基本機能は同じです。

## 0. 設定
　![スクリーンショット 2021-04-01 午後0.36.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643403/a50a1873-696b-427e-7445-24223b65a427.png)
　- iOS カーソルで最低限動かせるversionを設定します。画面の場合は13以降で動かせるということです。
　- どのdeviceで機能させるか選びます。私の場合はiPhoneだけにしました。
　- Device Orientation で画面の方向を決めます（Portraitで縦、Upside Downで逆さ、Landscape Leftで左を下にした横画面、最後はLeftの逆です）
　　私の場合は、普段と違う画面の使い方をすることで作業へのスイッチの切り替えをして欲しいなと思い横表示にしました。
　

## 1. Storyboardを消す
　消した理由は主に

  - JavaScriptなどでコード管理で実装してきたこと
  - テストなどがしやすいと読んだ本に書いてあった
  - そもそもSwiftUIではStoryboardがない（今後そちらに向かっていくと思いました）
  - 風の噂で現場では徐々になくして行っていると聞いた

　などの理由で初めから使う選択肢をなくしました。
　できるか心配でしたが現場で使っているところがある以上大丈夫だろうと思い記事を参考にはじめに消しました。

　参考記事
　　- [Storyboardを使わないiOSアプリ開発](https://zenn.dev/kazumalab/articles/76ca59f82c189cf33d43) 

　この段階でどのように実装内容が画面に出るのか色々試しました。UIButton,UIView,UILabelなどです。（簡素な物でいいのでとりあえず画面に出しました）
　エラーがでたり動かなかったりしても "swift without storyboard"などと検索すればたくさんやり方が出たのでちょこちょこ実装しました。
　Xib fileのことを知っていればここまでストイックにならなくてもよかったかもしれませんがこの時はあんまり考えず消しました。

　※ ios13以降はSceneDelegateを読み込みますが、それ以前はAppDelegateを読み読むので両方に対応したければ両方追加でcodeを書きます。
　※ もしやっぱりStoryboardを使いたいと思ったとしてもStoryboardの作り方など調べればたくさんでます。
　
## 2. 色の選定
　当方、デザインなど全くやってこなかったので苦手意識がありましたが、カラーリングはセンスではなくパターンがあると本に書いてあったので

　参考サイト
　　　-  [色の持つイメージ、与える影響](https://photoshopvip.net/102903)
　　　-  [Color hunt](https://colorhunt.co/palette/253088) (色の組み合わせを自動で選んでくれる。)

　などを見て参考にしてメインカラーやアクセントカラーを選びました。

　私の場合は作業効率化のアプリなので集中しやすい青、緑、黄色などが候補に上がりました。

```swift:Colors.swift
import UIKit

struct Colors {
    
    let deepBlue = UIColor(red: 40/255, green: 82/255, blue: 122/255, alpha: 1)
    let lightBlue = UIColor(red: 138/255, green: 196/255, blue: 208/255, alpha: 1)
    let yellow = UIColor(red: 244/255, green: 209/255, blue: 96/255, alpha: 1)
    let white = UIColor.white
}
```

　このようにColor fileを作りView側でインスタンスを作り呼び出すようにしました。
　完成品を見ていただけるとわかりますがなんかダサいのでやっぱりセンスは関係あるのではないのかなと個人的には思いました（）

## 3. ページの作成
　ここからやっとページの実装です。

　ViewControllerから始めました。どこからやってもいいと思いますがとりあえずトップページの実装からやるような気がします。（反省点としてViewControllerをそのまま使うのではなくMainViewControllerなど自分で作ったfileにした方がよかったように思いました。）
　実装方法は調べると色々あるのでどれが正解かわからなかったのでとりあえずわかりやすい物に書式を合わせて実装しました。

　各セクションごとに``` // MARK: - something ``` とタイトルをふりました。（youtubeで見かけた方法です）
　こうするとXcodeの場合はeditor内に線が引かれセクションの区切りがわかりやすくなります。そしてミニマップにもデカデカと文字が見えるので冗長になった時に探しやすくなる。（冗長になることがいいことではないと思いますがこの時の案でした）
![スクリーンショット 2021-03-31 午後2.26.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643403/27611f5e-fbe6-d763-0869-0d37ab9cec18.png)

### 要素の作成
　Propertiesのところにそれぞれのボタンやラベルを定義していきました。モデルなどに分けてボタンインスタンスにしたほうが使い勝手がよくなると思いましたが今回はとりあえず実装してversion２などで改善していこうと思います。
　これらは一例なので他にいい実装方法があったら教えてください。

　参考にしたサイトは

　　　- [公式のドキュメント](https://developer.apple.com/documentation/swift/swift_standard_library)
　　　- [FaBo Swift Docs](https://faboplatform.github.io/SwiftDocs/)
 

　まずはドキュメントを確認することを癖にしました。わかりにくい場合はその都度調べました。

```swift
    let titleLabel: UILabel = {
        let label = UILabel()
        label.text = "Pomodoro Timer"
        label.font = UIFont.boldSystemFont(ofSize: 68)
        return label
    }()
    
    let startButton: UIButton = {
        let button = UIButton()
        button.setTitle("Start", for: .normal)
        button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 30)
        button.layer.cornerRadius = 20
        button.layer.shadowOpacity = 0.5
        button.layer.shadowRadius = 10
        button.layer.shadowColor = UIColor.black.cgColor
        button.layer.shadowOffset = CGSize(width: 10, height: 10)
        button.addTarget(self, action: #selector(goToNext(_:)), for: .touchUpInside)
        return button
    }()
```

そしてviewDidload()内で要素をviewの上に置くようにしました。

```swift
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = colors.deepBlue

        view.addSubview(titleLabel)
        titleLabel.textColor = colors.yellow
        
        view.addSubview(startButton)
        startButton.backgroundColor = colors.lightBlue
        startButton.tintColor = colors.white
    }
```

```swift 
addSubView()
```
　で要素を親のViewの上に追加しています。

　viewに直接置くことはベストプラクティスではないと思いますが、表示はできました。

### AutoLayoutによる要素の位置決め

　今のままだと画面の左上に重なって要素が置かれる状態になるので位置を指定していきます。
　要素ごとにframeで位置決めすることもできるのですが、iPhoneの画面の大きさはiPhoneによって違うためframeで設定すると物によっては位置が思ったところからズレて表示されてしまうバグが出てしまいます。
　そこでAutoLayoutという方法を使います。
　この方法は簡単にいうとそれぞれの画面の幅を起点にして上から20, 下から20 など指定し、どの大きさの画面がきても起点の位置は変わらないので要素の位置がズレないという利点があります。

　はじめにNSLayoutAncherを使用して実装しました。

　参考にした記事
　　　- [Auto Layoutをコードで書いてみた](https://qiita.com/dddisk/items/8001598ea7951bcdcc30)


　記事にも書いてありますが　``` somethingView.translatesAutoresizingMaskIntoConstraints = false```
　この行がないとエラーが出ます。（私の場合はUIButtonなどの中に宣言していました。）

```swift
somethingView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
somethingView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
```
　これは一例ですが訳すと

```
　要素Viewの、X軸の真ん中の、制約は（　と同じです：　親ViewのX軸の真ん中と同じ）　が　アクティブですか？　＝　そうです。
　要素Viewの、Y軸の真ん中の、制約は（　と同じです：　親ViewのY軸の真ん中と同じ）　が　アクティブですか？　＝　そうです。
```
　isActiveを書き忘れるとエラーになります。（Xcodeが読み取れる状態にならないからです）
　これで要素が画面の真ん中にきます。
　記事を見ると細かい位置指定のやり方が書いてあるので色々試して見ると面白いです。

　そのままでも実行されて見た目はいいのですが、viewDidLoad内が冗長化して見にくくなるので私の場合は

```swift
func setPosition() {
   titleLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
   titleLabel.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constraint: -60).isActive = true

   startButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constraint: -40).isActive = true
   startButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
   ....etc
}
```

　などと位置を決める関数を作りまとめ、viewDidloadの最後に関数を呼び出すようにしました。

　ですが最終的にSnapKitを使った方が綺麗になるということがわかりCocoaPodsを使用してライブラリの導入を行い

```swift
    func setPosition() {
        titleLabel.snp.makeConstraints({ make in
            make.centerX.equalToSuperview()
            make.centerY.equalToSuperview().offset(-60)
        })
        startButton.snp.makeConstraints({ make in
            make.width.equalTo(200)
            make.top.equalTo(titleLabel.snp.bottom).offset(50)
            make.centerX.equalToSuperview()
        })
        modeButton.snp.makeConstraints({ make in
            make.width.equalTo(200)
            make.bottom.equalToSuperview().offset(-20)
            make.trailing.equalToSuperview().offset(-20)
        })
    }
```
　になりました。
　やっていることはNSLayoutAnchorと同じですが書き方がだいぶ変わりました。
　クロージャ内で書くのでどの要素に指定しているのかわかりやすいですし、translatesAutoresizingMaskIntoConstraintsを書かなくてもライブラリ側で処理してくれます。

　CocoaPodsはMac向けのライブラリを管理するsoftです。

　導入方法は
　　- [公式ドキュメント](https://cocoapods.org/)
　　- [【Swift】CocoaPods導入手順](https://qiita.com/ShinokiRyosei/items/3090290cb72434852460)

　などを参考にしました。
　私の場合だけかもしれませんが導入時公式のものにしたがってgemを使用してinstallするとライブラリをinstallしたときにwarnningが出たので、一度gemで入れたCocoaPodsを消してHomeBrew（Mac用のVersion管理soft）で入れ直しました。するとwarnningが消えました。どちらがいいかわかりませんがversion管理を手動でやりたくない場合はHomeBrewの方が個人的にはいいように思いました。

　SnapKitの書き方の参考にしたサイトは 
　　- [公式ドキュメント](http://snapkit.io/docs/)
　　- [【Swift4】SnapKitがめちゃくちゃ便利だった件](https://rightcode.co.jp/blog/information-technology/swift4-snapkit-takagai)　（swift4と書いてありますが5でも動きました）

　一度NSLayoutAnchorで書いていたので書き方に違いはあれど書き換えはスムーズにいきました。

## 4. 画面遷移の記入

　参考記事
　　- [Storyboardを使わないiOSアプリ開発](https://zenn.dev/kazumalab/articles/76ca59f82c189cf33d43)
　　- [Swift： Storyboardを使わない画面遷移まとめ！](https://programming-beginner-memo.com/?p=825)

```swift
addTarget(どの要素に, action: 処理関数, for: どんな操作をした時に（ボタンなど）)
```

　を使います。JavaScriptをやったことある人ならわかると思いますがaddEventListener()と同じです。引数が多いだけです。
　ですので画面遷移でばかり使う印象ですが、eventを発火させたい時にも使います。
　※　ios14から```addAction```になるみたいですが私の場合はios13に対応しているので使ってません。

```swift
buttonView.addTarget(self, action: #selector(goToSetting(_:)), for: .touchUpInside)

@objc func goToSetting(_ sender: UIButton) {
   let nextView = SettingView()
   nextView.modalPresentationStyle = .fullScreen
   nextView.modalTransitionStyle = .crossDissolve
   present(nextView, animated: true, completion: nil)
}
```
　この場合ですと```self```でボタン自身に、```#selecterで指定したgoToSettring()を行う```, ```.touchUpInside```のボタンイベントが行われた時にと訳せます。
　ボタンイベントの種類
　　-  [【Swift】UIButtonに設定できるイベントの種類と動作について。(Swift 2.1、XCode 7.2)](https://hajihaji-lemon.com/swift/uibutton_event/)
　　動きがイメージしやすいです。

　#selectorって？
　　- [Selector完全攻略、そして初学者特有のAddTarget()やAddObserver()のセレクタに変数を渡そうとする願望について](https://qiita.com/st43/items/43c5c0f5e1f727ba0ff0)
　　
　そして上のリンク見ていただければわかると思いますが#selectorで指定した関数は```@objc```をつけないといけません。決まりです。

#### Modal 遷移（下からシュッと出てくるやつ）
　関数内の```present(次の画面View, アニメーションをするかどうか, この関数が終わった後に処理をするか)```を使うとモーダルという下から画面がシュッと出てくるアニメーションで画面が遷移します。
　```modalPresentationStyle```で普段画面上に余白を残して見えるモーダルをフルスクリーンで表示するようにしています。
　```modaltransitionStyle```で画面遷移のエフェクトの種類を変えています。興味があったら他のものを試すと面白いですよ。 ```.```と打つと候補が出ます。
　
　モーダルの場合ですが遷移先から元の画面に戻りたい時はまた上のようなコードで遷移元の画面を指定するのではなく

```swift
    @objc func goBack() {
        self.dismiss(animated: true, completion: nil)
    }
```
　このように```dismiss(アニメーションをするかどうか, 戻る前に何か処理をするか)```を使用して戻ります。
　こうしないとviewがどんどん重なっていきメモリを圧迫していき、やがてクラッシュする可能性があります。

#### エラーになりました
　```top画面```⇨```Study画面```⇨```Break画面```(機能としてStudy,Breakの両画面からresetButtonを押すとtop画面に戻るようにしたかった)
　この遷移方法で3画面先まで行けるようにしてBreak画面からresetButtonを押してtop画面に一気に戻る機能のボタンを作るとエラーの正式名所は忘れてしまったのですが view hierarchy　エラーがコンソールに出るようになりました。これはviewが本来予期した順番で重なっていないのでエラーになっているようでした。
　debug機能を使うと確認しやすいです。
　　画面上の```Debug```⇨```View Debug```⇨```Capture View Hierarchy```を見ると![スクリーンショット 2021-03-31 午後9.51.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643403/91e011d2-5a0d-91fd-773e-b52fb3019d01.png)
![スクリーンショット 2021-03-31 午後9.51.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643403/7c0b6377-c12c-02c6-66fb-f7576d28f98a.png)

　このようにviewが現在どんな状態で重なっているかGUIで確認できます。直感的に画面をぐりぐり動かせるのでわかりやすいです。
　
　エラーを回避するために
　　- 今までのviewを消して新しく１から表示できないか？
　　- ２画面以上の遷移メソッドはないか？
　
　など調べて試行錯誤しましたがさらに調べると、どうやら```present（モーダル）では２画面以上一気に画面を遷移することはできない```ということがわかりました。収穫でした。とても勉強になりました。

#### Navigation 遷移準備
　２画面以上戻る遷移はNavigationしかできないようなので```top画面```⇄```Study画面```⇄```Break画面```の遷移をNavigationに書き換えます。
　
　はじめにtop画面の表示をそもそもNavigationViewでラップしないといけないようでしたので

```swift:SceneDelegate.swift
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UINavigationController(rootViewController: ViewController())
            self.window = window
            window.makeKeyAndVisible()
        }
    }
```
```swift:AppDelegate.swift
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = UINavigationController(rootViewController: ViewController())
        self.window = window
        window.makeKeyAndVisible()
        
        return true
    }
```
　に```window.rootViewController = UINavigationController(rootViewController: top画面にするview)```を追加してtop画面をNavigationViewにしました。

![Simulator Screen Shot - iPhone 8 - 2021-03-31 at 23.30.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643403/998f393b-a7ca-e8a8-988e-39ca716c31e5.png)

　そうすると副作用で画面上にNavigatinBarが常時表示されるようになり見栄えが悪いので消す方法を探しました。
　```self.navigatinController?.setNavigationBarHidden```を使うと隠すことができることがわかりましたのでとりあえずviewDidLoadに実装してみましたがうまくいきませんでした。
 さらに調べるとどうやらLifeCycleが関係しているようだということがわかり

```swift:ViewController
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.navigationController?.setNavigationBarHidden(true, animated: true)
    }
```
　[barを消すために参考にしたサイト](https://www.codegrepper.com/code-examples/swift/swift+hide+navigation+bar)
　古い情報やSwiftUIのものがよくヒットするので地味に探しずらかったです。

　を追加すると消えてくれました！

#### LifeCycle とは
　めちゃくちゃ大事な概念です！！！
　恥ずかしいですがこの時まであまり意識できていなかったです。Reactなど触ったことある人なら感覚がわかるかもしれません。
　言葉の通りですが、viewControllerの一生です。

```swift
viewLoad -> viewDidLoad -> viewWillAppear -> viewDidAppear -> viewWillDisappear -> viewDidDisappear -> didReceiveMemoryWarning
```
　の順番でviewControllerが表示から消えるまでのサイクルです。英語がわかるとイメージしやすいです。

　参考サイト
　　- [ViewControllerのライフサイクル](https://medium.com/@shiba1014/viewcontroller%E3%81%AE%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB-37151427bda5)
　　- [公式ドキュメント](https://developer.apple.com/documentation/uikit/uiviewcontroller)
　　- [【Swift】UIViewController ライフサイクル 簡易説明書 | ポケットリファレンス サンプル付き](https://blog.77jp.net/swift-uiviewcontroller-life-cycle)

　概念をしれてふわふわしていたものがスッキリした感覚がありました。

#### Navigation遷移（push遷移、横から今の画面を押すように出てくるやつ）
　```self.navigationController?.pushViewController(次のview, アニメーションをするかどうか)```で遷移させます。

```swift
    @objc func goToNext() {
        let nextView = StudyView()
        self.navigationController?.pushViewController(nextView, animated: true)
    }
```
　このように書き直して遷移に成功したのですが今度はまた遷移先のStudy画面で上にNavigationBarが表示されていました。
　遷移先のStudyViewにも

```swift:StudyView
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.navigationController?.setNavigationBarHidden(true, animated: true)
    }
```
　をつけると消えたのでBreakViewにも先に設置しました。

　戻るときは```self.navigationController.popViewController(アニメーションするかどうか)```で実装します。

```swift
    @objc func goToTop() {
        self.navigationController?.popViewController(animated: true)
    }
```

 ２画面以上の場合は

```swift
self.navigationController?.popToRootViewController(animated: true)
```

　うまくいきました。


　このようにpageを必要な分制作していきました。

## 4. Timerの実装
　タイマーなのでカウントダウン形式の機能を実装しないといけません。
　
　参考記事
　　- [【swift】swift4：Timerを利用して定期処理実行をおこなう方法](http://kussuue.com/2017/09/swift4_timer/)
　　- [公式ドキュメント](https://developer.apple.com/documentation/foundation/timer)
　　
　クロージャを使ってスッキリ書きたかったので上の記事を参考にしました。
　公式ドキュメントにあるようにTimerは公式が用意してくれているのでTimer()と宣言するだけで使えます。
　調べると色々な書き方がありDateを使ってカウントダウンなりアップなりする方法が多いように思いましたが、私は50分より多く表示しないのでこちらで必要な秒数を宣言する方法を取りました。

```swift:StudyView
    var timer: Timer()

    // 初期値としての秒数をセット 1分=60秒 *　25
    var currentTime = 1500 

    // 数字を表示する用のlabel - 分と秒で分けてます
    let minsLabel: UILabel = {
        let label = UILabel()
        label.text = ""
        label.font = UIFont.boldSystemFont(ofSize: 30)
        return label
    }()
    
    let secLabel: UILabel = {
        let label = UILabel()
        label.text = "00"
        label.font = UIFont.boldSystemFont(ofSize: 30)
        return label
    }()

    override func viewDidLoad() {
    ...
    view.addSubview(minsLabel)
    minsLabel.text = String(currentTime / 60) // 秒を分で割ると分がでる
    
    view.addSubview(secLabel)
    secLabel.text = ": 00" // 初期値を書いておく（書いてないと実際に表示するとき何も表示されなかった
    ...
    }

    override func viewDidAppear(_ animated: Bool) {
        updateTime() // 関数の呼び出しをviewDidLoadでやると挙動がおかしかったので表示直前に呼び出し
    }
    
    func updateTime() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (timer) in
            self.currentTime -= 1   // 処理が終わるまで１秒ずつ表示する秒を引いている
            
            let currentMin = self.currentTime / 60 // 現在の分を求める
            self.minsLabel.text = currentMin < 10 ? "0" + String(currentMin) : String(currentMin); // ゼロ埋めするために10分より分が少ない場合は頭に0をつけるようにする

            let currentSec = self.currentTime % 60 // 現在の秒を求める
            self.secLabel.text = currentSec < 10 ? ": 0" + String(currentSec) : ": " + String(currentSec); // ロジックは分と同じ。わかりやすいように分と秒の間に:を追加

            if self.currentTime == 0 { // ゼロ秒になったら処理
                self.currentTime = 1500 // 初期値に戻す
                timer.invalidate()   // timerを止める（メモリ対策、裏処理の防止）
                self.goToNext() // 画面遷移
            }
        })
    }
```

```swift
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (timer) in 処理 })
```

　```timer```をloop処理で更新、```Timer class```の```scheduledTimer```メソッドを呼び出し```withTimeInterval```で何秒に一回処理をするか```repeats```で繰り返すか```block```でwithTimeIntervalでセットした秒ごとに処理するものを定義　してます。（block内のtimerは初めのtimerと別物です。命名がややこしかったですね)

　```.invalidate()```は必ず使用してください。timerが止まらなくなるようです。
　参考記事
　　- [【Swift】メモリー対策！使ったタイマーは必ずinvalidate()](https://qiita.com/shu26/items/8911d6c2b7a73e6142c1)

　これで0秒になった時に自動で画面遷移ができるようになりました。
　画面遷移で戻る時も同じように実装しました。

## 5. Soundの実装
　画面の色を変えることで視覚的に[作業⇄休憩]をユーザーに気づいてもらえるようになっていますが、音も出たほうがわかりやすいので実装することにしました。

　参考記事
　　- [【Swift】AVAudioPlayerの使い方。音楽や効果音を鳴らす。(Swift 2.1、XCode 7.2)](https://hajihaji-lemon.com/swift/avaudioplayer/)
　　- [AVFoundationで音を再生する](http://harumi.sakura.ne.jp/wordpress/2019/12/01/avfoundation%E3%81%A7%E9%9F%B3%E3%82%92%E5%86%8D%E7%94%9F%E3%81%99%E3%82%8B/)
　　- [公式ドキュメント](https://developer.apple.com/documentation/avfoundation)

　音源を用意しないといけないので著作権フリーの音源をいくつか試してみてカウントダウンの音を探しました。

　参考サイト
　　- [OtoLogic](https://otologic.jp/free/se/countdown01.html)
　　- [mixkit](https://mixkit.co/free-sound-effects/countdown/)
　　- [Storyblocks](https://www.storyblocks.com/audio/search/countdown)

　音源が決まったらProjectの直下に私の場合はわかりやすいようにMusicフォルダを作りそこへドラッグ＆ドロップしました。これだけでXcodeへの音源取り込みが完了です。
　※　拡張子に多少縛りがあるので取り込む前に確認してください。

　参考サイト
　　- [iOS・Android音楽ファイル対応表と音声・楽曲ファイル変換方法まとめ (ACC, MP3, WAV, etc)](http://b.ruyaka.com/2014/01/28/ios-android-afconvert-lame-acc-mp3-wav-etc/)

```swift
import AVFoundation // 必要です

class .... {

    var avPlayer: AVAudioPlayer! // 変数の宣言

    func fireSound() {
        let pathToSound = Bundle.main.path(forResource: "zihou", ofType: "mp3")!
        // 音源のfile名、file拡張子を宣言する。
        let url = URL(fileURLWithPath: pathToSound)
        // fileURLWithPathに先ほど宣言した情報を渡しURLにする。
        do {
            avPlayer = try AVAudioPlayer(contentsOf: url) // AVAudioPlayerにurlのデータを渡し再生できる状態にする。
            avPlayer.prepareToPlay() // prepareToPlayでバッファに一度音を読み込み再生する時の遅延を防ぐ。
            avPlayer.play() // 再生する
        } catch { // 読み込み時に何らかのerrorが出た時の処理。
            print(error)
        }
    }

```

　prepareToPlayがないと音がズレて2秒になった時に流れたりしたのでセットした方がいいと思います。

　私の選んだ音源は３秒前からカウントダウンが始まるものだったので先ほどのupdateTimer関数の中で

```swift
            if self.currentTime == 3 { self.fireSound() }
```

　を追加してcurrentTimeが３秒になったら音が流れるようにしました。

## 6. サイクル数の表示
　何回作業したのか視覚的に表示したかったのでシンプルにタイマー横に追加することにしました。

　top画面に戻ってきた時に初期化しないといけないこと、StudyView, BreakViewでも同じ数字を表示しないといけないことからtop画面にstatic領域として保存することにしました。（ほんとはModelを作って入れないといけないですね。。。この時はMVCを意識できていなかったです）

```swift:ViewController
  static var setCount = 1 // 変数宣言
```

　StudyView, BreakViewに表示用のLabelを作りsetCountをタイマー横に設置しました。

```swift

    let setLabel: UILabel = {
        let label = UILabel()
        label.text = " / 10"
        label.font = UIFont.boldSystemFont(ofSize: 30)
        return label
    }()
```

　休憩が終わったらカウントの更新する。
　タイマーが0秒になった時の処理に追加

```swift:BreakView
  ViewController.setCount += 1
```

　setCountが9になった状態でStudyViewのタイマーカウントが0秒になるとCongratsViewが出て終わりを告げるようにしました。（10セットもやると４時間やったことになるので作業を止めて欲しいので今回は10セットまでにしました）

```swift:StudyView
                if ViewController.setCount == 9 {
                    self.present(Congrats(), animated: true, completion: nil)
                    timer.invalidate()
                }
```

　CongratsViewにはボタンを遷移させる機能をつけなかったので強制的に終われるようにしました。

## 7. Mode Setting機能の実装
　25分では短すぎるという人もいるかもしれないので、50分/10分Modeをオプションで選べるようにします。

　またstatic領域を使用しtrue/falseでmodeを判断するようにしました。

```swift:ViewController
    static var is_50mins = false // falseを初期値（25分）
```
 - SettingView内でボタンを作りアクションとして片方を押すともう片方の色が元に戻り、現在のモードの状態がわかるようにすること
 - 25分を選ぶとis_50minsがfalse、50分を選ぶとtrueになること

　を実装しました。

```swift:SettingView
    @objc private func mode25() {
        ViewController.is_50mins = false
        mode_25Button.backgroundColor = colors.yellow
        mode_50Button.backgroundColor = colors.lightBlue
    }
    
    @objc private func mode50() {
        ViewController.is_50mins = true
        mode_25Button.backgroundColor = colors.lightBlue
        mode_50Button.backgroundColor = colors.yellow
    }
```

 そしてcurrentTimerの初期値をmodeによって変更できるように

```swift:StudyView
    var currentTime = ViewController.is_50mins ? 3000 : 1500;
    
    //if ViewController.is_50mins {
    //    currentTimer = 3000
    //} else {
    //    currentTimer = 1500
    //}
```

　と三項演算子で簡素的に書きました。（下に書いたif文と同じことをしてます）

## 実装完
　これで一区切りとして実装を終わりました。

　　- 書き方に無駄があったり
　　- いらない文がある
　　- もっといいやり方やアーキテクチャを意識すること

　など改善点が盛りだくさんですが、簡素なアプリでもそれなりに考えることがたくさんありいい勉強になりました。今後はアップデートでリファクタリングや機能追加、デザイン変更などしていこうと思います。

 GitHub: https://github.com/soysan/Pomodoro

　⇨　[-申請編-](https://qiita.com/soysan/items/c2632c5f39b6da686c51)