arclabs-studio

arc-presentation-layer

0
0
# Install this skill:
npx skills add arclabs-studio/ARCKnowledge --skill "arc-presentation-layer"

Install specific skill from multi-skill repository

# Description

|

# SKILL.md


name: arc-presentation-layer
description: |
ARC Labs Studio Presentation layer patterns. Covers SwiftUI Views structure,
@Observable ViewModels with @MainActor, state management with LoadingState enum,
ARCNavigation Router pattern for navigation, data flow between View-ViewModel-UseCase,
accessibility, dark mode, SwiftUI previews, and MVVM+C pattern implementation.
INVOKE THIS SKILL when:
- Creating SwiftUI Views with proper structure
- Implementing @Observable ViewModels with @MainActor
- Setting up navigation with ARCNavigation Router
- Managing UI state (loading, error, success states)
- Handling user actions in ViewModels
- Structuring Presentation layer feature folders


ARC Labs Studio - Presentation Layer Patterns

When to Use This Skill

Use this skill when:
- Creating SwiftUI Views with proper structure
- Implementing ViewModels with @Observable
- Setting up navigation with ARCNavigation Router
- Managing UI state (loading, error, success)
- Handling user actions in ViewModels
- Structuring feature folders (View, ViewModel, Router)
- Implementing MVVM+C pattern
- Adding SwiftUI previews for testing

Quick Reference

Presentation Layer Structure

Presentation/
β”œβ”€β”€ Features/
β”‚   └── UserProfile/
β”‚       β”œβ”€β”€ View/
β”‚       β”‚   β”œβ”€β”€ UserProfileView.swift
β”‚       β”‚   └── ProfileHeaderView.swift
β”‚       └── ViewModel/
β”‚           └── UserProfileViewModel.swift
└── Shared/
    └── Components/
        └── LoadingView.swift

View Structure

import SwiftUI

struct UserProfileView: View {

    // MARK: Private Properties

    @State private var viewModel: UserProfileViewModel

    // MARK: Initialization

    init(viewModel: UserProfileViewModel) {
        self.viewModel = viewModel
    }

    // MARK: View

    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                profileHeader
                profileDetails
                actionButtons
            }
            .padding()
        }
        .navigationTitle("Profile")
        .task {
            await viewModel.onAppear()
        }
    }
}

// MARK: - Private Views

private extension UserProfileView {
    var profileHeader: some View {
        VStack {
            AsyncImage(url: viewModel.user?.avatarURL) { image in
                image.resizable().aspectRatio(contentMode: .fill)
            } placeholder: {
                ProgressView()
            }
            .frame(width: 100, height: 100)
            .clipShape(Circle())

            Text(viewModel.user?.name ?? "Unknown")
                .font(.title)
        }
    }

    var profileDetails: some View {
        VStack(alignment: .leading, spacing: 8) {
            DetailRow(title: "Email", value: viewModel.user?.email ?? "N/A")
        }
    }

    var actionButtons: some View {
        Button("Edit Profile") {
            viewModel.onTappedEditProfile()
        }
        .buttonStyle(.borderedProminent)
    }
}

// MARK: - Previews

#Preview("Loaded") {
    let mockViewModel = UserProfileViewModel.mock
    mockViewModel.user = .mock
    return UserProfileView(viewModel: mockViewModel)
}

#Preview("Loading") {
    let mockViewModel = UserProfileViewModel.mock
    mockViewModel.isLoading = true
    return UserProfileView(viewModel: mockViewModel)
}

ViewModel Structure

import ARCLogger
import ARCNavigation
import Foundation

@MainActor
@Observable
final class UserProfileViewModel {

    // MARK: Private Properties

    private(set) var user: User?
    private(set) var isLoading = false
    private(set) var errorMessage: String?

    private let getUserProfileUseCase: GetUserProfileUseCaseProtocol
    private let router: Router<AppRoute>

    // MARK: Public Properties

    var memberSinceText: String {
        guard let user = user else { return "N/A" }
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        return formatter.string(from: user.createdAt)
    }

    // MARK: Initialization

    init(
        getUserProfileUseCase: GetUserProfileUseCaseProtocol,
        router: Router<AppRoute>
    ) {
        self.getUserProfileUseCase = getUserProfileUseCase
        self.router = router
    }

    // MARK: Lifecycle

    func onAppear() async {
        await loadProfile()
    }

    // MARK: Public Functions

    func onTappedEditProfile() {
        guard let user = user else { return }
        router.navigate(to: .editProfile(user))
    }

    func onTappedRetry() async {
        await loadProfile()
    }
}

// MARK: - Private Functions

private extension UserProfileViewModel {
    func loadProfile() async {
        isLoading = true
        errorMessage = nil

        do {
            user = try await getUserProfileUseCase.execute()
            ARCLogger.shared.debug("Profile loaded successfully")
        } catch {
            errorMessage = "Failed to load profile"
            ARCLogger.shared.error("Profile load failed", metadata: [
                "error": error.localizedDescription
            ])
        }

        isLoading = false
    }
}

// MARK: - Mock

#if DEBUG
extension UserProfileViewModel {
    static var mock: UserProfileViewModel {
        UserProfileViewModel(
            getUserProfileUseCase: MockGetUserProfileUseCase(),
            router: Router()
        )
    }
}
#endif

Loading State Enum

enum LoadingState<T: Equatable>: Equatable {
    case idle
    case loading
    case loaded(T)
    case error(String)
}

@MainActor
@Observable
final class ContentViewModel {
    private(set) var state: LoadingState<[Item]> = .idle

    var items: [Item] {
        if case .loaded(let items) = state { return items }
        return []
    }

    var isLoading: Bool {
        if case .loading = state { return true }
        return false
    }
}

Route Definition (ARCNavigation)

import ARCNavigation
import SwiftUI

enum AppRoute: Route {
    case home
    case profile(userID: String)
    case settings
    case editProfile(User)

    @ViewBuilder
    func view() -> some View {
        switch self {
        case .home:
            HomeView(viewModel: HomeViewModel.create())
        case .profile(let userID):
            ProfileView(viewModel: ProfileViewModel.create(userID: userID))
        case .settings:
            SettingsView(viewModel: SettingsViewModel.create())
        case .editProfile(let user):
            EditProfileView(viewModel: EditProfileViewModel.create(user: user))
        }
    }
}
@MainActor
@Observable
final class HomeViewModel {
    private let router: Router<AppRoute>

    func onTappedProfile() {
        router.navigate(to: .profile(userID: currentUserId))
    }

    func onTappedSettings() {
        router.navigate(to: .settings)
    }

    func onTappedBack() {
        router.pop()
    }

    func onTappedHome() {
        router.popToRoot()
    }
}

Data Flow

View β†’ ViewModel β†’ Use Case β†’ Repository
  β”‚         β”‚           β”‚          β”‚
  β”‚ User    β”‚ Calls     β”‚ Business β”‚ Data
  β”‚ Action  β”‚ execute() β”‚ Logic    β”‚ Access
  ↓         ↓           ↓          ↓
Button  onTapped()  UseCase.execute()  fetch()

View Naming Conventions

  • Views: *View.swift β†’ UserProfileView, HomeView
  • ViewModels: *ViewModel.swift β†’ UserProfileViewModel
  • User actions: onTapped*, onChanged*, onAppear
  • Private methods: load*, format*, validate*

View Best Practices

// βœ… Handle all states
var body: some View {
    Group {
        switch viewModel.state {
        case .idle: Text("Ready")
        case .loading: ProgressView("Loading...")
        case .loaded(let data): dataView(data)
        case .error(let message): ErrorView(message: message)
        }
    }
}

// βœ… Extract subviews
var profileHeader: some View { /* ... */ }

// βœ… Use Button for interactions (accessibility)
Button("Add") { viewModel.onTappedAdd() }

// ❌ Don't use onTapGesture for buttons
Image(systemName: "plus").onTapGesture { }  // BAD

ViewModel Best Practices

// βœ… private(set) for mutable state
private(set) var isLoading = false

// βœ… Prefix user actions with "on"
func onTappedButton() { }
func onChangedText(_ text: String) { }

// βœ… Call Use Cases, not Repositories
let user = try await getUserUseCase.execute()

// ❌ Don't reference Views
var destinationView: ProfileView?  // NEVER

// ❌ Don't contain business logic
if rating > 4.0 && price < 50 { }  // Move to Use Case

Detailed Documentation

For complete patterns:
- @presentation.md - Complete Presentation layer guide

When working on the presentation layer, you may also need:

If you need... Use
Architecture patterns /arc-swift-architecture
Testing ViewModels /arc-tdd-patterns
UI/Accessibility guidelines /arc-quality-standards
Data layer implementation /arc-data-layer

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.