Rork Max が吐いたプロジェクトを Xcode で開いて、シミュレータで動かして、そのまま Archive してしまった日があります。審査には出せました。ただ実機に入れてスクロールした瞬間に、リストが目に見えて引っかかった。シミュレータでは一度も再現しなかった症状でした。
生成の速さと、出荷できる品質は別の軸にあります。前者を Rork Max が引き受けてくれるようになったぶん、後者の工程が相対的に重くなった、というのが個人開発で半年ほど運用してきた実感です。
以下は、その「後者」だけを工程順に並べたものです。時間を溶かした箇所には、溶かした理由も添えています。
Rork Max 生成プロジェクトの構造を理解する
まず Xcode でプロジェクトを開いたときに、ディレクトリ構成を正しく把握することが出発点です。
典型的な構成は次のような形になります。
MyApp/
├── MyApp.xcodeproj/
├── Sources/
│ ├── App/ ← アプリケーションのエントリポイント
│ ├── Views/ ← SwiftUI View 定義
│ ├── Models/ ← データモデル・Codable 定義
│ ├── Services/ ← API クライアント・永続化層
│ └── Utils/ ← 拡張関数・ヘルパー
├── Resources/
│ ├── Assets.xcassets/
│ ├── Localizations/
│ └── Fonts/
└── .rork/ ← Rork 生成メタデータ(重要)
この中で最も注意が必要なのは .rork/ ディレクトリです。これは Rork Max がプロジェクトの再生成や更新に使う内部メタデータで、手を入れると再生成の挙動が壊れます。
編集してよい場所とそうでない場所を、判断基準ごとに整理しておきます。
| 対象 | 手を入れてよいか | 理由 |
|---|---|---|
.rork/ | 不可 | 再生成時の差分計算の基準。書き換えると次回出力が破綻する |
Sources/Views/ | 可(衝突しやすい) | Rork が最も頻繁に書き換える領域。改変は最小差分で |
Sources/Services/ | 可 | 生成後に触る前提の層。API の実装差し替えはここで |
Sources/Extensions/(自作) | 推奨 | Rork が生成しない新規ディレクトリ。衝突がそもそも起きない |
*.xcodeproj | 可(要注意) | Build Settings の変更は残るが、ターゲット構成の改変は再生成で巻き戻る |
最後の行が実務上いちばん効きます。Build Settings のフラグ変更は再生成後も残りますが、ターゲットを分割したりスキームを作り替えたりすると、次の生成で静かに元へ戻されることがあります。プロジェクトファイルの構造そのものを触るのは、再生成を打ち切る覚悟が固まってからにしてください。
Instruments で診断すべき 3 つのプロファイル
生成されたコードは動きます。ただ初期状態では「動く」ところで止まっていることが多い。出荷品質へ持っていく判断材料は、実機での計測からしか出てきません。
冒頭に書いたスクロールの引っかかりも、シミュレータでは一度も観測できませんでした。Mac の CPU で走らせている限り、重い計算はコストとして表面化しないためです。
1. Time Profiler — CPU 時間の見直し
アプリ起動時やタブ切り替え時の体感速度が遅いと感じたら、最初に Time Profiler で計測します。生成コードで典型的に遅いのは、次のパターンです。
// 遅い: 毎回すべての要素を再計算
var body: some View {
List(items) { item in
Row(item: item, score: computeScore(for: item))
}
}computeScore が重い計算を含んでいると、スクロール時に毎フレーム再計算が走って体感速度が落ちます。対策は @State でキャッシュするか、コンピュテッドプロパティを事前計算しておくことです。
// 速い: 事前計算してキャッシュ
@State private var cachedScores: [UUID: Double] = [:]
var body: some View {
List(items) { item in
Row(item: item, score: cachedScores[item.id] ?? 0)
}
.task {
await computeAllScores()
}
}計測の際は、必ず Release ビルドを実機に流し込んでください。Debug ビルドの Time Profiler は最適化が効いていないぶんプロファイルの形が変わり、本来ボトルネックでない関数が上位に来ます。私はこれで一度、無関係な関数を丸一日かけて最適化しました。
2. Allocations — メモリ使用量の把握
生成コードは、画像の扱いで不要なメモリを消費するケースがあります。Allocations プロファイルで「Transient」ではなく「Persistent」なメモリの増加を追うと、解放漏れが見えてきます。
特に AsyncImage や UIImage(data:) を多用するリストは要注意です。画像のダウンサンプリングを入れるだけで、メモリ使用量が 1/5 になることもあります。
// 画像のダウンサンプリング関数
func downsample(image data: Data, to pointSize: CGSize, scale: CGFloat) -> UIImage? {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions) else {
return nil
}
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
return nil
}
return UIImage(cgImage: downsampledImage)
}kCGImageSourceShouldCache: false を最初のオプションに入れている点が要です。これを落とすと、CGImageSource がデコード済みビットマップを内部に抱え込み、ダウンサンプリングした先のメモリ削減がほぼ相殺されます。関数だけコピーして動かない、という相談を受けたときは、たいていこの行が消えていました。
3. SwiftUI Performance — 再描画の「原因」まで遡る
SwiftUI は宣言的なゆえに、どの View がどのタイミングで再描画されるかが直感と合わないことがあります。ここは Xcode 26 で計測手段が変わった領域なので、少し詳しく扱います。
Instruments 26 では SwiftUI 専用のテンプレートが追加され、Update Groups レーンで「SwiftUI がいつ更新作業をしているか」が時間軸上に並ぶようになりました。従来の Time Profiler が「どの関数が重いか」を教えてくれるのに対して、こちらは「どの更新がどれだけメインスレッドを占有したか」を教えてくれます。
さらに有用なのが Cause & Effect Graph です。更新を選択して「Show Cause & Effect Graph」を選ぶと、状態の変更から View の body 再評価までの伝播がノードとして表示されます。View ノードの左側に並ぶのが、その更新を引き起こしたイベントです。
これが効くのは、まさに Rork Max 生成コードで典型的に起こる問題に対してです。ObservableObject を監視している View が、関係のないプロパティの変更でも再描画される。従来はこれを目視と当て推量で切り分けていましたが、Cause & Effect Graph なら「この再描画の原因はこのプロパティの mutation だった」と一意に辿れます。
原因が特定できたあとの手当ては 2 通りです。
// Before: オブジェクト全体を観測 → 無関係な更新でも再描画
struct ScoreBadge: View {
@ObservedObject var store: SessionStore
var body: some View { Text("\(store.score)") }
}
// After: 必要な値だけを受け取る → 親でのみ観測が発生する
struct ScoreBadge: View, Equatable {
let score: Int
var body: some View { Text("\(score)") }
static func == (l: Self, r: Self) -> Bool { l.score == r.score }
}値だけを受け取る形に落として Equatable を付けておくと、SwiftUI は同値の場合に body の再評価を省けます。Rork Max の生成物は View に @ObservedObject を素直に配ってくる傾向があるため、リスト行のような高頻度描画の箇所だけでもこの形へ寄せておくと、Update Groups の占有時間が目に見えて縮みます。