How to Build a Piano Practice App in SwiftUI
A Piano Practice app helps musicians track sessions, read sheet music, and sharpen their ear with interactive interval and chord recognition exercises — perfect for students and hobbyists who want a focused, ad-free practice companion on iPhone.
Prerequisites
- Solid SwiftUI fundamentals including
@Observable,NavigationStack, and customCanvasdrawing. - Familiarity with AVFoundation — specifically
AVAudioEngine,AVAudioPlayerNode, and scheduling audio buffers. - An Apple Developer Program membership ($99/year) and basic understanding of StoreKit 2 for subscription paywalls.
Step-by-Step Build
1. Define the SwiftData practice model
Model each piece, session, and ear-training result as persistent @Model classes so progress survives app restarts.
import SwiftData
import Foundation
@Model
final class Piece {
var title: String
var composer: String
var tempoTarget: Int // BPM
var sessions: [PracticeSession]
init(title: String, composer: String, tempoTarget: Int = 80) {
self.title = title
self.composer = composer
self.tempoTarget = tempoTarget
self.sessions = []
}
}
@Model
final class PracticeSession {
var date: Date
var durationSeconds: Int
var tempoAchieved: Int
var earTrainingScore: Double // 0.0–1.0
init(date: Date = .now, duration: Int, tempo: Int, score: Double) {
self.date = date
self.durationSeconds = duration
self.tempoAchieved = tempo
self.earTrainingScore = score
}
}
2. Build the practice dashboard view
Present a NavigationStack-backed list of pieces with swipe actions and a floating "Start Session" button that pushes into the practice screen.
import SwiftUI
import SwiftData
struct PieceListView: View {
@Query(sort: \Piece.title) private var pieces: [Piece]
@Environment(\.modelContext) private var context
@State private var showAdd = false
var body: some View {
NavigationStack {
List(pieces) { piece in
NavigationLink(destination: PracticeSessionView(piece: piece)) {
PieceRow(piece: piece)
}
.swipeActions { Button("Delete", role: .destructive) {
context.delete(piece)
}}
}
.navigationTitle("My Pieces")
.toolbar { Button("Add", systemImage: "plus") { showAdd = true } }
.sheet(isPresented: $showAdd) { AddPieceSheet() }
}
}
}
3. Implement sheet music rendering and ear training
Use Canvas to draw a five-line staff with noteheads, then drive AVAudioEngine to synthesize tones for real-time interval recognition challenges.
import SwiftUI
import AVFoundation
struct StaffView: View {
let notes: [MusicNote] // your model type (pitch, duration, position)
var body: some View {
Canvas { ctx, size in
let lineSpacing: CGFloat = size.height / 8
for i in 0..<5 {
let y = lineSpacing * CGFloat(i + 2)
ctx.stroke(Path { p in p.move(to: .init(x: 0, y: y))
p.addLine(to: .init(x: size.width, y: y)) },
with: .color(.primary), lineWidth: 1.2)
}
for note in notes {
let cx = CGFloat(note.xPosition) * size.width
let cy = lineSpacing * CGFloat(note.staffLine)
ctx.fill(Ellipse().path(in: .init(x: cx-8, y: cy-5, width: 16, height: 10)),
with: .color(.primary))
}
}
}
}
// Ear training: play two tones and ask user to name the interval
func playInterval(rootMidi: Int, intervalSemitones: Int, engine: AVAudioEngine) {
let player = AVAudioPlayerNode()
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: nil)
try? engine.start()
[rootMidi, rootMidi + intervalSemitones].enumerated().forEach { idx, midi in
let freq = 440.0 * pow(2.0, Double(midi - 69) / 12.0)
let buf = sineBuffer(frequency: freq, duration: 1.0,
format: engine.mainMixerNode.outputFormat(forBus: 0))
player.scheduleBuffer(buf, at: idx == 0 ? nil :
AVAudioTime(hostTime: 0), completionHandler: nil)
}
player.play()
}
Common Pitfalls
- Audio session conflicts: Always configure
AVAudioSession.sharedInstance()with.playbackcategory and activate it before startingAVAudioEngine— otherwise audio silently fails on device. - Canvas layout thrashing: Avoid recomputing note positions inside the
Canvasdraw closure; pre-calculate layout in a view model and pass resolved coordinates to keep frame rate smooth. - SwiftData migration: Adding new properties to your
@Modelclasses after first install requires a versionedSchemaMigrationPlan— plan your schema early to avoid data loss on TestFlight updates.
Monetization: Auto-Renewable Subscription
A subscription tier fits Piano Practice apps naturally — offer a free tier with basic session tracking, then gate sheet music imports, advanced ear training exercises, and detailed analytics behind a monthly or annual plan using StoreKit 2's Product.purchase() API. Use Transaction.currentEntitlements to gate premium views on every app launch, and implement a SubscriptionStoreView (iOS 17+) to display your paywall with App Store–hosted localizations. Annual plans (e.g. $29.99/yr) convert better than monthly for practice apps because users think in "academic year" cycles — consider offering a 7-day free trial to reduce friction.
Ship Faster with Soarias
An advanced app like this — with custom Canvas rendering, AVFoundation audio synthesis, SwiftData persistence, and a StoreKit 2 paywall — can easily consume three to four weeks of scaffolding before a single note plays. Soarias ($79, one-time) integrates directly with Claude Code to generate your entire SwiftUI project structure, including the data model, audio engine wrappers, staff-drawing canvas, and subscription paywall, from a plain-English prompt. It then handles fastlane setup, App Store screenshot generation, and metadata submission — turning a 21-day build into a focused long weekend of refinement on the parts only you can decide.
Related Tutorials
FAQ
Do I need an Apple Developer account to build this app?
You can build and run the app on a personal device for free using a personal team in Xcode. However, to distribute on TestFlight, submit to the App Store, or use production StoreKit subscriptions, you need an Apple Developer Program membership at $99/year.
How do I submit a Piano Practice app to the App Store?
Archive your app in Xcode (Product → Archive), validate it through Organizer, then upload to App Store Connect. Fill in your app's metadata, screenshots for all required device sizes, privacy nutrition labels, and — because this app uses a subscription — a StoreKit configuration and a clear description of the subscription terms. Apple's review typically takes 1–3 business days for new submissions.
Last reviewed: 2026-05-12 by the Soarias team.