🍏

SwiftPlaygrounds使い倒し

2023/04/23に公開

はじめに

Swift Playgroundsは、AppleがmacとiPad向けに提供する統合開発環境です。Apple自身がプログラミング入門者向けの学習環境というイメージを全面に出して紹介していることもあり、ネットで検索しても、複数のサイトがヒットはするものの、表面的な機能の紹介やサンプルの解説程度にとどまっているのが現状です(筆者確認の限り)。プログラミング言語としてSwiftが使える、Apple版のScratch、程度にしか認識されていないかもしれません。

しかし、実際には、iPadというmacに比べてはるかに多くの台数が普及している端末で、iPhone/iPadのAppが開発でき、AppStoreへのアップロードまでできるという、Appleのエコシステム維持拡大のための野心的なツールであると言えます。

Swift PlaygroundsだけでAppを開発し、AppStoreに公開している筆者がその経験を元に、Swift Playgroundsで出来ること、出来ないことなど、Swift Playgroundsを使い倒すための極意を伝授します(大袈裟)。

イマココデコレオイマココデコレオ

イマココデコレオ+イマココデコレオ+

本書では、Swift Playgroundsの基本操作、入手方法などは記載の対象外といたします。Swift Playgroundsのバージョンは筆者の環境をベースとします。

本書は、筆者の開発における経験、調査を元に記載しています。記事執筆のための調査は、若干は実施していますが、基本的に経験をベースとしており、「反応が遅い」などの表現は定量的な測定に基づくものではありません。また、筆者はmacを所有していない(2024/01/22現在)ため、Xcodeやmac/macOSに関する情報は、インターネット上の情報、Swift Playgroundsの機能や操作などからの類推、過去にMacintoshを使用していた際の経験(漢字Talk7/System7時代)に基づきます。誤りがありましたら、より良い情報にしていくために、ぜひご指摘いただければと思います。

筆者の開発環境(2024/01/22現在)
iPad 8th / iPadOS 15.6.1 / Swift Playgrounds 4.1
iPad 9th / iPadOS 17.2 / Swift Playgrounds 4.4.1

1. Swiftプログラミングの出来ること、出来ないこと

macに付属する標準のフル機能の開発環境であるXcodeを使用していると、WWDCで新しい機能が紹介されたような場合でもない限り、プログラミングにおける「出来ないこと」を意識することはほとんどないでしょう。

Swift PlaygroundsはXcodeに比べ機能が限定されているため、「出来ないこと」があるのはしごく当然のことです。どの程度の機能のAppまで開発できるのか、プログラミング環境として何ができ、何が出来ないのか、を知っておくことは、Swift Playgroundsを使用する上で重要です。

Swift言語機能

Swift言語は、Swift Playgrounds 4.4では、Swift 5.9、iOS 17 SDKをサポートしています。これまでAppを開発している限りでは、Swift Playgrounds独自の表記や構文ルールに遭遇したことはなく、言語標準は全て使用できています。ただ、筆者は、iPad上で動作するSwift Playgroundsしか使用していないため、Xcodeで全く修正なしにコンパイルが通るか、については判断できません。

以下では、Swift言語機能の中で筆者が注目している機能について、Swift Playgroundsで使用する場合の動作について記載します(本来的には、次章に記載する方が適切かもしれません)。

型推論(Type Inference)

Swift言語は、型推論機能を有しており、効率的な可読性の高いコードになるように設計されています。インスタンス生成時にオブジェクトの型名やイニシャライザなど、同じような表記を繰り返し記載する必要がないというだけで、すっきりしたコードになります。

変数の型を、関数からの戻り値の型から特定するような書き方をした場合、一見すると型が判別できませんが、Swift Playgroundsの入力支援のヘルプ機能で、関数定義を参照することで把握することができます。

省略記法(Shorthands)

Swift言語は、特にクロージャにおいて、省略記法が用意され、コードを簡素化することができます。複数の省略ルールを組み合わせると、元のコードを推測することも難しいほど軽量になります。

ただ、省略記法は、どういうルールに基づいて、何がどう省略されてそのコードになったのか、理解できないと、改修に支障を来たす可能性があります。また、コーディングをする人の技量によって、同じ処理でもコードが変わってきますので、組織的に開発を行う場合には、表記揺れを減らすコーディング規約などを定めておくことが考えられます。

Swift Playgroundsの入力支援は、省略しない形式で補間するため、省略記法で記載することがわかっている場合には、不要な部分を削除するなど、一手間かかります。

Optional

Swift言語では、Optionalという仕組みが導入され、変数の初期化漏れを防げるだけでなく、値が入っていない状態(nil)を変数の状態の一つとして積極的に活用できます。

Swift Playgroundsでは、structなどの変数がOptionalである場合、入力支援のポップアップでプロパティを選択すると、「?」を補ってくれたりします。

エラーハンドリング

Swift言語では、エラーの発生する処理の呼び出しにtryを付記しますが、明らかにエラーが発生しない場合など、明示的にcatchしないことを宣言することができます。空のdo-catchブロックを記載せずに済むなど、コードが簡潔になります。

Swift Playgroundsの入力支援は、構文要素として、try!、try?なども列挙してくれますが、特筆するような支援機能は現時点ではなさそうでした。

Extension

Swift言語では、既存のstructやclassを拡張することが可能です。新たに定義したprotocolに準拠するように既存のオブジェクトを拡張することも可能です(protocolに準拠するために追加のメソッドが必要であれば、定義することは必要です)。

ただ、Extensionはスコープを限定する機能がなく、Appのグローバルスコープに適用されるため、ポイズニングする可能性があります。組織的に開発を行う場合には、使用場面や管理方法をコーディング規約などで定めておくことが考えられます。

Swift Playgroundsの入力支援では、Extensionで追加定義した関数も含めてポップアップに列挙されます。

文字列補間(String Intreporation)

Swift言語では、Stringリテラルの中で、変数や関数の戻り値を展開したり、計算処理の結果を含めたりすることが可能です。

筆者がSwift言語の構文の評価順序を正確に理解していないことが原因かもしれませんが、条件演算子が含まれる場合など、文字列補間が想定通りに動作しないことがありました。

非同期処理(Concurrency)

Swift言語では、言語レベルで非同期処理がサポートされています。iPadというリソースが限定された環境ではありますが、async/awaitなど、問題なく動作しています。

動作しているコード
Task {
	try await Task.sleep(nanoseconds: UInt64(configValues.searchFilterWait*1000_000_000))
}                            

SwiftUI

Swift Playgrounds自体が、実際のSwift言語、実際のフレームワークを用いた開発ができることを志向していることもあり、iPadOSとSwift Playgroundsのバージョンに依存しますが、SwiftUIのフル機能が使用できています。swiftファイルにおいて、import SwiftUIを記載する必要があります。

@と$

SwiftUI(というかSwift言語)では、@Stateや@Publishedなどプロパティラッパーを付けた変数を宣言する際に「@」を使用します。また、プロパティラッパーを付けた変数についてラッパーにアクセスする際に「$」を使用します。

Swift Playgroundsでは、変数の宣言時に「@」を入力すればコード補間候補としてプロパティラッパーを含んだリストがポップアップ表示されます。変数名を入力する位置で表示される候補には、「$」付きの変数と、付いていない変数が列挙されます。

Appコンテナ

プロパティ値などを保存するUserDefaults.standard、データファイルなどを格納するFileManager.default.urls(for:in:)などはSwiftUIに含まれ、問題なく動作します。

動作しているコード
@Published var selectedPage:Int {
	didSet {
		UserDefaults.standard.set(selectedPage, forKey: keys.selectedPage.rawValue)
	}
}
動作しているコード
guard let docDirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
	return
}

Swift Playgroundsでは、App固有のデータは、同一のプレイグラウンド(プロジェクト)の実行中は保持されます。コードを修正しプレビューが再起動されても、保持されたデータに継続してアクセス可能です。他のプレイグラウンドに切り替えた場合は、再度、以前のプレイグラウンド戻っても、データは保持されません。プレビューでAppがクラッシュした際には、プレビューデータの削除が必要になる場合があります。

JSON

JSONEncoder、JSONDecoderなどはSwiftUIに含まれ、問題なく使用できます。

動作しているコード
guard let data:Data = try? JSONEncoder().encode(allEvents) else {
	return
}
try? data.write(to: fullURL, options: .atomic)
動作しているコード
guard let existings = try? JSONDecoder().decode([EventJSON].self, from: data) else {
	return
}

Combineフレームワーク

import Combineを記載すれば、App設定の「機能」などの設定なしに利用可能です。

動作しているコード
var cancellables:[(id:UUID, cancellable:AnyCancellable)] = []

MapKit

App設定の「機能」で「位置情報」を設定する必要があります。「位置情報」を設定するためには、「Core Location(使用中)」または「Core Location(常時)」を追加します。その上で、import MapKitを記載すれば使用可能です。

動作しているコード
guard !disableRecordLocation else { return }
self.manager.requestLocation()

Swift Playgroundsで「位置情報」を使用する場合、初回のプレビュー時に、通常のAppと同様、位置情報利用の承認のダイアログが表示されます。iPad自体の設定の、プライバシーとセキュリティの位置情報サービスに、Swift PlaygroundsのApp設定で設定した名前で表示され、後から「Appの使用中のみ」などの動作の変更が可能です。

StoreKit

App設定の「機能」には、App内課金やAppStoreへの接続に関する設定項目はありません。import StoreKitを記載すれば、入力支援でオブジェクトやメソッドは表示され、コンパイルエラーにはなりません。
しかし、Swift Playgroundsには、Xcodeには付属するAppStoreのテストツールであるStoreKit Testingは付属しないため、試験はできません。AppStoreにアップロードし、App内課金アイテムの登録を行い、申請を試みましたが、Product.products(for:)で、Productの配列が取得できないため、正常に動作していると見なされず、承認を得られませんでした。

コンパイルはできるが動作しないコード
do {
	storeProducts = try await Product.products(for: productIdentifiers)
	if storeProducts.isEmpty {
		products = previewProducts
	} else {
		products = storeProducts
	}
} catch {
	products = previewProducts
}

共有コンテナ

App設定の「機能」には、共有コンテナに関する設定項目はありません。Apple Developerサイトにおいて、Certificates, Identifiers & ProfilesのIdentifiersのセクションでApp GroupのIDを登録し、FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:)を使用するコードを記載しましたが、コンパイルエラーにはならないものの、オブジェクトが取得できず期待した動作をしませんでした。
試しにAppStoreにアップロードし、TestFlightでiPhoneに配信して確認しましたが、同様でした。

コンパイルはできるが動作しないコード
guard let docDirURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedContainer) else {
	return
}
let fullURL = docDirURL.appendingPathComponent(dataFile)

iCloud

App設定の「機能」には、iCloudに関する設定項目はありません。共有コンテナと同様、ユビキタスコンテナの利用も出来ないと判断しています。

AppExtension

App設定の「機能」には、AppExtensionに関する設定項目はありません。他のAppとの連携機能やWidgetの開発は出来ないと判断しています。

2. Swift Playgrounds機能の出来ること、出来ないこと

Swift Playgroundsは、Swift言語を学ぶための特徴的なコンテンツやコードをコンテンツとしてシェアする機能など、プログラミング入門者向けの学習環境としてのイメージが先行しがちです。そのため、統合開発環境としての機能に関する情報はほとんどありません。本章では、Swift Playgroundsの開発環境としての支援機能や使い勝手について記載します。

エディタ機能

プログラムのコードを記載するエディタは、最も多用する機能です。Swift Playgroundsは、外部エディタを使用する機能はありませんので、内臓のエディタの使用感は生産性に直結します。

構文解析

コードを入力すると構文解析が行われ、予約語のハイライト、カーソルのある外側のブロックの表示、等がなされます。

ブロックの表示は、forやifなどのコントロールフロー、関数やコンピューテッドなプロパティ変数の宣言等で行われますが、クロージャの宣言では行われません。表現方法は異なりますが、配列も対応する開始と終了の括弧をハイライトします。enum型の宣言はブロック表示は行われません。

構文解析による入力の遅れはそれほど感じませんが、オブジェクトのメソッド名に誤りがあると、Appのグローバルスコープで探索を行うためか、場合によっては入力を全く受け付けなくなったり、最悪、Swift Playgrounds自体が落ちることがあります。Swift言語は、Stringなど言語に組み込みの標準的な型に対してもextension宣言でメソッド関数やプロパティ変数の追加ができるため、どこまでも探索範囲が広がることが考えられます。

また、メソッド名の誤り等で対応する宣言が見つからないと、場合によっては、リーゾナブルな時間ではタイプチェックができなかった、と表示して探索を停止することがあります。停止した場合には、どこでタイプチェックができなかったのか表示されず、探す必要があります。

この「リーゾナブルな時間」はSwift Playgroundsのバージョンによって異なるようで、4.1ではダンマリや落ちることが多い一方、4.2以降では止まることの方が多いように感じられます。筆者は、両方の環境を用意して、落ちると止まるの二者を使い分けています。

コメントは、色が淡色になり区別が容易になりますが、コメント化やアンコメント化の際にSwift Playgroundsが落ちる場合があります。

自動インデント

コントロールフローや宣言などで{}を入力すると、インデントが設定されます。コードのペーストや}を入力すると、インデントの深さが自動で調整されます。

物理的なキーボードを使用し、矢印キーでカーソルを移動する場合、行末からは、次行のインデントされた先頭文字に移動し、逆も同様の動作です。

先頭文字でバックスペースで削除操作をするとインデントが削除され、深さを変更することが出来ます。4.1までは先頭文字で削除操作をすると、前行末の改行文字が削除され、インデントの深さの調整が手動ではできませんでした。4.2以降は深さの調整はできるようになりましたが、カーソル移動とバックスペースの削除で、カーソルの動き方が異なるのは違和感があり、改善の余地があると言えます。

入力支援

シンタックスルール上、予約語、型名、関数名、変数名を入力する位置では、文字入力するとポップアップで、候補が表示されます。二文字目以降は絞り込まれていきますが、先頭一致ではなく、連続していなくてもヒットします。うる覚えでも入力できると言えますが、構文解析処理中はポップアップ表示に時間がかかり、出てこない場合があります。候補には、静的な定数オブジェクトも表示され、型が明確な場合には、enum型の要素も表示されます。

エディタ上で文字をタップすると、予約語、型名、関数名、変数名など、文字列全体が選択され、メニューが表示されます。「ヘルプ」を選択すると、定義やAPIの説明が表示されますが、直にカーソル移動して欲しい場合の方が多いと感じます。

型推論や省略記法など、Swift言語機能に関連した入力支援の動作については、前章で触れています。

プレビュー

コードを変更すると、即座にコンパイルが行われ、プレビューに反映されます。プレビューはiPhoneの比率に近い、コードとプレビューを同時に表示するモードと、iPadの比率に近い、全幅でプレビュー表示するモード、iPadの全画面を使って表示するモードが切り替えられます。4.1まではiPhoneの比率にかなり近い表示でしたが、4.2以降は実際の比率とは異なる表示になりました。iPhoneのポートレイト(縦持ち)で、ナビゲーションの画面遷移や操作が確認できる程度と割り切った方が賢明と言えます。

Appプレビュー(iPhoneに近いモード)Appプレビュー(iPhoneに近いモード)

Appプレビュー(iPadに近いモード)Appプレビュー(iPadに近いモード)

全画面を使用するモードでは、iPadのAppとしてインストールされたような形で起動し、Appの切り替えで使用するAppスイッチャー上にも個別のAppと同様に表示されます。AppコンテナはSwift Playgrounds上で動作する場合とは共有されません。

Appプレビュー(全画面モード)Appプレビュー(全画面モード)

4.2からPreviewProviderを宣言したビューを含むSwiftファイルを表示していると、プレビューの上部にボタンが現れ、そのビューのプレビューを直接表示できるようになりました。表示するまでに多段階の操作のが必要なビューの確認は楽になったと言えます。ただ、@Binding宣言した変数やenvironmentObjectで受け渡すオブジェクトの状態が、表示するまでの操作の中で設定され、表示が変わるようなビューの場合には、想定通りに表示されない可能性があります。

画面プレビュー(直接表示)画面プレビュー(直接表示)

プレビューは、一時停止することはできますが、エラーやiPadのスリープからの復帰で一時停止した場合は、リスタートしないことがあります。そのような場合は、Swift Playgrounds自体を再起動する必要があります(待てば再起動するのかは不明)。コンパイル(構文解析)を優先して欲しい場合もあり、プレビューの明示的な停止、確実な復帰機能の実装が待たれます。

コンソール

Appにprint文を入れると、print文に指定した文字列はコンソールに表示されます。コンソールは手動で内容を消去するまで、表示が累積されていきます。コンソールは、全画面のプレビューでも表示することができます。

コンソール(iPhoneに近いモード)コンソール(iPhoneに近いモード)

コンソール(全画面モード)コンソール(全画面モード)

通常、コンソール出力に文字列が出力されないことはほとんどありませんが、マルチスレッドで動作する場合なのか、コンソールに出力されない時がありました。

Swift Playgrondsには、デバッガはなく、ブレークポイントの設定や変数の中身の値の確認などはできません。

コードスニペット

ビューや関数宣言など、宣言に必要な予約語、型アノテーション、括弧などからなるスケルトンコードが登録されており、選択して貼り込むことができます。変数名などの箇所はプレースホルダーが配置され、同じプレースホルダーは自動的に同じ値を入れてくれます。

コードスニペットコードスニペット

ビューのスニペットは、4.1までは中身が空でしたが、4.2以降はNavigationSplitViewを含むものと、NavigationStackを含むものが、それぞれ用意されました。一方で4.1までのビューのスニペットにはプレビューのための宣言(PreviewProviderの宣言)が含まれていましたが、4.2以降は含まれなくなりました。

PreviewProviderは、@Binding宣言した変数やenvironmentObjectでのオブジェクトの受け渡しの記載が必要であるため、スケルトンコードの恩恵は少なかったと言えますが、プレビューのボタンが付くようになったことを考えると、チグハグな印象を受けます。

SF Symbols

ツールとして内蔵されています。種別分けされ、シンボルアイコンを見ながら選択することができます。image型を含めたコードを貼り込みます。検索して絞り込むことが可能です。

SF SymbolsSF Symbols

ケーパビリティ

App設定の「機能」で、予め用意されている中から選択して設定します。ケーパビリティ自体を追加する機能はなく、明示的に詳細を設定することもできません。

App設定の「機能」App設定の「機能」

写真、ミュージック、カレンダー、リマインダーなどへのアクセスを許可できます。カメラ、マイク、GPS、モーションセンサ等のiPhone/iPadのデバイス機能の利用の他、FaceIDの使用も許可できます。4.3では以下の機能を選択できます(Swift Playgrounds画面から筆者転記)。

機能名 説明
App Tracking Transparency AppやWebサイトを超えてトラッキングすることを目的に、エンドユーザに関するデータを収集し、そのデー夕をほかの会社と共有するために、AppでAppTrackingTransparencyフレームワークを使用する場合に必要です。
App Transport Security HTTP接続用にAppでデフォルトのセキュリティ設定を調整する必要がある場合に必要です。
Bluetooth BluetoothをAppで使用する場合に必要です。
Core Location(使用中) Appの使用中に、Appからユーザの位置情報にアクセスする場合に必要です。
Core Location(常時) 常時、Appからユーザの位置情報にアクセスする必要がある場合に必要です。
Core Motion Appからモーションデータにアクセスする場合に必要です。
Face ID Face IDを使ってAppで認証したい場合に必要です。
カメラ ユーザのデバイスに搭載されているカメラのいずれかをAppで使用したい場合に必要です。
カレンダー ユーザのカレンダーにAppでアクセスする場合に必要です。
ネットワーク接続(macOS) Appで受信したい場合や、ネットワーク送信接続を実行したい場合に必要です。
ファイルアクセス(macOS) Appからディスク上のファイルにアクセスしたい場合に必要です。
フォトライブラリ Appからユーザの写真ライブラリにアクセスする必要がある場合に必要です。
マイク Appからデバイスのマィクにアクセスする場合に必要です。
メディアライブラリ ユーザのiCloud ミュージックライブラリまたはApple MusicのカタログをApp で使用したい場合に必要です。
リマインダー Appからユーザのリマインダーにアクセスする場合に必要です。
ローカルネットワーク 直接または間接的にローカルネットワークを使用するAppはこの説明を含める必要があります。これには、Bonjour、Bonjourを使って実装されたサービス、ローカルホストに対する直接のユニキャストまたはマルチキャスト接続を使用するAppが含まれます。
写真ライブラリ(追加のみ) ユーザの写真ライブラリに対する追加のみのアクセスがAppで必要な場合に必要です。
近くのデバイスとの連携 近くの機器との連携"フレームワークをAppで使用する場合に必要です。
連絡先 Appからユーザの連絡先にアクセスする場合に必要です。
音声認識 Apoleのサーバを使って音声認識を実行する場合に必要です。

位置情報

App設定の「機能」において「Core Location(使用中)」、「Core Location(常時)」を追加すると、MapKitなどのAPIを使い、Appがデバイスの位置情報を取得できるようにできます。Appの初回起動時などに、常に位置情報を取得するか、Appの起動中だけにするか、といったプライバシー設定上の動作を確認するダイアログが表示されますが、この時表示される位置情報取得の目的に関する説明文を登録することができます。

App内課金

App設定の「機能」にApp内課金に関する項目はなく、ケーパビリティとして追加することはできません。Xcodeに付属しているAppStoreのテストツールであるStoreKit Testingも付属していません。App内課金、サブスクリプションの購入といった、AppStoreとのトランザクションの発生するAppを開発することはできないと判断しています。

ローカライゼーション

Swift Playgroundsには、ローカライゼーションの言語の指定や、Localizable.stringsファイルを作成する機能はありません。Localizable.stringsフォルダを手動で作成し、Localizable.strings(Japanese)ファイルを配置してみましたが、動作しませんでした。

LocalizedStringKeyを使用するAPIは、ローカライズ対象の言語が見当たらず、マッピング情報も見に行かないと考えられます。

Info.plistの編集

Swift Playgroundsには、明示的にInfo.plistを編集する機能はありません。App設定の「機能」で設定すると内部的にInfo.plistが生成されていると推測されます。「Supported interface orientations」などの設定もできないため、ポートレイト(縦持ち)/ランドスケープ(横持ち)の回転を制限し向きを限定することもできません。

iOSのターゲットバージョン設定

Swift Playgrounds 4.4からターゲットバージョンの指定ができるようになりました。

最小導入バージョンと互換性表示最小導入バージョンと互換性表示

最小導入バージョンの選択最小導入バージョンの選択

本機能はリリースされた直後であるため、筆者は、まだ、本機能を使用していませんが、ターゲットバージョンより高いバージョンのAPIを使用すると、検知してエラーになると推測します。

4.3では、iOS16で追加されたNavigationSplitView/NavigationStack、MultiDatePicker、Chartsなどを、iOSバージョンの判別処理なし使用するとエラーになり、バージョンチェック用のコードの挿入を促されました。

エラーメッセージエラーメッセージ

バージョンチェック用コードの挿入の提案バージョンチェック用コードの挿入の提案

Swift Playgroundsには、XcodeのGeneral設定に相当する機能は提供されていないため、今回実装されたiOSのターゲットバージョン以外は変更することはできません。

バージョン管理

Swift Playgroundsには、バージョン管理機能はありません。外部のバージョン管理と連携する機能もありません。プレイグラウンド(この場合は、Xcodeでのプロジェクトの意味。swiftpmファイル。)単位で複製する程度しかできません。

筆者は変更点の確認やAppStoreのリリース記録のためにGithubを使用しています(手動で同期)。

Apple IDによるサインイン

Swift Playgroundsでは、開発用に取得したApple IDでサインインすることが可能です。このApple IDは、Swift Playgroundsの動作しているiPadにおいて、サインインしているApple IDとは異なっていても問題ありません。

Apple IDによるサインインApple IDによるサインイン

AppStore Connect接続

Swift Playgroundsでは、App設定において、チームおよびバンドル識別子、バージョン番号、Appカテゴリを設定することで、AppStore Connectに接続することが可能です。「チームおよびバンドル識別子」において、Swift PlaygroundsにサインインしたApple IDを使用します。

App設定のチームおよびバンドル識別子設定App設定のチームおよびバンドル識別子設定

また、App設定において、Appのアイコンを設定することができます。

App設定のアイコン設定App設定のアイコン設定

3. 外でやっておかないといけないこと

Swift Playgroundsは、AppStore Connect接続などの機能はあるものの、Appleのエコシステムの一部でしかないため、それだけでは完結しません。Xcodeでも同様であると考えますが、AppleのDeveloperサイトなど、Swift Playgroundsの外で実施する必要のある手続き的な事項が存在します。

筆者は、macを所有していないため、iPadからのみAppleのエコシステムに参加しています。Swift Playgroundsに連携機能がないためにDeveloperサイトで実施した事項であっても、Xcode上であればシームレスに実施できた可能性があります。

Appを公開するまでの手続きで、Swift Playgrounds上では実施できなかった事項について記載しますが、調査が目的ではないので、手続きを再確認することはしていません。個々の操作方法や実施事項については、より細かく記載されたサイトが存在していますので、そちらをご覧ください。ルールや制度には疎い状態からスタートしていますので、より良い方法があればお知らせいただければ幸いです。

Apple Developerサイト

AppStore ConnectサイトにAppを登録し、バイナリをアップロードしたり、審査を受けたりするために必要なDeveloper Programへの参加登録等を行います。
Swift Playgroundsは、それ自体はiPadのAppの一つであり、インストールさえしてあればDeveloper Programへの参加なしに使用することが可能です。

Developer Program登録

Swift Playgroundsから、AppStore Connectに接続する際に、チームIDが必要になります。Apple Developer Programに登録すると、チームID(個人での参加でも同様)が発行されます。年間登録料が12,980円(2022/12/21時点)かかります。

筆者は後述のApple Developer App上でDeveloper Programのメンバーシップの購入をしましたが、Developerサイト上でも購入が可能なようでした。

AppID(Bundle ID)作成

Swift PlaygroundsからAppStore Connectに接続し、Appのバイナリをアップロードする際に、AppID(Bundle ID)が必要になります。AppIDは、Certificates, Identifiers & ProfilesのIdentifiersのセクションで作成します。Xcodeであれば、シームレスに登録できるかもしれませんが、Swift Playgroundsの場合は、サイトで予め登録しておく必要があります。

AppIDの登録時に、ケーパビリティも登録するチェックボックスがありますが、どのように機能するのか、Swift PlaygroundsのApp設定の「機能」とどのように関連するのか、については把握していません。

AppGroupID作成

AppGroupIDは、App間でデータを共有するグループコンテナを使用する際に使用します。AppGroupIDもCertificates, Identifiers & ProfilesのIdentifiersのセクションで登録します。Xcodeであれば、ケーパビリティの登録からシームレスに作成できる可能性があります。

登録はしたものの、Swift PlaygroundsではApp設定の「機能」に該当する項目がなく、共有コンテナは使用できませんでした。

Apple Developer App

iPhone/iPadのAppとして提供されています。Developer向けのさまざまなコンテンツにアクセス可能です。

メンバーシップの購入

個人としてApple Developer Programに登録する場合には、Developer App上で、Developer Programのメンバーシップの購入が可能です。

登録者の情報を入力し購入に進むと、Developer Appを起動したiPhone/iPadのAppStoreで購入処理が行われます。請求は、通常のAppStoreでの購入と同様、iPhone/iPadのApple IDにされますので、Developerメンバーシップ登録するApple IDとは異なっていても問題ありません。

AppStore Connectサイト[1]

Swift Playgroundsでは、App設定において、チームおよびバンドル識別子でチームIDとAppIDを設定することで、作成したAppをAppStore Connectサイトにアップロードすることが可能です。Appがアップロードされると、Appが登録され、Appレコードが作成されます。

AppStore ConnectサイトAppStore Connectサイト

App情報

AppStoreでのAppの配信を申請するためには、App情報、価格および配信情報、Appのプライバシーの情報を登録する必要があります。Swift Playgroundsにおいて、Appをアップロードするために、バージョン番号、Appカテゴリを設定しますが、これの情報はApp情報でも設定可能です(どちらか優先があるのか、後勝ちかは未確認)。価格および配信情報の価格情報は、文字通りAppStoreからAppを入手する際にかかる価格を設定します。筆者は、予め用意された価格から選択しました。配信地域の通貨での価格も設定されています。配信情報は配信する地域を指定します。App名や内部の表記が日本語であっても、配信地域として非日本語圏を含めることができます。Appのプライバシーの情報は、外部のWebページをホストするサービス等を利用し、プライバシーポリシーを作成したら、そのURLを登録します。配信地域は未登録でもAppの審査自体は承認されますが、公開が差し止められます。

Appにおいて、App内課金機能を提供する場合には、App内課金の情報を登録します。App内課金においては、製品ID(ProductID)を登録しますが、これはDeveloperサイトでの登録は不要です。製品IDは、App内課金アイテムを削除しても、使用したことのあるIDは再使用できないため注意が必要です。

Appのベータ版を配布して実機で試験をする場合には、TestFlightの情報を登録する必要があります。後述の「ユーザとアクセス」で予め登録したユーザをメンバとしたテストグループを定義し、内部テストの配信先として指定することが可能です。テストグループとは別に、個別テスターを設定することも可能ですが、未登録のユーザを配信先とする場合、テスト情報として、ベータ版App情報や、ベータ版App Review情報の登録が必要となります。

ユーザとアクセス

TestFlightでテストグループに登録するユーザなどを登録します。ユーザはApple IDを保持している必要があります。App内課金やサブスクリプションを販売する場合、AppStoreで試験をするためのユーザとして、Sandboxテスターを登録することが可能です。

TestFlightをインストールしたiPhone/iPadでは、設定のAppStore設定において、SANDBOXアカウントの登録欄が表示されます。Sandboxテスターを設定しておくことで、TestFlightで配信したベータ版のAppにおいて、App内課金やサブスクリプションの購入時の動作に関する試験をAppStore上で実施できるようになります。

契約、税金、口座情報登録

AppStoreでは、価格面から分けると有料Appと無料Appの2種類が公開できます。本情報は、有料Appを公開する際に登録が必要となりますが、無料Appを公開するだけであれば登録は不要です。Appの審査自体は、本情報を登録しなくても承認されますが、App情報で価格情報を設定していると本情報の登録が完了するまで公開が差し止められますので、注意が必要です。

契約は、契約のタイプとして、有料App、無料Appがあり、それぞれ必要に応じて、開発者がAppleと契約します。筆者は先に無料版を作成してAppStoreに公開しましたが、この時点では、無料Appの契約しかしていませんでした。後に有料版を公開しましたが、Appの審査は通ったものの、有料Appが未契約であったために公開されない、という事態が発生しました。

税金は、Appleが米国の企業であるため、W-8BENと呼ばれる免税申請を行います。有料Appの収益に対しては、日本の所得税がかかりますので、二重課税を避けるために行うようです。有料Appの契約と同様、税金の申請が未実施であったため、公開されない事態になりました。

口座情報は、有料Appの売り上げの振り込み先の金融機関の口座を登録します。これも無料Appでは未登録でも問題ありませんが、有料Appの場合には事前に登録しておく必要があります。筆者は途中まで登録した状態で完了していない状態でした。

TestFlight App

ベータ版の試験をする際に、配信を受ける側のiPhone/iPadにAppStoreから入手しておく必要があります。AppStore ConnectのTestFlight情報を登録する際に、ベータ版のテスターにどのように通知するか指定します。メールの場合には、ベータ版の試験をするiPhone/iPadでメールを受信し、メール内のリンクをタップするとTestFlight Appにベータ版の配信情報が登録されます。AppStore Appに近い操作でiPhone/iPadにベータ版をインストールできます。

TestFlight Appでは、不具合の報告ができ、ベータ版Appのスクリーンショットをコメントと共にAppStore Connectに送信することができます。

また、不具合を改修して再配信すると、結果として複数のベータ版を配信することになりますので、最新のベータ版だけでなく、過去のバージョンに遡ってインストールし直すことが可能です。

Keynote App

KeynoteはiPad上で動作するAppleのプレゼンテーションソフトですが、スライドサイズを指定することができ、イメージでの書き出し機能があるため、Swift PlaygroundsのApp設定で登録するAppのアイコンや、AppStore ConnectのApp情報に登録するスクリーンショットの作成に使用できます。

Keynote アイコン作成Keynote アイコン作成

Keynote スクリーンショット作成Keynote スクリーンショット作成

Keynoteは、オブジェクトのサイズ、角度、位置を数値で指定することが可能であるため、複数のスクリーンショットを準備する場合に、予め構成を決めてスライドを複製し、画面ショットを数値指定して、サイズや位置合わせをする、という作り方ができます。

また、Keynoteは、スライドをムービーで書き出すことで動画として保存することが可能です。しかし、AppStore ConnectのApp情報のAppプレビューへの動画登録を試みましたが、形式が合っていないとエラー出力され公開に含めることはできませんでした。ただ、iPhoneの画面収録機能で保存した画面操作の動画も同様にエラーになったので、動画のどの属性が原因でエラーになったのか不明です。

スライド間に切り替えのアニメーションを設定し、自動でスライドを切り替える設定をした上で、ムービーで書き出すことで作成した動画は、YouTubeにアップロードしています。AppStoreでAppを公開するにあたり、Swift Playgroundsの次に多く使用したソフトウェアであると言えます。

外部サイト

AppStore ConnectでAppを公開する際に、インターネットからアクセスできるコンテンツを準備する必要があります。Appleはエコシステムの一環としてさまざまなサービスや素材を提供していますが、Webサイトのホスティングは提供範囲に含まれないため、開発者が自身でサイトを準備する必要があります。特に制限などはないため、一般的なブログサイトやGithubなど、必要なコンテンツが保持できれば審査は通ると考えています。

プライバシーポリシー

Appを公開する際に登録するApp情報において、プライバシーポリシーを記載したWebコンテンツのURLの登録が必須になっています。プライバシー情報の取り扱いに関するポリシーの他に、免責事項を含める場合があります(筆者も同様)。

【イマココデコレオ(+)共通】プライバシーポリシー

開発者情報

App内課金機能を含むAppや有料Appを配信する場合であると推測していますが、開発者情報への到達が求められます。独立した連絡先のページを作成してURLを登録しました。

【イマココデコレオ(+)共通】連絡先

筆者は、App内課金機能を含むAppを申請した際に、当初、運営するWebページ(ブログサイトを利用)のトップページのURLを登録していました。トップページは、開発者情報は記載していましたが、それをメインのコンテンツにはしていない状態でした。

到達できない、というコメントと共に、申請が却下されました。開発者情報として、当初、筆者のわかりやすくすることが求められていると判断し、連絡先のページを作成してURLを登録したところコメントがつかなくなりました。

その他

筆者は、PCは、Windowsを使用しています。iPhone/iPadのAppを作成するにあたり、コーディングにはiPad上のSwift Playgroundsを使用していますが、できる限りiPadだけで完結できないかと考えながら進めてきました。ほぼ達成できている状況ではありますが、それでもPCを使用せざるを得ない場面がありました。

dmgファイルの解凍

AppStore ConnectにApp情報を登録する際に、スクリーンショットを登録します。スクリーンショットと言っても、iPhone/iPadの画面キャプチャをそのまま使うことは少なく、多くの場合、縮小してiPhoneのベゼルの内容として縮小した画面キャプチャを表示し、メッセージや説明語句を入れます。

スクリーンショット自体は、Keynoteで作成し、iPhone/iPadのベゼル画像は、Apple DeveloperサイトでAppleが配布しているものを使用しました。

この配布ファイルの形式がdmgで、iPadでは解凍する方法が見つかりませんでした。最終的にWindows PC上のツールで解凍し、iPadにベゼルの画像ファイルを持って行きました。

終わりに

筆者のSwift PlaygroundsでのApp開発経験に基づき、Swift Playgroundsで出来ること、出来ないことを記載しました。

Swift Playgroundsは、カメラ、マイク、GPS等のiPhone/iPadのデバイス機能の利用や、写真、ミュージック、カレンダー、リマインダーなどへのアクセスはできるので、ローカルで完結するようなAppの開発は可能ですが、WidgetなどのApp間の連携機能や、iCloud、AppStoreなど、ネットワークを介したAppleのサービスを利用するAppの開発には向かないと言えます(今後、理解が進むと変わる可能性はあります)。

Swift Playgroundsは、このような制限はあるものの、それらを理解して使用すれば、とても手軽にApp開発ができる強力なツールです。iPadでApp開発をする一助になれば幸いです。

参考情報

読まなくていいよ

筆者が手元で書いていた開発日記から参考情報を作成する予定でしたが断念しました。そのまま、貼ります。文章の記載で、筆者の解釈に関する部分は、誤っている可能性があります。ご容赦ください(その後、理解が進み、解釈が変わった箇所もありますが、そのまま記載しています)。

【2022/08/21】

昨日から何か新しいことをやろうと思い立って、また付属のSwift Playgroundsをいじり始めた。今まで、一通りのプレイグラウンドは触って、子どもたちにもやらせたりしていたけれど、Swiftのバージョンが5.5になり、以前のプライグラウンドから変わっていた。

Appギャラリーの「予定表」を色々眺めて見ることにした。@Published、ObservableObjectなど、更新時のレンダリングを自動でやってくれそうな仕組みの片鱗が見えて、よく出来ている感じ。ただ、文法がよくわからない。これでは、せっかくのPlaygroundsのサンプルを活かせない。どうしたものか。

classとstructの使い分けもよくわからない。機能の違いを一生懸命説明しているサイトはあるが、根源的にSwiftの設計者がどのように意図して2つを用意したのか。意図に沿うとどういう使い分けになるのか。とりあえず、以前から開いていたSwift.orgの言語ガイドに目を通すことにした。

The Basics
https://docs.swift.org/swift-book/LanguageGuide/TheBasics.html

基本(Basics)の1/3がオプショナルに割かれている。型推論とかに絡んでいるのか、大ごとになっている感じ。要するに定義した変数に値が入っていない場合があるなら、折り込もうってことね。オプショナルでwrapするってのがあるか無いか不明な状態で、値が入っているならunwrapして使いなさい、ってのはその通りだけど、なかなかにやってみないと使いこなせるかわからない。

日付も変わるのし、そろそろ疲れたので、今日はこれでおしまい。

【2022/08/22】

Swift言語ガイドの続き。

オペレータはそんな新しいものないか、と思っていたら、意外に面白かった。オプショナル関連のnilチェックとか、範囲のオペレータとかは興味深い。

Basic Operators
https://docs.swift.org/swift-book/LanguageGuide/BasicOperators.html

コレクションは、サッと見る感じにしたけど、Javaでコレクションフレームワークが一般的になって、基本的なところは言語仕様に近いところに取り込まれるようになってきたか。 Dictionaryのリテラルは面白い。

Collection Types
https://docs.swift.org/swift-book/LanguageGuide/CollectionTypes.html

コントロールフローは制御文。コレクションの存在が前提になっってきて、foreach的な処理が前面。switch文が、オプショナルと範囲のオペレータを組み合わさって、色々できる。まぁ、自分の好みの書き方に落ち着くんだろうけど。

Control Flow
https://docs.swift.org/swift-book/LanguageGuide/ControlFlow.html

関数。returnの省略とか、引数のラベルはよくわからないし、さらに省略もあるともう。タプルを使った複数値のリターンはいいとして、タプルにオプショナルが組み合わさると訳がわからない。可変長もあるし。インアウト引数とかタプルも、微妙にオブジェクトの参照渡しを隠蔽している感じ。関数タイプが登場して、クロージャも出てくるよね、と。

Functions
https://docs.swift.org/swift-book/LanguageGuide/Functions.html

クロージャは、使いこなせばすごく便利なんだろうけど、そのために、型推論できるところは省略しまくれるようになっている。あとで読んでわかるんだろうか。

Closures
https://docs.swift.org/swift-book/LanguageGuide/Closures.html

ちょっと疲れて、とりあえず文字の時計のAppのサンプルが無いか探してみた。Xcodeや古そうな実装のものが多いが、参考にしたいと思えるものを見つけた。コンバインを使う、というのでどういうことなんだろう、と思ったら、期待通りの実装。

SwiftUIで超簡単に時計アプリを作る
https://note.com/taatn0te/n/n74bd932b0704

Javaのコンカレントフレームワークは知らないけど、古い実装だと、runnable インタフェースを実装して、スレッドの中で1000ミリ秒休んで文字列を書き換えて、みたいになるけど、コストがかかると考えていた。

文字列を書き換えるコールバック関数を共通的なスケジューラに登録して、1秒ごととか間隔を設定して呼び出してもらう、みたいな処理がいいよな、と考えていたら、本当にそういう実装だった。スケジューラで処理を束ねるのでコンバインか。

Publish & Sbscribeのデザインパターンを知っていると、そうだよね、という感じではあるけど。非同期処理のフレームワークとしては、jQueryのDeferredが鮮烈だった。あまり使いこなせななったけど。

しかし、onReceiveやTimerのドキュメントがAppleの開発者サイトでなかなか行き着けない。Googleで検索すれば出てくるので、結果からはどのフレームワークのどのAPIコレクションにあるかはわかる。なかなかしんどい。onReceiveはViewで宣言されているようだが、TextはViewを継承しているのか、実装しているのか、Java Docのように追えないのでわからない。

【2022/08/26】

時計Appの続き。

ボタンを追加することにした。ボタンを押した時の動作は特に定義せず、アイコンとテキストを貼った。Button {} label: {}という記載だけでも、色々省略されてこの表記になっているのだろうけど、何がどう省略されているのかも理解できない。

Button
https://developer.apple.com/documentation/swiftui/button

次に、過去にHTML5とBootstrapで作ったアプリを思い出し、3値から1つを選択するインタフェースを作ることにした。

HTMLだとグループ化したラジオボタンになるので、「ラジオボタン」や「ボタングループ」というキーワードでGoogleで調べたがなかなか引っかからない。Xcodeでの実装の仕方はあっても、Swiftの実装が出てこない。

Swift Playgroundsのサンプルの「予定表」でViewを生成する際に、enumでフィールド名を定義して、ForEachで回す、ということをしていた。とりあえず、ボタン名のenumを定義した。しかし、Javaでもenumはあったはずだが、C系言語でこのかた使ったことがない。Swiftだとこう書くのか、という感じ。

Enumerations
https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html

さらに、ForEachがわからない。swiftの言語ガイドの制御文にはfor inしかない。普通に考えればコレクションや配列のメソッドで、処理を記載すれば、要素の数だけ繰り返して実行されるようなイメージだ。だけどサンプルをよくよく見ると、enumは引数になっている。いくらswiftでもそういう記法はあるまい、と思って見ていたら、SwiftUIのViewの一種で、コレクション等から複数のViewを作成して保持するコレクションコンテナというものだった。まさかViewの名前がForEachだったとはね。

ForEach
https://developer.apple.com/documentation/swiftui/foreach

しかしラジオボタンの作り方が出てこない。そう言えば、通常、iOSだとどんな外観になっているか。3値からの選択であれば、ボタンが3つ並んで、そのうち1つだけが押せるようなインタフェースだ。そもそもラジオボタンというキーワードが違うのか。チェックボックスもiOSなら外観はトグルだし。

SwiftUIのAPIドキュメントを見ていたら、Pickerが目に留まった。複数の選択肢から選ぶんだからPickerってはわからなくもないけれど、値のピッカーと色のピッカーと日付のピッカーでは、UI実装の規模がかなり異なる。これが並んでいる、というのは粒度の点でかなり違和感がある。

とは言えこれだ。enumから作成する方法が丁寧に書かれている。文法もライブラリもよくわかっていないので、「$」付きの変数が出てきたり、CaseIterableやらIdentifiableやらキーワードが出てきたりすると、いちいち調べないとわからない。

Picker
https://developer.apple.com/documentation/swiftui/picker

Var id: Self {self}の「Self {self}」はどういう意味なんだろう。

【2022/08/27】

var id: Self {self}はどういう意味なのか。ちゃんと知っておいた方が後々の理解が早い。

Selfは自分自身の型を指すキーワード、selfは自分自身のインスタンスを指すキーワードだった。selfは、Javaとかだとthisか。

今回、変数idは、enumの中で宣言した。

enum宣言
enum EventType: String, CaseIterable, Identifiable {
    case Start = "Start"
    case End = "End"
    case None = "None"
    var id: Self {self}
}

先に{self}の部分は、変数宣言の後の{}は普通に使うようなので、逆に何なのか調べてもなかなか出て来ない。どうも、プロパティのgetter/setterの宣言で、Read-Only Computed Propertyだと、getなし表記ができ、さらにreturn selfのreturnが省略されている、という理解に行き着いた。{return self}でもエラーにはならない。

Properties
https://docs.swift.org/swift-book/LanguageGuide/Properties.html

var id: Selfの: Selfは明示的な型を示すアノテーションで自分自身の型を指すから、EventType型になる。

Types#Self Type
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/types/#Self-Type

Pickerを生成するのに、enumで定義した値のcaseの数だけForEachを回す実装では、enumはイテレーションするためのCaseIterableと識別するためのIdentifiableのプロトコルに適合しないといけない。Identifiableに適合するためには、idプロパティが必要になる。

Identifiable
https://developer.apple.com/documentation/swift/identifiable

「予定表」で、var id = UUID()という記述があったので、そのままペーストしたら、enumはStored Property (値を保持するプロパティ)は含められない、とメッセージが出た。Computed Propertyじゃないといけないらしいが、enumの意味を考えるとその通りだ。

var idだけでもStoredになる。var id {self}にすると、Computed Propertyは明示的な型の宣言が必要、と言われる。selfは、EventType型に違いないから、var id: Self {self}は全く妥当だ。

それならば、と、var id {UUID()}としてみたが、これも明示的な型の宣言が必要と言われる。UUID()は、UUID型を返すので、var id: UUID {UUID()}は、Swift Playgroundsの文法チェックは通るは通る。idを参照するごとに、毎回IDを生成するから、意味があるかどうかは別の問題。実際、Pickerは表示はするが選択操作はできなかった。

UUID
https://developer.apple.com/documentation/foundation/uuid

【2022/09/15】

過去に作成したJavaScriptで記載したHTML5アプリを見ていて、ため息が出てきた。いろいろ考えたものだ。とてもではないが、今作成したら思いつかない実装をしている。その一つがブラウザでのデータの保存のしかたで、windows.localStorageを使用している。key-value方式で、key値にDateから生成した日付と日時の文字列を使っている。valueの側にDateを入れていないというのが想定外だった。

Swiftではデータの保存をどうするか調べると、流儀があるらしい。AppStoreに出す審査において、ルールに則っていることが求められる、ということだ。ルールに沿えば、iCloudに保存してくれる、とか自動的に新しいテクノロジを使ってくれるかもしれない。

File System Programming Guide
https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/

Swift Playgroundsでどう使うかを見ていくと、配列などで保持するアプリのデータを永続化するには、Core Data、Realm、SQLiteあたりが使えるらしい。ただ、Swift PlaygroudsだとRealm、SQLiteはObjective-Cで書かれており使用できず、Core Dataになる。JavaScriptのlocalStorageほど使い易くなさそう。

Core Data
https://developer.apple.com/documentation/coredata

【2022/09/16】

Core Dataは、Xcodeでの使用が前提で、Xcodeだとスキーマエディタが付属している。これを使用せずに、内部的にXcodeが生成している?処理をSwiftでは手動で記載しないといけない、ということだ。とはいえ、手動でも書けば使えるのはありがたい。

【iOS】デバイス(ローカル)にデータを保存する方法
https://qiita.com/shiz/items/c7a9b3218269c5c92fed

【Swift】コードのみでCoreDataを実装
https://qiita.com/SNQ-2001/items/61cef2153aad5c4d542f

ただ、これを毎回、手で書くのはやりたくないので、それこそエクセルとかを使って、セル関数でSwiftのコードを生成して、Swift Playgroundsのコードエディタに貼り付けるのがいいか。

Core Dataの定義を単純化するために、JavaScriptのWindow.localStorageに倣うと、一意のインデックスと1つ文字列の2フィールド構成にして、文字列フィールドにJSONで格納することが考えられる。JSONからデコードしてオブジェクトを取り出す処理の責任は負わないといけないけれど、定義は使い回しができるかもしれない。

SwiftでJSONを使う方法が無いか探したら、Foundationにあった。JavaのSerializableインタフェースでシリアライズ処理を自分で実装したことはないけど、今やJSONが主流か。

Archives and Serialization
https://developer.apple.com/documentation/foundation/archives_and_serialization

【2022/09/18】

変数の中身を参照する際に$付きの場合がある。これがどういう時なのか、なかなか出てこなかった。@Stateを付けて宣言した変数は、$を先頭に付けて参照する。@Stateをつけた変数は、更新されるとSwiftUIが自動的にレンダリングし直してくれる。UIの更新処理を自分で呼び出さなくていいのは楽ちんだ。

State
https://developer.apple.com/documentation/swiftui/state

ただ、この@をつけ$で参照する仕組みは、Stateに限定されているものではなく、プロパティラッパーとして、Swift言語で定義されている。StateはSwiftUIがプロパティラッパーの機能を用いて実装したものであって、$は射影値を参照することを意味していた。これはなかなか深淵だ。経験を積めば、また見えて来るものがあるかもしれない。

Properties
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/

あと、Viewを定義していて、someというキーワードが気になった。これは何なのか。Viewのプロトコルに準拠した何かが定義されていることはわかるが、文法的にはsomeは無くても動作しそうに見える。 Swift言語的にどういう機能があるのか。

Viewのsomeキーワード
struct ContentView: View {
   var body: some View {
   }
}

調べてみると、これは、Opaque Result Typeというものだった。someを使うことで、一見するとプロトコルと同じような記載量で、内部的にはジェネリックの動作になり、コンパイル段階で型が決まるということのようだが、どうも示されている例がよくわからない。内部の実装を晒すのはよくない、という問題からスタートしているが、英語と日本語のロジックが違うせいか説明が入ってこない。

Opaque Types
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/opaquetypes/

言語ガイド、言語リファレンス、AppleのAPIドキュメント、どこに何が書かれているか、やっと少し探し方がわかってきたような。

【2022/09/19】

Swiftの基本的な文法を見た後、SwiftUIを使うと、やはりいろいろと読めない部分が出てくる。別に読めなくても、呪文として受け入れればいいのかもしれないが、どの部分かによる。読めなければ書けず、結局、実装に制約が出てくる。

正直、まず最初に出てくる以下で、これはどういう構文なんだろうか、となってしまう。ただ、これは、変数bodyに関する getter宣言で、returnまで省略された形、であれば理解できる。

SwiftUIのよくわからない構文
struct ContentView: View {
   var body: some View {
	Text(Hello World.)
   }
}

以下のVStackの中で、Textのインスタンスが複数並んでいるのはどういう構文なんだろうか。

VStackの中に複数のTextが並ぶ構文
struct ContentView: View {
   var body: some View {
        VStack{
            Text(Text1)
            Text(Text2)
            Text(Text3)
        }
   }
}

どうもこれはFunction Builderという公開されていない仕様を用いた、Swiftの中でもあまり見られない(記載自体は多用するが)、特殊な実装らしい。

SwiftUIにおけるVStack,HStackとFunction Builderの関係
https://blog.personal-factory.com/2019/06/12/relationship-between-vstack-hstack-and-function-builder-in-swiftui/

予め10個まで予約され、列挙されたViewがViewBuilderのbuildBlock関数の引数として渡される。このような実装のため、個数に上限があるということか。buildBlockの中で、モジュールのコレクションにappendされているのだろう。また、ForEachを使うと列挙の仕組みが変わるので10個という上限がなくなるのだろう。これは呪文として受け入れる部分だ。

ViewBuilder
https://developer.apple.com/documentation/swiftui/viewbuilder

【2022/09/25】

少し複雑なことをしようと思うとViewが複数になり、連携する必要が出てくる。これはどうすればいいのか。このような場合に使用するのがEvnironmentObjectのようだ。親ビューでenvironmentObjectメソッドで共有するデータを格納するオブジェクトを登録すると、子ビューで@EnvironmentObjectを付けて変数を定義することで、親ビューで登録されたオブジェクトを参照できる。

EnvironmentObject
https://developer.apple.com/documentation/swiftui/environmentobject

オブジェクト、参照ということから容易に想像がつくが、データを格納するための器は、structでは無く、classで宣言する。また、オブジェクトはObservableObjectプロトコルに準拠する必要がある。

ObservableObject
https://developer.apple.com/documentation/combine/observableobject

親ビューのオブジェクトストアに登録され、子ビューからは参照経由で見にいくような動作だが、@EnvironmentObjectを宣言した際には、型しか紐付ける要素がない。同じ型のオブジェクトが登録されていないと、その型のオブジェクトが見当たらない、とエラーが出る。environmentObjectはインスタンスを登録し、同じ型のインスタンスを複数登録してもエラーにはならないが、@EnvironmentObjectでは最初に登録したオブジェクトへの参照としか紐付けてくれない。

複数のView間でデータを共有すると、さらに@Stateのように、値の更新を監視して自動的に再レンダリングさせたい、という要求が出てくる。これも仕組みがちゃんとある。親ビュー側でインスタンスを作成する際に、@StateObjectを付加しておく。こうすると、インスタンスを更新の監視対象にできる。

StateObject
https://developer.apple.com/documentation/swiftui/stateobject

また、オブジェクトのフィールドを監視対象にする場合、オブジェクトのフィールド定義に@Publishedを付加する。インスタンスに対する@StateObjectと、オブジェクトのフィールドに対する@Publishは対象が異なるし、@StateObjectと似たものに@ObservedObjectというものもあるし、この辺りの使いわけはイマイチわからない。

Published
https://developer.apple.com/documentation/combine/published

一方で、あるViewで定義されたローカル変数を他のViewで更新できるようにする方法もある。変数を定義した親Viewから、子のViewを生成する際に引数で渡し、子のViewでは@Bindingを付けて変数宣言する。子のViewでは、暗黙的にコンストラクタが定義され、@Bindingを付けた変数の初期値として渡すことが必要になる。@Bindingが展開されると、Bindingオブジェクトでラップされ参照渡しになるのか。

Binding
https://developer.apple.com/documentation/swiftui/binding

@Bindingを付けた変数を、さらに孫のViewでも@Bindingしておいても受け取っても、きちんと動作した。親Viewでは@Stateを付けて宣言する例しかなく、自動的に再レンダリングされているのはわかりやすいが、@Stateは必須なのかよくわからない。

@Stateに加え、@Bindingや@EnvironmentObjectなどは、ちゃんと定義しておくと、当然ながらとても綺麗に動作するが、理解しにくい部分もあった。特にわかりにくいのが、Swift Playgroundのテンプレートにコードが含まれるPreviewProviderに起因したエラーで、本体の定義には問題が見当たらないのに、PreviewProvider側の記述不足でエラーが発生した。

PreviewProvider
https://developer.apple.com/documentation/swiftui/previewprovider

PreviewProviderでViewのオブジェクトを生成する箇所でも、Bindingオブジェクトは引数で渡さないといけないし、EnvironmentObjectはenvironmentObject関数を呼び出してViewに登録しておくことが必要になる。PreviewProviderのpreviews変数はstaticなので、引数で渡すオブジェクトもstaticで宣言しておかないといけない。こういうのは親切なサイトから情報が得られる。インスタンスを渡すオブジェクトは、とりあえず生成して渡せば動作するようだが、これでいいのかよくわからない。

PreviewProviderに関するエラーに対処した箇所
struct EventList_Previews: PreviewProvider {
    @State static var event = Event()
    static var previews: some View {
        EventList(newEvent:$event)
            .environmentObject(ConfigValues())
            .environmentObject(EventData())
    }
}

@Bindingのpreviewについて
https://thwork.net/2020/04/16/test/

複数のViewを遷移するための機能として、NavigationLinkがあるが、これが意外に使いにくい。 TextとImageを使ってボタンのようにラベル表示され、タップするとdestinationで指定したViewに切り替わる。NavigationLinkは、可視なラベルを表示した状態で定義すればタップするとすぐに遷移するし、isActiveのフラグでプログラマブルに遷移させることもできる。NavigationLinkの定義では遷移先のViewの生成はできるが、タップした際の動作の定義はできない。動作を定義する場合、遷移先のViewのonAppear関数が使えるかもしれないが、Viewの生成前に動作させたいような処理やタップに付随する処理は書きずらい。

NavigationLink
https://developer.apple.com/documentation/swiftui/navigationlink

NavigationLinkのラベル表示にEmptyViewを指定することで不可視Viewとして生成しておき、動作はButton側で定義して、最後にisActiveをtrueにすることで遷移させる方が使い勝手がいい。不可視Viewで定義してもListに含めるとブランクで表示されてしまうので、VStackなどで囲み、Listの外に置く必要があった。

EmptyView
https://developer.apple.com/documentation/swiftui/emptyview

従来のNavigationViewはDeprecatedになり、iOS16からはNavigationStackとNavigationSplitViewに移行する。NavigationViewでのNavigationLinkは行先のViewを指定はするものの、特にiPadではそのViewがスプリットされたどのペーンに割り付けられるか、制御できるのかもしれないが、指定のしかたがわからなかった。

NavigationView (Deprecated)
https://developer.apple.com/documentation/swiftui/navigationview

iPhoneとiPadの構成の違いもよくわからない。iOSでは遷移すると遷移前のViewは非表示になるが、iPadでは最初のViewは左右にスプリットされた左のペーンに残り、右側の幅広のペーン内で遷移する。iPadで最初のViewも切り替えたい場合は一工夫必要になる。思ったようにViewの切替動作をさせるのは結構手間がかかる。

Navigation
https://developer.apple.com/documentation/swiftui/navigation

とりあえず、Swift Playgroundsのプレビュー表示で、標準の?NavigationLinkのdestinationで指定できる範囲で問題が無いように作ることにした。Swift Playgroundsのテンプレートでは、Scene、WindowGroup、NavigationView、Viewの階層で構成されている。View以下を自分で定義すれば、NavigationViewの流儀で遷移してくれる。NavigationView辺りの階層で複数のViewを宣言し、遷移先のペーンとして指定すればいいのかもしれないが、ハードルが高い。

WindowGroup
https://developer.apple.com/documentation/swiftui/windowgroup

Darkモードなどもあるので、勝手に色の定義をすると、統制を欠くアプリになる。以下のようにシステムの規定色を取得するのが良さそう。このての情報は当たり前なのか、なかなか出てこない。

システムの規定色を使う
.foregroundColor(Color(UIColor.systemBackground))

How to set background color?
https://developer.apple.com/forums/thread/117455

ライブラリや標準でも利便性のために拡張してしまえ、というのは斬新だった。Date型にメソッドを追加。どの範囲で有効なスコープになるのだろうか。

既存の型の拡張
// Convenience methods for dates.
extension Date {
    var formattedDataString: String {
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "ja_JP")
        dateFormatter.dateStyle = .medium
        dateFormatter.dateFormat = "yyyy/MM/dd"
        
        return dateFormatter.string(from: self)
    }
}

Extensions
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/extensions/

【2022/09/27】

iPadで左右にスプリットされた画面の構成は、MasterDetailと呼ぶようだ。NavigationViewで実現する方法はDeprecatedになっているのでわかりにくいが、コンストラクタに説明があった。NavigationViewのContentに2つのViewが設定でき、1つ目がMaster、いわゆるリストが並ぶ左のペーンで、2つ目がDetail、リストを選択すると詳細を表示する右のペーンになる。

NavigationView#init(content:) (Deprecated)
https://developer.apple.com/documentation/swiftui/navigationview/init(content:)

もともとiPhoneしか存在しないところにiPadが追加になった名残なのか。NavigationSplitViewとNavigationStackで整理されてよくなったか。どちらもiOS/iPadOSの16以降なので、15環境だと動かないか。今度試してみよう。

【2022/10/01】

SwiftUIでListに対して、ForEachでString型の配列を回して、Textなどを追加しようとすると、StringがIdentifiableに適合していない、というエラーが出る。配列の中身が自分で宣言したStructであれば、予めIdentifiableに適合させてIDプロパティを定義しておけばいいが、プリミティブ型のStringだとそういうわけにはいかない。

SwiftUI Initialzier requires String conform to Identifiable
https://stackoverflow.com/questions/67977092/swiftui-initialzier-requires-string-conform-to-identifiable

以下のように「id: \.self」という書き方が使えるが、この「\」は何を意味するのか。

Stringの配列をForEachで回す
ForEach([Text1,Text2,Text3], id: \.self) {
}

これは、Key-Path式というものだった。type(of:)とかでStructの型は取れるが、さらにその先のプロパティなどの値をパス形式の記述で動的に取得する仕組み、ということのようだ。

Key-Path Expressions
https://developer.apple.com/documentation/swift/key-path-expressions

プロパティの情報を得る際に、「\」から開始する構文を使用するので、その「\」だった。

Key-Path Expression
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/expressions/#Key-Path-Expression

ForEachのid:はKey-Pathで、どのプロパティをIDとして使用するか、示すために使用する。ただのselfだと、インスタンスが自分自身を指すことになるが、Key-Pathで.selfとする場合、ClassやStructなどオブジェクトの定義自身を指すのか。なかなか難しい。

【2022/10/02】

Swift Playgroundsのサンプルの中で、Image(systemname: “xxxxxx”)という指定のしかたで画像を表示していた。これで指定できる画像にはどんなものがあるのか。

これはSF-Symbolsなるもので、Appleが提供しているライブラリらしい。4000近くのアイコンが用意されているということで、是非、使用したいが、さすがにこれだけの数になると、一覧を見ながらでないと選びようが無い。

SF Symbols
https://developer.apple.com/design/human-interface-guidelines/foundations/sf-symbols/

一覧はアプリになっていてAppleのサイトからダウンロードできるらしいので行ってみたらMacOS版しかない。Windows版があれば普段調べる際にはとても楽だし、そもそもiPadでSwift Playgroundsで開発する場合には、iPad版が無いとどうしようもない。

SF Symbols 5
https://developer.apple.com/sf-symbols/

何か手はないものか探したが、Webで検索しても一向にiPad版の話は出てこない。と思っていたら、すでにSwift Playgroundsに組み込まれている、という記載を見つけた。Swift Playgroundsの上のバーの+マークでコードスニペットを呼び出し、ピッカーで丸星を選ぶとシンボルが並んでいた。タップすると、Image(systemname: “xxxxxx”)を丸ごと貼り込んでくれる。組み込まれているのだから、探すまでもない。どうりで検索しても出てこないはずだ。

【2022/10/07】

TextFieldとMenuを組み合わせてコンボボックスのようなインタフェースを作成した。これは、以前にjQuery + HTML5でアプリを作っていた際も使用したアイディアで、SwiftUIでも同じようにして再現することにした。

Menuを使わず、Pickerをメニュー型で使用しても同じようなことができるが、Pickerを使うとメニューに選択済みのチェックマークを付けることができない。ライブラリで提供されるUI部品は、作り込まれていたとしても、希望にそぐわない時が得手してある。

コンボボックスなので、直接入力もできるが、メニューにプリセットされた値も選べるようなUIにしている。チェックの付いていない項目をメニューで選ぶと、コンボのテキスト部分に半角空白文字を区切り文字として分かち書きで末尾に追加する。

テキスト部分にメニューの中身が含まれていると、メニュー側にチェックが付く。複数の中身が含まれていれば複数のチェックが付く。チェックの付いた項目をメニューで選択すると、テキストから削除する。

チェックの動作は、jQuery版の時はあまりよく考えず、チェックは最後に選択した項目につけていたし、チェックの付いた項目を選択した場合も追加にしていた。よくよく考えると動作がおかしい。

テキストにメニューの文字列が含まれるか含まれないかは、単純に文字列のマッチで判断している。テキストを編集して、同じ文字列を複数含めた場合や文字列の一部が重なるような場合は、削除する動作では最初にマッチした部分が対象となる。この辺りは、どういう動作なら正解、というのがあるわけでもないが少し癖があるかもしれない。

Swiftでの文字列マッチは、containsメソッドを使っているが、メニューから選択する場合は、半角空白で分かち書きしているので、一工夫必要になる。単純にマッチした文字列だけ削除する場合、必ず空白文字が残る。また分かち書きしていない文字列は逆にマッチしないようにして、削除もしないようにしたい。

contains(_:)
https://developer.apple.com/documentation/swift/string/contains(_:)

当初、削除する文字列の位置範囲をrangeで調べてremoveSubrangeで削除することも考えたが、テキストの途中の場合、両端の空白文字が2つ残るし、分かち書きされていない場合も削除されるので都合が悪い。削除すれば、後の空白文字は元の位置から変わるし、先頭と途中と末尾の場合で処理を分ける、というのもやりたくない。

removeSubrange(_:)
https://developer.apple.com/documentation/swift/string/removesubrange(_:)-9twng

RangeメソッドはNSStringで定義されているらしいがよくわからない。

range(of:)
https://developer.apple.com/documentation/foundation/nsstring/1410144-range

そもそもとして、分かち書きされていない場合にマッチしないようにするにはどうしたらいいか。マッチしてから前後に空白文字がないか探すようなこともしたくない。

思いついたのは、削除する文字列に予め空白文字を付加して探す、だが、先頭と末尾だと今度はヒットしなくなる。それで、マッチング処理をする際にテキスト側にも前後に空白文字を付加することにした。これであれば先頭、末尾でもヒットする。最後に前後の空白を取り除けばいい。

さらに削除処理も、削除する文字列の前後に空白文字を付加した文字列のまま、replacingOccurrencesで空白文字1文字と置換するようにすればいい。下手に自分で書かずに、上手く標準を使いこなして実現できると気持ちがいい。もっといいやり方はあるかもしれないけれど。

replacingOccurrences(of:with:)
https://developer.apple.com/documentation/foundation/nsstring/1412937-replacingoccurrences

String型を拡張して、メソッド定義しておくと使い勝手がいいが、最初にif文でcontainsWordで含まれるかどうか調べて、removeWordAndTrimを呼び出すと、removeWordAndTrim内でもcontainsWordを使っているので効率が悪い。ただ、この辺りは、ある意味、部品化すれば仕方がない。

空白文字で分ち書きした単語単位で処理をするString型の拡張
extension String {
    func containsWord(_ word: String) -> Bool {
        self == word || (" "+self+" ").contains(" "+word+" ")
    }
    
    func removeWordAndTrim(_ word: String) -> String {
        if self.containsWord( word ) {
            return (" "+self+" ").replacingOccurrences(of: " "+word+" ", with: " ").trimmingCharacters(in: CharacterSet.whitespaces)
        } else {
            return self
        }
    }
    
    func appendWordAndTrim(_ word: String) -> String {
        (self+" "+word).trimmingCharacters(in: CharacterSet.whitespaces)
    }
}

【2022/10/10】

先日からトラブル。今まで、日付の変更やリストの再作成の処理は問題なく動いているつもりだったが、リストを降順、昇順に並び替える機能やリストに対するインクリメントサーチを入れたら動かなくなった。インクリメンタルサーチは、自前で実装したものだ。

まずインクリメンタルサーチ。英語でアルファベット入力している分には、入力に合わせて絞り込まれるListの動作自体に問題はないが、日本語でローマ字入力すると、先頭の1文字だけ変換の途中で勝手に確定されてサーチが走る。変換候補のポップアップも出ない。

さらに、TextFieldにフォーカスが残らない。これは英語でアルファベット入力でも同じ。インクリメンタルサーチなのに、入力途中でフォーカスが外れるのでは、インクリメンタルにならない。

さらに、たまに以下のワーニングがコンソールに表示されるときがある。これがどういう時なのか、規則性が見えない。全く出ない時もあれば、連続して出る時もある。

Binding action tried to update multiple times per frame.

フォーカスの方は、.focusedのモディファイアをTextFieldに設定し、そのためのenumも定義して、@FocusStateを付けた変数も宣言してみたところ、少しはフォーカスが外れなくなるが、完全ではない。そもそもとして、これは本来のフォーカスの使い方とは異なる。どうしたものか。

focused(_:equals:)
https://developer.apple.com/documentation/swiftui/view/focused(_:equals:)

これの対策は、Listの宣言を見ていて思い当たった。Listの先頭要素にTextFieldを入れていた。これだと、TextFieldの入力に合わせて、アイテムの絞り込みが発生して、Listが更新されるとTextFieldも影響を受ける。フォーカスが外れたり、変換が途中で邪魔されたような動作になることは十分に考えられる。

まず、Listの要素からTextFieldを外すことにした。Listの外側にVStackを宣言し、TextFieldとListを縦に並べる構造に変更したら、フォーカスが外れる問題も、変換が途中でおかしくなる事象も、発生しなくなった。多分ワーニングも出なくなっただろう。

ワーニングの「フレーム毎に何度も更新が発生」は、フレームが、時間のことか画面のことか拙いSwiftUIの知識と英語力では判別できないが、TextFieldの変更によるListの更新中に、TextField自体が更新される中で、強制的にTextFieldにフォーカスを移して入力したことで、さらにListに更新が発生した、というのはわからなくもない。

いい機会なので、Viewの構造や、NavigationLinkの遷移条件などを見直した。1ヶ月ほど前のコードを見ると、わからずに手探りで書いていた形跡が結構ある。後片付けしていかないといけない。

【2022/10/11】

引き続きトラブル。ListにおいてScetionで区切ってアイテムを表示したい。データモデルの構造体のオブジェクトを更新するとエラーを出してAppがクラッシュする。

'NSInternalInconsistencyException', reason: 'UITableView internal inconsistency: encountered out of bounds global row index while preparing batch updates (oldRow=6, oldGlobalRowCount=6)'

データモデルのStructにDate型のフィールドがあり、Sectionには年月日を表示し、その下のListにはButtonを並べて時分秒を表示したい。Buttonを押すとStructの各アイテムの内容を表示する画面に飛ぶ。

Sectionに相当するオブジェクトは定義しておらず、アイテムの配列から動的に生成してForEachで回し、同じ年月日を持つアイテムをフィルタで抽出して、Buttonに持たせていた。

UIで、StructのDateを編集し保存するとListはSectionを含めて更新されるが、降順、昇順の並び替えをすると、上記のエラーが発生してアプリがクラッシュする。Date型以外のフィールドの変更やStruct自体の配列からの削除の実行後に並び替えをしても問題なく動作する。これはどうしたことか。

調べていくと、Sectionを生成するためのForEachと、アイテムのButtonを生成するForEachはちゃんと動作するが、Sectionの生成を加えると途端にクラッシュすることがわかった。エラーは「インデックスの整合性が取れなくなった」というような内容に読めるが、これはSectionに関連したインデックスなのか。

Date型のフィールドは@Publishedを付けており変数に実体があるので、値を変更すれば、Viewも自動的に更新される。ところがSectionを生成する年月日の文字列の配列は動的に生成するので、Viewは自動的に更新されず、そこで整合性が取れなくなるとの考えに至った。どうしたものか。

Swift UIのドキュメント見ていたら、Listの概要の章でSectionを使う際の構造が示されていた。CompanyとDepartmentとPersonの階層を配列でガッチリ構成していた。

Displaying data in lists
https://developer.apple.com/documentation/swiftui/displaying-data-in-lists

確かにこのような構造を作り、@Publishedラッパーを付ければちゃんと動くのだろう。大した手間ではないし、動的な生成を止めることにした。Company、Department、Personの構造は人事異動でもなければ変わらないかもしれないが、こちらは、Listのアイテムは増え、変更の内容によってはSectionも変わる。階層構造のメンテナンスをする処理を作る必要がある。とは言え、表示時に同じ配列に多重にfilterを呼ぶような処理よりも単純になりそう。

【2022/10/12】

Sectionを生成するForEachの引数に設定する配列は、現時点ではmap、filter、enumeratedを使用して動的に生成している。オブジェクトを更新するとAppがクラッシュはするものの、配列のmap、filter、enumeratedなどの関数は、今までの初期のJavaしか知らない身には新鮮で使いやすい。

オブジェクトの配列にmapを使うと、特定のプロパティだけの配列を作ることができる。
map(_:)
https://developer.apple.com/documentation/swift/array/map(_:)-87c4d

これにenumeratedを呼び出すと、配列の先頭からの位置(offset)と要素(element)の組みのタプルの配列が得られる。
enumerated()
https://developer.apple.com/documentation/swift/array/enumerated()

この配列にfilterを適用して、条件に合うelementのみを含むタプルだけ取り出すと、元の配列のインデックスが0から始まっている場合には、offsetで元の配列の中の位置がわかる。
filter(_:)
https://developer.apple.com/documentation/swift/sequence/filter(_:)-5y9d2

map、enumerated、filter
let dateStrs = sections.map {$0.dateString}.enumerated().filter {$0.element == updateEvent.date.formattedDateString}

let aEvents = sections[dateStrs[0].offset].events.enumerated().filter {$0.element.id == updateEvent.id}
if aEvents.isEmpty {
   sections[dateStrs[0].offset].events.append(updateEvent)
}

元の配列が、さらに別の配列の一部を抜き出したようなで、インデックスが0から始まらないような場合、offsetは元の配列のインデックスとは一致しない。enumeratedのドキュメントには、zipを使え、との記載があるが、これはまた今度使ってみよう。

Swiftは、Structを使用する限りは変数は値渡しになるので、コピーされた値を変更しても元の変数の値は変わらない。コピーされた値は、捨てることが前提になるから、変更はどんどん行いつつ、必要な変更だけ大元の変数に書き戻すような作り方になる。これは意識しておかないといけない基本原理ということだ。

Swiftのmap, filter, reduce(などなど)はこんな時に使う!
https://qiita.com/motokiee/items/cf83b22cb34921580a52

[Swift] enumerated()はindexを返さない
https://qiita.com/a-beco/items/0fcfa69cca20a0ba601c

【2022/10/15】

List内でSectionとListのアイテムを保持する階層構造を親子の配列でガッチリ構成することにした。Listの表示では、Sectionにはsectionオブジェクトが対応し、dateStringに保持した年月日の文字列を、ListのアイテムにはEventが対応し、dateから生成した時分秒の文字列を、それぞれ表示する。

List内でSectionとListのアイテムを保持する階層構造
var sections: [section]

struct section: Identifiable {
    var id = UUID()
    var dateString: String
    var events: [Event]
}

struct Event: Identifiable, Hashable {
    var id = UUID()
    var date = Date()
}

Eventは追加、削除、変更が可能で、dateの変更をすれば、親のsectionが変わる。この辺りの処理を作成したら、意外に時間がかかった。

そして、Listで降順、昇順の並び替えをすると、以下のエラーが発生してアプリがクラッシュする問題は、このsection-Eventでガッチリ構成した構造をデータモデルとしてListにForEachで食わせる、という方法で解決すると思っていた。

'NSInternalInconsistencyException', reason: 'UITableView internal inconsistency: encountered out of bounds global row index while preparing batch updates (oldRow=6, oldGlobalRowCount=6)'

しかし、解決しなかった。もちろん、上記の=6の部分は、その時の入力データの内容で変わってくるが、そもそもとして、データモデルが固定的に存在するStored Propertyなのか、導出するComputed Propertyか、には依拠するものではなかった。Computedの処理の問題かとも思ったが、そういうものではなかったということだ。

Event側の日付を変更し、それに合わせて親のsectionを付け替える、というデータモデル側の更新処理をしても、全体をソートし直してListを作り直すと同じエラーが発生する。Listを作る際に変に動的な処理をしていないだけに、余計に原因が思い当たらない。

途方に暮れていたところ、UITableViewが内部的にアイテムを再利用しており、IDで識別している、という記事をネットで見つけた。IDを変更すれば新規扱いされるらしい。Sectionに子要素が増えているのに配列が更新されないまま再利用され、インデックスが溢れた、であれば合点がいく。sectionのIDの変更をしてみると確かにエラーは発生しなくなった。少々強引ではあるがこれでいくことにしよう。

【SwiftUI】Listでセル(子View)が再描画されない件について
https://www.m2game.net/entry/2022/05/12/114001

Xcodeとかで中身を見ながら開発ができれば気づけようが、Swift PlaygroundsでSwiftUIしか知らないと中の動きまではわからない。Eventで保持するdateプロパティに基づいてソートしているので、ドラッグして並び替えはそぐわないが、EditModeを使うと対話的に変更できるようになる。EditModeで、Sectionがどう振る舞うのかまでは把握していないが、再利用する階層の構造を変更しているはずだ。IDを変更しなくても整合性をとってくれる仕組みがあるのかもしれない。

EditMode
https://developer.apple.com/documentation/swiftui/editmode

おしまい

脚注
  1. https://developer.apple.com/jp/help/app-store-connect/ ↩︎

Discussion