足し算ゲームのiPhoneアプリを作ってみた話 3. SwiftUIでのMVCとビジネスロジック

SwiftUIのViewは、画面表示をリフレッシュするときの挙動を記述するというものでした。しかし、その画面に表示される内容はどのように持てば良いのでしょうか。はるか昔から、MVC(Model View Control)という考え方がオブジェクト指向の世界には浸透しています。アプリケーションが本質的に処理するべきデータとその取扱(Model)と、画面表示についての詳細(View)と、UIからの入力を受け取ってModelを制御する(Controller)の3つに分けて考えてみようとするものです。Modelを変更したら、自動的にViewが変更されるような世界があったら素敵です。そして、SwiftUIはそんな素敵な世界を実現する方法の一つです。

ところで、今、私が「アプリケーションが本質的に処理するべきデータとその取扱」と書いたものは、一般的には「ビジネスロジック」と呼ばれます。ゲームを作っていても、お絵かきソフトを作っていても、ビジネスロジックという言い方をします。

Viewそのものは半永続的な状態を保持できません

Viewは、その画面レイアウトを計算するためにメモリ上に作成され、画面描画を完了すると破棄される動作をするそうです。そのため画面に表示するべき数字を覚えておく、などといったことはできません。しかし、これでは不便なので、SwiftUIはいくつかの仕組みを用意しています。@State, @Binding, @ObservedObject, @Publishedなどです。めちゃくちゃざっくり説明すると、これらの仕組みを使うことで、View(画面)が把握しておくべき状態についてのデータを、SwiftUIのフレームワークに管理してもらうことができるようになります。

「このデータ、あとから使うんで、ちょっと持ってて!」

と、SwiftUIのアプリケーションフレームワークにお願いすることができるわけですね。すると、SwiftUIのフレームワークは、値の変化をモニターし、必要に応じて画面の再描画などを指示してくれるようになります。めちゃくちゃ便利です。

SwiftUIにデータとViewの関係を管理してもらう

Appleの公式チュートリアルでは@Stateの説明からはじまり、データの管理をSwiftUIに任せることができるという利点を説明しています。これはもちろん間違いではないのですが、本当に大事なことはViewとデータの関係をSwiftUIに把握させ、管理してもらうことができるということだと思います。というのは、このデータ(Model)が、どの画面描画(View)に影響するのかというのを把握するのは、一般的に、とても大変な作業であるからです。

「この画面部品(たとえばテキスト表示)は、あのデータを使ってるからね。データが変わったら、この画面部品の表示内容も変えないといけないので、その辺、うまいこと管理しといてよ!」とお願いするわけです。

Appleの説明動画を拝見するかぎり、@Stateは画面の構成要素についての(見た目の制御に関係するような)情報を持つように意図されているように感じています。もう少し高度な、アプリケーションにとって本質的なデータとそのロジックを取り扱うときには、ObservableObject, @ObservedObject, @Published といった機能を使っていきます。

足し算ゲームのモデルオブジェクトについて

ようやく本題です。ざっくりと足し算ゲームが把握していなければいけないデータについて考えていこうと思います。

- 問題(足される数と足す数)
- ユーザが入力した答え

PythonでCLIの足し算ゲームを作ったときには、これだけを考えていれば良かったのですが、筆算ゲームではもう少し凝ったデータモデルが必要になります。順番に説明していこうと思います。

多桁の数から、それぞれの桁の数字を取り出す

ところで、ここで作成したいのは筆算による足し算ゲームです。問題と入力された答えのどちらも、一桁の数による、各桁の情報の形でやりとりする必要があります。複数桁の数から、ある桁の数字だけを取り出すにはどうしたらよいでしょうか。ここでは、一般的な手法として剰余(割り算の余り)を使うことで解決しようと思います。

たとえば、987という数があるとき、1の位の数字を抜き出すには10で割った余りを求めれば7を得るというのはご理解いただけると思います。

987 ÷ 10 = 98 あまり 7

10の位の数字を抜き出しだければ、まず10で割って(1の位の数字を消してしまってから)あらためて10で割った余りを求めればよいです。

987 ÷ 10 = 98 あまり 7
98 ÷ 10 = 9 あまり 8 

Swiftを始め多くのプログラミング言語で、% 記号を使って剰余を求めることができるようになっています。ですので、ある数nからmの位の数を抜き出すには、以下の式で実現できるということになります。

n/m%10

ですので、問題を構成する足す数と足される数の2つは、Pythonのときと同様、単純に2つの数として保持しておき、画面描画の際にはそれぞれの桁の数字を導き出すようにすればよいということになります。

ユーザの入力は必ずしも有効な数の体をなすわけではない

これが大人向けの実用アプリであれば、ユーザーから受け取る回答についても同様に一つの数として受け取ることを考えたかと思います。しかし、このアプリの目的は筆算のやり方を子供に教えるためのものなので、各桁の数字を個別に入力させるようにしたいのです。100の位から入力するのではなく、1の位から入力するべきだと思うのです。そこで、モデルオブジェクトとしては違和感が残るのですが、受け取った回答については一つの変数として管理するのではなく、それぞれの桁別に数字を管理するようにしようと思います。

桁別に別れた数を組み合わせて、もともとの数を作るというのは数学の授業でも良くでてきたパターンだと思います。xが100の位、yが10の位、zが1の位の数を表現しているなら、以下のような形でもともとの数を表現することができます。

100 * x + 10 y + z 
​

本質的にはこれだけのロジックで足し算ゲームを組み立てることができます。当初はこれをもとにプログラミングのロジックを息子くんに説明しようと思っていたのですが、以下の理由で断念しました。

- そもそも幼稚園児の息子くんに剰余の説明をするのが無理(今、彼は一生懸命九九を暗器してる最中なのです)
- 足し算についてのロジックよりも、繰り上がりを表現するための画面表示や、直感的な入力をしてもらうためのGUI周りのコーディングが複雑になってしまった

と、いうわけで、変数名などが整理されないままになっていますが、こんなオブジェクトになりました。

class Question :ObservableObject {
   @Published var a = Int.random(in: 10...99)
   @Published var b = Int.random(in: 10...99)
   @Published var c100 = ""
   @Published var c10 = ""
   @Published var c1 = ""
   @Published var focusedDigit = 1 // 1,10,100
   @Published var carry1 = false
   @Published var carry10 = false
   
   var a10 : String {
       String(a/10%10)
   }
   
   var a1 : String{
       String(a%10)
   }
   
   var b10 : String{
       String(b/10%10)
   }
   
   var b1 : String{
       String(b%10)
   }
   
   var correctAnswer : Bool {
       let ic100 = Int(c100) ?? 0
       let ic10 = Int(c10) ?? 0
       let ic1 = Int(c1) ?? 0
       return (a + b) == ic100*100 + ic10*10 + ic1
   }
   
   func renewQuestion() -> Void{
       a = Int.random(in: 10...99)
       b = Int.random(in: 10...99)
       c100 = ""
       c10 = ""
       c1 = ""
       focusedDigit = 1
       carry1 = false
       carry10 = false
   }
   
   func press( _ num : String ) -> Void
   {
       if focusedDigit == 1 {
           c1 = num;
           if num == "" {
               carry1 = false
           }
       }else if focusedDigit == 10 {
           c10 = num;
           if num == "" {
               carry10 = false
           }
       }else if focusedDigit == 100 {
           c100 = num;
       }
       
       if num != "", focusedDigit != 100 {
           focusedDigit *= 10
       }

   }
   
   
}

この程度のコード量で足し算ゲームのロジックとUIが制御できるので、SwiftUIでのプログラミングはとても有用かもしれません。

実は、本稿を起草したときには、これらの機能についてきちんと説明しようと思っていたのですが、あまりにも内容が大きくなるというか、普通に入門書として上梓できちゃいそうな内容になってしまうので、一旦断念することにしました。時間ができたら、あらためて起草してみようかと思っています。

以下、投げ銭です。本文はありません。

ここから先は

0字

¥ 100

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