9月4日から始まるインターンでSwiftを用いる。 それにもかかわらず、1ミリとしてSwiftに触れたことがなかった。 そのため、予習として、macOS及びiOS向けの日誌記録アプリ開発を通してSwift(というよりはSwiftUI)を学習した。 学習した内容やSwift及びSwiftUIに対する所感を本記事にまとめる。
M2 MacBook Air (macOS Sonoma 14)+Visual Studio Codeで開発した。 曰く、AppleはXcodeでの開発を推奨しているようであるが、私はCLI信者であるため、これを拒否した。 残念ながら(あるいは当然ながら)、CLIでのビルド方法に関する文献は少ない。 まともにビルドできるようになるまで苦労を要した。
まず、Homebrewをインストールした時点で次のような状態であった。 幾つかエラーが見える。 このエラーのために、Swiftをビルドできない。
$ swift --version swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4) Target: arm64-apple-macosx14.0 $ xcode-select -p /Library/Developer/CommandLineTools $ xcrun --show-sdk-path /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk $ xcrun --show-sdk-platform-version xcrun: error: unable to lookup item 'PlatformVersion' from command line tools installation xcrun: error: unable to lookup item 'PlatformVersion' in SDK '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk' $ ls -la /Library/Developer/CommandLineTools/SDKs total 0 drwxr-xr-x 7 root wheel 224 7 21 18:46 . drwxr-xr-x 5 root wheel 160 7 21 18:46 .. lrwxr-xr-x 1 root wheel 14 7 21 18:46 MacOSX.sdk -> MacOSX14.4.sdk drwxr-xr-x 7 root wheel 224 5 12 2023 MacOSX13.3.sdk lrwxr-xr-x 1 root wheel 14 7 21 18:46 MacOSX13.sdk -> MacOSX13.3.sdk drwxr-xr-x 7 root wheel 224 2 20 09:28 MacOSX14.4.sdk lrwxr-xr-x 1 root wheel 14 7 21 18:46 MacOSX14.sdk -> MacOSX14.4.sdk $ ls -la /Library/Developer/CommandLineTools/SDKs/MacOSX14.4.sdk total 40 drwxr-xr-x 7 root wheel 224 2 20 09:28 . drwxr-xr-x 7 root wheel 224 7 21 18:46 .. -rw-r--r-- 1 root wheel 127 2 10 2024 Entitlements.plist -rw-r--r-- 1 root wheel 6564 2 10 2024 SDKSettings.json -rw-r--r-- 1 root wheel 4871 2 10 2024 SDKSettings.plist drwxr-xr-x 5 root wheel 160 2 20 09:25 System drwxr-xr-x 7 root wheel 224 2 20 09:25 usr $ sudo xcode-select -s /Applications/Xcode.app/Contents/Developer xcode-select: error: invalid developer directory '/Applications/Xcode.app/Contents/Developer' $ ls /Applications/X* zsh: no matches found: /Applications/X*
Homebrewインストール時にXcode Command Line for Toolsが自動でインストールされる。 しかし、どうやらこのCLTでは不十分であり、Xcodeをインストールしなければならないらしい。 さて、私はCLI信者であるため、xcodesというXcodeのバージョン管理アプリを用いてXcodeをインストールした。 ただし、どうやらXcodeを運用するにはApple Developerに登録する必要があるらしく、登録済みのAppleアカウントへのログインが要求された。
$ brew install xcodesorg/made/xcodes $ brew install aria2 $ xcodes install 15.4 $ cd /Applications $ ln -s Xcode-15.4.0.app Xcode.app $ sudo xcode-select -s /Applications/Xcode.app/Contents/Developer $ xcrun --show-sdk-platform-version 14.5
以上でswiftコマンドが利用できるようになった。 しかし、SwiftUIを用いる場合、swiftコマンドでは不十分であり、xcodebuildコマンドを用いてビルドする必要がある。 xcodebuildを用いるにはXcodeプロジェクトが必要である。 幸いにもxcodegenというCLI操作でYAMLからそれを作成できるアプリがあるため、これを用いる。 簡単なYAMLファイルは次のようである。
name: ProjectName settings: base: MARKETING_VERSION: 1.0.0 CURRENT_PROJECT_VERSION: 1 targets: TaskRecorder: type: application platform: [macOS, iOS] sources: - Sources settings: base: PRODUCT_BUNDLE_IDENTIFIER: product.bundle.identifier
次のようにxcodegenを用いる。 尚、2個あるxcodebuildについて、1個目がmacOS向け、2個目がiOS向けである。 ただし、$TEAM_IDはデベロッパーチームのIDのことであるが、無料のApple Developerにおいてはそれを取得する正規の手段が提供されていない。 そのため、Xcodeでプロジェクトを作成し、その.xcodeprojectディレクトリから発見するしかない。 また、$SDKは後述する方法でインストールし、「xcodebuild -sdk -version」を実行して確認できる。 xcodebuildの結果、build/(macOS|iOS)/Build/Products/(Debug|Release)/ProjectName.appが生成される。
$ brew install xcodegen $ xcodegen $ xcodebuild \ -project ProjectName.xcodeproj \ -scheme ProjectName_macOS \ -derivedDataPath ./build/macOS \ -configuration (Debug | Release) \ GENERATE_INFOPLIST_FILE=YES \ build $ xcodebuild \ -project ProjectName.xcodeproj \ -scheme ProjectName_iOS \ -sdk $SDK \ -derivedDataPath ./build/iOS \ -configuration (Debug | Release) \ GENERATE_INFOPLIST_FILE=YES \ DEVELOPMENT_TEAM=$TEAM_ID \ build
iPhoneやiPad等のシミュレーションを行うためには、次のように実行する。 ただし、予めApple Developer websiteからランタイムをダウンロードし、「xcrun simctl runtime add "/path/to/runtime-file.dmg"」によってインストールしておく必要がある。 伴ってSDKとシミュレータがインストールされる。 また、$DEVICE_TYPEと$SIMULATORは「xcrun simctl list」を実行して確認できる。
$ xcrun simctl create $DEVICE_NAME $DEVICE_TYPE $SIMULATOR $ xcrun simctl boot $DEVICE_NAME $ xcrun simctl install iPad8 $PATH_TO_APPLICATION $ open -a /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator
以上で開発環境を構築できた。
尚、この開発環境は マゾ CLI信者向けである。
こんな不毛な環境構築作業をしないために、また特にプレビューの恩恵を受けるために、素直にXcodeを用いるのが良い。
初見の印象は「Rustみたいだなあ」だった。 実際、Swiftのパラダイムはオブジェクト指向ではなくプロトコル指向なるものらしく、このプロトコルがRustのトレイトのようなものなので、ある程度の馴染みがあった。 しかし、主要パラダイムではないとしながらオブジェクト指向をサポートしていることや・下記の要素から、Rustのような言語だと言うのは少々過言かと思う。
nilの可能性があることを表すオプショナル型には、TypeScriptや新しいC#等のモダンな言語の雰囲気を感ぜられる。 nilや例外の除去のためのguard文もまた、そのシンタックス自体が独立しているながら、矢張りTypeScriptのif文による型推論のような機能である。
一方で、1モジュール内において名前空間の概念がなく、トップレベルに宣言を連ねる点には、CやJavaScriptのような古い言語を想起させる。 グローバル変数を定義できる点も、これに拍車を掛けている。 スクリプト言語のように書ける開発用言語といった印象である。
個人的に面白いと思ったのは、名前付き引数とクロージャのシンタックスである。 まず、名前付き引数を半ば強要するような言語は珍しいと思った。 これまで扱ったことのある言語の中ではPythonにも名前付き引数があったが、強制ではなかったと記憶している。 この機能のおかげか、構造体の初期化を関数呼出しのように書ける。 エレガントである。 また、クロージャのシンタックスは次のようであり、なんか、こう……、なんというか、不気味で面白いなと思った。
["foo", "bar"].forEach({ print($0) }) ["foo", "bar"].forEach({ n in print(n) }) ["foo", "bar"].forEach { n in print(n) } ["foo", "bar"].forEach { (n: String) in print(n) } ["foo", "bar"].forEach { (n: String) -> Void in print(n) }
まず、宣言的UIである点が好印象である。 私は必要最小限の依存で開発する・所謂フルスクラッチ厨であり、そのためReactもVueも業務でもない限りはまず使わない。 しかし、だからこそ、DOMをJavaScriptで操作したり・.NETのUIを直書きすることの面倒臭さを理解している。 そこで、宣言的にUIを記述できるというのはありがたい仕様であった。
次は発展的な知識である。
設計方針にはMVVMだとかTCAだとかがある。
Binding
ビューをメンバ変数として持つ場合は、その型をViewを実装する総称型とする。また引数は@ViewBuilderとして受け取る。
Formにはスクロール機能がある。「設定」アプリはFormに統一されていそう。
ShareLinkのURLはローカルファイルパスでも良い。
宣言的UIはありがたい、と言ったものの。 UI再描画の発火条件に非常に悩まされた。 とにかく、配列が悪さをする。 例えば、次のコードである。 Observableなクラスの要素である配列について、メソッドを介して取得すると変更が反映されない。
struct Element: Identifiable { var text: String = "foo" } class Elements: Observable { private var privateElements: [Element] = [] var elements: [Element] = [] func get() -> [Element] { return self.privateElements } func append() { self.privateElements.append(Element()) self.elements.append(Element()) } } /* ... */ List(elements.get(), id: \.self) { element in Text(element.text) // 追加が反映されない } List(elements.elements, id: \.self) { element in Text(element.text) // 追加が反映される } Button("Add") { elements.append() }
また、Listの中のNavigationLinkによって遷移した別画面での配列の要素の変更は反映されない。
import SwiftUI struct ModelData { var array = [false] } struct MyButton: View { @Binding var value: Bool var body: some View { Button(value ? "1" : "0") { value.toggle() } } } @main struct MainApp: App { @State var modelData: ModelData = ModelData() var body: some Scene { WindowGroup { NavigationStack { Text("Array") NavigationLink { // 再描画されて変更が反映される MyButton(value: self.$modelData.array[0]) } label: { MyButton(value: self.$modelData.array[0]) } List { Text("Array") NavigationLink { // 再描画されず、変更が反映されない MyButton(value: self.$modelData.array[0]) } label: { MyButton(value: self.$modelData.array[0]) } } } } } }
他にも、次の例がある。 真理値を持つモデルの配列から、真偽判定によるフィルタリングによって2個の配列を出し分けるプログラムである。 しかし、各モデルの真理値は変更され、その外見も再描画されるが、フィルタリングの結果が変わらない。 そして、配列に要素を追加することでフィルタリングの結果が正常になる。
import SwiftUI class Model: ObservableObject { @Published var value: Bool = false } @Observable class ModelData { var models = [Model()] } struct MyButton: View { @ObservedObject var value: Model var body: some View { Button(value.value ? "1" : "0") { value.value.toggle() } } } struct ContentView: View { @Environment(ModelData.self) var modelData var falses: [Model] { return modelData.models.filter({ n in !n.value }) } var trues: [Model] { return modelData.models.filter({ n in n.value }) } var body: some View { // 配列に要素を追加すると下のForEachの内容が変わる。 Button("add") { modelData.models.append(Model()) } Text("Falses") ForEach(falses.indices, id: \.self) { i in // 反映されない! MyButton(value: falses[i]) } Text("Trues") ForEach(trues.indices, id: \.self) { i in // 反映されない! MyButton(value: trues[i]) } } } @main struct MainApp: App { @State var modelData = ModelData() var body: some Scene { WindowGroup { ContentView().environment(modelData) } } }
SwiftUIに対する不可解な挙動は再描画だけではない。 まず、macOS向けのビルドにおいては、.editMode()を使うことができない。 そのため、同様の機能を持たせるには.onMove()や.onDelete()を使うことになる。 しかし、まず、List>ForEach(.onMove())>TextFieldとしたとき、TextFieldのフォーカスが著しく遅くなる。 これはQiitaにも報告が上がっている。 また、その.onDelete()はmacOS向けのビルドに利用できない。 そのため、.onDeleteCommand()を用いてキーバインドするか、削除UIを別途追加する必要がある。
macOSにおいてウィンドウを閉じたときにプロセスを終了するには、次のようにしなければならない。
import SwiftUI #if os(macOS) class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } } #endif @main struct MainApp: App { #if os(macOS) @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate #endif var body: some Scene { WindowGroup { ContentView() } } }
細かいところでは、次のようなものがある。
main.swiftにAppな構造体を記述できない
ScrollViewにListを配置できない
Listの最初の要素としてSectionが置かれると見た目がおかしくなる
中身のないときのSectionの見た目がおかしい
Formのデフォルトスタイルがハチャメチャ
.groupedスタイルのFormにおいて1個しか要素のないForEachの見た目が少しおかしい
これまでの人生で何度も仕様に殺されたことはあったが、SwiftUIは中でもトップクラスの殺意を感じた。
とはいえ、慣れてしまえば簡単にそれっぽいものが作れるので、悪い体系とも言い難い。 インターンで経験を積んで、もっと理解を深めたい。
■