15.4 C
Canberra
Thursday, May 21, 2026

swift – SwiftUI iOS 26: ViewModel protocol + generic views for multi-platform Xcode preview injection. Greatest apply?


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

  1. Is protocol: AnyObject, Observable + generic view (struct RecipeList) the really useful strategy in SwiftUI 6 / iOS 26?

  2. 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?

  1. How do generics propagate to little one views? RecipeList is generic over VM, however little one views like RecipeRow solely want a Recipe worth 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?
}
  1. #Preview does not appear to help rendering a number of units concurrently. Is PreviewProvider + .previewDevice() nonetheless the proper strategy for side-by-side iPhone / iPad / Mac previews in iOS 26?

  2. Any architectural crimson flags within the sample above?

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

[td_block_social_counter facebook="tagdiv" twitter="tagdivofficial" youtube="tagdiv" style="style8 td-social-boxed td-social-font-icons" tdc_css="eyJhbGwiOnsibWFyZ2luLWJvdHRvbSI6IjM4IiwiZGlzcGxheSI6IiJ9LCJwb3J0cmFpdCI6eyJtYXJnaW4tYm90dG9tIjoiMzAiLCJkaXNwbGF5IjoiIn0sInBvcnRyYWl0X21heF93aWR0aCI6MTAxOCwicG9ydHJhaXRfbWluX3dpZHRoIjo3Njh9" custom_title="Stay Connected" block_template_id="td_block_template_8" f_header_font_family="712" f_header_font_transform="uppercase" f_header_font_weight="500" f_header_font_size="17" border_color="#dd3333"]
- Advertisement -spot_img

Latest Articles