Back to list
tienbm92

ios-mvvm-foundation

by tienbm92

Personal AI context base used for bootstrapping new projects and feature development. Acts as the backbone for architecture, domain rules, and reusable design patterns.

0🍴 0📅 Jan 26, 2026

SKILL.md


name: ios-mvvm-foundation description: 'iOS MVVM + Clean Architecture foundation. Sử dụng skill này khi tạo màn hình mới, implement features, hoặc cần guidelines chi tiết về architecture, navigation, animation, testing.'

iOS MVVM Foundation

Skill này cung cấp guidelines đầy đủ cho iOS development với MVVM + Clean Architecture.

🎯 Khi Nào Sử Dụng Skill Này

User RequestAction
Tạo màn hình mớiĐọc references/presentation-patterns.md
Implement animationĐọc references/animation-guidelines.md
Setup navigationĐọc references/navigation-patterns.md
Viết unit testsĐọc references/testing.md
Dependency injectionĐọc references/di.md
ViewState patternĐọc references/viewstate-pattern.md
Theme systemĐọc references/theme-system.md
NetworkingĐọc references/networking.md
State managementĐọc references/state-management.md
SecurityĐọc references/security.md
Storage / PersistenceĐọc references/storage.md
Code templatesĐọc references/templates/index.md
App Store submissionĐọc references/app-store-submission.md
IAP / PaywallĐọc references/iap.md
iOS 26 Liquid GlassĐọc references/liquid-glass.md

📐 Architecture Overview

Pattern: Clean Architecture + MVVM

┌─────────────────────────────────────────────────────────────┐
│                    PRESENTATION LAYER                        │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐      │
│  │    View     │───▶│  ViewModel  │───▶│  ViewState  │      │
│  │  (SwiftUI)  │    │(Observable) │    │ (Optional)  │      │
│  └─────────────┘    └──────┬──────┘    └─────────────┘      │
│                            │                                 │
└────────────────────────────┼─────────────────────────────────┘
                             │ inject
┌────────────────────────────▼─────────────────────────────────┐
│                      DOMAIN LAYER                            │
│  ┌─────────────┐    ┌─────────────┐                         │
│  │  UseCases   │───▶│   Entities  │                         │
│  │ (Protocols) │    │  (Models)   │                         │
│  └──────┬──────┘    └─────────────┘                         │
│         │                                                    │
└─────────┼────────────────────────────────────────────────────┘
          │ inject
┌─────────▼────────────────────────────────────────────────────┐
│                       DATA LAYER                             │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐      │
│  │ Repositories│───▶│   Network   │    │   Storage   │      │
│  │   (Impl)    │    │  Services   │    │  Services   │      │
│  └─────────────┘    └─────────────┘    └─────────────┘      │
└──────────────────────────────────────────────────────────────┘

Dependency Flow

Presentation → Domain ← Data
     │            ▲         │
     │            │         │
     └────────────┴─────────┘

🧩 MVVM Components

1. ViewModel (2-4 KB mỗi feature)

final class LoginViewModel: ObservableObject {
    // MARK: - Published State (Business)
    @Published private(set) var email: String = ""
    @Published private(set) var password: String = ""
    @Published private(set) var isLoading: Bool = false
    @Published private(set) var error: AppError?
    @Published var navigationIntent: NavigationIntent?
    
    enum NavigationIntent: Equatable {
        case home
        case forgotPassword
    }
    
    // MARK: - Dependencies
    private let loginUseCase: LoginUseCaseProtocol
    
    init(loginUseCase: LoginUseCaseProtocol) {
        self.loginUseCase = loginUseCase
    }
    
    // MARK: - User Actions
    func updateEmail(_ email: String) {
        self.email = email
    }
    
    func updatePassword(_ password: String) {
        self.password = password
    }
    
    func login() async {
        isLoading = true
        defer { isLoading = false }
        
        do {
            try await loginUseCase.execute(email: email, password: password)
            await MainActor.run { navigationIntent = .home }
        } catch {
            await MainActor.run { self.error = AppError(error) }
        }
    }
    
    // MARK: - Computed Properties
    var isLoginEnabled: Bool {
        !email.isEmpty && !password.isEmpty && !isLoading
    }
}

// MARK: - Factory (for DI)
extension LoginViewModel {
    static func live() -> LoginViewModel {
        LoginViewModel(loginUseCase: Resolver.resolve())
    }
    
    static func mock() -> LoginViewModel {
        LoginViewModel(loginUseCase: MockLoginUseCase())
    }
}

2. View (SwiftUI)

struct LoginView: View {
    // MARK: - ViewModel
    @StateObject private var viewModel: LoginViewModel
    
    // MARK: - Animation State (CRITICAL: phải ở @State)
    @State private var buttonScale: CGFloat = 1.0
    @State private var shakeOffset: CGFloat = 0
    
    // MARK: - Navigation
    @EnvironmentObject private var router: AppRouter
    
    init(viewModel: LoginViewModel = .live()) {
        _viewModel = StateObject(wrappedValue: viewModel)
    }
    
    var body: some View {
        VStack(spacing: 20) {
            emailField
            passwordField
            loginButton
        }
        .padding()
        .onChange(of: viewModel.navigationIntent) { intent in
            handleNavigation(intent)
        }
        .onChange(of: viewModel.error) { error in
            if error != nil { triggerShakeAnimation() }
        }
    }
    
    // MARK: - Subviews
    private var emailField: some View {
        TextField("Email", text: Binding(
            get: { viewModel.email },
            set: { viewModel.updateEmail($0) }
        ))
        .textFieldStyle(.roundedBorder)
    }
    
    private var passwordField: some View {
        SecureField("Password", text: Binding(
            get: { viewModel.password },
            set: { viewModel.updatePassword($0) }
        ))
        .textFieldStyle(.roundedBorder)
    }
    
    private var loginButton: some View {
        Button("Đăng nhập") {
            Task { await viewModel.login() }
        }
        .disabled(!viewModel.isLoginEnabled)
        .scaleEffect(buttonScale)
        .offset(x: shakeOffset)
        .onChange(of: viewModel.isLoading) { loading in
            withAnimation(.spring()) {
                buttonScale = loading ? 0.95 : 1.0
            }
        }
    }
    
    // MARK: - Navigation
    private func handleNavigation(_ intent: NavigationIntent?) {
        guard let intent = intent else { return }
        switch intent {
        case .home:
            router.push(.home)
        case .forgotPassword:
            router.push(.forgotPassword)
        }
        viewModel.navigationIntent = nil
    }
    
    // MARK: - Animations
    private func triggerShakeAnimation() {
        withAnimation(.spring(response: 0.1, dampingFraction: 0.2)) {
            shakeOffset = 10
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            withAnimation(.spring()) { shakeOffset = 0 }
        }
    }
}

⚡ Quick Reference

Animation Values

TypeĐặt ở đâuVí dụ
Scale, Opacity, Offset@State (View)@State private var scale: CGFloat = 1.0
Rotation, Color@State (View)@State private var rotation: Angle = .zero
Business state@Published (ViewModel)@Published var isLoading: Bool

Property Wrappers

WrapperKhi nào dùng
@StateObjectView SỞ HỮU ViewModel
@ObservedObjectView NHẬN ViewModel từ parent
@StateAnimation, transient UI state
@PublishedBusiness state trong ViewModel
@EnvironmentTheme, locale, system values

Dependency Injection

LayerInject gì
ViewModelUseCases (protocols)
UseCaseRepositories (protocols)
RepositoryNetwork/Storage services

📁 Reference Files

FileNội dungKhi nào đọc
presentation-patterns.mdMVVM chi tiếtTạo màn hình mới
animation-guidelines.mdAnimation rulesCó animation
navigation-patterns.mdNavigation patternsSetup navigation
viewstate-pattern.mdViewState patternComplex formatting
state-management.mdState patterns@Published, @State
testing.mdUnit testingViết tests
di.mdDependency InjectionDI setup
theme-system.mdTheme systemStyling
networking.mdAPI callsNetwork requests
security.mdSecurity best practicesTokens, encryption
storage.mdData persistenceKeychain, Realm
app-store-submission.mdApp StoreSubmit app
liquid-glass.mdiOS 26+Liquid Glass UI
iap.mdIn-App PurchasePaywall, subscriptions
templates/Code templatesGenerate code

✅ Code Generation Checklist

Trước khi generate code, validate:

ViewModel

  • class conform ObservableObject
  • @Published chỉ cho business state
  • KHÔNG có CGFloat, Angle cho animation
  • Inject UseCases (KHÔNG Repository)
  • static func mock()live()

View

  • @StateObject khi sở hữu ViewModel
  • @State cho animation values
  • .onChange() để trigger animation từ business state
  • KHÔNG có business logic
  • Intent-based (default) hoặc Navigator protocol
  • Consume intent sau khi handle

Score

Total Score

70/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon