この記事はフリューAdvent Calendar 2024の21日目の記事となります。
はじめに
私が所属するチーム向けにSwiftUI勉強会を開催することとなったため、資料を記事として投稿します。SwiftUI初学者なので間違いなどあれば、教えていただきたいです!Part1の記事はこちら
目次
事前準備
事前準備についてはPart1をご確認ください。今回は省略させていただきます。主に、Xcodeでのプロジェクト作成の手順を記載しています。
Part2のお品書き
ユーザー入力で用いる部品
ボタンやトグルスイッチ、文字入力のテキストフィールドなど、ユーザー入力で使用される部品はたくさんあります。その中から一部をピックアップして紹介します。
ボタン
ユーザーからの入力を受けてアクションする部品として、まずはボタンを使ってみましょう。早速ですが、以下のようにコードを書いてみてください。
import SwiftUI
struct ContentView: View {
var body: some View {
Button("Button") {
}
}
}
#Preview {
ContentView()
}
プレビュー画面を確認すると、デザインは簡素ですが画面中央にボタンが表示されます。
また、次のように、Buttonを日本語のボタンに変更してみましょう。
import SwiftUI
struct ContentView: View {
var body: some View {
Button("ボタン") {
}
}
}
#Preview {
ContentView()
}
プレビュー画面を確認すると、表示が日本語のボタンに変更されたことが確認できます。
ボタンは押すことができ(当然)、ボタンを押すことで何かしらのアクション(別の画面に遷移するなど)が発生します。続いて、ボタンをタップした時のアクションの実装方法について説明します。
先ほどの日本語のボタンで実装したコードを見てみましょう。察しのいい方は以下のコードの中で気になる箇所があるのではないでしょうか?
import SwiftUI
struct ContentView: View {
var body: some View {
Button("ボタン") {
}
}
}
#Preview {
ContentView()
}
Buttonの{}内にコードが何も記載されていませんね。ボタンを押した時のアクションを実装したい場合、{}内にコードを書いていくことで実現することができます。では、ボタンを押すと、Xcodeのデバックエリアにテキストを表示できるようにしてみましょう。デバックエリアに表示はprint文で実現できます(知ってるわ!って方もいらっしゃると思いますが、念の為書いておきます)。
import SwiftUI
struct ContentView: View {
var body: some View {
Button("ボタン") {
print("タップされたよ")
}
}
}
#Preview {
ContentView()
}
次の画像の順に操作してデバックエリアを確認すると、画像のようにデバックエリアにタップされたよと表示されることが確認できると思います。デバックエリアに表示するだけでは、ユーザーはボタンを押しても画面上では何も変化が起きないので、次の章からはボタンを押すと、画面が更新されるような処理を実装してみましょう。
ここからは、数字が1ずつ増えるカウントアップボタンを実装して、数字が1ずつ増える様子を見てみましょう。まずは、前述のコードに数字表示用のテキストを追加します。
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("1")
Button("カウントアップ") {
print("タップされたよ")
}
}
}
}
#Preview {
ContentView()
}
この状態でプレビュー画面を確認すると、下記のようにボタンの上に数字が表示されるようになりました。
ただ、このままではボタンを押してもタップされたよしか表示されない上に、数字をカウントアップさせることもできません。そこで、数字をカウントアップさせるために、変数を宣言しましょう。
Swiftで変数を宣言したい場合はvar 変数名: 型 = ~~と宣言します。本来、その変数が何を表しているか理解できる命名にすべきですが、今回は特に問いません。ただ、型は数値を扱うのでInt型で宣言しましょう(これもprint文の時と同じく、知ってるわ!かもしれませんが念の為です)。そして、宣言した変数をテキストで表示させるようにしましょう。テキストはString型でInt型の変数numをそのまま中に入れることはできないので、下記のように対応します。
import SwiftUI
struct ContentView: View {
var num: Int = 0
var body: some View {
VStack {
Text("\(num)")
Button("カウントアップ") {
print("タップされたよ")
}
}
}
}
#Preview {
ContentView()
}
この状態でプレビュー画面を確認すると下記のように表示されると思います。これで、カウントアップした数値を表示させるための変数numを用いて、テキストに表示させることができました。
次はボタンをタップすると数字が1増える処理を記載します。具体的にはボタンをタップすると、変数numの値を1増やせば良いので以下のようにコードを記載すればよさそうですね。
import SwiftUI
struct ContentView: View {
var num: Int = 0
var body: some View {
VStack {
Text("\(num)")
Button("カウントアップ") {
num += 1
}
}
}
}
#Preview {
ContentView()
}
と、思うじゃないですか。上記の通りコードを実装すると、下記のようにXcode上ではエラーが表示されてしまいます。
エラーメッセージ
Left side of mutating operator isn't mutable: 'self' is immutable
直訳:変更演算子の左側は変更可能ではありません: 'self' は不変です
Swiftでは変数・定数の宣言としてvarで宣言する方法と、letで宣言する方法が基本的です。varで宣言した変数は何度でも再代入が可能ですが、letの場合は一度代入したものを後から別のものを代入して変更することはできません。
これを踏まえてもう一度エラーメッセージを確認してみましょう。
おい!numはvarで宣言しているのに変更できないってどういうことや!ってなるかもしれませんね。
このようなエラーメッセージが表示される原因は構造体structにあります。構造体struct内で宣言されたプロパティ(上記のサンプルコードではnum)は変更することができません。そのため、先述のエラーメッセージが表示されてしまいます。
じゃあどうやって値を更新すればいいんだ?になりますが、これを解決する方法として@Stateをつけるという方法があります。@Stateはプロパティの値の変更を監視し、値が変更されたときにViewが再描画されます。@Stateを付与することで上記のエラーメッセージも表示されなくなります。
では、変数numを@Stateを付与した変数に変更し、プレビュー画面のカウントアップボタンを押して値が1ずつ増加するか確認してみましょう。
import SwiftUI
struct ContentView: View {
@State var num: Int = 0
var body: some View {
VStack {
Text("\(num)")
Button("カウントアップ") {
num += 1
}
}
}
}
#Preview {
ContentView()
}
カウントアップボタンを押すと数字が1ずつ増えるようになりましたね。画面の更新処理の実装方法の説明は以上となります。
トグルスイッチ
トグルスイッチはON/OFFを切り替えるボタンです。iPhoneの設定画面でよく見るアレです。
では、早速ですがトグルスイッチの実装をしてみましょう。以下のようにコードを実装してみてください。
import SwiftUI
struct ContentView: View {
@State var flag = true
var body: some View {
Toggle(isOn: flag) {
}
}
}
#Preview {
ContentView()
}
トグルスイッチはユーザーからの入力を受けてON/OFFが切り替わらないといけないため、@Stateをつけた変数flagを用意し、Toggle()の引数isOnに変数flagを指定してあげます。これでプレビュー画面を確認してみましょう。
と、行きたいところなのですが(またかよ)、上記の通りコードを実装すると、下記のようにXcode上ではエラーが表示されてしまいます。
エラーメッセージ
Cannot convert value 'flag' of type 'Bool' to expected type 'Binding<Bool>', use wrapper instead
直訳:型 'Bool' の値 'flag' を予期された型 'Binding' に変換できません。代わりにラッパーを使用してください`
変数flagはBool型であるのに対し、Toggle()の引数isOnに指定できる変数の型はBinding<Bool>型です。そのため、このエラーメッセージは「トグルが受け付ける引数のパラメータはBool型ではなく、Binding<Bool>という型ですよー」という意味になります。
では、どう修正すれば良いかなのですが、エラーメッセージの最後にInsert '$'とFixボタンが表示されていますね。一旦、Fixボタンを押してみましょう。
すると、flagの前に$が挿入され、エラーメッセージも表示されなくなりました。Bindingについての説明は省きます(私も知らないので勉強します)が、Binding<型>の引数にパラメータを指定する場合は$をつけないといけないということみたいですね。
では、これでプレビュー画面を確認してみましょう。
すると、画面右端にトグルスイッチが表示されていることが確認できます。続いて、iPhoneの設定画面のように表示させてみましょう。Formを用いて実装してみます。
import SwiftUI
struct ContentView: View {
@State var flag = true
var body: some View {
Form {
Toggle(isOn: $flag) {
Text("Wi-Fi")
}
}
}
}
#Preview {
ContentView()
}
プレビュー画面を確認すると、iPhoneの設定画面のようにトグルスイッチとテキストが表示されていることが確認できると思います。
では、続いてトグルスイッチのON/OFFでテキストの表示を切り替えられるようにしてみましょう。以下のようにコードを実装してください。
import SwiftUI
struct ContentView: View {
@State var flag = true
var body: some View {
Form {
Toggle(isOn: $flag) {
Text(flag ? "ON" : "OFF")
}
}
}
}
#Preview {
ContentView()
}
flag ? "ON" : "OFF"について少しだけ解説をします。flag ? "ON" : "OFF"の形は三項演算子と呼ばれ論理式 ? 論理式がtrueの時の値 : 論理式がfalseの時の値という形になっています。上記のコードの場合、変数flagがtrueの場合は文字列のONが返却され、falseの場合は文字列のOFFが返却されます。このように実装することでトグルスイッチのON/OFFに合わせてテキストを更新することができます。
それでは、プレビュー画面でトグルスイッチのON/OFFを切り替えて文字が切り替わるか確認してみましょう。
ON/OFFそれぞれで文字が切り替わっていることが確認できました。
テキストフィールド
テキストフィールドはユーザーが自由に文字入力することができるUI部品です。テキストフィールドは文字列を扱うので入力で使用できるのはString型です。早速ですが、テキストフィールドを使ってみましょう。
import SwiftUI
struct ContentView: View {
@State var text = ""
var body: some View {
TextField("入力してください", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 200)
}
}
#Preview {
ContentView()
}
TextFieldの引数として@Stateで宣言されたtextを指定していますが、この中にユーザーから入力された文字が入ります。また、この引数はBinding<String>という型であるため、$textと指定してください。前述のトグルスイッチの章で紹介したエラーと同様のエラーが表示されてしまいます。
また、.textFieldStyle()というモディファイアがありますが、この引数にRoundedBorderTextFieldStyle()を指定してあげることでテキストフィールドに角丸の枠線が表示されます。
では、プレビュー画面を確認してみましょう。
少し確認しづらいですが、テキストフィールド画面中央に表示され、文字入力ができるようになっていること確認できると思います。また、第一引数として指定した"入力してください"がプレースホルダ(未入力時、テキストフィールド内に表示されるテキスト)として表示されていることも確認できますね。
続いて、入力した文字をテキストとして表示させてみましょう。表示させる用のテキストをテキストフィールドの上に実装します。
import SwiftUI
struct ContentView: View {
@State var text = ""
var body: some View {
VStack {
if !text.isEmpty {
Text(text)
}
TextField("入力してください", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 200)
}
}
}
#Preview {
ContentView()
}
テキストフィールドの上にif文を実装しています。text.isEmptyはtextの中身が空である時にtrueを返す論理式で、その前に!をつけることで空でない時にtrueを返すようにします。こうすることで文字が入力されていないときにテキストは表示されず、文字が入力されると入力された文字が表示されるようになります。
では、シミュレータを起動してテキストフィールドに文字を入力してみましょう。
文字を入力するとテキストフィールドの上部に入力された文字が表示されていますね。ちなみにですがシミュレータでキーボードを表示させて文字を入力したい場合、シミュレータメニューのI/O > Keyboard > Toggle Software Keyboardを選択してください。
テキストフィールドには文字だけではなく数値を入力する場面もあります。そこで次は数値を入力できるようにしてみましょう。先ほどのコードにモディファイアを1つ追加してください。
import SwiftUI
struct ContentView3: View {
@State var text = ""
var body: some View {
VStack {
if !text.isEmpty {
Text(text)
}
TextField("入力してください", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numberPad) // 追加
.frame(width: 200)
}
}
}
#Preview {
ContentView3()
}
追加したモディファイアは.keyboardType(.numberPad)です。キーボードの種類を指定するモディファイアで、.numberPadとすることで0〜9の数字のみのキーボードを表示させることができます。
では、シミュレータを起動して数字を入力してみましょう。
これで数字の入力ができるようになりましたね。本来は数値以外の文字や指定範囲外の数値が入力された場合などに対応する必要がありますが、今回は省略します。
セキュアフィールド
セキュアフィールドはテキストフィールドと似ていますが、テキストフィールドと異なるのは入力された文字が隠れる仕様になっている点です。よくアプリのアカウントログイン時のパスワード入力欄で使用されています。
セキュアフィールドの実装方法はテキストフィールドとかなり似ています。早速ですが、セキュアフィールドを使ってパスワード入力欄を作成し、入力されたパスワードをデバックエリアに表示してみましょう。
import SwiftUI
struct ContentView: View {
@State var text = ""
var body: some View {
SecureField("パスワード入力", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 200)
.onSubmit {
print(text)
text = ""
}
}
}
#Preview {
ContentView()
}
ここで、あまり見慣れないのが.onSubmit{}だと思います。これはキーボード上のreturnキーが押された後に{}内の処理を実行してくれます。セキュアフィールドで入力された文字を変数textで受け取り、returnキーが押されると.onSubmit{}内のprint文でパスワードを表示し、セキュアフィールド内に入力されたテキストを空文字にしています。
では、シミュレータを起動してパスワードを入力してみましょう。
パスワードを入力してキーボードのreturnキーを押すと、
XCodeのデバックエリアに入力したパスワードを表示させることができました。
続いて、パスワード入力時によく見る「パスワードは◯文字以上で入力してください」の注意文言をテキストとして表示させてみましょう。今回はパスワードを6文字以上入力してもらうように設定してみます。
import SwiftUI
struct ContentView: View {
@State var text = ""
var body: some View {
VStack {
SecureField("パスワードを入力", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 200)
.onSubmit {
print(text)
text = ""
}
Text(text.count < 6 ? "6文字以上で入力してください" : "")
.font(.system(size: 14, weight: .bold, design: .default))
.foregroundStyle(.red)
}
}
}
#Preview {
ContentView()
}
セキュアフィールドの下に注意文言用のテキストを追加しました。Textの中には三項演算子を記載しており、text.countは文字数の数を表しています。そのため、文字数が6未満の場合、6文字以上で入力してくださいを表示させ、6以上になった場合は注意文言を表示しない(空文字)にしています。 .font(.system(size: 14, weight: .bold, design: .default))でフォントの設定をしています。各種設定は以下のとおりです。
| 引数 | 説明 | 今回の設定 |
|---|---|---|
| size | フォントサイズ | 14 |
| weight | フォントの太さ | .bold |
| design | フォントタイプ(明朝体等) | .default |
では、シミュレータを確認してパスワードを入力してみましょう。
パスワード未入力時と6文字未満のときは注意文言が表示され、
6文字以上入力すると注意文言が表示されなくなりました。次節から課題を掲載しているので取り組んでみてください。
課題
課題のサンプルコードを記載していますが、サンプルコードと完全一致していなくても他の方法でレイアウトを作成できていればOKです。
Level1
次のような画面を作成してください。また、以下のように各要素を設定してください。
- 各種設定
-
トグルスイッチ
- 最初はOFF
- 幅:
150 - 高さ:指定なし
-
マイクと表示されるテキストを追加する(ラベルが追加できるトグルで実装)- フォント設定
- size:
25 - weight:
bold - design:
default
- size:
- フォント設定
-
マイクの画像
- ON/OFFで画像を切り替えられるようにする
- 画像は
Image()で表示できる - ONの画像:
mic.fill - OFFの画像:
mic.slash.fill
- 画像は
- 画像設定
- サイズをフレームサイズに合わせる(モディファイア)
- 縦横比を維持(モディファイア)
- サイズ:
60x100
- ON/OFFで画像を切り替えられるようにする
サンプルコード
import SwiftUI
struct ContentView: View {
@State var flag = false
var body: some View {
VStack {
Toggle(isOn: $flag, label: {
Text("マイク")
.font(.system(size: 25, weight: .bold, design: .default))
})
.frame(width: 150)
Image(systemName: flag ? "mic.fill" : "mic.slash.fill")
.resizable()
.scaledToFit()
.frame(width: 60, height: 100)
}
}
}
#Preview {
ContentView()
}
Level2
次のようなログイン画面を作成してください。画面レイアウトの各種設定に加え、下記条件を満たしてください。
-
各種設定
- 縦に「ログイン画面」のテキスト、ID入力欄(テキストフィールド)、Password入力欄(セキュアフィールド)、ログインボタンを並べる
- 各UI部品同士は縦方向に
20のスペースを設けること
-
「ログイン画面」
- サイズ:指定なし
- フォント設定
- size:
30 - weight:
bold - design:
default
- size:
-
ID入力欄(テキストフィールド)
- サイズ:
300x50 - 角丸の枠線をつける
- プレースホルダは
ID
- サイズ:
-
Password入力欄(セキュアフィールド)
- サイズ:
300x50 - 角丸の枠線をつける
- プレースホルダは
パスワード
- サイズ:
-
ログインボタン
- ラベルを指定する(ラベルを指定できるボタンで実装する)
- ラベル設定
- 「ログイン」テキスト
- サイズ:
150x50 - 文字色:白
- 背景色:青
- 形は
カプセル
- ラベル設定
- ボタンをタップすると「ログイン成功」か「ログイン失敗」が表示される
- IDとPasswordを確認して成功か失敗を表示させる関数を実装すると良いかも
- ラベルを指定する(ラベルを指定できるボタンで実装する)
-
条件
- 「ログイン成功」と「ログイン失敗」をデバックエリアに表示させること
- 成功:下記IDとPasswordでログインボタンをタップ
- ID:
hoge - Password:
hoge1234
- ID:
- 失敗:上記ID、Password以外、もしくは未入力でログインボタンをタップ
- 成功:下記IDとPasswordでログインボタンをタップ
- 「ログイン成功」と「ログイン失敗」をデバックエリアに表示させること
サンプルコード
import SwiftUI
struct ContentView: View {
@State var id = ""
@State var password = ""
private let myId = "hoge"
private let myPassword = "hoge1234"
var body: some View {
VStack(spacing: 20) {
Text("ログイン画面")
.font(.system(size: 30, weight: .bold, design: .default))
TextField("ID", text: $id)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 300, height: 50)
SecureField("パスワード", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 300, height: 50)
Button(action: {
canLogin(id: id, password: password)
}, label: {
Text("ログイン")
.frame(width: 150, height: 50)
.foregroundStyle(.white)
.background(.blue)
.clipShape(Capsule())
})
}
}
private func canLogin(id: String, password: String) {
if id == myId && password == myPassword {
print("ログイン成功")
} else {
print("ログイン失敗")
}
}
}
#Preview {
ContentView()
}
Level3
次のような計算アプリを作成してください。
- 各種設定
- 「計算アプリ」、計算式作成欄、演算子入力ボタン一覧、「計算する」ボタンはそれぞれのUI部品同士の間に縦方向
30のスペースを設けること
-
「計算アプリ」
- サイズ:指定なし
- フォント設定
- size:
30 - weight:
bold - design:
default
- size:
-
計算式作成欄
- 入力欄、演算子表示用テキスト、入力欄、計算結果表示用テキストの順に横方向へ並べる(部品同士のスペースは横方向に
30) - 入力欄(テキストフィールド)×2
- サイズ:
70x50 - キーボードは数字を入力できるようにする
- 角丸の枠線をつける
- プレースホルダは
左辺と右辺
- サイズ:
- 演算子表示用テキスト
- サイズ:指定なし
- 演算子(足し算、引き算、掛け算のみ)を表示する
- 計算結果表示用テキスト
- サイズ:
70x50 - 表示させるテキストは
=計算結果
- サイズ:
- 入力欄、演算子表示用テキスト、入力欄、計算結果表示用テキストの順に横方向へ並べる(部品同士のスペースは横方向に
-
演算子入力ボタン一覧
- 足し算、引き算、掛け算ができるようにする
- ボタンを押すと演算子表示用テキストに演算子が表示される
- サイズやモディファイアでの設定は特になし
-
「計算する」ボタン
- ボタンを押すと計算結果表示用テキストに計算結果が表示される
- このボタンを押した時に計算用の関数を呼び出すと良いかも
- 入力された演算子によって計算を変える必要がありそう
-
switch文を使って計算を切り替えると良さそう
- サイズやモディファイアでの設定は特になし
サンプルコード
import SwiftUI
struct ContentView: View {
@State var num1 = 0
@State var num2 = 0
@State var operation = ""
@State var result = 0
var body: some View {
VStack(spacing: 30) {
Text("計算アプリ")
.font(.system(size: 30, weight: .black, design: .default))
HStack(spacing: 30) {
TextField("左辺", value: $num1, format: .number)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 70, height: 50)
.keyboardType(.numberPad)
Text(operation)
TextField("右辺", value: $num2, format: .number)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 70, height: 50)
.keyboardType(.numberPad)
Text("= \(result)")
.frame(width: 70, height: 50)
.multilineTextAlignment(.center)
}
HStack(spacing: 30) {
Button("+") {
operation = "+"
}
Button("-"){
operation = "-"
}
Button("×") {
operation = "×"
}
}
Button("計算する") {
result = calculation(num1: num1, num2: num2, operation: operation)
}
}
}
private func calculation(num1: Int, num2: Int, operation: String) -> Int {
switch operation {
case "+":
return num1 + num2
case "-":
return num1 - num2
case "×":
return num1 * num2
default:
return 0
}
}
}
#Preview {
ContentView()
}
まとめ
Part2の勉強会資料は以上となります。Part1の記事を作成した時期と比較して、SwiftUIの理解はより深まってきたと思います。この勉強会資料作成を通して、SwiftUI初学者を脱却したいなと思っています。次回作(Part3)をお楽しみに!











