In this post I describe how the Generative AI App Builder tool outlined in the introduction can be used to build a set of views using MVVM as a pattern for the view - domain model structure, and using an environment based Router with NavigationStack to perform the navigation between 2 of the views.
To check out the source code used for these experiments in automated generation the repository is here.
To generate views we distinguish between collections / single item models. We have to define the name, where to create the files and the domain models used by the view.
static func savedDecksViewFeatureSpec() -> MVVM.ViewSpecification {
.init(
viewName: "SavedDecksView",
viewFolderPath: "\(AiderControl.Constants.appModuleRoot)Views/",
models: [
.init(
variableName: "savedDecks",
modelType: "LocalDeck",
modelPath: "\(AiderControl.Constants.appModuleRoot)Domain/LocalDeck.swift",
isCollection: true
)
]
)
}static func deckDetailViewSpecification() -> MVVM.ViewSpecification {
.init(
viewName: "DeckDetailsView",
viewFolderPath: "\(AiderControl.Constants.appModuleRoot)Views/",
models: [
.init(
variableName: "deck",
modelType: "LocalDeck",
modelPath: "\(AiderControl.Constants.appModuleRoot)Domain/LocalDeck.swift",
isCollection: false
)
]
)
}To generate navigation links we need to know where the navigation is from and to, we describe what triggers the navigation. One of the advantages of using LLMs to generate code is that unlike traditional code generation, we have a lot more readable descriptions of how a product behaves in our specification files.
static func savedDecksToDeckDetailNavigationSpec() -> EnvironmentRouterNavigation.NavigationLink {
.init(
from: .init(
// mapping from mvvm views to router navigation spec.
// These 2 packages are de-coupled to allow flexibilty to choose other architectures
mvvmView: .savedDecksViewFeatureSpec()
),
to: .init(mvvmView: .deckDetailViewSpecification()),
navigateWhen: "a deck is selected from the list of decks",
dataMappings: [
.init(use: "savedDecks items in collection", toCreate: "deck")
],
routes: .init(
filePath: "\(AiderControl.Constants.appModuleRoot)Navigation/Route.swift",
name: "Router.Routes"
),
viewBuilder: .init(
filePath: "\(AiderControl.Constants.appModuleRoot)Navigation/SwiftUIRouteViewBuilder.swift",
name: "SwiftUIRouteViewBuilder"
)
)
}The full code produced can be seen here:
This is the view, view model and navigation for the collection of Saved Decks defined above. It’s simple but functional, and demonstrates that the system does produce working output.
struct SavedDecksView: View {
@State var viewModel: ViewModel
@Environment(Router.self) private var router: Router
var body: some View {
SavedDecksContentView(
decks: viewModel.decks,
viewActionOne: viewModel.viewActionOne,
navigateToDeckDetails: { deck in
router.navigateTo(.deckDetails(deck: deck))
}
)
}
}
struct SavedDecksContentView: View {
let decks: [LocalDeck]
let viewActionOne: () -> Void
let navigateToDeckDetails: (LocalDeck) -> Void
var body: some View {
VStack {
Button(action: viewActionOne) {
Text("View Action 1")
}
ForEach(decks) { deck in
Button(action: {
navigateToDeckDetails(deck)
}) {
Text(deck.name)
}
}
}
}
}
extension SavedDecksView {
@Observable
class ViewModel {
var decks: [LocalDeck]
init(decks: [LocalDeck]) {
self.decks = decks
}
func viewActionOne() {
// Placeholder for view action.
if let firstDeck = decks.first {
decks[0] = LocalDeck(id: firstDeck.id, name: firstDeck.name + "!", questions: []) // Question is not defined, so I'm using an empty array here.
}
}
}
}