Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component...
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))
}
}
}
Navigation in ViewModel
@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
Related Skills
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.