WWDC2024から見る開発Tipsまとめ
ベースのセッション
コード補完
- Swift用にトレーニングされたオンデバイスコーディングモデルを使用することでより強力なコード補完が実現
- 周りのコードやコメントも参照する
- Apple Silicon, MacOS Sequoia, Xcode16, UnifiedMemory16GBの環境で使用できる
使用している環境が対応しているかどうかはXcode16の設定>TextEditing>Editing>Predictive Code Completionでチェック可能
Swift6
Upcoming Features
- Swift6では、同時実行の安全性を保証する新しい言語モードが追加される
- これらを利用するには、Swift6言語モードを採用する必要がある
- しかしXcode16ではそれぞれの今後の言語機能の警告を段階的に有効にすることが可能で、小さく始めることができる
Build Settings > Swift Compiler - Upcoming Featuresで今後の言語機能を一つずつ段階的に有効にできる
Xcode15以前はBuild Settings > Swift Compiler - Custom Flags > Other Swift Flagsに-enable-upcoming-feature ExistentialAnyなどを指定していたが、プルダウンから選択するだけでよくなっている
Swift Language Version
Swift6が追加された
Preview
Previewには記述がより簡単になり、再利用しやすく、モデルと統合しやすい2つの新しいAPIが追加
@Previewable
- Bindingを引数にとるViewをPreviewで表示するために、従来はPreview専用のラッパーViewを作る必要があった
- このラッパーに@Stateを保持させることでPreview上でBindingを操作していた
struct SampleView: View {
@Binding var isOn: Bool
var body: some View {
Toggle(isOn: $isOn) {
Text("Toggle")
}
}
}
struct SampleViewPreviewWrapper: View {
@State private var isOn = false
var body: some View {
SampleView(isOn: $isOn)
}
}
// ラッパーを使用
#Preview {
SampleViewPreviewWrapper()
}
// もしくは固定値を渡す
#Preview {
SampleView(isOn: .constant(false))
}
今回追加された、@Previewableを使用することでこのラッパーを作成しなくてもPreviewで@Bindingを操作することが可能になった
struct SampleView: View {
@Binding var isOn: Bool
var body: some View {
Toggle(isOn: $isOn) {
Text("Toggle")
}
}
}
#Preview {
@Previewable @State var isOn = false
SampleView(isOn: $isOn)
}
Preview Modifier
- EnvironmentやPreview用のデータを簡単に共有できるようになる
- データはPreviewシステムがデータをキャッシュできるようにもなる
少し説明を追加すると
- PreviewModifierはPreview環境だけでViewに適用するModifier(のようなもの)
- 従来はProcessInfoなどを見てPreview環境の場合は.environmentにPreview用のオブジェクトを注入する、といったことを時前でやっていた(筆者)
#Preview {
HogeView()
.environment(\.hogeObject, HogeObject())
}
- この代替手段がPreviewModifier
- .environment以外にもViewへの作用はなんでも書けそう
- そして使用するデータはPreview環境がキャッシュしてくれる、という優れもの
- ついでに補足するとPreviewマクロで適用できるPreviewTraitに、PreviewModifierを指定できる.modifierというstaticメソッドがiOS18で追加された
(PreviewTraitとか諸々は最低OSバージョンを上げないと使えないの、なんか理不尽ですよね、、)
struct SampleBookProvider: PreviewModifier {
typealias Context = BookProvider
static func makeSharedContext() async throws -> Context {
// Preview専用のインスタンスなどをここで作成する
// async, tryで書くことが可能、重い処理でもキャッシュされるので安心
BookProvider()
}
func body(content: Content, context: Context) -> some View {
// Preview環境で適用されるViewへの作用
content.environment(context)
}
}
extension PreviewTrait where T == Preview.ViewTraits {
@MainActor static var sampleNamer: Self = .modifier(SampleBookProvider())
}
#Preview(traits: .sampleNamer) {
RobotNameSelectorView()
}
PreviewModifierの実装手順
- 1つ目はContextの指定
- これはPreview環境で共有したいオブジェクト(=Context)の型を指定する
- 2つ目はmakeSharedContext()の実装
- このメソッド内で共有されるContext(sharedContext)を実際に作成する
- そのため、メソッドはstaticになる
- 3つ目はbody(content:context:)の実装
- これはPreviewされるViewに対してどのような操作をするかを記述する
- Previewごとに呼び出されるためこちらはstaticではない
Explicitly Built Modules
- 新しいExplicitly Built Moduleにより、ビルドの並列性が向上し、診断が改善され、デバッグが高速化される(セッションではそう言っている)
- Obj-CやC++ではデフォルトでこの機能がON
- Swiftではオプトインになっているため、Build Settingsから自分でONにする必要がある
Build Settings > Swift Compiler - General > Explicitly Built Modulesで設定が可能
実際にどの程度ビルドが早くなるのか、どんなプロジェクトでも効果が期待できるのかなど、ここで話すにはコンテンツが大きすぎるため詳細は割愛します。詳しくは下記セッションを御覧ください。
Disk write diagnostics
ディスク書き込みのdiagnosticsにも少し機能が追加
Xcodeのツールバー > Window > Organizerを選択、左側のタブからDisk Writesを選択
タイトルの先頭に優先度を示す矢印マークが追加された
Launch diagnostics
起動時間が長くなってしまってい部分、起動経路を検知することができる
Xcodeのツールバー > Window > Organizerを選択してOrganizerを表示、Launchesというカテゴリが追加されている
Unified Backtrace View
- Stacktraceが見やすくなった
- Backtrace in Editorを有効にした状態でブレイクポイントを止めると、それぞれのフレームの周辺コードが表示され、スクロールするだけでコールスタックをたどることができる
DebugBarのEnable Backtrace in Editor(6つ目のアイコン)で切り替えが可能
ホバーで変数の中身も見られる(ここは従来通り)
Swift Testing
Swift言語のための新しいTestフレームワークです。
@Testや#assertなどのマクロ、パラメタライズドテスト、テストケースへのラベル付け、より見やすいdiagnosticsなど使いやすい機能がたくさん入っています。
こちらもTipsとして話すには長くなってしまうため、詳細は下記のセッションを御覧ください。
Frame Graph
InstrumentsのCPU Profilerに追加された新しい機能で、実行中のフレームを見やすく可視化してくれます。例としてハングの原因を特定する手順を下記に記載します。
struct HangView: View {
var body: some View {
Text("HangView")
.task {
saveFiles()
}
}
private func saveFiles() {
let line = Array(repeating: "a", count: 1000).joined(separator: "")
let content = Array(repeating: line, count: 10000).joined(separator: "\n")
for i in 0..<500 {
saveFile(fileName: "file_\(i)", content: content)
}
}
private func saveFile(fileName: String, content: String) {
guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return
}
let fileURL = url.appendingPathComponent(fileName)
do {
try content.write(to: fileURL, atomically: true, encoding: .utf8)
print("File saved successfully at \(fileURL)")
} catch {
print("Failed to save file: \(error.localizedDescription)")
}
}
}
ToolBar > Product > Profile(⌘I)
Instrumentsが起動するのでCPU Profilerを選択
レコードボタン(左上の赤丸)をクリックして記録開始
Hang(を含むCPUProfile)が記録される
Hangを右クリックしてSet Inspection Rangeを選択
Frame Graph(画面右下のちっちゃいアイコン)を選択
FrameGraphが表示される
Frame Graphは選択した範囲のうち、実行している割合が多い処理ほど左に表示され、そのCallStackが縦に並んでいく。
今回の例だと、Hangしている間の99.9%をContentView.saveFile(fileName:content:)が占めていることがわかる。
Crashlogs
- Appleプラットフォームではどこでクラッシュしてもプログラムの状態を収集し、ログとして残す
- LLDBはこのクラッシュログを使用してその時のデバッグセッションを再現することが可能
CrashLogを右クリックしてOpen withでXcodeを選択
対象のプロジェクトを選択してOpenをクリック
クラッシュ時のデバッグセッションが再現される
このとき、クラッシュしたアプリのバージョンがローカルでチェックアウトされていること、dSYMバンドルが利用可能になっている必要がある
Breakpoint
Xcode16からの新機能ではないが、紹介されていたので一応記載
- ブレイクポイントを貼ると、デバッグ時にLLDBが行のブレイクポイントから実際に止まる処理(コードパス)を検出する
- 例えばButtonではコンストラクタ、ラベル、タップ時のクロージャという3つのコードパスがある
1つのブレイクポイントに対して止まる箇所3つあることがBreakpoint Navigatorで確認できる
コードパスを個別に有効、無効を切り替えることも可能
LLDBのコマンドbreak listでブレイクポイントのコードパスを一覧で表示することも可能
LLDBのコマンドpで変数の中身を見られる
- 他にもブレイクポイントに止まった際にLLDBコマンドを自動実行する機能、LLDBコマンドを実行した後に止めずに続けるオプションがある
コードパスを右クリックしてEdit Breakpointを選択、Add Actionを選択してDebugger Commandに任意のLLDBコマンドを設定、Automatically continue after evaluating actionsにチェックを付ければ止めずに実行できる
- ブレイクポイントに差し掛かった際に任意の条件下でのみ止めたい場合はConditionを設定する
コードパスを右クリックしてEdit Breakpointを選択、Conditionに条件を入力
- 上記のConditionを設定する場合、for文などで大量の繰り返し処理が走るとその都度LLDBが条件を評価する必要があり実行に時間がかかってしまう
- そういった場合は直接Swiftに条件分岐のコードを書き、if文の中でブレイクポイントを設定するのが良い
また、ブレイクポイントを設定せずともraise(SIGSTOP)でコードを止めることができる
- 一時的なブレイクポイントは、ブレイクポイントに差し掛かった際に一度だけ止まるブレイクポイント
- 下記LLDBコマンドの項目でも触れているがtbreakコマンドで一時的なブレイクポイントを作成できる
- 例えばAという箇所が実行された場合にのみBで止めたい場合、Aにブレイクポイントを貼ってDebugger Commandに"tbreak <Bの場所>"を設定するとこれを実現できる
コードパスを右クリックしてEdit Breakpointを選択、Add Actionを選択してDebugger Commandに"tbreak <任意の場所>"を入力
- 指定された回数はブレイクポイントを止めずに無視することが可能
コードパスを右クリックしてEdit Breakpointを選択、ignoreに回数を設定
- Xcode16でSwiftのErrorがどこかでthrowされた際に止まるブレイクポイントが新たに追加された
Breakpoint Navigatorの左下の+ボタンからSwift Error Breakpointを選択、あとはどこかでErrorがthrowされれば自動で止まってくれる
LLDBコマンド
ブレイクポイントの追加
b DetailView.swift:70
ブレイクポイントのリストを表示(ブレイクポイントのIDなどはここでチェック)
break list
ブレイクコマンドの追加
止めたくない場合はcontinueでDONEまでがセット
<LLDB command>は任意のLLDBコマンドで、例えばp "last video is (watchLater.last?.name)"など
break command add <breakpoint-id>
<LLDB command>
continue
DONE
ブレイクコマンドの削除
break command delete <breakpoint-id>
ヘルプ
help <command>
help <command> <option>
ヘルプの中を特定キーワードで検索(”Add LLDB commands”で検索するとbreak command addがヒットする)
aproprops <keyword>
ブレイクポイントに止まる条件を追加
<condition>は止める条件で、例えばビデオを読み込むFor文の中でビデオが60分以上だった場合のみ止めたい場合は"video.length > 60"など
break modify <breakpoint-id> --condition <condition>
一時的なブレイクポイントを貼る
(ブレイクポイントに差し掛かったときに一回のみ止まるブレイクポイント)
tbreak Importer.swift:67
指定された回数はブレイクポイントに差し掛かっても無視する
break modify <breakpoint-id> --ignore-count <ignore count(int)>
- 変数の中身を見たり、Expressionを評価するコマンド
- この類のコマンドとして他にもregister readやmemory readもあるが、Xcode15からは大体の場合はpコマンドで事足りる
- ただし、プリミティブな型でない場合は引き続きpoなどを使用する必要がある
p <variable-name>
余談だが、pコマンドはdwim-printのエイリアス。dwimは"Do What I Mean"の略
@DebugDescription
実装そのままだとVariable Views(デバッグ中に左下のタブで見られる変数のインスペクタ)で配列の中身がパッと見られない(展開すれば見られる)
Variable Viewsだと配列はこんな表示になっている
Swift6では@DebugDescriptionマクロを使用してpコマンドの結果やVariable Viewsでの表示をカスタマイズすることができる
@DebugDescription
struct WatchLaterItem {
let video: Video
let name: String
let addedOn: Date
var debugDescription: String {
"\(name)-\(addedOn)"
}
}
debugDescriptionが表示されている
既にCustomDebugStringConvertibleプロトコルを実装している場合、@DebugDescriptionマクロに置き換えることができるが、上述の通りdebugDescriptionにStoredPropertyとStringInterpolationを使用している場合に限る点に注意が必要
また、今までpoコマンドを使用していたケースでもpコマンドで代用可能になる
Discussion