Context
I am constructing a multi-platform SwiftUI app focusing on iOS 26 / macOS 26 (iPhone, iPad, Mac). I am utilizing MVVM with a Repository layer and the trendy @Observable macro all through. The app has listing -> element navigation and performs one-shot async information fetching.
My objective: allow wealthy Xcode Previews on all three platforms with out spinning up actual dependencies (community, database), whereas preserving the manufacturing ViewModel quick (no existential boxing overhead).
The sample I landed on
1. ViewModel protocol
protocol RecipeListViewModelProtocol: AnyObject, Observable {
var recipes: [Recipe] { get }
var isLoading: Bool { get }
func fetch() async
@discardableResult func delete(id: UUID) async -> Bool
}
2. Manufacturing ViewModel
@Observable
ultimate class RecipeListViewModel: RecipeListViewModelProtocol {
non-public(set) var recipes: [Recipe] = []
non-public(set) var isLoading = false
non-public let repository: RecipeRepositoryProtocol
init(repository: RecipeRepositoryProtocol = RecipeRepository()) {
self.repository = repository
}
func fetch() async {
isLoading = true
defer { isLoading = false }
recipes = (attempt? await repository.fetchAll()) ?? []
}
@discardableResult func delete(id: UUID) async -> Bool {
guard (attempt? await repository.delete(id: id)) != nil else { return false }
await fetch()
return true
}
}
3. Mock ViewModel (preview injection)
@Observable
ultimate class RecipeListViewModelMock: RecipeListViewModelProtocol {
var recipes: [Recipe]
var isLoading: Bool
init(recipes: [Recipe] = Recipe.samples, isLoading: Bool = false) {
self.recipes = recipes
self.isLoading = isLoading
}
func fetch() async {}
@discardableResult func delete(id: UUID) async -> Bool { true }
}
4. Generic view (static dispatch, no any)
struct RecipeList: View {
@State var vm: VM
var physique: some View {
Checklist(vm.recipes) { recipe in
Textual content(recipe.title)
}
.overlay { if vm.isLoading { ProgressView() } }
.process { await vm.fetch() }
}
}
5. Multi-platform PreviewProvider
struct RecipeList_Previews: PreviewProvider {
static var vm = RecipeListViewModelMock()
static var previews: some View {
Group {
RecipeList(vm: vm)
.previewDevice(PreviewDevice(rawValue: "iPhone 17 Professional"))
.previewDisplayName("iPhone")
RecipeList(vm: vm)
.previewDevice(PreviewDevice(rawValue: "iPad Professional 11-inch (M5)"))
.previewDisplayName("iPad")
RecipeList(vm: vm)
.previewDevice(PreviewDevice(rawValue: "My Mac"))
.previewDisplayName("Mac")
}
}
}
Questions
-
Is
protocol: AnyObject, Observable+ generic view (struct RecipeList) the really useful strategy in SwiftUI 6 / iOS 26? -
Ought to the mock dwell on the ViewModel layer or the Repository layer for Xcode Previews? The choice to the mock VM above could be injecting a mock repository into the actual VM:
@Observable
ultimate class RecipeRepositoryMock: RecipeRepositoryProtocol {
func fetchAll() async throws -> [Recipe] { Recipe.samples }
func delete(id: UUID) async throws {}
}
// within the preview:
static var previews: some View {
RecipeList(vm: RecipeListViewModel(repository: RecipeRepositoryMock()))
...
}
This workout routines the actual VM’s fetch/delete logic however requires the VM to be constructible within the preview. Is that this the popular strategy?
- How do generics propagate to little one views?
RecipeListis generic overVM, however little one views likeRecipeRowsolely want aRecipeworth and don’t have any dependency on the VM. Is passing the mannequin instantly adequate, or does the generic constraint have to circulation down?
struct RecipeRow: View {
let recipe: Recipe
var physique: some View { Textual content(recipe.title) }
}
// used inside RecipeList:
Checklist(vm.recipes) { recipe in
RecipeRow(recipe: recipe) // no VM generic wanted right here?
}
-
#Previewdoes not appear to help rendering a number of units concurrently. IsPreviewProvider+.previewDevice()nonetheless the proper strategy for side-by-side iPhone / iPad / Mac previews in iOS 26? -
Any architectural crimson flags within the sample above?
