From ios-from-web-guide
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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ios-from-web-guide:ios-feature-scaffoldThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
1. **One feature = five files.** Model, ViewModel, View, APIClient extension, ViewModel test stub. Generate them all — don't stop after the View.
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.NavigationStack, not inside the new feature's View. Hoisting is a hard rule from swiftui-navigation-foundations.Codable, Hashable, Identifiable, Sendable — always all four. Sendable for Swift 6 concurrency, the rest for JSON/SwiftUI.APIClient, not hitting the network. A failing test skeleton is worse than no test skeleton — leave it green.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.
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.
// 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.)
// 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.
// 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.
// 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.
// 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.
// 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.
URLSession inlineNo. Every network call goes through APIClient. If a new endpoint shape is needed, add a method there.
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.
navigationDestination(for:) inside the new ViewThe destination never fires if the parent is a lazy container. Always hoist to MainView / the root NavigationStack.
SendableSwift 6 concurrency will complain the moment you pass the model across an actor boundary. Always all four: Identifiable, Hashable, Codable, Sendable.
@StateObject in the generated ViewRegression to iOS-16-era patterns. Always @State for @Observable classes.
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.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)npx claudepluginhub j-morgan6/ios-from-web-guide --plugin ios-from-web-guideGuides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Resolves in-progress git merge or rebase conflicts by analyzing history, understanding intent, and preserving both changes where possible. Runs automated checks after resolution.