Xcode ToolsMidOpen-ended
How do you create and install an Xcode File Template for a SwiftUI MVVM architecture feature?
Explanation & Code
Answer: An Xcode MVVM Feature template generates three pre-wired files for SwiftUI screens (Model with explicit state enum, ViewModel with injectable service protocol, and View with StateObject autoclosure and previews) to guarantee unidirectional data flow and instant previewability without backend dependencies.
File ▸ New ▸ File from Template… ▸ SwiftUI ▸ MVVM Feature ▸ "Profile"
Generates:
ProfileModel.swift Domain model and single-source-of-truth State enum
ProfileViewModel.swift ObservableObject ViewModel with protocol-based service and stub
ProfileView.swift SwiftUI View with StateObject, task lifecycle, and interactive previews
1. Installation Script
Run this shell script in Terminal to install the template into Xcode's user templates directory:
set -e
TPL=~/Library/Developer/Xcode/Templates/"File Templates"/SwiftUI/MVVMFeature.xctemplate
mkdir -p "$TPL"; cd "$TPL"
V='___VARIABLE_productName:identifier___'
H='//
// ___FILENAME___
// ___PACKAGENAME___
//
// Created by ___FULLUSERNAME___ on ___DATE___.
// ___COPYRIGHT___
//'
cat > TemplateInfo.plist <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Kind</key>
<string>Xcode.IDEFoundation.TextSubstitutionFileTemplateKind</string>
<key>Identifier</key>
<string>com.template.SwiftUIMVVMFeature</string>
<key>Name</key>
<string>MVVM Feature</string>
<key>Description</key>
<string>Creates a SwiftUI MVVM feature: Model, ViewModel and View.</string>
<key>Summary</key>
<string>Create SwiftUI MVVM Feature</string>
<key>SortOrder</key>
<string>1</string>
<key>Platforms</key>
<array>
<string>com.apple.platform.iphoneos</string>
</array>
<key>Options</key>
<array>
<dict>
<key>Identifier</key><string>productName</string>
<key>Name</key><string>Feature Name:</string>
<key>Description</key><string>Base name of the feature, e.g. Profile (no suffix).</string>
<key>Type</key><string>text</string>
<key>Required</key><true/>
<key>NotPersisted</key><true/>
</dict>
</array>
</dict>
</plist>
EOF
cat > ___FILEBASENAME___Model.swift <<EOF
$H
import Foundation
/// One row of data shown by the screen.
struct ${V}Item: Identifiable, Equatable {
let id: UUID
let title: String
init(id: UUID = UUID(), title: String) {
self.id = id
self.title = title
}
}
/// Every state the screen can be in, as a single value.
enum ${V}State: Equatable {
case loading
case loaded([${V}Item])
case failed(String)
}
EOF
cat > ___FILEBASENAME___ViewModel.swift <<EOF
$H
import Foundation
/// The screen's data source, behind a protocol so it can be swapped
/// for a stub in tests and previews.
protocol ${V}Service {
func fetchItems() async throws -> [${V}Item]
}
@MainActor
final class ${V}ViewModel: ObservableObject {
@Published private(set) var state: ${V}State = .loading
private let service: ${V}Service
init(service: ${V}Service) {
self.service = service
}
func load() async {
state = .loading
do {
state = .loaded(try await service.fetchItems())
} catch {
state = .failed(error.localizedDescription)
}
}
}
#if DEBUG
/// Stub for previews and tests.
struct ${V}StubService: ${V}Service {
var items: [${V}Item] = [${V}Item(title: "Example")]
var error: Error?
func fetchItems() async throws -> [${V}Item] {
if let error { throw error }
return items
}
}
#endif
EOF
cat > ___FILEBASENAME___View.swift <<EOF
$H
import SwiftUI
struct ${V}View: View {
@StateObject private var viewModel: ${V}ViewModel
init(viewModel: @autoclosure @escaping () -> ${V}ViewModel) {
_viewModel = StateObject(wrappedValue: viewModel())
}
var body: some View {
content
.task { await viewModel.load() }
}
@ViewBuilder
private var content: some View {
switch viewModel.state {
case .loading:
ProgressView()
case .loaded(let items):
List(items) { item in
Text(item.title)
}
case .failed(let message):
VStack(spacing: 12) {
Text(message)
.multilineTextAlignment(.center)
Button("Try Again") {
Task { await viewModel.load() }
}
}
.padding()
}
}
}
#if DEBUG
#Preview("Loaded") {
${V}View(viewModel: ${V}ViewModel(service: ${V}StubService()))
}
#Preview("Failed") {
${V}View(viewModel: ${V}ViewModel(
service: ${V}StubService(error: URLError(.notConnectedToInternet))
))
}
#endif
EOF
plutil -lint TemplateInfo.plist
2. Architecture Details
- Single Enum State: Rather than juggling multiple boolean flags (
isLoading,errorMessage,data), one enum prevents impossible states (e.g. loading while displaying an error). @autoclosureStateObject Injection: Wrapping the ViewModel initialiser parameter in an autoclosure prevents unnecessary re-instantiation across parent view re-evaluations.- Protocol-driven Service: Separating data fetching behind a
${V}Serviceprotocol enables stubbing in SwiftUI Canvas Previews and fast unit testing. - Swift Concurrency: The
.taskmodifier handles automatic cancellation when views disappear, while@MainActoron the ViewModel prevents race conditions.
3. Unit Testing the ViewModel
import XCTest
@MainActor
final class ProfileViewModelTests: XCTestCase {
func test_load_succeeds() async {
let item = ProfileItem(title: "Example")
let viewModel = ProfileViewModel(
service: ProfileStubService(items: [item])
)
await viewModel.load()
XCTAssertEqual(viewModel.state, .loaded([item]))
}
func test_load_failure() async {
let viewModel = ProfileViewModel(
service: ProfileStubService(error: URLError(.badServerResponse))
)
await viewModel.load()
guard case .failed = viewModel.state else {
return XCTFail("expected .failed state")
}
}
}
Further reading
- Managing model data in your app — Apple Developerdeveloper.apple.com (opens in a new tab)
- Create Xcode Templates: A Comprehensive Guide — Fabio Giannellimedium.com (opens in a new tab)
- How to create a custom Xcode template for coordinators — Hacking with Swifthackingwithswift.com (opens in a new tab)
- How to create custom Xcode templates — Hacking with Swifthackingwithswift.com (opens in a new tab)
Rate your understanding: