The Real Payoff of SwiftUI Migration: Building Features Faster
When we migrated ScaleDegreeChord from UIKit to SwiftUI using Loop Engineering in our Music Theory Learning app, the goal wasn't just to modernize an existing screen. It was to establish a reusable architecture. That migration introduced a SwiftUI + MVVM foundation that has already proven its value.
Our new CadenceView, designed to practice chord progressions instead of individual chords, was built on top of this existing architecture. Rather than starting from scratch, we reused the established state management, navigation, settings integration, and scoring logic, allowing us to focus almost entirely on the new learning experience.
Not every similarity should become a shared abstraction. Avoiding premature abstraction often leads to simpler, more maintainable code. For features that solve related but evolving problems, copying a proven implementation and adapting it can be a better choice than introducing a generic framework that must accommodate every future variation.
This article demonstrates how a well-factored MVVM layer - a shared RunMode state machine, ClassKit/AAC scoring pipeline, and settings/notification bridging - pays for itself across features. The real benefit of migrating off UIKit isn't the rewrite itself; it's that the resulting ObservableObject view models become a reusable substrate, cutting the marginal cost of each subsequent SwiftUI feature down to just its task model and view layer.
Code examples
The goal wasn't to copy an existing feature, but to reuse a proven architecture. While the exercise logic and UI are completely new, much of the surrounding infrastructure could be adapted from the existing `ScaleDegreeChord` implementation.
1. Reusing the state machine
Both exercises follow the same state-driven workflow: the user enters an answer, validates it, reviews the result, and moves on to the next task.
enum RunMode {
case userInput
case showResult
}
func didTapGo() {
selectNextState()
}
func didTapClear() {
guard runMode == .userInput else { return }
clearAnswer()
}
By keeping the interaction model identical, the new feature behaves consistently with the existing practice screens while requiring very little new infrastructure.
2. Reusing the application infrastructure
The surrounding plumbing for progress tracking, scoring, navigation, and settings was already established. Only the exercise-specific scoring logic had to be implemented.
Shared infrastructure for ClassKit integration:
MNode.updateActivity(
forPath: presentationContext.path,
progress: progress,
score: settings.presetCurScore,
maxScore: settings.presetMaxScore
)
New scoring logic for the cadence exercise:
settings.presetCurScore += Int32(correctCount())
This separation allowed the new feature to integrate seamlessly with the rest of the app without duplicating complex infrastructure.
3. New exercise, new UI
This is where the new feature actually differs. Instead of identifying a single chord on a piano keyboard, users now complete an entire cadence by selecting chord symbols.
@Published var answeredSymbols: [String?] = []
...
func didTapCard(_ card: CadenceChordCard) {
answeredSymbols[currentIndex] = card.symbol
}
The input method is equally different. A custom `InputCardsView` replaces the keyboard with a card-based interface specifically designed for cadence practice.
InputCardsView(
cards: availableCards,
isEnabled: runMode == .userInput
) { card in
viewModel.didTapCard(card)
}
Although the learning experience is completely new, it is built on top of the same architectural foundation established during the earlier SwiftUI migration. The architecture enables new SwiftUI-based features to interface seamlessly with existing, well-tested Objective-C layers, allowing stable foundations to remain in place while the application evolves.
