【Swift】iOSで放置型育成ゲームを作るよ(3) ~キャラクターをクラス化してViewControllerから分離する~

# 今回のゴール
- これからのことを考えてキャラクターをクラス化する
- ついでにステータスを表示してみる
- ViewControllerから生成の処理を剥がす

![status.gif](https://qiita-image-store.s3.amazonaws.com/0/115590/b1ebd262-3ef9-04c7-3fff-f78e84f6cda7.gif)


# 今回のキーワード
- クラス設計
- プロパティ
- initialize
- ステータス表現

# やったこと
## なんかちょっとリファクタしたくなった
- ViewControllerでやってる処理を整理したよ
    - せっかくだしオブジェクト指向でプログラミングしよう
    - キャラ用の画像を生成してUIImageViewに当て込んでTimerで定期的に動かしているけど、キャラ画像の生成とImageに当て込むのはキャラクターのクラス作ってそいつにもたせた方がよくね？
    - ついでにViewControllerにキャラをプロパティとしてもたせた方がよくね？

```ViewController.swift
        // まずは素材画像からUIImageを宣言する
        let image = UIImage(named: "sozai.png")
        
        // 素材のimageからgifのURLが生成できたかをnilチェックする
        if let url = image?.createGifURL() {
            // urlが生成されていたらimageViewのimageに代入
            imageView.image = UIImage.gif(url: url.absoluteString)
        }

これを

        // これをこんな感じにした方がよくね？
        
        // キャラクタークラスからインスタンスを生成
        let character = Character(init処理)
        
        // ViewControllerのプロパティに設定する
        self.character = character
        
        // キャラのimageをViewControllerのimageViewにセットする
        self.imageView.image = self.character.image

```
  - ということでキャラクター生成用のクラスを作るよ

## クラス設計とかしてみる
- まずはキャラクターがもつべき要素を考えるよ
    - 色々考えちゃうと複雑になってしまうのでとりあえず最低限だけ
    - キャラクターとは
        - **名前(name)をもっている**
        - **gif画像の情報を持っている**
        - 右向き・左向きのフラグを持っている
        - **攻撃力、防御力などのステータス**をもっている


って感じに考えてたけど、gif画像をどこにもたせてやればいいかすごく悩んだよ(というか今も悩んでるよ)

## imageの情報はどこでもつべきなんだ・・？？
1. CharacterクラスにUIImageViewをもたせる
    1. ViewControllerのViewDidLoad時にそのimageViewをaddSubViewする
2. ViewControllerにUIImageViewをもたせる
    1. ViewDidLoadでImageViewのimageにキャラクターのimageをセットする ちょうど上のコードで書いているような感じ

この二つのやり方があるかなと思ったよ
1のやり方だとmoveとstartTimerもCharacterクラスに移動させてしまえば、キャラのインスタンスが「動く」メソッドを自動的に実行してくれる形にできるかなと思ったけど、左向き・右向きの判定のためにCharacterクラスにwindowのframeを教えてあげなきゃいけなくてめんどくさそうだったので2のやり方を採用するよ(多分このへん将来的に壁にぶち当たる気がするけど、今は動くことを優先するよ)


## クラス設計を基にクラスを作成していくよ
- File → New Fileを選択して「Character.swift」というファイルを作成するよ
- こんな感じで書いていくよ

```Character.swift
import Foundation
import UIKit

final class Character {
    private let name: String   // 生成したら基本変更しない方針なのでletで宣言
    private let image: UIImage // 生成したら基本変更しない方針なのでletで宣言
    var isLeft: Bool = true {
        willSet(newValue) {
           imageView?.transform =  newValue ? CGAffineTransform(scaleX: 1, y: 1) : CGAffineTransform(scaleX: -1, y: 1)
        }
    }
    
    // 後々APIを追加した時に、レスポンスがnilだったりすることを考慮してFailable Initializerにする
    init?(name: String?, charaImage: UIImage?) {
        guard let name = name, let charaImage = charaImage, let gifURL = charaImage.createGifURL()?.absoluteString, let image = UIImage.gif(url: gifURL) else {
            print("Error: Faild to Initialize Character")
            return nil
        }
        
        self.name = name
        self.image = image
    }
}
```
## isLeftいる・・・？？
- ここまで書いてCharacterにisLeftのプロパティいらねんじゃねぇ・・？という疑問にぶちあたったよ
- そもそもCharacterがimageViewをもたないのでimageView.transformのあたりで当たり前にコンパイルエラーが出るよ
- てことはViewControllerで管理すればいいのかな・・？かな？
    - ViewのControllerだしとりあえずそれでいっか！の精神で先に進むよ
    - CharacterクラスのisLeftはとりあえず消すよ

## こうなった
- せっかく名前も設定したので表示用のUILabelをstoryboardにおいてIBOutletでつないで表示できるようにしたよ

```ViewController.swift
class ViewController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var name: UILabel!
    private var timer = Timer()
    
    private var valkyrie: Character?

    private var isLeft: Bool = true {
        willSet(newValue) {
            // 新しい値がtrueだったら正しい方向に、falseだったら反転させる
            if newValue {
                imageView.transform = CGAffineTransform(scaleX: 1 , y: 1)
            } else {
                imageView.transform = CGAffineTransform(scaleX: -1 , y: 1)
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // キャラクターのインスタンスを生成してViewControllerのプロパティに設定
        valkyrie = Character(name: "【戦乙女】ヴァルキリー", charaImage: UIImage(named: "sozai.png"))
        
        // imageViewのimageにキャラのimageを設定
        imageView.image = valkyrie?.image
        name.text = valkyrie?.image
        
    }
~~~省略~~~
```
出面は全然変化ないけど、とりあえずでけた！
![class.gif](https://qiita-image-store.s3.amazonaws.com/0/115590/7af6b86f-7653-d420-9455-2915eacc297c.gif)

## なんか寂しいのでステータスのパラメーター決めて表示しようとしたけど沼にハマる
- 名前だけだとなんか寂しいので仮でパラメーターを設定して表示しようと試みたよ

### パラメータ表現どうしましょ問題
- とりあえず攻撃力(Str)、防御力(Def)、運(Luc)を設定しよう

```Character.swift
final class Character {
    let name: String   // 生成したら基本変更しない方針なのでletで宣言
    let image: UIImage // 生成したら基本変更しない方針なのでletで宣言
    let str: Int       // 攻撃力
    let def: Int       // 防御力
    let luc: Int       // 運
~~~省略~~~
```
こんな感じで設定してもよかったんだけど、なんかもっとSwiftyに書きたくてenumとか使おうとしたよ

```Character.swift
final class Character {
    let name: String   // 生成したら基本変更しない方針なのでletで宣言
    let image: UIImage // 生成したら基本変更しない方針なのでletで宣言
    let status: Status
    
    // ステータスを表すenum
    enum Status {
        case Base(str: Int, def: Int, luc: Int)
        
        func all() -> (str: Int, def: Int, luc: Int) {
            switch self {
            case .Base(let str, let def, let luc):
                return (str, def, luc)
            }
        }
    }
    
    // 後々APIを追加した時に、レスポンスがnilだったりすることを考慮してFailable Initializerにする
    init?(name: String?, charaImage: UIImage?) {
        guard let name = name, let charaImage = charaImage, let gifURL = charaImage.createGifURL()?.absoluteString, let image = UIImage.gif(url: gifURL) else {
            print("Error: Faild to Initialize Character")
            return nil
        }
        
        self.name = name
        self.image = image
        self.status = Status.Base(str: 100, def: 100, luc: 100)
    }
}
```

うーん・・・ このステータス呼び出す側が使いにくいなあ・・・ これってどうなんだ・・？？
let str = character.status.all().str
そもそもステータス表現にenumは合ってない気がするぞ・・・

### タプルにするか
こうするとどうだろう

```Character.swift
final class Character {
    let name: String   // 生成したら基本変更しない方針なのでletで宣言
    let image: UIImage // 生成したら基本変更しない方針なのでletで宣言
    let status: (str: Int, def: Int, luc: Int)
    
    
    // 後々APIを追加した時に、レスポンスがnilだったりすることを考慮してFailable Initializerにする
    init?(name: String?, charaImage: UIImage?) {
        guard let name = name, let charaImage = charaImage, let gifURL = charaImage.createGifURL()?.absoluteString, let image = UIImage.gif(url: gifURL) else {
            print("Error: Faild to Initialize Character")
            return nil
        }
        
        self.name = name
        self.image = image
        self.status = (str: 100, def: 100, luc: 100)
    }
}
```
こうすれば呼び出し側は分かりやすい気がするよ
let str = character.status.str

でも後々ステータスの数だけTableViewのセル作りたいとかなったら困るだろうなあ・・・
タプルの要素数だけ返すことってできるんかなあ・・・
なおかつ育成ゲームだしステータス上がることを考えればgetter, setterあった方がいいんだろうなあ・・・

・・・あああああああああああ

まあいっか動くし　将来の自分が綺麗に解決してくれることを祈ります:pray:
とりあえず表示用のラベルを3つ用意してそれぞれのtextに設定するよ

```ViewController.swift
~~省略~~~
    @IBOutlet weak var strLabel: UILabel!
    @IBOutlet weak var defLabel: UILabel!    
    @IBOutlet weak var lucLabel: UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // キャラクターのインスタンスを生成してViewControllerのプロパティに設定
        valkyrie = Character(name: "【戦乙女】ヴァルキリー", charaImage: UIImage(named: "sozai.png"))
        
        // imageViewのimageにキャラのimageを設定
        imageView.image = valkyrie?.image
        name.text = valkyrie?.name
        name.adjustsFontSizeToFitWidth = true
        
        strLabel.text = "攻撃力: \(String(describing: valkyrie!.status.str))"
        defLabel.text = "防御力: \(String(describing: valkyrie!.status.def))"
        lucLabel.text = "天運: \(String(describing: valkyrie!.status.luc))"
    }
~~~省略~~~
```
それっぽくなってきたな・・・？:thinking:
![status.gif](https://qiita-image-store.s3.amazonaws.com/0/115590/b1ebd262-3ef9-04c7-3fff-f78e84f6cda7.gif)
(Forced unwrapしてるけど表示のためだけということでご愛嬌・・・)

## 結果
- CharacterのinitをFailable Initializerにする意味あるんかな・・・？？
    - エラーの時どうするか考えていかなければ・・・
- ステータス表示すると途端にそれっぽくなるな:thinking:
- さあ本格的にブログ案件になってきました
- 今日の作業ブランチはこちら
    - https://github.com/nasteng/Dejikore/tree/divide-character-from-vc

# 次回
[BGMと選択音を鳴らすよ](http://qiita.com/nasteng/items/4ffae3cf605175674105)

# 最後に
ご指摘・リファクタ大歓迎です:pray: