RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-05-12上級

Rork Max で複数アプリ共通UIライブラリを Swift Package Manager で構築する実践ガイド

壁紙・癒し系など複数アプリを運営する個人開発者が Swift Package Manager で共通UIライブラリを設計し、Rork Max 生成コードの重複を排除した実践手順を詳解。

Rork Max229Swift Package Manager2SwiftUI63個人開発187コンポーネント設計マルチアプリ

2019年頃、私は5本の壁紙アプリを同時に運営しながら、ある単純な事実に気づきました。「カード型UIのタップ時アニメーションを直すたびに、5本全てのプロジェクトを開き直している」という現実です。同じバグ修正を5回繰り返す。コードはほぼ同じなのに、プロジェクトは5つに分かれています。

Rork Max で SwiftUI ネイティブアプリを生成していると、この問題はさらに鮮明になります。AI が生成するコードは高品質ですが、複数アプリにまたがった設計の一貫性は自動では保たれません。似たようなカードビュー、ほぼ同じグラデーション定義、わずかに違うフォントサイズ——これらが5本のアプリに散在すると、「ブランドをちょっと変えたい」という判断が5倍のコストになります。

Swift Package Manager(SPM)で共通UIライブラリを構築すると、この問題は根本的に解消されます。修正は1箇所、テストも1箇所、リリースも連鎖的に反映されます。

ここでは私が実際に行った複数アプリ共通UIライブラリの設計と、Rork Max 生成コードとの統合パターンを詳しく解説します。


なぜ Rork Max の生成コードに SPM が必要なのか

Rork Max は SwiftUI のコードを自動生成しますが、その設計単位は「1アプリ」です。2本目のアプリを同じクオリティで作ろうとすると、最初のアプリで確立したデザインシステムを、別プロジェクトで一から再現することになります。

具体的に何が問題になるか整理します。

テーマカラーの分散: アプリごとに Color("PrimaryColor") のような定義が存在しますが、その RGB 値やダークモード対応は各プロジェクトで個別管理されます。ブランドカラーを #7B68EE から #6A5ACD に変更する際、5本を全部直す必要があります。

アニメーション定数の重複: spring(response: 0.4, dampingFraction: 0.75) のような定数が各ファイルに埋め込まれています。アニメーションのフィールを少し変えたいとき、同様に5本全部を直します。

ビューコンポーネントの微妙な差異: 「カード型コンポーネント」という共通概念があっても、各アプリで微妙に実装が異なります。これは品質のばらつきと、後のメンテナンスコストに直結します。

SPM でライブラリ化することで、これら全てが1つの Package.swift に集約されます。


パッケージ構成の設計

共通UIライブラリの構成を考えるとき、私は「何が変わらないか」と「何がアプリごとに違うか」を分けることから始めます。

変わらないもの(ライブラリに入れる):

  • アニメーション定数とプリセット
  • シャドウ・ブラー・コーナーラジアスのスタイル定数
  • カスタムボタンスタイル(ButtonStyle プロトコル準拠)
  • リピートできるビューコンポーネント(カード、バナー、タグチップ等)
  • ネットワーク画像ローダー
  • ハプティックフィードバックの共通ラッパー

アプリごとに違うもの(アプリ側で定義する):

  • ブランドカラー(プライマリ・セカンダリ)
  • フォントファミリー
  • アプリ固有のアイコン
  • ビジネスロジック

この区別を最初にしっかり決めておかないと、ライブラリがアプリ依存のコードを引き込んでしまいます。実際に私が失敗したのもここでした。最初に作ったライブラリは AdMob の GADBannerView ラッパーまで入れてしまい、SDK の依存関係がライブラリを汚染してしまいました。


Swift Package を作成する

Rork Max でプロジェクトを開き、ターミナルで以下を実行します。

# アプリプロジェクトと同じ階層にパッケージを作成
mkdir -p ~/Developer/DoliceUIKit
cd ~/Developer/DoliceUIKit
swift package init --type library --name DoliceUIKit

生成される構造は次のようになります。

DoliceUIKit/
├── Package.swift
├── Sources/
│   └── DoliceUIKit/
│       └── DoliceUIKit.swift   ← ここにコンポーネントを追加
└── Tests/
    └── DoliceUIKitTests/
        └── DoliceUIKitTests.swift

次に Package.swift を設定します。iOS 17 以上で @Observable を活用するため、最低サポートバージョンを明示します。

// Package.swift
// swift-tools-version: 5.10
import PackageDescription
 
let package = Package(
    name: "DoliceUIKit",
    platforms: [
        .iOS(.v17)
    ],
    products: [
        .library(
            name: "DoliceUIKit",
            targets: ["DoliceUIKit"]
        ),
    ],
    dependencies: [],
    targets: [
        .target(
            name: "DoliceUIKit",
            dependencies: [],
            path: "Sources/DoliceUIKit"
        ),
        .testTarget(
            name: "DoliceUIKitTests",
            dependencies: ["DoliceUIKit"],
            path: "Tests/DoliceUIKitTests"
        ),
    ]
)

ここで重要なのは、外部 SDK を依存に入れないことです。後からどうしても必要になった場合は、別のターゲットとして分離します。


テーマシステムの実装

アプリごとに異なるカラーテーマを注入できる仕組みを作ります。SwiftUI の EnvironmentValues を使うのが最もクリーンなパターンです。

// Sources/DoliceUIKit/Theme/AppTheme.swift
 
import SwiftUI
 
/// アプリのブランドテーマを定義するプロトコル
/// 各アプリはこのプロトコルに適合したテーマ型を用意し、環境に注入する
public protocol AppThemeable {
    var primary: Color { get }
    var secondary: Color { get }
    var surface: Color { get }
    var onSurface: Color { get }
    var fontTitle: Font { get }
    var fontBody: Font { get }
    var fontCaption: Font { get }
}
 
/// デフォルトテーマ(壁紙アプリ系の紫グラデーション基調)
public struct DefaultAppTheme: AppThemeable {
    public var primary: Color { Color(hex: "#7B68EE") }
    public var secondary: Color { Color(hex: "#48D1CC") }
    public var surface: Color { Color(.systemBackground) }
    public var onSurface: Color { Color(.label) }
    public var fontTitle: Font { .system(.title2, design: .rounded, weight: .bold) }
    public var fontBody: Font { .system(.body, design: .default) }
    public var fontCaption: Font { .system(.caption, design: .monospaced) }
 
    public init() {}
}
 
/// 環境キー
private struct AppThemeKey: EnvironmentKey {
    static let defaultValue: any AppThemeable = DefaultAppTheme()
}
 
public extension EnvironmentValues {
    var appTheme: any AppThemeable {
        get { self[AppThemeKey.self] }
        set { self[AppThemeKey.self] = newValue }
    }
}

各アプリ側では、ルートビューでテーマを注入するだけです。

// WallpaperApp/ContentView.swift(アプリ側)
import DoliceUIKit
 
// アプリ固有テーマの定義
struct WallpaperAppTheme: AppThemeable {
    var primary: Color { Color(hex: "#FF6B9D") }  // ピンク系のブランドカラー
    var secondary: Color { Color(hex: "#FFB347") }
    var surface: Color { Color(.systemBackground) }
    var onSurface: Color { Color(.label) }
    var fontTitle: Font { .custom("ZenMaruGothic-Bold", size: 20) }
    var fontBody: Font { .custom("ZenMaruGothic-Regular", size: 16) }
    var fontCaption: Font { .custom("ZenMaruGothic-Regular", size: 12) }
}
 
@main
struct WallpaperApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(\.appTheme, WallpaperAppTheme())
        }
    }
}

この設計の良い点は、AppThemeable プロトコルに変更を加えれば(新しいカラーロールを追加するなど)、コンパイルエラーで全アプリ側の対応漏れをすぐに検出できることです。


カードコンポーネントの実装

壁紙・癒し系アプリで最も頻出するカード型UIを共通化します。@Environment でテーマを取得するパターンです。

// Sources/DoliceUIKit/Components/ContentCard.swift
 
import SwiftUI
 
/// 汎用コンテンツカード
/// - `content`: カード内に表示するビュー(ジェネリック)
/// - `style`: カードのスタイルプリセット
public struct ContentCard<Content: View>: View {
    @Environment(\.appTheme) private var theme
    @Environment(\.colorScheme) private var colorScheme
 
    private let content: Content
    private let style: CardStyle
 
    public enum CardStyle {
        case standard      // 標準シャドウ
        case elevated      // 強めのシャドウ、プレミアムな印象
        case flat          // シャドウなし、ボーダーあり
    }
 
    public init(style: CardStyle = .standard, @ViewBuilder content: () -> Content) {
        self.style = style
        self.content = content()
    }
 
    public var body: some View {
        content
            .background(cardBackground)
            .clipShape(RoundedRectangle(cornerRadius: cardCornerRadius))
            .shadow(
                color: shadowColor,
                radius: shadowRadius,
                x: 0,
                y: shadowY
            )
    }
 
    private var cardBackground: some View {
        theme.surface
            .overlay(
                style == .flat
                    ? RoundedRectangle(cornerRadius: cardCornerRadius)
                        .strokeBorder(theme.primary.opacity(0.2), lineWidth: 1)
                    : nil
            )
    }
 
    private var cardCornerRadius: CGFloat { 16 }
 
    private var shadowColor: Color {
        switch style {
        case .standard: return Color.black.opacity(colorScheme == .dark ? 0.3 : 0.08)
        case .elevated: return theme.primary.opacity(0.2)
        case .flat:     return .clear
        }
    }
 
    private var shadowRadius: CGFloat {
        switch style {
        case .standard: return 8
        case .elevated: return 20
        case .flat:     return 0
        }
    }
 
    private var shadowY: CGFloat {
        switch style {
        case .standard: return 4
        case .elevated: return 8
        case .flat:     return 0
        }
    }
}

Rork Max が生成するコードの典型的なカードコンポーネントと比較してみましょう。

Before(Rork Max 生成コード、各アプリに個別存在):

// WallpaperApp でのカード定義
struct WallpaperCard: View {
    var body: some View {
        content
            .background(Color(.systemBackground))
            .clipShape(RoundedRectangle(cornerRadius: 16))
            .shadow(color: Color.black.opacity(0.08), radius: 8, x: 0, y: 4)
    }
}
 
// HealingApp でのカード定義(微妙に違う)
struct HealingCard: View {
    var body: some View {
        content
            .background(Color(.systemBackground))
            .clipShape(RoundedRectangle(cornerRadius: 14))  // ← 微妙に違う
            .shadow(color: Color.black.opacity(0.1), radius: 6, x: 0, y: 3)  // ← 微妙に違う
    }
}

After(DoliceUIKit を使った統一実装):

// どのアプリでも同じコンポーネント、テーマだけが違う
ContentCard(style: .elevated) {
    WallpaperThumbnailView(wallpaper: item)
}

コーナーラジアスが 16 か 14 か、シャドウの透明度が 0.08 か 0.1 かで品質がぶれていた問題が、一度の修正で全アプリに伝播するようになります。


アニメーションプリセットの共通化

Rork Max が生成するアニメーションコードは表現力がありますが、アプリをまたぐと統一感がなくなります。Animation の拡張として共通プリセットを定義します。

// Sources/DoliceUIKit/Animation/AnimationPresets.swift
 
import SwiftUI
 
public extension Animation {
    /// 標準的なスプリングアニメーション(タップフィードバック等)
    static let doliceSpring = Animation.spring(
        response: 0.4,
        dampingFraction: 0.75,
        blendDuration: 0
    )
 
    /// 素早いフィードバック(ボタン押下等)
    static let doliceSnap = Animation.spring(
        response: 0.25,
        dampingFraction: 0.8,
        blendDuration: 0
    )
 
    /// ゆったりとした登場アニメーション(画面遷移等)
    static let doliceEase = Animation.easeOut(duration: 0.35)
 
    /// 壁紙・背景が切り替わる際のフェード
    static let doliceContentTransition = Animation.easeInOut(duration: 0.5)
}
 
/// スプリングトランジション付きのスケールエフェクト
public struct DolicePressEffect: ButtonStyle {
    @Environment(\.appTheme) private var theme
 
    public init() {}
 
    public func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .scaleEffect(configuration.isPressed ? 0.94 : 1.0)
            .opacity(configuration.isPressed ? 0.85 : 1.0)
            .animation(.doliceSnap, value: configuration.isPressed)
    }
}

このプリセットをプロジェクト全体に適用することで、「なんとなくアニメーションが違う気がする」という品質のばらつきを防げます。


Rork Max プロジェクトへの統合

Xcode で Rork Max の生成プロジェクトを開き、SPM パッケージを追加します。

File → Add Package Dependencies → ローカルパッケージを選択(開発中はローカルパスを指定)

/Users/masaki/Developer/DoliceUIKit

ローカルパッケージとして追加することで、ライブラリを変更した際に即座にアプリ側に反映されます。本番環境では GitHub リポジトリに push し、バージョンタグで管理します。

// GitHub で管理する場合の Package.swift (アプリ側)
dependencies: [
    .package(
        url: "https://github.com/dolice/DoliceUIKit.git",
        from: "1.0.0"
    )
],

統合後、Rork Max で新しいコンポーネントを生成するたびに「これは DoliceUIKit にあるか」を確認する習慣が重要です。あるなら Rork Max 生成コードを DoliceUIKit の使用に置き換え、ないならライブラリに追加するかどうか判断します。

Rork Max の生成コードを本番レベルに仕上げるリファクタリング手法については、Rork Max が生成した SwiftUI コードを本番品質まで磨き上げる10のリファクタリング・パターンで詳しく解説しています。また、React Native(Rork 通常プロジェクト)での共通コンポーネント化についてはTurborepo モノレポで複数アプリの共有コードを一元管理するも参考になります。


テストとバリデーション

ライブラリのコンポーネントは @testable import でテストできます。SwiftUI コンポーネントのユニットテストは難しいですが、ビジネスロジックを持つものは確認できます。

// Tests/DoliceUIKitTests/ThemeTests.swift
 
import XCTest
@testable import DoliceUIKit
 
final class ThemeTests: XCTestCase {
    func testDefaultThemeColors() {
        let theme = DefaultAppTheme()
        // プライマリカラーが定義済みであること
        // Color の比較は直接できないため、nilでないことを確認
        XCTAssertNotNil(theme.primary)
        XCTAssertNotNil(theme.secondary)
    }
 
    func testAnimationPresetsExist() {
        // コンパイル通過でプリセットが正しく定義されていることを確認
        _ = Animation.doliceSpring
        _ = Animation.doliceSnap
        _ = Animation.doliceEase
    }
}

本格的なビジュアルテストには swift-snapshot-testing の導入を検討する価値があります。コンポーネントの見た目が変わっていないことをCIで保証できます。


複数アプリへの展開フロー

ライブラリが安定したら、複数アプリへの展開は次のフローで進めます。

まず、GitHub でセマンティックバージョニングを使います。破壊的変更は v2.0.0、新機能は v1.1.0、バグ修正は v1.0.1 とタグを付けます。

各アプリの Package.swift では、バージョン範囲を適度に広めに設定することで、バグ修正が自動的に取り込まれます。

// アプリ側 Package.swift での依存定義
.package(
    url: "https://github.com/dolice/DoliceUIKit.git",
    .upToNextMinor(from: "1.0.0")  // 1.0.x の最新を自動取得、1.1.0 は手動アップデート
),

破壊的変更(プロトコルの追加フィールドなど)は upToNextMinor で自動取得を防ぎ、各アプリで意図的に対応してからアップデートします。

壁紙アプリ・癒しアプリ・引き寄せアプリで共通テーマが 80% 一致していても、残り 20% のブランド個性は守られます。これが AppThemeable プロトコルを設計した動機でもあります。コアの品質と表現の多様性を、同時に成立させることができます。


実運用で分かった落とし穴

プレビューがクラッシュする問題: SwiftUI Preview で環境値が正しく渡らないケースがありました。PreviewProvider では必ず .environment(\.appTheme, DefaultAppTheme()) を明示する必要があります。

#Preview {
    ContentCard {
        Text("サンプルカード")
            .padding()
    }
    .environment(\.appTheme, DefaultAppTheme())  // 必須
}

Xcode Cloud でのローカルパス問題: CI 環境では当然ローカルパスは無効です。開発中はローカルパスを使い、CI ブランチに push する前に GitHub URL に切り替えるか、.xcode-version で Xcode バージョンを固定した上で GitHub Actions でも同じ切り替えを自動化します。

AppStore 審査でのバイナリサイズ: ライブラリに使わないコードが大量にあると、バイナリサイズが増加します。dead_code_stripping = YES が Xcode デフォルトで有効なため通常は問題になりませんが、Rork Max の生成プロジェクトでリリースビルドの設定を確認しておくと安心です。


全体を振り返って

Swift Package Manager で共通UIライブラリを構築することは、1本のアプリを作っている段階では「やりすぎ」に見えます。しかし2本目を作り始めた瞬間、投資は確実に回収されます。

まず小さく始めることをおすすめします。全てのコンポーネントを移行しようとすると労力がかかりすぎます。「最もよく使うコンポーネント3つ」を SPM に移行するところから始めて、自分のワークフローに合っているかを確認してから範囲を広げる方が、長続きします。

Rork Max のプロジェクトから ContentCard.swift を1つ切り出して SPM パッケージに移すことで、このアーキテクチャを体験してみてください。2本目のアプリを作るとき、そのコンポーネントが既に「完成されたライブラリ」として待っているときの感覚は、かなり気持ちがよいものです。

実装の参考になれば幸いです。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-05-17
Rork Max の SwiftUI 機能を壁紙アプリ開発で検証した結果 — 実際に動いた機能と手を入れた機能
累計5,000万DLの壁紙アプリを開発してきた立場で、Rork MaxのSwiftUIネイティブ生成機能を実際に検証しました。「動いた機能」「手を入れた機能」「生成を諦めた機能」を具体的なコードと共に解説します。
開発ツール2026-04-18
Rork Max で SwiftUI と React Native を同じ仕様で作り比べた結果レポート
「SwiftUI ネイティブ vs React Native」という問いに実際の数字で答えます。Rork Max を使って同じ仕様のアプリを両方で実装し、開発速度・パフォーマンス・保守性を比較した実験レポートです。
開発ツール2026-07-18
AR で置いたものが翌朝には消えている — ARWorldMap による配置の永続化とリローカライズ設計
Rork Max が生成する AR アプリは、置いた 3D オブジェクトがアプリ再起動で消えます。ARWorldMap の保存タイミング、カスタムアンカーの符号化、リローカライズ待ちの見せ方、成立しない時の逃げ道までを設計として整理しました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →