TextFieldから数字の場合だけ取り出してBMIを計算する方法-処理の切り分け、struct-

`TextField`から数字の場合だけを取り出して処理を行いたい時があると思います。
BMIと適正体重の計算を例に、その処理の書き方をいくつかまとめてみました。

<img width="350" alt="simulator_screenshot_0D5EF4E7-A499-4017-837A-77A787F10E78.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/843253/4685ba21-cab7-7b87-6738-250a51a5a467.png">

#目次
* 前提確認
* TextFieldから数字を取り出してみる
* 処理を切り出してみる
* 全体のコード
* まとめ


#前提確認
* MacOS Catalina 10.15.4  
* Xcode 12.1
* Swift version 5


今回計算するBMIと適正体重の求め方は次の通りです。
計算結果は小数点第3位で四捨五入し、Double型で表示することにします。

```swift:求め方

BMI = 体重kg * (身長m)^2
適正体重 = (身長m)^2/22

```

#TextFieldから数字を取り出してみる
`TextField`の値(=`TextField.text`)を数字として取り出すには3つのステップがあります。

①`TextField.text`が`nil`かどうかチェック、`nil`なら空文字を返す
②①の値をDouble型で取り出す
③②のnilチェック(もし`TextField.text`が空文字もしくは文字が入っていた場合、②は`nilと`なるため）

こうして取り出した値は初めて数字として計算することができます。
では、③を`guard`を使いつつ、コードを書いてみましょう。

```swift:ViewController

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak private var textField1: UITextField! //身長cmを記入
    @IBOutlet weak private var textField2: UITextField! //体重kgを記入

    @IBOutlet weak private var bmiOutputLabel: UILabel! //bmiを出力するLabel
    @IBOutlet weak private var standardWeightOutputLabel: UILabel! //適正体重を出力するLabel

    @IBAction func calculate(_ sender: Any) {

        let height = Double(textField1.text ?? "") //①textFeild.textがnilかチェックし、②Double型として身長を取り出す
        let weight = Double(textField2.text ?? "") //①textFeild.textがnilかチェックし、②Double型として体重を取り出す

        var bmi = Double()
        var standardWeight = Double()

        //③nilチェック、アンラップする
        guard let _height = height, let _weight = weight else {
            return //nilならここで処理を止める
        }

        //身長をcmからmに直す
        let meterHegiht = _height/100

        //それぞれ小数点第3位で四捨五入する
        bmi = round((_weight/(meterHegiht*meterHegiht))*100)/100
        standardWeight = round(((meterHegiht*meterHegiht)*22)*100)/100

        //Labelに表示
        bmiOutputLabel.text = "\(bmi)"
        standardWeightOutputLabel.text = "\(standardWeight)"
    }

}

```

`TextField`から数字を取り出す処理としては、こんな感じです。慣れないうちは面倒だなと思うかもしれません。
もちろんこれでも良いのですが、あとあと保守しやすいよう、この計算を更に別のクラスに切り出してみましょう。


#処理を切り出してみる

別クラスにこの処理を書いてみましょう。
イメージとして次のように書いてみたいです。

```swift:イメージ

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak private var textField1: UITextField! 
    @IBOutlet weak private var textField2: UITextField!

    @IBOutlet weak private var bmiOutputLabel: UILabel! 
    @IBOutlet weak private var standardWeightOutputLabel: UILabel! 

    private var calculate = Calculate()

    @IBAction func calculate(_ sender: Any) {

        calulate.calculate(inputHeight:textField1.text, inputWeight:textField2.text)    
    }

//計算の処理を別クラスに切り分ける
class Calculate{

    func calculate(inputHeight:String?, inputWeight:String?){

        let height = Double(input.height ?? "")
        let weight = Double(input.weight ?? "")

        guard let _height = height, let _weight = weight else {
            return
        }

        let meterHegiht = _height/100

        let bmi = round((_weight/(meterHegiht*meterHegiht))*100)/100
        let standardHeight = round((meterHegiht*meterHegiht)*22)*100/100

    }

}


```

ただ、これにはいくつか問題が含まれています。
一つ一つ見ていきましょう。

##①返したい値が2つ
`Calulate`クラスの`calculate`メソッドで返したい値は`BMI`と`適正体重`の2つです。
2つのDouble型を返のにはどうすれば良いでしょうか。

```swift:Calculateクラス
func calculate(inputHeight:String?, inputWeight:String?) -> //2つのDouble型？
```

そこで2つ以上の値を管理するのに便利な`struct`が使えます。

```swift

struct Output {
    let bmi : String
    let standardWeight :String
}
```

今回、出力する値も2つなら、入力する値も2つ(身長と体重)なので、こちらも`struct`を使ってみましょう。
ただ、こちらは`TextField`からの値が入り、`nil`になる可能性もあるため、オプショナル型とします。

```swift

struct Input {
    let height : String?
    let weight : String?
}
```
さて、上二つを使うと次のようにまとめられそうです。

```swift:

import UIKit

class ViewController: UIViewController {
(略)
}

struct Input {
    let height : String?
    let weight : String?
}

struct Output {
    let bmi : String
    let standardWeight :String
}

class Calculate{

    func calculate(input: Input) -> Output{

        let height = Double(input.height ?? "")
        let weight = Double(input.weight ?? "")

        guard let _height = height, let _weight = weight else {
            return
        }

        let meterHegiht = _height/100

        let bmi = round((_weight/(meterHegiht*meterHegiht))*100)/100
        let standardHeight = round((meterHegiht*meterHegiht)*22)*100/100

        return Output(bmi: "\(bmi)", standardWeight:"\( standardHeight)")
    }
}


```
`struct`を使用して入力値と出力値をまとめられて、スッキリしましたね。
ただ、実はこれだと`guard`の中身でエラーが発生します。
というのも、返り値として`Output`を設定しているのに、`return`だけだと何も返さないので怒られます。
次のようにすることもできますが、せっかくならもう少し汎用性のある方法で書いてみましょう。

```swift:
guard let _height = height, let _weight = weight else { //heightまたはweightがnilであれば次の値を返す  
    return Output(bmi: "誤入力", standardWeight: "誤入力") //今回たまたまOutputの中身がStringなのでこういう書き方ができる
}
```

##②ErrorTypeを定義
ということで、`guard`の条件をクリアしなかった場合はそれを`Error`として補足しましょう。
例の`ErrorType`プロトコルを適合させた`enum`でエラーパターンを定義します。この辺理解曖昧な方は[こちら](https://qiita.com/ShenQC/items/3def8c64b3617b13f106)の記事をご参照。

```swift
enum Validation: Error {
        case 正しく入力されていない(result: String)
    }
```

`guard`をクリアしなかった場合、このエラーバターンを`throw`することにします。

```swift
struct Input {
    let height : String?
    let weight : String?
}

struct Output {
    let bmi : String
    let standardWeight :String
}

class Calculate{

    enum Validation: Error {
        case 正しく入力されていない(result: String)
    }

    func calculate(input: Input) throws -> Output{

        let height = Double(input.height ?? "")
        let weight = Double(input.weight ?? "")

        guard let _height = height, let _weight = weight else {
           throw Validation.正しく入力されていない(result: "正しく入力されていません")
        }

        let meterHegiht = _height/100

        let bmi = round((_weight/(meterHegiht*meterHegiht))*100)/100
        let standardHeight = round((meterHegiht*meterHegiht)*22)*100/100

        return Output(bmi: "\(bmi)", standardWeight:"\( standardHeight)")
    }
}
```
これで切り分けた方の処理を完了です。
最後に`ViewController`内の処理を書きましょう。

##③do構文で仕上げ
今回エラーパターンを含む処理を書くので、例の`do構文`を使います。
と言っても非常にシンプルです。

```swift:ViewController

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak private var textField1: UITextField! 
    @IBOutlet weak private var textField2: UITextField!

    @IBOutlet weak private var bmiOutputLabel: UILabel! 
    @IBOutlet weak private var standardWeightOutputLabel: UILabel! 

    private var calculate = Calculate()

    @IBAction func calculate(_ sender: Any) {

        do {
            let Output = try calculate.calculate(input: Input(height: textField1.text, weight: textField2.text))
            bmiOutputLabel.text = "\(Output.bmi)"
            standardWeightOutputLabel.text = "\(Output.standardWeight)"
        }catch let Calculate.Validation.正しく入力されていない(result: msg){
            print(msg)
        }catch{
        }
    }
}

```

#全体のコード
まとめるとこうなります。

```swift:ViewController

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak private var textField1: UITextField! 
    @IBOutlet weak private var textField2: UITextField!

    @IBOutlet weak private var bmiOutputLabel: UILabel! 
    @IBOutlet weak private var standardWeightOutputLabel: UILabel! 

    private var calculate = Calculate()

    @IBAction func calculate(_ sender: Any) {

        do {
            let Output = try calculate.calculate(input: Input(height: textField1.text, weight: textField2.text))
            bmiOutputLabel.text = "\(Output.bmi)"
            standardWeightOutputLabel.text = "\(Output.standardWeight)"
        }catch let Calculate.Validation.正しく入力されていない(result: msg){
            print(msg)
        }catch{
        }
    }
}

struct Input {
    let height : String?
    let weight : String?
}

struct Output {
    let bmi : String
    let standardWeight :String
}

class Calculate{

    enum Validation: Error {
        case 正しく入力されていない(result: String)
    }

    func calculate(input: Input) throws -> Output{

        let height = Double(input.height ?? "")
        let weight = Double(input.weight ?? "")

        guard let _height = height, let _weight = weight else {
           throw Validation.正しく入力されていない(result: "正しく入力されていません")
        }

        let meterHegiht = _height/100

        let bmi = round((_weight/(meterHegiht*meterHegiht))*100)/100
        let standardHeight = round((meterHegiht*meterHegiht)*22)*100/100

        return Output(bmi: "\(bmi)", standardWeight:"\( standardHeight)")
    }
}

```

#まとめ
いかがでしたでしょうか。
最初のコードより少し長くなってしまいましたが、処理を切り分けられたの保守しやすくなったのではないでしょうか。
また、`struct`で値も一括管理しているので、可読性も良くなったと思います。

自分がこの処理を書くまでにで辿った思考を一つ一つ言語していきましたが、何かの参考になれば幸いです。
ご指摘等ございましたら頂けますと幸いです。