agentsclimarketplace

Ios feature scaffold

Skill j-morgan6/iOS-from-web-guide/skills/ios-feature-scaffold

MANDATORY when the user asks to add or scaffold a new feature or screen. Chains Model + ViewModel + View + APIClient extension + test stub in one pass.From its SKILL.md

Install
npx -y skills add j-morgan6/iOS-from-web-guide --skill ios-feature-scaffold

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

8.2 KB, as published. Nobody here has run it

iOS Feature Scaffold

RULES — Follow these with no exceptions

  1. One feature = five files. Model, ViewModel, View, APIClient extension, ViewModel test stub. Generate them all — don't stop after the View.
  2. Every generated file follows the relevant Track A skill. Model respects swiftui-equatable-hashable-for-diffing. ViewModel respects swiftui-observable-viewmodel-boilerplate. APIClient extension respects ios-api-client-foundation. View respects swiftui-navigation-foundations + swiftui-layout-pitfalls.
  3. Register navigation destinations at the root NavigationStack, not inside the new feature's View. Hoisting is a hard rule from swiftui-navigation-foundations.
  4. Model conforms to Codable, Hashable, Identifiable, Sendable — always all four. Sendable for Swift 6 concurrency, the rest for JSON/SwiftUI.
  5. View has three states: loading, empty, error, data. Four, actually. Every generated View includes placeholders for all four. Empty-state copy is better than a blank screen.
  6. The ViewModel test stub starts passing with a mocked APIClient, not hitting the network. A failing test skeleton is worse than no test skeleton — leave it green.

When to invoke

  • User types or says: "add a new feature called X", "scaffold a Bookmarks feature", "create the Explore screen".
  • You're about to create Views/X/XView.swift and realize you also need Model + ViewModel + APIClient extension.

This is a user-invoked skill — it triggers on the request, not on a file path. It's the highest-level orchestration skill: it chains every Track A pattern.

The generated shape (example: Bookmarks feature)

Models/Bookmark.swift
ViewModels/BookmarksViewModel.swift
Views/Bookmarks/BookmarksView.swift
Services/APIClient+Bookmarks.swift
YourAppTests/BookmarksViewModelTests.swift

Plus one edit to the root MainView.swift registering navigationDestination(for: Bookmark.self) if the feature is pushable.

Canonical Model

// Models/Bookmark.swift
import Foundation

struct Bookmark: Identifiable, Hashable, Codable, Sendable {
    let id: Int
    let postId: Int
    let createdAt: Date
    var note: String?
}

Conform to all four protocols. No custom ==. (See swiftui-equatable-hashable-for-diffing.)

Canonical ViewModel

// ViewModels/BookmarksViewModel.swift
import SwiftUI

@MainActor
@Observable
final class BookmarksViewModel {
    var bookmarks: [Bookmark] = []
    var isLoading = false
    var errorMessage: String?

    private let api: APIClientProtocol

    init(api: APIClientProtocol = APIClient.shared) {
        self.api = api
    }

    func load() async {
        isLoading = true
        errorMessage = nil
        do {
            let response: PaginatedResponse<Bookmark> = try await api.get(path: "/bookmarks")
            bookmarks = response.data
        } catch {
            errorMessage = "Couldn't load bookmarks."
        }
        isLoading = false
    }

    func remove(_ bookmark: Bookmark) {
        bookmarks.removeAll { $0.id == bookmark.id }
        Task {
            _ = try? await api.delete(path: "/bookmarks/\(bookmark.id)") as EmptyResponse
        }
    }
}

See swiftui-observable-viewmodel-boilerplate and swiftui-optimistic-ui-pattern.

Canonical APIClient extension

// Services/APIClient+Bookmarks.swift
extension APIClient {
    func fetchBookmarks() async throws -> PaginatedResponse<Bookmark> {
        try await get(path: "/bookmarks")
    }

    func removeBookmark(id: Int) async throws -> EmptyResponse {
        try await delete(path: "/bookmarks/\(id)")
    }
}

See ios-api-client-foundation.

Canonical View (four states)

// Views/Bookmarks/BookmarksView.swift
import SwiftUI

struct BookmarksView: View {
    @State private var viewModel = BookmarksViewModel()

    var body: some View {
        Group {
            if viewModel.isLoading && viewModel.bookmarks.isEmpty {
                ProgressView()
            } else if let message = viewModel.errorMessage {
                ContentUnavailableView("Error", systemImage: "exclamationmark.triangle", description: Text(message))
            } else if viewModel.bookmarks.isEmpty {
                ContentUnavailableView("No bookmarks yet", systemImage: "bookmark", description: Text("Tap the bookmark icon on any post to save it."))
            } else {
                List {
                    ForEach(viewModel.bookmarks) { bookmark in
                        NavigationLink(value: bookmark) {
                            BookmarkRow(bookmark: bookmark)
                        }
                    }
                    .onDelete { indexSet in
                        for i in indexSet { viewModel.remove(viewModel.bookmarks[i]) }
                    }
                }
                .listStyle(.plain)
            }
        }
        .containerRelativeFrame(.horizontal, alignment: .leading)
        .navigationTitle("Bookmarks")
        .task { await viewModel.load() }
    }
}

See swiftui-navigation-foundations and swiftui-layout-pitfalls.

Canonical ViewModel test stub

// YourAppTests/BookmarksViewModelTests.swift
import XCTest
@testable import YourApp

@MainActor
final class BookmarksViewModelTests: XCTestCase {
    func testLoadPopulatesBookmarks() async {
        let mockAPI = MockAPIClient()
        mockAPI.stubbedGetResponse = PaginatedResponse<Bookmark>(
            data: [Bookmark(id: 1, postId: 42, createdAt: Date(), note: nil)],
            cursor: nil
        )
        let vm = BookmarksViewModel(api: mockAPI)

        await vm.load()

        XCTAssertEqual(vm.bookmarks.count, 1)
        XCTAssertFalse(vm.isLoading)
        XCTAssertNil(vm.errorMessage)
    }
}

The mock starts passing immediately. Real network isn't touched.

Root navigation destination registration

// Views/MainView.swift
NavigationStack(path: $router.path) {
    BookmarksView()
        .navigationDestination(for: Bookmark.self) { bookmark in
            BookmarkDetailView(bookmark: bookmark)
        }
        // plus other destinations
}

This edit must happen in MainView.swift, not anywhere else. Hoisted destinations — see swiftui-navigation-foundations.

Common pitfalls

Skipping the APIClient extension and calling URLSession inline

No. Every network call goes through APIClient. If a new endpoint shape is needed, add a method there.

Skipping the test stub

A ViewModel with no test on merge day becomes a ViewModel with no test ever. Generate the stub even if it only verifies load() populates the array.

Registering navigationDestination(for:) inside the new View

The destination never fires if the parent is a lazy container. Always hoist to MainView / the root NavigationStack.

Generating a Model without Sendable

Swift 6 concurrency will complain the moment you pass the model across an actor boundary. Always all four: Identifiable, Hashable, Codable, Sendable.

Using @StateObject in the generated View

Regression to iOS-16-era patterns. Always @State for @Observable classes.

Template reference

No single template — this skill composes the Track A templates and patterns. The generated files follow:

  • templates/APIClient.swift for the client layer.
  • templates/APIClientProtocol.swift — the injection seam the generated ViewModel depends on.
  • templates/MockAPIClient.swift — drop into YourAppTests/ so the generated test stub compiles and passes immediately.
  • templates/project.yml.template for where files are rooted in the source tree.

Related skills

Every Track A skill. This is the orchestrator that invokes them.

  • ios-project-structure (where files go)
  • ios-api-client-foundation (APIClient extension)
  • swiftui-observable-viewmodel-boilerplate (ViewModel shape)
  • swiftui-navigation-foundations (destination registration)
  • swiftui-layout-pitfalls (View layout)
  • swiftui-equatable-hashable-for-diffing (Model conformances)
  • swiftui-async-image-with-backend-paths (for Views displaying remote images)
  • swiftui-optimistic-ui-pattern (mutation methods)

Keep looking

Skills are one crate of 628,853. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.