0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUI × Firebase × PencilKitで作る「Fuyomi」:多機能演奏支援アプリの実装ノウハウまとめ

Posted at

概要

この記事では、2025年6月1日にリリース予定のiOSアプリ「Fuyomi」の開発過程で得られた、以下の技術的知見をまとめます:

  • SwiftUIでPDFと画像を同一構造で扱う方法
  • PencilKitでの描画内容をStorageに保存・同期する構造
  • Firebase Authentication(匿名認証)+ Firestore/Storageの統合
  • Core Hapticsによる振動付きメトロノームの実装

アプリ概要と実装方針

Fuyomiは、演奏者向けに以下の機能を1つに統合した音楽支援アプリです:

機能 実装技術
メトロノーム Core Haptics + Timer + 音/光連携
チューナー AudioEngine + FFT(周波数解析)
楽譜ノート(PDF/画像) PDFKit + UIImageView + PencilKit
自動スクロール ScrollView + Timer or スライダー制御
クラウド同期 Firebase Auth + Firestore + Storage

PDF/画像+手書き描画の統合表示

背景

SwiftUIにおいてPDFKitはUIViewRepresentableを通じて使う必要がありますが、同一画面でPencilKitの描画も重ねたい場合、レイヤー構造とサイズ調整が煩雑になります。

解決アプローチ

  1. PDF/画像はどちらも CanvasView に共通処理を持たせる
  2. 表示用には ZStack を使い、背景(PDF/画像)と描画レイヤーを重ねる
  3. 描画領域のスケーリングとオフセットは、PDFのpage.bounds(for:)や画像のsizeから動的に計算
swift
ZStack {
    if isPDF {
        PDFPageView(page: pdfPage)
    } else {
        Image(uiImage: image)
            .resizable()
            .scaledToFit()
    }

    CanvasView(drawing: $drawing)  // PencilKit
}

PencilKit描画とFirebase Storageの連携

保存処理(書き込み)

let drawingData = drawing.dataRepresentation()
let ref = storageRef.child("users/\(uid)/notes/\(noteId)/drawings/page_0.data")
ref.putData(drawingData, metadata: nil)

読み込み処理(復元)

ref.getData(maxSize: 5 * 1024 * 1024) { data, error in
    if let data = data {
        self.drawing = try? PKDrawing(data: data)
    }
}
  • PKDrawingはシリアライズ可能なDataとしてStorage保存が可能
  • Firestoreではページごとのメタ情報(ページ数・ページ順など)を保存

Firebase匿名認証+Firestore構成

認証

Auth.auth().signInAnonymously { result, error in
    // uidは result.user.uid
}

Firestoreドキュメント構造(例)

users/{uid}/notes/{noteId}
 ├── title: String
 ├── createdAt: Timestamp
 ├── pageCount: Int

メトロノームの振動制御(Core Haptics)

do {
    let engine = CHHapticEngine()
    try engine.start()
    let event = CHHapticEvent(
        eventType: .hapticTransient,
        parameters: [.init(parameterID: .hapticIntensity, value: 1.0)],
        relativeTime: 0
    )
    let pattern = try CHHapticPattern(events: [event], parameters: [])
    let player = try engine.makePlayer(with: pattern)
    try player.start(atTime: 0)
} catch {
    print("Haptics Error: \(error)")
}
  • 拍ごとに振動パターンを変更可能(1拍目は強く、それ以外は弱く等)

スクロール制御の失敗談と方針転換

当初、BPMに合わせた自動譜めくりをTimer制御で実装していましたが:

  • スクロール速度とテンポの微妙なズレ
  • 長時間演奏時の累積誤差

…といった問題から、BPM連動は廃止し、ユーザーが速度を調整できるスライダーUIに変更しました。

Dreamifyと今後の展望

このアプリは、非営利団体「Dreamify」のプロジェクトとして開発されました。
Dreamifyは、2025年6月中に非営利型一般社団法人として法人化予定で、今後も教育・音楽・災害支援などの分野でアプリ開発を行っていく予定です。

Fuyomiの今後の機能追加予定:

  • MP3音源からの耳コピ機能(楽譜自動生成)
  • AIによる演奏解析/自動テンポ制御
  • アカウント機能と他端末共有
  • Android / watchOS 対応

ご意見・Issue・Fork歓迎です!

本記事の内容やFuyomiに関する技術質問・提案は大歓迎です!
コメント欄で気軽にどうぞ!

リンク集

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?