agentsclimarketplace

Ios swiftui architecture and state

Skill hamzabellouch/agent-skills/Mobile Development/ios-swiftui-architecture-and-state

Production-grade iOS SwiftUI architecture, state management (@Observable, @State, @Binding, @Environment), Modern Swift Concurrency (async/await, Actors, Task Groups), NavigationStack routing, and Clean Architecture implementation patterns. Use when designing, building, or refactoring iOS apps in SwiftUI.From its SKILL.md

Install
npx -y skills add hamzabellouch/agent-skills --skill ios-swiftui-architecture-and-state

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

2 things to look at

  • 23 days oldThe repository was created 23 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
  • 3 stars3 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

12.9 KB, ~2.4k tokens by cl100k_base, as published. Nobody here has run it

SwiftUI Architecture & State Management

A comprehensive guide for building scalable, high-performance, and testable iOS applications using SwiftUI, Swift 5.9+ @Observable, Modern Swift Concurrency, and Clean Architecture principles.


1. Core State Management Rules (Swift 5.9+ / Observation Framework)

Swift 5.9 introduced the @Observable macro, replacing ObservableObject, @Published, @StateObject, and @ObservedObject.

Property Wrapper Selection Matrix

ToolScope / Use CaseOwnership / Lifetime
@StateLocal view-private value types or @Observable object instantiationView owns the lifecycle
@BindingTwo-way binding passed from parent to childParent owns the value; child mutates
@EnvironmentDependency injection & shared environment valuesSystem or ancestor view owns
@ObservableReference type domain models & ViewModelsManaged via @State or @Environment
let / varRead-only properties or local computed propertiesDependent on parent view initialization

Rules for State Management

  1. Never use ObservableObject in new Swift 5.9+ code. Prefer @Observable final class MyViewModel.
  2. Instantiate @Observable view models in views using @State:
    struct FeedView: View {
        @State private var viewModel = FeedViewModel()
        var body: some View { ... }
    }
    
  3. Pass down @Observable classes as non-wrapped properties or environment objects:
    // Shared via Environment
    FeedDetailView()
        .environment(viewModel)
    
    // Received in Subview
    struct FeedDetailView: View {
        @Environment(FeedViewModel.self) private var viewModel
    }
    

2. SwiftUI Clean Architecture

The recommended architecture separates responsibilities into Presentation (Views + ViewModels), Domain (Use Cases + Entities), and Data (Repositories + Data Sources).

   ┌────────────────────────────────────────────────────────┐
   │                    Presentation Layer                   │
   │  ┌──────────────────────┐    ┌──────────────────────┐  │
   │  │    SwiftUI View      │───>│      ViewModel       │  │
   │  └──────────────────────┘    └──────────────────────┘  │
   └─────────────────────────────│──────────────────────────┘
                                 │ Depends on abstraction
   ┌─────────────────────────────▼──────────────────────────┐
   │                      Domain Layer                       │
   │  ┌──────────────────────┐    ┌──────────────────────┐  │
   │  │      Use Case        │───>│ Repository Interface │  │
   │  └──────────────────────┘    └──────────────────────┘  │
   └─────────────────────────────▲──────────────────────────┘
                                 │ Implements
   ┌─────────────────────────────│──────────────────────────┐
   │                       Data Layer                        │
   │  ┌──────────────────────────────────────────────────┐  │
   │  │           Repository Implementation              │  │
   │  └──────────────────────────────────────────────────┘  │
   │           │                               │            │
   │  ┌────────▼─────────┐             ┌───────▼─────────┐  │
   │  │  Remote Service  │             │   Local Database│  │
   │  │  (URLSession)    │             │   (SwiftData)   │  │
   │  └──────────────────┘             └─────────────────┘  │
   └────────────────────────────────────────────────────────┘

Complete Code Implementation Example

Domain Layer: Model & Repository Protocol

import Foundation

struct Article: Identifiable, Hashable, Sendable {
    let id: UUID
    let title: String
    let summary: String
    let publishedAt: Date
}

protocol ArticleRepository: Sendable {
    func fetchArticles() async throws -> [Article]
    func markAsFavorite(id: UUID) async throws
}

Data Layer: Remote Data Source & Repository Implementation

actor ArticleRemoteDataSource {
    private let urlSession: URLSession

    init(urlSession: URLSession = .shared) {
        self.urlSession = urlSession
    }

    func fetchArticlesDTO() async throws -> [ArticleDTO] {
        let url = URL(string: "https://api.example.com/v1/articles")!
        let (data, response) = try await urlSession.data(from: url)

        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }

        return try JSONDecoder().decode([ArticleDTO].self, from: data)
    }
}

struct ArticleDTO: Decodable {
    let id: UUID
    let title: String
    let body: String
    let createdAt: Date

    func toDomain() -> Article {
        Article(id: id, title: title, summary: body, publishedAt: createdAt)
    }
}

final class DefaultArticleRepository: ArticleRepository {
    private let remoteDataSource: ArticleRemoteDataSource

    init(remoteDataSource: ArticleRemoteDataSource = ArticleRemoteDataSource()) {
        self.remoteDataSource = remoteDataSource
    }

    func fetchArticles() async throws -> [Article] {
        let dtos = try await remoteDataSource.fetchArticlesDTO()
        return dtos.map { $0.toDomain() }
    }

    func markAsFavorite(id: UUID) async throws {
        // Local or remote update
    }
}

Presentation Layer: ViewModel & View

import SwiftUI

@Observable
@MainActor
final class FeedViewModel {
    enum ViewState: Equatable {
        case idle
        case loading
        case loaded([Article])
        case error(String)
    }

    private(set) var state: ViewState = .idle
    private let repository: ArticleRepository

    init(repository: ArticleRepository) {
        self.repository = repository
    }

    func loadArticles() async {
        state = .loading
        do {
            let articles = try await repository.fetchArticles()
            state = .loaded(articles)
        } catch {
            state = .error(error.localizedDescription)
        }
    }
}

struct FeedView: View {
    @State private var viewModel: FeedViewModel

    init(repository: ArticleRepository = DefaultArticleRepository()) {
        _viewModel = State(wrappedValue: FeedViewModel(repository: repository))
    }

    var body: some View {
        NavigationStack {
            Group {
                switch viewModel.state {
                case .idle, .loading:
                    ProgressView("Loading feed...")
                case .loaded(let articles):
                    List(articles) { article in
                        NavigationLink(value: article) {
                            VStack(alignment: .leading, spacing: 4) {
                                Text(article.title)
                                    .font(.headline)
                                Text(article.summary)
                                    .font(.subheadline)
                                    .foregroundStyle(.secondary)
                            }
                        }
                    }
                case .error(let message):
                    ContentUnavailableView("Failed to Load", systemImage: "wifi.exclamationmark", description: Text(message))
                }
            }
            .navigationTitle("Articles")
            .navigationDestination(for: Article.self) { article in
                ArticleDetailView(article: article)
            }
            .task {
                await viewModel.loadArticles()
            }
        }
    }
}

struct ArticleDetailView: View {
    let article: Article

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                Text(article.title)
                    .font(.largeTitle.bold())
                Text(article.publishedAt.formatted())
                    .font(.caption)
                    .foregroundStyle(.secondary)
                Text(article.summary)
                    .font(.body)
            }
            .padding()
        }
        .navigationTitle("Details")
        .navigationBarTitleDisplayMode(.inline)
    }
}

3. NavigationStack & Type-Safe Decoupled Routing

Never couple views directly via NavigationLink(destination: NextView()). Use value-based navigation with custom routes.

enum AppRoute: Hashable {
    case articleDetail(Article)
    case userProfile(userId: UUID)
    case settings
}

@Observable
final class Router {
    var path = NavigationPath()

    func navigate(to route: AppRoute) {
        path.append(route)
    }

    func pop() {
        if !path.isEmpty {
            path.removeLast()
        }
    }

    func popToRoot() {
        path.removeLast(path.count)
    }
}

struct RootView: View {
    @State private var router = Router()

    var body: some View {
        NavigationStack(path: $router.path) {
            HomeContentView()
                .navigationDestination(for: AppRoute.self) { route in
                    switch route {
                    case .articleDetail(let article):
                        ArticleDetailView(article: article)
                    case .userProfile(let userId):
                        UserProfileView(userId: userId)
                    case .settings:
                        SettingsView()
                    }
                }
        }
        .environment(router)
    }
}

4. Anti-Patterns & Performance Pitfalls

❌ Anti-Pattern 1: Performing heavy computation or side effects in body

Bad:

var body: some View {
    let filteredList = items.filter { $0.isValid } // Re-computed on every frame render!
    List(filteredList) { ... }
}

Good: Perform filtering in ViewModel or memoize state using computed properties on @Observable ViewModel.

❌ Anti-Pattern 2: Captured self retain cycles in .task or async closures

Bad:

.task {
    self.viewModel.startPolling() // Retain risk if not properly cancelled
}

Good: .task modifier automatically handles task cancellation when the view disappears. Ensure ViewModel tasks respect Task.isCancelled.

❌ Anti-Pattern 3: Over-using @StateObject / @ObservedObject with Swift 5.9+

Mixing ObservableObject and @Observable causes redundant UI redraws and invalidates observation tracking. Standardize the codebase on @Observable.


5. Testing Patterns

import XCTest
@testable import MyApp

@MainActor
final class FeedViewModelTests: XCTestCase {
    func test_loadArticles_success_setsLoadedState() async {
        // Arrange
        let mockArticles = [Article(id: UUID(), title: "Test", summary: "Body", publishedAt: Date())]
        let mockRepository = MockArticleRepository(result: .success(mockArticles))
        let sut = FeedViewModel(repository: mockRepository)

        // Act
        await sut.loadArticles()

        // Assert
        XCTAssertEqual(sut.state, .loaded(mockArticles))
    }
}

final class MockArticleRepository: ArticleRepository, @unchecked Sendable {
    let result: Result<[Article], Error>

    init(result: Result<[Article], Error>) {
        self.result = result
    }

    func fetchArticles() async throws -> [Article] {
        try result.get()
    }

    func markAsFavorite(id: UUID) async throws {}
}

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,782. 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.