はじめに
筆者はwebアプリケーションフレームワーク(Rails)しか触ったことがなく、「俺はこのままでいいのか?」と思って今回iOSアプリに挑戦してみました。今までいろいろwebアプリを個人的に作ってきましたが、友達に見せたい時、いちいち「このURLにアクセスしてみて」と言わないといけないし、真っ先に「AppStoreにないの?」と言われてしまう。「このアプリ使ってみて!」とAppStore見せられるほうがカッコいい。
今後デバイスはなんであれApple製にインストールできるアプリの需要は確実にあるはずだから、学んでおいて損はないだろう!と思い今回挑戦しました。
最初に思ったこと
- iOSアプリのライフサイクル(webページだったらリロードすれば更新されるけど...)
- データベースってどうなるんだ?(MySQLとかリレーショナルデータベース使えるの?)
- デザインどうするの?
- 静的型付け言語か...初めてだな(RubyとかJSしかやってこなかったしC#は理解する前にやめた)
- VScode使えるのかな?(Xcodeじゃないとだめ?)
- デバッグってどうやるんだ?(ターミナルから?Xcodeから?)
と、このレベルです。
わかったこと
1. VScodeの出番はない
VScodeに色々いれたプラグインが使えないのか...
以下しばらく文法の基礎をメモしただけです
わかる人は読み飛ばしてください。
2. 変数と定数
var
:変数
let
:定数(JSだとどっちも変数なのに...)
3. 型について
特に明示的にしない場合、よしなに型を定義してくれる
じゃあこうやったらどうなるんだろう
ちゃんとエラーが出るんですね。
型を変換したいときはどうするんだろう
Stirng()
で囲えばいいんですね。ふむふむ、じゃあ逆はどうだろう
だめなんですね。
Int(bar)がnil(変換できなかった場合)を返す可能性があるためです。Swiftでは、Int(bar)の戻り値がオプショナル型であるため、nilの可能性を考慮する必要があります。
オプショナル型とはなんだろう...。
4.オプショナル型とは
値が存在しない可能性があることを表現するための型なんですね。
Swiftでは、通常の型(整数、文字列など)が必ず値を持つ一方で、オプショナル型は値が存在しないことを示すことができるということか。
そこで
強制的アンラップ
!
をつけて呼び出すことで強制的に値を取り出すことができる
なるほど。ということはさっきのコードを以下のように変えれば...
できました。が、nil落ちするので使い所は要注意ですね。
オプショナルバインディング
var foo = 1
var bar: Int? = 1
if let unwrappedBar = bar {
print(foo + unwrappedBar)
}
else {
print("bar is nil")
}
このようにすれば
var foo = 1
var bar: Int? = nil //変更箇所
if let unwrappedBar = bar {
print(foo + unwrappedBar)
}
else {
print("bar is nil")
}
nilの時でも安全に処理をわけることができる。
5. if, switch, 関数, クロージャ, クラス, 継承, インスタンスについて
想像通りだったので割愛。
クロージャというのは初めて聞いたが無名関数みたいなものだなと思った
6. 構造体とプロトコル
クラスを作ってインスタンスを生成する時、以下のようにするのは思った通りというところですが
struct Monster {
var name: String
var dropItem: String
init(name: String, dropItem: String) {
self.name = name
self.dropItem = dropItem
}
func attack() {
print("\(name)の攻撃!▼")
}
}
// 呼び出し例
var slime = Monster(name: "スライム", dropItem: "やくそう")
slime.attack()
プロトコルというものを設定できるんですね。
「約束ごと」という名前の通り、構造体の内容を保証するものなんですね。
定義し忘れちゃった!とかがなくなっていいですね。
またプロパティに対してget
やset
と定義するだけでそのプロパティが参照しかできるか、変更が可能か、またその両方かを定義できるんですね。
ゲッターメソッド、セッターメソッドをいちいち定義しなくていいイメージ。
(Rubyでいうattr_xxx
みたいなものですね。)
protocol MonsterProtocol {
var name: String { get }
func attack()
}
struct Monster: MonsterProtocol { //プロトコルの指定
//以下同文
7. as(型キャスト)
その前に型キャストとは
インスタンスの方をほかの型として扱うこと。
例えばAny型のインスタンスをString型で扱うなど。
アップキャスト
サブクラスのインスタンスをスーパークラスの型に変換する。
as
を利用する
ダウンキャスト
スーパークラスのインスタンスをサブクラスの型に変換する。
as?
:安全な型キャストを行える。キャストが行えないときはnil
が返される
as!
:強制的な型キャスト。失敗するとエラーが発生して意しまうため注意が必要
なるほど、Swiftで!
は強制的〇〇なのか🤔
var any: Any = 1024
var str: String = "String"
var int: Int = 1024
var anyStr = str as Any
print(anyStr)
8. @State
値を画面から変更する際に利用する(ボタンをタップしたときなど)
9. @ObservedObject
@State
と違いインスタンスをの値を画面から変更する際に利用する
import SwiftUI
class Monster: ObservableObject { // ObservableObjectというプロトコルを指定
@Published var name: String // 画面から変更したい値には@Publishedをつける
@Published var hp: Int // 画面から変更したい値には@Publishedをつける
init(name: String, hp: Int) {
self.name = name
self.hp = hp
}
}
import SwiftUI
struct ContentView: View {
@ObservedObject var monster = Monster(name: "スライム", hp: 10)
var body: some View {
VStack {
List {
Text(monster.name)
Button(action: {monster.name = "ドラキー"}) {
Text("名前をかえる")
}
Text("HP:\(monster.hp)")
Button(action: {monster.hp = 15}) {
Text("HPをかえる")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
10. @EnvironmentObject
ではなぜ@ObservedObject
以外にもう一個@EnvironmentObject
を理解しないといけないのか。
わかりやすい例を用意しました。
import SwiftUI
class Monster: ObservableObject {
@Published var name = "スライム"
@Published var hp = 10
}
import SwiftUI
struct ContentView: View {
@ObservedObject var monster = Monster()
var body: some View {
VStack {
Text("ContentView")
List {
Text(monster.name)
Button(action: {monster.name = "ドラキー"}) {
Text("名前をかえる")
}
Text("HP:\(monster.hp)")
Button(action: {monster.hp = 15}) {
Text("HPをかえる")
}
}
anotherContentView()
}
}
}
// もう一つ別のcontentViewを用意する
struct anotherContentView: View {
@ObservedObject var monster = Monster()
var body: some View {
Text("anotherContentView")
VStack {
List {
Text(monster.name)
Button(action: {monster.name = "ドラキー"}) {
Text("名前をかえる")
}
Text("HP:\(monster.hp)")
Button(action: {monster.hp = 15}) {
Text("HPをかえる")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Viewを二つ用意してみました。この状態で名前を変えることができるか試してみます。
ボタンを押しても片方しか変わりません。
これはViewごとに独立したインスタンスをもっているためです。
「そうじゃなくて両方値がわかって欲しい!(共通のインスタンスであって欲しい)」という時に@EnvironmentObject
の出番です
import SwiftUI
struct ContentView: View {
@EnvironmentObject var monster: Monster //変更箇所
var body: some View {
VStack {
Text("ContentView")
List {
Text(monster.name)
Button(action: {monster.name = "ドラキー"}) {
Text("名前をかえる")
}
Text("HP:\(monster.hp)")
Button(action: {monster.hp = 15}) {
Text("HPをかえる")
}
}
anotherContentView()
}
}
}
struct anotherContentView: View {
@EnvironmentObject var monster: Monster //変更箇所
var body: some View {
Text("anotherContentView")
VStack {
List {
Text(monster.name)
Button(action: {monster.name = "ドラキー"}) {
Text("名前をかえる")
}
Text("HP:\(monster.hp)")
Button(action: {monster.hp = 15}) {
Text("HPをかえる")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
// 以下を追加
.environmentObject(Monster())
}
}
@EnvironmentObjectを利用することで共通のインスタンスとして扱えるようになりました。
11. @Bindning
@Binding
とは、別のViewでそのViewの@State
変数を変更するために使う。
インスタンスではないけれど別のViewの@State
の値を変えたい時に使うんですね。
実際にTODOアプリを作ってみよう
SwiftUIの基本的な文法であるHStack
やText
については解説していません。
1. とりあえず、見た目の部分を作成
表示画面であるのContentViewに加え、今回はTodo.swift
という構造体を用意します。
import SwiftUI
struct Todo: View {
var todo: String
var isChecked: Bool
var body: some View {
HStack {
Image(systemName: isChecked ? "checkmark.square.fill" : "square")
.resizable()
.frame(width: 18, height: 18)
// チェックされている場合は青色、そうでない場合はグレー色にする
.foregroundColor(isChecked ? .blue : .gray)
Text(todo)
}
}
}
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List {
// とりあえずハードコーディング
Todo(todo: "朝食", isChecked: true)
Todo(todo: "筋トレ", isChecked: false)
Todo(todo: "洗濯", isChecked: true)
}
// タイトル部分(NavigationViewの中で使えるモディファイア)
.navigationBarTitle(Text("TODO"))
}
}
}
#Preview {
ContentView()
}
動的に値を変更できるように上記を修正していきます。
2. Todoという構造体を用意する
import SwiftUI
struct Todo: Identifiable, Equatable {
let id = UUID() // 一意となる識別子
var title: String
var isChecked: Bool
init(title: String, isChecked: Bool) {
self.title = title
self.isChecked = isChecked
}
}
//本当はTodoData.swiftファイルを作成して別に書き出した方が良いかもしれないが、今回は記述量が少ないので同一ファイルに入れる
class TodoData: ObservableObject {
@Published var todos: [Todo] = [] // 画面に表示するTodoリストの配列
@Published var isEditing: Bool = false // 追加中かどうかを判定する
}
Todoインスタンスを作成するための雛形を用意します。
Todoについて
title, isCheckedは画面から参照するためinit内に定義しています。
TodoDataについて
画面にはTodoの一覧を表示するのでTodoインスタンスの配列をここに定義しておきます。
isEditing
は新たにTodoを追加している状態かどうかを判定するために使います。
3. ContentView
import SwiftUI
struct ContentView: View {
// TodoData環境オブジェクトを利用
@EnvironmentObject var todoData: TodoData
var body: some View {
NavigationView {
List {
// TodoData内の全てのtodoをリスト表示
ForEach(todoData.todos) { todo in
// リスト内の各ToDoに対するトグル可能なボタン
Button(action: {
// 選択されたToDoのインデックスを取得
// todoData.todos内に対象のToDoが存在しない場合、firstIndex(of:)メソッドはnilを返すため、guard文を使用して取得したインデックスがnilでないことを確認し、nilであれば早期に処理を終了する。
guard let index = self.todoData.todos.firstIndex(of: todo) else {
return
}
// 選択されたToDoのisCheckedプロパティをトグル
// もしToDoがチェックされている状態(isCheckedがtrue)であれば、この行の実行によってisCheckedがfalseになり、逆にチェックされていない状態(isCheckedがfalse)であれば、isCheckedがtrueになる。
self.todoData.todos[index].isChecked.toggle()
}) {
// TodoViewにToDoのタイトルとisChecked状態を渡す
TodoView(title: todo.title, isChecked: todo.isChecked)
}
}
// 編集モードの場合はToDoの作成フォームを表示
if self.todoData.isEditing {
CreateTodo()
}
// 編集モードでない場合は「追加する」ボタンを表示
else {
Button(action: {
// 編集モードに切り替え
self.todoData.isEditing = true
})
{
Text("追加する")
.fontWeight(.bold)
}
}
}
// NavigationViewのタイトルを設定
.navigationBarTitle(Text("TODO"))
}
}
}
では次にTodoリストの見た目をどのように整えているか、TodoViewについて説明していきます。
import SwiftUI
struct TodoView: View {
var title: String // ToDoのタイトル
var isChecked: Bool // ToDoがチェックされているかどうかを示すフラグ
var body: some View {
HStack {
// チェックされている場合はチェックマークを表示し、そうでない場合はチェックボックスを表示
Image(systemName: isChecked ? "checkmark.square.fill" : "square")
.resizable()
.frame(width: 18, height: 18)
// チェックされている場合は青色、そうでない場合はグレー色にする
.foregroundColor(isChecked ? .blue : .gray)
// ToDoのタイトルを表示
Text(title)
.foregroundColor(Color.black)
}
}
}
では次にどのようにTODOリストへの新規追加を実現しているか、解説していきます。
import SwiftUI
struct CreateTodo: View {
// 入力されたToDoのタイトルを保持する状態変数
@State var todoTitle = ""
// TodoData環境オブジェクトを利用
@EnvironmentObject var todoData: TodoData
var body: some View {
// ユーザーが入力するテキストフィールド
TextField("やることを入力してください", text: $todoTitle)
.onSubmit {
// ユーザーがEnterを押したときにToDoを作成
self.createTodo()
// 編集モードを終了
self.todoData.isEditing = false
}
}
// 新しいToDoを作成するメソッド
func createTodo() {
// 入力されたタイトルを使用して新しいToDoを作成
let newTodo = Todo(title: self.todoTitle, isChecked: false)
// ToDoリストの先頭に新しいToDoを挿入
self.todoData.todos.insert(newTodo, at: 0)
// 入力フィールドをクリア
self.todoTitle = ""
// 編集モードを終了
self.todoData.isEditing = false
}
}
@EnvironmentObjectは、アプリケーション全体で共有されるデータを示すために使用されます。このプロパティラッパーを使用すると、アプリケーション内の任意のビューで同じオブジェクトを共有できます。
今回の場合、TodoDataはToDoアプリ全体で共有されるデータであり、複数のビューで利用されるため、@EnvironmentObjectを使用して、TodoDataをアプリケーション全体で共有可能な状態にします。一方で、@ObservedObjectは特定のビューが特定のオブジェクトを監視する場合に使われるため、この場合は適切ではないわけですね。
つまり、@EnvironmentObjectを使用することで、TodoDataをアプリ全体で共有可能にし、必要なビューでそれを使用できるようにしています。
おわりに(感想)
@State, @Binding, @ObservedObjectなどの@マークを使う変数が慣れないですね...。有効な範囲がいまいち掴みづらいです。
今回はTodoアプリで非常に簡易的なものですが、SwiftUIでつくられた大規模なアプリではファイルやクラス、構造体をどのような単位で分けて書かれているのか非常に興味があるのでみてみたいです。
また本記事において、おかしな内容が含まれていましたら忌憚なくご指摘くださいm(_ _)m
ありがとうございました。
参考文献