[第一回] PlaygroundでSwiftUIの仕様を学ぶ

Swift

Playgroundとは

Swift Playgrounds
Swift Playgrounds is an app for iPad that teaches you to write Swift code in a fun, interactive way. Program robots and drones directly from the app.

通常、Appleのアプリの開発言語であるSwiftはMacでしかコンパイルできず、Windowsでコンパイルしようとするとハードルが高くてなかなか手が出しづらいものでした。

PlaygroundはMacだけでなくiPad上でも簡易的にSwiftのコードを実行することができる仕組みを提供するものです。

コピペして値を少し変えるだけで自作プログラムが動くところがリアルタイムで見るのでかなり面白いです。今回はプログラム初学者向けに(最低限の知識はある前提で)、自分がSwiftプログラミングでハマったところや便利なところを紹介していこうと思います。

変数は初期化しよう

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    private var name: String
    var body: some View {
        Text("Player name is \(name)")
    }
}

PlaygroundPage.current.setLiveView(ContentView())

上のコードはどう見ても動作しないコードである。というのも、String型でnameが定義されているにも関わらずどこでも値が代入されていないためである。

なんにもデータが入っていないのにそのデータを表示するのは不可能です。

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    private var name: String
    var body: some View {
        Text("Player name is tkgling")
    }
}

PlaygroundPage.current.setLiveView(ContentView())

無の変数を参照しない上のコードもやはり動作しない。Swiftは型を宣言したのであればそれがオプショナル(変数名の最後に?を付ける)でないのであれば必ず初期化を行わなければいけない。

オプショナルを理解しよう

オプショナルの場合はコンパイラが自動的にnil(C言語で言うところのnullのようなもの)で初期化してくれているようなものです。

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    private var name: String?
    var body: some View {
        Text("Player name is tkgling")
    }
}

PlaygroundPage.current.setLiveView(ContentView())

このようにオプショナルであればちゃんと実行できる。ただこれは宣言した変数を使っていないので意味がないと言えるが…

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    private var name: String? = "tkgling"
    var body: some View {
        Text("Player name is tkgling")
    }
}

PlaygroundPage.current.setLiveView(ContentView())

オプショナルに代入するの場合は、普通の型と全く同じように扱える。それはオプショナルが通常の型に比べて「nilも許容する」というより大きな集合だからである。

具体的に言えば「犬(整数型)」と「動物(オプショナル整数型)」で考えると良いだろう。

この場合「犬」を動物病院につれていくのは全く問題ない。それが「犬」であることはわかっているのだから「動物であるか」を確認する必要もない。なのでコンパイラはエラーを出さない。

ところが逆はそうはいかない。「動物」をそのまま「ドッグラン」に参加させることはできないことを考えればわかるだろう。型を宣言しているのでそれが「動物」であることは間違いないのだが、その「動物」が「犬」であるかどうかを保証していない。確認せずに代入して、実は「猫」でしたでは困るのである。

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    private var name: String? = "tkgling"
    var body: some View {
        Text("Player name is \(name)")
    }
}

PlaygroundPage.current.setLiveView(ContentView())

よって、一見するとうまくいきそうな上のコードは正しく動作しない、何故か?

Text("Player name is \(name)")こそがまさにすべての原因である。Text()は引数として文字列であるString型しか受け付けていない。そして"Player name is"は間違いなく文字列型なので問題ない。

問題となるのは\(name)の箇所である。これは「変数の中身を文字列として展開せよ」という書き方である。わかりやすく言えば、以下の二つのコードは全く同じ意味を持つ。

let number: Int = 12345
Text("Number is" + String(number))

let number: Int = 12345
Text("Number is \(number)")

ここで大事なのは文字列型にオプショナル文字列型を混ぜて(結合して)Text()に渡してしまっているということである。これはまたわかりやすく例えると「全員女性である」ことが確認できているグループに「人間」であることが確認できている人を加えて新たなグループをつくり、そのグループで女子会を開こうとするくらい無謀なことをしているのである。

つまり、ここでコンパイラは「女性であることが確認できていないのでグループには加えられません」とエラーを返すのである。

オプショナルのアンラップ

しかしながら「確認できていないから加えられません」では困るのがプログラマである。もちろんSwiftには加えようとしている「人間」が「女性」であることを確かめるためのコーディング方法が存在する。

ええい、やってしまえ!!

let name: String? = "tkgling"
let message: String = "Player name is " + name!
Text(message)

オプショナル変数および関数に対して!を付けるのは「強制アンラップ」とおぼえておくと良い。

これをさっきの例でいうなら(エラーの可能性を無視して)「女性かどうかを確認せずに加えてみました」という感じになる。すっごい無責任な感じがするが、実際こんな感じである。確認自体を行っていないので「グループに加える時点(上のコードだと二行目)」ではコンパイルエラーが発生しない。

ただ、中身が例外(男性)だった場合には「加えられないものを加えようとする」ことをしているので実行時にエラーが発生する。

コンパイルエラー実行時エラー
発生しない発生時、クラッシュ

やってみようかな??

それに対して「女性かどうかを確かめてみます」という処理を行うのが??を用いたコーディングである。

let name: String? = "tkgling"
let message: String = "Player name is " + (name ?? "tkgling")
Text(message)

こちらは女子会の例でいうならば「もし女性じゃなかったらこの方を変わりに代役にします」という保険付きの処理に該当する。もちろんこの保険付きの代役は必ず「女性」であることを確認していなければいけない。

確認しないままであればコンパイルエラーが発生する。

コンパイルエラー実行時エラー
発生する発生しない

その代わり、実行時には必ず保険が用意されているのでアプリがクラッシュするようなことは発生しない。

上手い扱い方

クラッシュしないので精神面的には嬉しい??だが、オプショナルかもしれない値を何度も使う場合にはその都度代役の情報を書かなければならず面倒である。

ここでは「最初にオプショナル型で宣言しておき、入力された名前を表示する」といったプログラムを想定しよう。名前はちゃんと入力されていることが望ましいが、変なことをするユーザが名前を入力せず(nilが代入されるとする)にプログラムを進めてしまう可能性も考慮すべきである。

なので入力(変数名input)はnilを受け付けるようにオプショナル型にすべきである。それらのデータを表示するときに先ほど説明したようにその都度??でオプショナルをアンラップするのは何度も同じことを書いてめんどくさいので以下のようにコーディングしたとしよう。

let input: String?
// Processing
let name = input ?? "tkgling"
let message: String = "Player name is " + name
Text(message)

さて、これ自体はちゃんと動作しクラッシュもしないプログラムだが、果たしてこれは意味のあるコードだろうか?なぜなら、ユーザ名として何も入力されなかった時点で「ユーザ名を表示する」という機能自体が全く意味をなしていないためだ。

??を使うことでアプリのクラッシュ自体は防ぐことができるが、クラッシュしなかったとして代役の値を表示することに何の意味があるだろう?

例えば、もしこのユーザ名入力のあとで重い処理があった場合を考えてみよう。??を使ってアンラップしてしまうとプログラムはクラッシュしないがユーザが意図した値とは違うデータで処理を無意味に続けてしまうのだ。当然、返ってくるデータはユーザが想定したものとは異なっている。これでは意味がないのだ。

要するに、我々がプログラムに求める動作というのは「入力された名前を表示する」「空っぽだったらすぐにエラーが起きたことを通知する」ということなのである。

ただし、!による強制アンラップは使えない。これをするとユーザの入力が誤った時点でアプリがクラッシュしてしまう。アプリをクラッシュさせず、おかしな値が入力されたことをすぐに検知する仕組みが構文である。

guard文

guardとは??を発展させたようなコーディングで「チェックの際の代役は用意していないがアプリはクラッシュしない」といういいことづくめの記法である。

let input: String? = "tkgling"
guard let name = input else { return }
let message: String = "Player name is " + name
Text(message)

さて、骨子となるのが二行目でこれはもはやテンプレとして覚えておいたほうがよい。今回はreturnを使っているがthrowも使うことができる。

guard let variable = optional else { return }
guard let variable = optional else { throw }

自分がこうやって書く場合が多いので一行で書いているが、最終的にreturnthrowのどちらかが実行されればよいので、else節の中で処理を書くこともできる。

returnthrow
処理をせず戻るエラーが起きたことを通知する

プログラミング初学者のうちはとりあえず何も考えずにreturnを使っておけばよいだろう。というのも、returnであればただこれを書くだけだが、throwでエラーを返す場合にはエラーを受け取って処理するメカニズムを整えないといけないためだ。

ただ、returnを使う場合に注意しておかなければいけないのは「エラー通知を返さない」「アプリがクラッシュしない」ということである。

コンパイルエラーはguard文の書き方を間違えればでるものの、慣れれば間違えることがないので「コンパイルエラーも発生しない」ものと考えてよい。ナイナイ尽くしでいいことのように思えるが、デバッグの観点からみればこれは最悪のケースとも言えます。

なぜなら、想定通りに動かなかった場合に「どこの処理を間違えて正しく動作していないのか」が全くわからないからです。先ほどの例でいえば、コーディングミスで常にinputにnilが代入されているような場合、プログラムはguard文のおかげで常に「安全に」終了し「処理は何もされず」「エラー通知もない」という状況になってしまうのです。

なので真面目にコーディングするのであればreturnだけでなくthrowも使わねばなりません。

まとめ

なんだかオプショナル型の説明ばっかりになってしまいましたが、割と真面目にここが一番最初に引っかかるところだと思うのでしっかりと説明付きで書きました。

かなりわかりやすく書いたつもりですが、よーわからんっていう人がいたらコメントで連絡ください。

次はSwiftUIでのStateとBindingについて(自分で学びながら)まとめレポートを書こうと思います。

記事は以上。

コメント

タイトルとURLをコピーしました