LoginSignup
28
29

WebアプリエンジニアがSwiftUI Tutorialをやってみた

Posted at

はじめに

はじめまして。
普段は業務でWebアプリケーションの開発を行っているのですが、嬉しいことに長らく興味があったiOS周りにも携わらせていただくこととなりました。そこでとりあえずSwiftUIの公式チュートリアルを触ってみました。
個人的にかなりとっつきやすく、Nextを触っていた身からすると入りやすいフレームワークだと感じました。結構好みです。

本記事ではチュートリアルを進めるうえで詰まったところや調べたところ、面白いなと思ったところなどをまとめていきます。解説記事の体裁を取った感想文みたいなものですが、今後似たような境遇の方や学びはじめの方の助けになればと思います。
Qiita初投稿です。何卒温かい目で読んでいただければ幸いです。

前提条件

本記事を読むにあたって、筆者の前提情報は以下です。

  • Web周りの開発経験はある程度
  • iOSは全く触ったことがない
  • swiftももちろん触ったことがない
  • どちらかというとフロントは苦手寄り

以上を踏まえたうえでお読みいただけると幸いです。

SwiftUI Essentials

本節はXCodeでのプロジェクト作成方法から、基本的な操作方法、そして簡単なアプリの作成までを実施します。はじめからアプリの画面作成まで実施できる代わりに理解しておかないといけない内容が盛り沢山です。
私が詰まった部分、まだ理解が怪しい部分について紹介します。

.の意味は何?

チュートリアルを進めていると、以下のようなコードが出てきます。

import SwiftUI


struct ContentView: View {
    var body: some View {
        Text("Turtle Rock")
            .font(.title)
            .foregroundColor(.green)
    }
}


#Preview {
    ContentView()
}

fontやforgroundColorは前の要素にかかっているメソッド(ビュー修飾子と呼ばれます)であることはわかりますが、では「.title」や「.green」の前には何が存在しているのでしょうか?
これはtitleやgreenという要素を持った構造体の名前が省略されています。
実はtitleの前にはFont、greenの前にはColorが存在しているのですが、それが明白であるため省略された書き方をしています。
(参考資料:https://developer.apple.com/documentation/swiftui/font)
.のみの表記があった際はその前に何が省略されているのかを調べてみるとより理解が深まるかもしれません。

余談ですが、Fontの定義元実装に行くと@Frozenアノテーションが付与されています。
これは付与されたstrictやenumが将来のバージョンで内部構造が変わらないことを保証するものであり、型の変更などを制限するものです。これにより将来的にも利用側は安心してこの構造体を利用することができるわけです。詳細なドキュメントはこちら

Foundationってなに?

Swift Viewではなく、Swift Fileでファイルを作成した際にデフォルトでimportされているFoundation。これは一体何をimportしているのでしょうか?
ドキュメントによると以下のように書いています。

The Foundation framework provides a base layer of functionality for apps and
frameworks, including data storage and persistence, text processing, date and time 
calculations, sorting and filtering, and networking. The classes, protocols, 
and data types defined by Foundation are used throughout the macOS, iOS, watchOS,
and tvOS SDKs.

つまり、アプリに対して基本的な機能を提供してくれていることがわかります。
基本的な機能はドキュメントのTopics/Fundamentalsに記述されています。どうやらこれがないと始まらないようです。
試しにこのコードでimportを消してみます。

import Foundation

struct Profile {
    var username: String
    var prefersNotifications = true
    var seasonalPhoto = Season.winter
    var goalDate = Date()


    static let `default` = Profile(username: "g_kumar")


    enum Season: String, CaseIterable, Identifiable {
        case spring = "🌷"
        case summer = "🌞"
        case autumn = "🍂"
        case winter = "☃️"


        var id: String { rawValue }
    }
}

すると、Date()の部分でエラーが発生します。これより、Date構造体はFoundationにより利用可能になっていることがわかります。定義元に飛んでみても、Foundation.swiftに飛ぶことからも分かるかと思います。
Dateのような一般的な機能は、他の言語では組み込みAPIとして用意されているためにわざわざimportを書く必要がないのでちょっと違和感がありましたが、こういうものとして受け入れるしかなさそうですね。

プロトコルについて

Building lists and navigationのsection1にて、Landmarkのmodel定義を行っている際にプロトコルの指定を行う場面が出てきます。

import Foundation
import SwiftUI


struct Landmark: Hashable, Codable {
    var id: Int
    var name: String
    var park: String
    var state: String
    var description: String


    private var imageName: String
    var image: Image {
        Image(imageName)
    }


    private var coordinates: Coordinates


    struct Coordinates: Hashable, Codable {
        var latitude: Double
        var longitude: Double
    }
}

このコードにおける、HashableやCodableがプロトコルに当たるものです。
Codableの定義元に飛ぶとtypeAliasと出てきます。これはEncodableプロトコルとDecodableプロトコルの両方に準拠している型で云々…とこれはまた別のお話なので割愛しますが、本記事では便宜的にプロトコルと扱うことにします。

このプロトコルですが、ドキュメント曰く「特定のタスクまたは機能に準拠するメソッド、プロパティ、およびその他の要件の設計図を定義します。クラス、構造体、または列挙型がプロトコルに準拠でき、それらの要件の実装を提供します。プロトコルの要件を満たす全ての型は、そのプロトコルに準拠しているといいます。」とのことです。
ちょっとわかりにくいですが、他の言語で例えるとJavaならinterfaceがこれに当たるのかなと思います。
要するに、〇〇の機能を提供するものを〇〇プロトコルと定義し、それに準拠するメソッドやクラス、構造体に対してはそのプロトコルを付与してあげるということですね。
プロトコルを付与しているにもかかわらず、その要件が満たされていない場合は当然ですがコンパイルエラーが発生します。これでswiftの特徴の一つである型安全が保たれているのかもしれません。

では今回付与されている2つのプロトコルはどのような動作を定義しているのでしょうか?

  1. Codableについて
    これが付与されていることで、JSONやプロパティリストを任意のデータ型に変換することが可能となります。
    つまり、今回はJSONとして与えられているLandmarkファイルを元に、プロパティ名が一致するものを抽出してswiftコード内で利用可能なインスタンスにしてくれるわけです。これによりデータの永続化やネットワーク通信でのやり取りを容易にすることができます。(参考文献

  2. Hashableについて
    これが付与されていることで、Landmarkインスタンスをハッシュ値として扱うことが出来るようになります。
    つまり今回は複数のLandmarkをListとして表示する際に、ForEachなどで繰り返しを行うさいにidを利用して処理を回すことが可能になります。(参考文献

余談ですが、swiftは「プロトコル指向言語」と言われています。WWDC2015の中で明確にプロトコル指向言語であると言われており、そのようなパラダイムの中では初の言語と言われています。
参考に当時の記事を貼っておくので、気になる方はぜひ読んでみてください。→ https://www.wwdcnotes.com/notes/wwdc15/408/

また、世の中にはたくさんのプロトコルに関する解説記事が存在するため、より詳しく知りたい方はそちらを御覧ください。
本記事では、CodableとHashableの意味がわかっておけばOKです。

@Environmentなどのアノテーション?について

Handling user inputのsection5を進めていると、以下のようなコードを追加する必要が出てきます。

    @Environment(ModelData.self) var modelData
    @State private var showFavoritesOnly = false

二行目の@State云々は状態を管理していることがこれまでのチュートリアルからわかります。イメージとしてはNextのuseStateに近いですね。
問題は一行目です。@Environmentは何だ?またselfもなんだ?という気持ちです。selfについては次の項で触れるので一旦おいておきます。
そもそもこの項のタイトルで「アノテーション」という言葉を使いましたが、@Environmentのようなものは正しくは「プロパティラッパー」と呼ぶそうです。この他にもチュートリアルには@Binding@Bindableなどが出てきます。
それぞれについて具体的に使い方を調べてみましょう。

@Environment

これはSwiftUIで環境依存の値を取得するためのプロパティラッパーです。
ここで引数にあるModelDataはsection4で作成したModelData.swiftの

@Observable
class ModelData {
   var landmarks: [Landmark] = load("landmarkData.json")
}

の部分を指しています。
@Observableについては後ほど解説しますが、これがついているクラスは環境オブジェクトとしてアプリケーション全体で共有されるデータモデルとして利用することが可能になります。
この環境オブジェクトを取得し、modelDataにバインドをしているのがこのコードの正体です。
他にもこのプロパティラッパーを利用することで、様々な環境依存の値を取得できます。

  • テーマカラー(色のスキーム)
  • ロケール(言語や地域)
  • ダークモードの設定
  • デバイスのスクリーンサイズや方向
  • フォントサイズやスタイル
  • その他、カスタムに設定した環境値

実装例は以下のようなものです。

struct ContentView: View {
    // ダークモードの設定を取得する
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        if colorScheme == .dark {
            Text("ダークモードです")
        } else {
            Text("ライトモードです")
        }
    }
}

@Binding

固い言葉を敢えて使うと、「データの双方向バインディングを実現するプロパティラッパー」となります。
次の項で紹介する@Bindableも同じ括りではありますが、厳密には使用用途が異なってきます。
Bindingは親ビューから子ビューへ値を渡して、子ビューがその値を変更可能にします。そうすることで親ビューと子ビューの間でデータの同期が保たれるわけです。チュートリアル内の例で見てみましょう。

以下はSection6のStep2でのコードです。

import SwiftUI


struct FavoriteButton: View {
    @Binding var isSet: Bool


    var body: some View {
        Button {
            isSet.toggle()
        } label: {
            Label("Toggle Favorite", systemImage: isSet ? "star.fill" : "star")
                .labelStyle(.iconOnly)
                .foregroundStyle(isSet ? .yellow : .gray)
        }
    }
}


#Preview {
    FavoriteButton(isSet: .constant(true))
}

BindingプロパティラッパーでisSetが宣言されています。これはLandmarkDetailで利用される子ビューです。
つまりLandmarkDetailでもisSetなる変数が利用されていて、FavoriteButtonでもその値を利用する必要があるということです。なおかつその値は常に同期されている必要があります。そのためこのプロパティラッパーが利用されているわけですね。
では親ビューの方を見てみましょう。

Section6のStep6でのコードですが、長いため一部省略しています。

import SwiftUI


struct LandmarkDetail: View {
    @Environment(ModelData.self) var modelData
    var landmark: Landmark
    ...

            VStack(alignment: .leading) {
                HStack {
                    Text(landmark.name)
                        .font(.title)
                    FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
                }
    ...

#Preview {
    let modelData = ModelData()
    return LandmarkDetail(landmark: modelData.landmarks[0])
        .environment(modelData)
}

FavoriteButtonにはisSetを引数として渡す必要がありますが、そこに格納される値はお気に入りされているかどうかのbool値であるため、modelDataから各LandmarkのisFavoriteを参照して格納しているわけです。
ここで気になるのは$記号ですが、これは変数のバインディングを表す記号です。つまりisSetとしてこの値を親ビューと子ビューでバインディングしますよということを表しています。そして@Bindingで双方向バインディングされているためにFavoriteButton側で$のついた値を変更することが可能だということです。

@Bindable

では次に先程のBindingに似たこちらについてです。
Bindableも双方向バインディングに利用されますが、Bindingと利用される場面が異なります。
Bindingは先程の項で示したように、親ビューと子ビューの間でのバインディングに利用されます。それに対してBindableはデータモデルとビューの間でのバインディングに利用されます。
Section6のstep5のコードを見ると確かにデータモデルをバインディングしていることがわかります。

import SwiftUI


struct LandmarkDetail: View {
    @Environment(ModelData.self) var modelData
    var landmark: Landmark


    var landmarkIndex: Int {
        modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }


    var body: some View {
        @Bindable var modelData = modelData

    ...

左辺のmodelDateは@Environmentプロパティラッパーで定義されているmodelDataになります。
ではModelDataの実装がどうなっているのかも見に行ってみましょう。
定義元に飛ぶと以下のコードが出てくると思います。

import Foundation

@Observable
class ModelData {
    var landmarks: [Landmark] = load("landmarkData.json")
    var hikes: [Hike] = load("hikeData.json")
    var profile = Profile.default
    
    var features: [Landmark] {
        landmarks.filter { $0.isFeatured }
    }
    
    var categories: [String: [Landmark]] {
        Dictionary(
            grouping: landmarks,
            by: { $0.category.rawValue }
        )
    }
}

...

このように@Observableがついているデータモデルの変更を監視し、自動的な更新を行うものがBindableです。

@Observable

では先程の最後に出てきたObservableは何を意味するのでしょうか?
これはSwift5.7で登場した比較的新しいプロパティラッパーです。Bindableと併用して利用されます。
これはObservableが付与されたクラス(データモデル)の状態の変更を監視しています。変更が行われた場合はビューの再描画を行うため、Bindableと併用される理由もなんとなくつかめるのではないかなと思います。

@ObservedObject

この項は余談になります。なぜなら役割がObservableと被っているどころかObservableがObserveObjectの上位互換に当たるためです。
もともとBindingはObserveObjectとPublishedプロパティラッパーの両方が対応することで本来の双方向バインディングが実現されていたのですが、Swift5.7で両方を一つにまとめたObservableが導入されたことで新規の開発ではほぼほぼ利用されないのではないでしょうか。
そもそもObservableはObserveObjectの代わりの利用が意図されており、従来の2つの組み合わせを簡素化する目的があるので当然っちゃ当然です。
もしこれから触るプロジェクトが古くてまだObserveObjectが利用されている等の場合はObservableへの移行を検討してみても良いかもしれません。JSなどを見ているとこういうのってそのうち非推奨になったりするので…

selfってなに?

JavaScriptを書いているとthisというものが出てくるかと思います。似て非なるこの2つですが、どういう点が一緒でどういう点が違うのでしょうか?
まず似ている点ですが、どちらもオブジェクト指向におけるインスタンス自身を参照するためのキーワードであるという点です。逆にいうと同じ点はそのくらいです。
次に違う点ですが、selfはswiftで動いているので参照先するオブジェクトのインスタンスが静的に決定され、thisはJavaScriptで動いているので動的に決定されるという点です。これはもはやselfとthisの違いというよりswiftとJavaScriptの違いのような気がしますが…

ではself自体の仕様についてです。
利用例を示します。

import SwiftUI


struct LandmarkDetail: View {
    @Environment(ModelData.self) var modelData
    var landmark: Landmark


    var landmarkIndex: Int {
        modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }


    var body: some View {
        @Bindable var modelData = modelData
    ,,,

また先程と同じ例ですが、ここのselfは何を意味するのでしょうか?
これはModelDataという型自体を指しています。selfはインスタンスそのものを指しているのがポイントです。
この他にもselfには様々利用出来る場面があるのですが、今回はチュートリアルベースの解説なのでselfの他の意味についてはぜひ調べてみてください!

チュートリアル全体を通して思ったこと

PritterとかESLint的なものはXCodeには存在しないのか?

今までの開発は主にVSCode(時々IntelliJ)でJavaやTSを使って開発を行ってきた身としては、PrettierのようなコードフォーマッターやESLintのような静的解析ツールがあれば使いたいわけです。
しかしXCodeにはそのようなExtensionを追加出来るようなところは見当たらず… もし私の見落としてここからいろんな拡張機能入れられるよ!って知っている方がいたらぜひ情報提供お願いしますmm

そこで探してみたところ以下のようなものが見つかりました。

どうやら有志が作成した機能のようです。
本格的に開発を初めてこちらを使ってみたらまた使用感などシェアできればと思います!

まとめ

最後まで読んでいただきありがとうございます!
チュートリアルを通して感じたところとしては「一般的なフロントエンドフレームワークと思想は似ているな」ということです。例えば@StateプロパティラッパーなどはuseStateにかなり機能としては近いなとも感じましたし、コンポーネントに分割してUIを作っていく点等は既視感を感じながら学ぶ瞬間もありました。
Swiftに興味を持ったきっかけはTry! Swift TokyoというSwiftのデベロッパーカンファレンスに参加したことでした。とても温かいコミュニティで開発者として参加したいと感じました。
何がきっかけで開発に興味を持つかは分かりませんし、「なんかネイティブアプリ開発って難しそう」「Webフロントとは全く違いそうと思ってなんとなく手が出しづらいな…」といった方々がiOS開発の第一歩を踏み出したときに本記事がなにかの助けになれば幸いです。

2023/05/24 初版投稿段階で、Drawing and AnimationとApp design and layout については絶賛執筆中です!後日追記予定なので少々お待ち下さいmm

28
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
29