# Core Architecture Standards for SwiftUI
This document outlines the core architectural standards for developing robust, maintainable, and scalable SwiftUI applications. It emphasizes modern SwiftUI practices and aims to provide clear guidance for developers of all levels.
## 1. Architectural Patterns
Choosing the right architectural pattern is crucial for organizing your code and managing complexity. SwiftUI inherently encourages a declarative, data-driven approach.
### 1.1 Model-View-ViewModel (MVVM)
* **Standard:** Adopt MVVM as the primary architectural pattern for most SwiftUI projects.
* **Do This:** Separate data (Model), UI logic (ViewModel), and the UI representation (View).
* **Don't Do This:** Place business logic directly within the View. Avoid tight coupling between View and Model.
**Why:** MVVM promotes testability, reusability, and separation of concerns, resulting in a more maintainable codebase. It aligns well with SwiftUI's declarative nature.
**Example:**
"""swift
// Model
struct User {
let id: UUID = UUID()
let name: String
let email: String
}
// ViewModel
import SwiftUI
class UserViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@Published var errorMessage: String? = nil
init() {
loadUsers()
}
func loadUsers() {
isLoading = true
errorMessage = nil // Reset error message
// Simulate network request
DispatchQueue.global().async {
do {
// Simulate network delay
Thread.sleep(forTimeInterval: 1)
// Simulate fetching data
let fetchedUsers = [
User(name: "Alice Smith", email: "alice.smith@example.com"),
User(name: "Bob Johnson", email: "bob.johnson@example.com")
]
DispatchQueue.main.async {
self.users = fetchedUsers
self.isLoading = false
}
} catch {
DispatchQueue.main.async {
self.errorMessage = "Failed to load users: \(error.localizedDescription)"
self.isLoading = false
}
}
}
}
}
// View
struct UserListView: View {
@ObservedObject var viewModel = UserViewModel()
var body: some View {
VStack {
if viewModel.isLoading {
ProgressView("Loading Users...")
} else if let errorMessage = viewModel.errorMessage {
Text("Error: \(errorMessage)")
.foregroundColor(.red)
} else {
List(viewModel.users, id: \.id) { user in
Text(user.name)
}
}
}
.padding()
}
}
struct UserListView_Previews: PreviewProvider {
static var previews: some View {
UserListView()
}
}
"""
**Anti-Pattern:** Storing complex data transformation logic directly in the View's "body".
### 1.2 Coordinator Pattern
* **Standard:** Use the Coordinator pattern for managing navigation, especially in complex applications with multiple screens and deep-linking requirements.
* **Do This:** Create Coordinator objects responsible for instantiating and presenting ViewControllers or other Views.
* **Don't Do This:** Handle navigation directly within Views, leading to tight coupling and difficulty in testing navigation flows.
**Why:** The Coordinator pattern centralizes navigation logic, making it easier to manage complex navigation flows, support deep linking, and test navigation independently of views. While less common in pure SwiftUI, it's crucial when integrating UIKit or managing complex route logic.
**Example (UIKit example):** While Coordinators are traditionally a UIKit pattern, using a "UIViewControllerRepresentable" can bridge the gap with SwiftUI, but this is generally discouraged unless absolutely unavoidable.
"""swift
// Coordinator (For UIKit navigation - use sparingly in SwiftUI)
import UIKit
import SwiftUI
class MainCoordinator: Coordinator {
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let viewController = UIHostingController(rootView: HomeView(coordinator: self))
navigationController.pushViewController(viewController, animated: false)
}
func showDetails(for user: User) { // Assuming User is your model
let detailViewController = UIHostingController(rootView: DetailView(user: user))
navigationController.pushViewController(detailViewController, animated: true)
}
}
protocol Coordinator {
var navigationController: UINavigationController { get set }
func start()
}
// Example SwiftUI View Utilizing the Coordinator
struct HomeView: View {
var coordinator: MainCoordinator // Store a reference to the associated Coordinator
var body: some View {
Button("Show Details") { // Assume the button is interacting with a dummy user
let dummyUser = User(name: "John Doe", email: "John.doe@email.com")
coordinator.showDetails(for: dummyUser)
}
.navigationTitle("Home")
}
}
struct DetailView: View {
let user: User
var body: some View {
Text("Details for \(user.name)")
.navigationTitle("Details")
}
}
// Wrapping within the UIViewControllerRepresentable.
struct CoordinatorView: UIViewControllerRepresentable {
let coordinator: MainCoordinator
func makeUIViewController(context: Context) -> UINavigationController {
return coordinator.navigationController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
}
}
// Entry Point (App)
@main
struct CoordinatorExampleApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
CoordinatorView(coordinator: appDelegate.coordinator)
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
var coordinator: MainCoordinator = {
let navController = UINavigationController()
let coordinator = MainCoordinator(navigationController: navController)
coordinator.start()
return coordinator
}()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
return true
}
}
"""
**Anti-Pattern:** Scattering navigation calls throughout the application, making it difficult to trace the user's journey. Using NavigationLinks excessively without properly managing the NavigationStack.
### 1.3 State Management Strategies (Beyond ObservedObject)
* **Standard:** Choose the appropriate state management technique based on the complexity of your application and the scope of the data. Understand the implications of environment objects, app storage, state objects and bindings.
* **Do This:** For simple, view-specific state, use "@State". For shared state within a screen or feature, leverage "@ObservedObject" or "@StateObject" connected with shared view models. For application-wide state, use "@EnvironmentObject" or "@AppStorage".
* **Don't Do This:** Overuse "@EnvironmentObject" for localized state—this makes it harder to trace data flow. Force-unwrap optional environment objects! Rely on passing bindings down through many layers of views; often a view model or "EnvironmentObject" is a better solution.
**Why:** Proper state management ensures predictable data flow, facilitates UI updates, and simplifies testing. Choosing the right tool improves maintainability.
**Example:**
"""swift
// App-wide settings (using AppStorage)
import SwiftUI
struct ContentView: View {
@AppStorage("isDarkMode") private var isDarkMode = false //Default dark mode to false
var body: some View {
VStack {
Text("Current Mode: \(isDarkMode ? "Dark" : "Light")")
Button("Toggle Dark Mode") {
isDarkMode.toggle()
}
}
.frame(width: 300, height: 200)
.background(isDarkMode ? Color.black : Color.white)
.foregroundColor(isDarkMode ? Color.white : Color.black)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// Sharing data between views on the same screen (using ObservedObject)
class ScreenViewModel: ObservableObject {
@Published var sharedMessage: String = "Initial Message"
}
struct ScreenView: View {
@ObservedObject var viewModel = ScreenViewModel()
var body: some View {
VStack {
TextField("Enter Message", text: $viewModel.sharedMessage)
.padding()
SecondView(viewModel: viewModel)
}
}
}
struct SecondView: View {
@ObservedObject var viewModel: ScreenViewModel
var body: some View {
Text("Message from ScreenView: \(viewModel.sharedMessage)")
.padding()
}
}
// Sharing data across the *entire* app (EnvironmentObject)
class AppSettings: ObservableObject {
@Published var accentColor: Color = .blue
}
struct AppContainerView: View {
@StateObject var settings = AppSettings()
var body: some View {
MainView()
.environmentObject(settings)
}
}
struct MainView: View {
var body: some View {
NavigationView {
NavigationLink("Go to Settings") {
SettingsView()
}
.navigationTitle("Main View")
}
}
}
struct SettingsView: View {
@EnvironmentObject var settings: AppSettings
var body: some View {
VStack {
Text("Accent Color:")
ColorPicker("Select Accent Color", selection: $settings.accentColor)
.padding()
}
.navigationTitle("Settings")
}
}
"""
**Anti-Pattern:** Using "@State" for complex data models that should reside in a ViewModel. Relying solely on closures for passing data between views – use view models or the environment for more structured communication.
## 2. Project Structure and Organization
A well-organized project structure is essential for scalability and collaboration.
### 2.1 Feature-Based Folders
* **Standard:** Organize your project into feature-based folders, grouping related Models, Views, ViewModels, and other resources within dedicated directories.
* **Do This:** Create a top-level "Features" directory. Inside, create subdirectories for each feature (e.g., "Login", "Profile", "Settings").
* **Don't Do This:** Dumping all project files into a single directory.
**Why:** Feature-based folders improve code discoverability, reduce merge conflicts, and make it easier to understand the application's structure.
**Example:**
"""
MyProject/
├── MyProjectApp.swift
├── Assets.xcassets/
├── Features/
│ ├── Login/
│ │ ├── LoginView.swift
│ │ ├── LoginViewModel.swift
│ │ ├── LoginModel.swift
│ │ ├── LoginService.swift
│ ├── Profile/
│ │ ├── ProfileView.swift
│ │ ├── ProfileViewModel.swift
│ │ ├── ProfileModel.swift
│ │ ├── ProfileService.swift
│ ├── Settings/
│ │ ├── SettingsView.swift
│ │ ├── SettingsViewModel.swift
│ │ ├── SettingsModel.swift
│ │ ├── SettingsService.swift
├── Services/ (Shared services)
│ ├── APIClient.swift
│ ├── PersistenceManager.swift
├── Models/ (Shared models)
│ ├── User.swift
├── Resources/ (Images, fonts, etc)
│ ├── Assets.xcassets/
├── Utilities/ (Extensions, helper functions)
│ ├── ColorExtensions.swift
├── Supporting Files/
│ ├── Info.plist
"""
**Anti-Pattern:** Creating folders based on file type (e.g., "Views", "ViewModels") instead of feature.
### 2.2 Modularization
* **Standard:** Break down large projects into smaller, independent modules or Swift Packages.
* **Do This:** Identify reusable components or logical groups of functionality. Package them into separate modules. Use local Swift Packages for internal modularization or remote Swift Packages for sharing code across projects using the Swift Package Manager (SPM).
* **Don't Do This:** Having a single monolithic module for everything – can be difficult to manage and compile.
**Why:** Modularization promotes code reuse, improves build times, isolates functionality, and facilitates team collaboration.
**Example:** Imagine "APIClient.swift" and "User.swift" from the above structure formed their own Swift Package for reuse across multiple apps. This can be done through Xcode.
**Anti-Pattern:** Creating overly granular modules that lead to unnecessary complexity.
### 2.3 Naming Conventions
* **Standard:** Adhere to clear and consistent naming conventions for all project elements.
* **Do This:**
* Use descriptive names that clearly indicate the purpose of each entity (e.g., "LoginViewModel", "UserListView").
* Follow Swift's naming conventions (e.g., camelCase for variables and functions, PascalCase for types).
* Suffix Views with "View" and ViewModels with "ViewModel".
* Prefix custom SwiftUI modifiers for namespacing (e.g., "myAppCustomStyle()").
* **Don't Do This:** Using ambiguous or cryptic names. Inconsistent naming across the project.
**Why:** Consistent naming improves code readability and maintainability, and reduces cognitive load.
**Example:**
"""swift
// Good Naming
struct UserProfileView: View { ... }
class UserProfileViewModel: ObservableObject { ... }
func calculateTotal(items: [OrderItem]) -> Double { ... }
// Bad Naming
struct UserView: View { ... } // Not descriptive enough
class ProfileVM: ObservableObject { ... } // Abbreviation is unclear
func calc(items: [OrderItem]) -> Double { ... } // Ambiguous
"""
**Anti-Pattern:** Using single-letter variable names (except in very localized contexts, such as within small loops).
## 3. Coding Practices
Writing clean and maintainable code is essential for any successful project.
### 3.1 Readability and Formatting
* **Standard:** Prioritize code readability and follow consistent formatting rules.
* **Do This:**
* Use proper indentation.
* Keep lines of code reasonably short (e.g., 80-120 characters).
* Use blank lines to separate logical blocks of code.
* Add comments to explain complex or non-obvious logic.
* **Don't Do This:** Writing excessively long lines of code. Having inconsistent indentation. Omitting comments for complex sections.
**Why:** Readability improves code comprehension and reduces the likelihood of errors.
**Example:**
"""swift
// Good Formatting
struct ContentView: View {
@ObservedObject var viewModel: ContentViewModel
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Button("Tap Me") {
viewModel.performAction()
}
}
} // End of body
// Comment explaining the purpose of this method, if needed
private func helperFunction() {
//Implementation details
}
}
// Bad Formatting
struct ContentView:View{ @ObservedObject var viewModel:ContentViewModel
var body: some View {VStack{Text("Hello, world!").padding();Button("Tap Me"){viewModel.performAction()}}} // Incomprehensible
}
"""
**Anti-Pattern:** Putting multiple statements on a single line.
### 3.2 Reusability
* **Standard:** Design reusable components and avoid code duplication.
* **Do This:**
* Create custom SwiftUI views for common UI elements.
* Use SwiftUI modifiers to encapsulate styling and behavior.
* Employ generics to create flexible and type-safe components.
* **Don't Do This:** Copying and pasting code. Creating tightly coupled components.
**Why:** Reusability reduces code size, simplifies maintenance, and improves consistency.
**Example:**
"""swift
// Custom Button Style (Modifier)
struct CustomButtonStyle: ViewModifier {
let backgroundColor: Color
let foregroundColor: Color
func body(content: Content) -> some View {
content
.padding()
.background(backgroundColor)
.foregroundColor(foregroundColor)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
extension View {
func customButtonStyle(backgroundColor: Color, foregroundColor: Color) -> some View {
self.modifier(CustomButtonStyle(backgroundColor: backgroundColor, foregroundColor: foregroundColor))
}
}
// Usage
struct MyView: View {
var body: some View {
Button("Tap Me") {}
.customButtonStyle(backgroundColor: .blue, foregroundColor: .white)
}
}
//Reusable component: Generic List
struct GenericListView: View {
let items: [T]
let contentBuilder: (T) -> Content
init(items: [T], @ViewBuilder contentBuilder: @escaping (T) -> Content) {
self.items = items
self.contentBuilder = contentBuilder
}
var body: some View {
List {
ForEach(items, id: \.self) { item in // Assuming T is Hashable/Identifiable for this context
contentBuilder(item)
}
}
}
}
//Usage:
struct ContentViewExample: View {
let names = ["Alice", "Bob", "Charlie"]
let numbers = [1, 2, 3, 4, 5]
var body: some View {
VStack {
GenericListView(items: names) { name in
Text("Name: \(name)")
.padding()
}
GenericListView(items: numbers) { number in
Text("Number: \(number)")
.padding()
}
}
}
}
"""
**Anti-Pattern:** Repeating the same UI code in multiple views.
### 3.3 Error Handling
* **Standard:** Implement robust error handling to prevent crashes and provide informative feedback to the user.
* **Do This:**
* Use Swift's "Result" type for handling asynchronous operations.
* Catch and handle errors using "do-catch" blocks.
* Display user-friendly error messages.
* Implement logging for debugging purposes.
* **Don't Do This:** Ignoring potential errors. Force-unwrapping optionals without checking for "nil". Crashing the app due to unhandled exceptions.
**Why:** Proper error handling ensures a stable and reliable application.
**Example:**
"""swift
// Using Result Type
enum NetworkError: Error {
case invalidURL
case requestFailed(Error)
case invalidData
}
func fetchData(from urlString: String, completion: @escaping (Result) -> Void) {
guard let url = URL(string: urlString) else {
completion(.failure(.invalidURL))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(.requestFailed(error)))
return
}
guard let data = data else {
completion(.failure(.invalidData))
return
}
completion(.success(data))
}.resume()
}
//Handling networking calls within ViewModel
class ViewModel: ObservableObject {
@Published var data: String = ""
@Published var errorMessage: String? = nil
func loadData() {
fetchData(from: "https://example.com/data") { result in
switch result {
case .success(let data):
if let stringData = String(data: data, encoding: .utf8) {
DispatchQueue.main.async {
self.data = stringData
}
} else {
DispatchQueue.main.async {
self.errorMessage = "Invalid data format"
}
}
case .failure(let error):
DispatchQueue.main.async {
self.errorMessage = "Error fetching data: \(error)"
}
}
}
}
}
//Displaying errors to the user.
struct ErrorDisplayView: View {
@ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
if let errorMessage = viewModel.errorMessage {
Text("Error: \(errorMessage)")
.foregroundColor(.red)
} else {
Text("Data: \(viewModel.data)")
}
Button("Load Data") {
viewModel.loadData()
}
}
}
}
"""
**Anti-Pattern:** Using "try!" or "as!" without proper error handling or type checking (respectively).
### 3.4 Asynchronous Operations
* **Standard:** Handle long-running tasks asynchronously to prevent blocking the main thread and ensure a responsive UI.
* **Do This:**
* Use "async/await" for modern asynchronous code.
* Utilize "Task" to execute asynchronous operations from synchronous contexts.
* Update UI elements on the main thread using "@MainActor".
* **Don't Do This:** Performing network requests or heavy computations on the main thread. Causing UI freezes or unresponsive behavior.
**Why:** Asynchronous operations keep the UI responsive and improve the user experience. SwiftUI's concurrency model simplifies these operations.
**Example:**
"""swift
// Using async/await
class ImageLoader: ObservableObject {
@Published var image: Image? = nil
@Published var isLoading: Bool = false
@Published var errorMessage: String? = nil
@MainActor // Ensures UI updates happen on MainThread
func loadImage(from urlString: String) async {
isLoading = true
errorMessage = nil
guard let url = URL(string: urlString) else {
errorMessage = "Invalid URL"
isLoading = false
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
guard let uiImage = UIImage(data: data) else {
errorMessage = "Failed to convert data to image"
isLoading = false
return
}
self.image = Image(uiImage: uiImage)
isLoading = false
} catch {
errorMessage = "Failed to load image: \(error.localizedDescription)"
isLoading = false
}
}
}
struct AsyncImageView: View {
@ObservedObject var imageLoader = ImageLoader()
let urlString: String
init(urlString: String) {
self.urlString = urlString
}
var body: some View {
VStack {
if imageLoader.isLoading {
ProgressView("Loading Image...")
} else if let image = imageLoader.image {
image
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
}
else if let errorMessage = imageLoader.errorMessage {
Text("Error: \(errorMessage)")
.foregroundColor(.red)
}
}.task { // Perform Async operations from Struct, which isn't an @MainActor itself
await imageLoader.loadImage(from: urlString)
}
}
}
"""
**Anti-Pattern:** Using "DispatchQueue.main.async" excessively without understanding SwiftUI's automatic UI updating mechanisms.
## 4. Testing
Writing tests is crucial for ensuring the quality and reliability of your application.
### 4.1 Unit Testing
* **Standard:** Write unit tests to verify the behavior of individual components, especially ViewModels and Models.
* **Do This:**
* Use the XCTest framework to write unit tests.
* Mock dependencies to isolate units under test.
* Test different scenarios, including success and failure cases.
* **Don't Do This:** Neglecting unit tests. Writing tightly coupled tests that are difficult to maintain.
**Why:** Unit tests catch bugs early, improve code quality, and facilitate refactoring with confidence.
**Example:**
"""swift
import XCTest
@testable import MyProject // Replace with your project name
final class UserViewModelTests: XCTestCase { // Make the class final so there's no overriding it
func testLoadUsersSuccess() async throws {
// Given
let viewModel = UserViewModel()
//When
await viewModel.loadUsers()
//Then
XCTAssertFalse(viewModel.users.isEmpty, "Users should not be empty on successful data loading.")
XCTAssertNil(viewModel.errorMessage, "Error message should be nil on successful data loading.")
XCTAssertFalse(viewModel.isLoading, "Is Loading should be set back to false after loading")
}
func testLoadUsersFail() async throws {
// Given
let viewModel = UserViewModel()
//When
await viewModel.loadUsers()
//Then
XCTAssertNotNil(viewModel.errorMessage, "Error message should not be nil on when failed to load data loading.")
}
}
"""
**Anti-Pattern:** Writing tests that cover only happy-path scenarios.
### 4.2 UI Testing
* **Standard:** Automate UI testing to verify the user interface and navigation flows.
* **Do This:**
* Use the XCUITest framework to write UI tests.
* Test common user interactions, such as button taps and text input.
* Verify that UI elements are displayed correctly.
* **Don't Do This:** Skipping UI tests. Writing brittle tests that break easily due to UI changes.
**Why:** UI tests ensure that the application functions correctly from the user's perspective.
**Example:** (Conceptual example – XCUITests often involve launching the app and interacting with elements via their accessibility identifiers).
"""swift
import XCTest
@testable import MyProject // Replace with your project name
class MyProjectUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testSuccessfulLogin() throws {
let app = XCUIApplication()
app.launch()
// Get handles to UI elements by their accessibility identifiers
let usernameTextField = app.textFields["usernameTextField"]
let passwordSecureField = app.secureTextFields["passwordSecureField"]
let loginButton = app.buttons["loginButton"]
// Enter username and password and log in
usernameTextField.tap()
usernameTextField.typeText("testuser")
passwordSecureField.tap()
passwordSecureField.typeText("password")
loginButton.tap()
// Get handle to log out button
let logoutButton = app.buttons["logoutButton"]
XCTAssertTrue(logoutButton.exists) // If it exists, we are logged in.
}
func testFailedLogin() throws {
let app = XCUIApplication()
app.launch()
// Get handles to UI elements by their accessibility identifiers
let usernameTextField = app.textFields["usernameTextField"]
let passwordSecureField = app.secureTextFields["passwordSecureField"]
let loginButton = app.buttons["loginButton"]
// Enter username and password and log in
usernameTextField.tap()
usernameTextField.typeText("wronguser")
passwordSecureField.tap()
passwordSecureField.typeText("wrongpassword")
loginButton.tap()
// Get handles to log out button
let errorLabel = app.staticTexts["errorMessage"]
XCTAssertTrue(errorLabel.exists) // If it exists, login failed
}
}
"""
**Anti-Pattern:** Hardcoding UI element positions or text values in UI tests.
## 5. Modern SwiftUI Features (Latest Version)
Leverage the latest SwiftUI features to write more concise, efficient, and expressive code.
### 5.1 "Grid" Layout
* **Standard:** Use the "Grid" view for creating flexible and dynamic grid-based layouts.
* **Do This:** Define rows and columns using "GridRow", "GridColumn", "GridCell" and modifiers like "gridCellColumns", "gridCellRows" and "gridColumnAlignment".
* **Don't Do This:** Overusing "HStack" and "VStack" for complex layouts that are better suited to a grid.
**Why:** The modern "Grid" is more flexible and performant. It replaces older, less efficient layout techniques.
**Example:**
"""swift
import SwiftUI
struct GridLayoutExample: View {
var body: some View {
Grid {
GridRow {
Color.red
Color.green
Color.blue
}
GridRow {
Color.yellow
Color.orange
Color.purple
}
}
.padding()
}
}
"""
### 5.2 "@Bindable" (SwiftUI 5.0+)
* **Standard:** Employ "@Bindable" for improved data binding within ViewModels.
* **Do this:** Adopt "Bindable" for classes that are designed for handling user facing data.
* **Don't do this:** Don't overuse "Bindable" objects. Keep them limited to a limited scope and avoid unnecessary memory allocations.
### 5.3 "PhaseAnimator" (SwiftUI 5.0+)
* **Standard:** Utilize "PhaseAnimator" for creating complex, multi-stage animations.
* **Do This:** Define a "Phase" enum and animate properties with custom animation curves for each phase.
* **Don't Do This:** Relying on older animation techniques for complex sequences that are easily managed by "PhaseAnimator".
**Example:**
"""swift
struct PhaseAnimatorExample: View {
@State private var isAnimating = false
var body: some View {
PhaseAnimator([0.0, 1.0, 0.0]) { phaseValue in
RoundedRectangle(cornerRadius: 20)
.fill(.blue)
.scaleEffect(1 + phaseValue)
.opacity(1 - Double(abs(phaseValue)))
} animation: { phase in
switch phase {
case 0:
return .easeInOut(duration: 1.0)
case 1:
return .easeInOut(duration: 0.5)
default:
return .easeInOut(duration: 1.0)
}
}
.onTapGesture {
isAnimating.toggle()
}
}
}
"""
### 5.4 "@Observable" (SwiftUI 5.0+)
* **Standard:** Prefer using "@Observable" for simpler state management when applicable
* **Do This:** Conform class to the "Observable" macro. Let SwiftUI handle the state management
* **Don't Do This:** If more control is needed for the object and its state, "ObservableObject" is the preferred method!
This document provides a strong foundation for building well-architected SwiftUI applications. Adhering to these standards will promote maintainability, scalability, and overall code quality. Be sure to stay up-to-date with the latest SwiftUI releases and adapt these standards accordingly.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Testing Methodologies Standards for SwiftUI This document outlines the coding standards for testing methodologies in SwiftUI projects. It will serve as a guide for developers and as context for AI coding assistants, ensuring consistent, maintainable, and well-tested SwiftUI code. ## 1. Introduction to SwiftUI Testing Testing is a crucial aspect of software development, ensuring the reliability and correctness of our SwiftUI applications. SwiftUI's declarative nature and tight integration with Xcode's testing frameworks make it well-suited for various testing approaches. This document covers unit testing, integration testing, and UI testing, providing guidelines and examples for each. ### 1.1. Why Testing Matters in SwiftUI * **Reliability:** Tests ensure that your UI components render and function as expected under different conditions. * **Maintainability:** Well-tested code is easier to refactor and maintain, as tests provide confidence that changes haven't introduced regressions. * **Early Bug Detection:** Testing helps identify bugs early in the development cycle, reducing the cost and effort of fixing them later. * **Code Quality:** Writing tests often encourages better code design and clearer APIs. * **Living Documentation:** Tests serve as a form of documentation, illustrating how different parts of the application are intended to work. ## 2. Unit Testing in SwiftUI Unit testing involves testing individual components or functions in isolation. In SwiftUI, this often means testing your data models, view models (if applicable), and utility functions. ### 2.1. Key Principles for Unit Tests * **Focus on Single Units:** Each test should verify the behavior of a single unit of code (function, method, class). * **Isolation:** Isolate the unit under test from its dependencies using techniques like dependency injection or mocks/stubs. * **Fast Execution:** Unit tests should execute quickly to enable frequent testing during development. * **Clear Assertions:** Tests should have clear and unambiguous assertions that verify the expected behavior. * **Comprehensive Coverage:** Aim for high code coverage, testing all key code paths and edge cases. ### 2.2. Dependency Injection for Testability Dependency injection is a powerful technique for improving the testability of your SwiftUI code. **Do This:** Use dependency injection to provide mock or stub implementations of dependencies during testing. **Don't Do This:** Create hard dependencies within your views or view models that are difficult to mock or replace during testing. """swift // Example: Using dependency injection for a data service // Protocol defining the data service protocol DataService { func fetchData() async throws -> [String] } // Concrete implementation of the data service (uses a real network call) class RealDataService: DataService { func fetchData() async throws -> [String] { //Simulate network call try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 Second return ["Item 1", "Item 2"] } } // Mock implementation of the data service (returns static data) class MockDataService: DataService { func fetchData() async throws -> [String] { return ["Mock Item 1", "Mock Item 2"] } } // ViewModel that depends on the data service class ViewModel: ObservableObject { @Published var data: [String] = [] let dataService: DataService // Declares dependency via protocol init(dataService: DataService) { self.dataService = dataService } func loadData() async { do { data = try await dataService.fetchData() } catch { print("Error fetching data: \(error)") } } } // SwiftUI View struct ContentView: View { @StateObject var viewModel: ViewModel var body: some View { List(viewModel.data, id: \.self) { item in Text(item) } .task { await viewModel.loadData() } } } """ **Unit Test:** """swift import XCTest @testable import YourAppName // Replace with your app's module name final class ViewModelTests: XCTestCase { func testLoadData_Success() async { // Arrange let mockDataService = MockDataService() let viewModel = ViewModel(dataService: mockDataService) // Act await viewModel.loadData() // Asynchronously load data // Assert XCTAssertEqual(viewModel.data, ["Mock Item 1", "Mock Item 2"], "Data should be loaded from the mock service.") } func testLoadData_Empty() async { // Arrange let mockDataService = MockDataService() let viewModel = ViewModel(dataService: mockDataService) // Act await viewModel.loadData() // Asynchronously load data // Assert XCTAssertFalse(viewModel.data.isEmpty) } } """ **Explanation:** * We define a "DataService" protocol that abstracts the data-fetching logic. * We create a "RealDataService" for the actual implementation and a "MockDataService" for testing. * The "ViewModel" takes a "DataService" as a dependency in its initializer. * In the unit test, we inject the "MockDataService" to control the data returned during the test. * We make sure to test async functions properly with "async" and "await". This allows us to test the "ViewModel"'s logic in isolation, without relying on a real network connection. ### 2.3. Testing SwiftUI View Models When using MVVM (Model-View-ViewModel) or similar architectures, unit testing view models is essential. **Do This:** Test the view model's logic, ensuring that it correctly transforms data and updates the UI state. **Don't Do This:** Attempt to directly test SwiftUI views in unit tests (use UI testing for that). Limit unit tests to the ViewModel only. """swift // View Model (same as above) class ViewModel: ObservableObject { @Published var data: [String] = [] let dataService: DataService init(dataService: DataService) { self.dataService = dataService } func loadData() async { do { data = try await dataService.fetchData() } catch { print("Error fetching data: \(error)") } } } // SwiftUI View (same as above) struct ContentView: View { @StateObject var viewModel: ViewModel var body: some View { List(viewModel.data, id: \.self) { item in Text(item) } .task { await viewModel.loadData() } } } """ **Unit Test:** """swift import XCTest @testable import YourAppName final class ViewModelTests: XCTestCase { func testLoadData_Success() async { // Arrange let mockDataService = MockDataService() let viewModel = ViewModel(dataService: mockDataService) // Act await viewModel.loadData() // Assert XCTAssertEqual(viewModel.data, ["Mock Item 1", "Mock Item 2"], "Data should be loaded from the mock service.") } } """ **Explanation:** The unit test for the view model is the same as outlined in 2.2. The focus is on testing the output and side effects of the ViewModel functions (ex: loading data or publishing values) given different injected dependencies and inputs. ### 2.4 Asynchronous Testing SwiftUI often involves asynchronous operations (e.g., network calls, background processing). Properly testing asynchronous code is crucial. **Do This:** Use "async" and "await" in your test functions to handle asynchronous operations. Employ "XCTWaiter" for complex asynchronous flows where predictable timing is critical. **Don't Do This:** Rely on "Thread.sleep" or other blocking techniques, which can make your tests slow and unreliable. """swift import XCTest @testable import YourAppName final class AsyncViewModelTests: XCTestCase { func testAsyncOperation_Success() async throws { // Arrange let dataService = RealDataService() let viewModel = ViewModel(dataService: dataService) // Act await viewModel.loadData() // Await the UI to update by using a slight delay on the main thread. // This ensures the UI has enough time to update before asserts. try await Task.sleep(nanoseconds: 100_000_000) //0.1 seconds // Assert XCTAssertEqual(viewModel.data.count, 2, "Data should be loaded asynchronously.") } } """ **Important Considerations:** * **MainActor Concurrency:** "Published" properties and UI updates in SwiftUI happen on the main thread. Tests need to account for timing and synchronization. * **"task()" Modifier:** When using the ".task" modifier in your SwiftUI view, ensure that any asynchronous operations performed in the task are properly handled in your tests. ## 3. Integration Testing in SwiftUI Integration testing verifies the interaction between different parts of your application. In SwiftUI, this might involve testing how views interact with view models, or how different data services work together. ### 3.1. Key Principles for Integration Tests * **Test Interactions:** Focus on testing the interactions between multiple components. * **Use Real Dependencies (Where Possible):** Unlike unit tests, integration tests may use real implementations of some dependencies, but consider mocking external services or slow dependencies. * **Larger Scope:** Integration tests typically have a larger scope than unit tests. * **Verify End-to-End Flows:** Ensure that integration tests cover complete user flows or business processes. ### 3.2. Testing View Interactions with View Models **Do This:** Verify that views correctly display data from view models and that user interactions trigger the expected updates in the view model. **Don't Do This:** Attempt to exhaustively test the view's rendering logic directly; focus on the data flow and event handling. UI testing will be used for view rendering. """swift // Model struct Item { let id = UUID() let name: String } // Modified Data Service protocol DataService { func fetchData() async throws -> [Item] } class RealDataService: DataService { func fetchData() async throws -> [Item] { try? await Task.sleep(nanoseconds: 1_000_000_000) return [Item(name: "Item 1"), Item(name: "Item 2")] } } class MockDataService: DataService { func fetchData() async throws -> [Item] { return [Item(name: "Mock Item 1"), Item(name: "Mock Item 2")] } } // Modified ViewModel class ViewModel: ObservableObject { @Published var data: [Item] = [] let dataService: DataService init(dataService: DataService) { self.dataService = dataService } func loadData() async { do { data = try await dataService.fetchData() } catch { print("Error fetching data: \(error)") } } } // Modified SwiftUI View struct ContentView: View { @StateObject var viewModel: ViewModel var body: some View { List(viewModel.data, id: \.id) { item in Text(item.name) } .task { await viewModel.loadData() } } } """ **Integration Test:** """swift import XCTest import SwiftUI @testable import YourAppName final class ContentViewIntegrationTests: XCTestCase { func testContentView_DisplaysDataFromViewModel() async throws { // Arrange let mockDataService = MockDataService() let viewModel = ViewModel(dataService: mockDataService) let contentView = ContentView(viewModel: viewModel) // Create app and set the Content View let app = XCUIApplication() app.launch() sleep(3) // Short delay for the UI to load. Alternatives like XCTWaiter preferred. // Access the view let listView = app.lists["dataList"] // Give your List an accessibility identifier // Assert XCTAssertTrue(listView.staticTexts["Mock Item 1"].exists) XCTAssertTrue(listView.staticTexts["Mock Item 2"].exists) } } """ **Explanation:** * The integration test focuses on validating that the "ContentView" correctly displays data loaded by the "ViewModel". * We use a "MockDataService" to provide controlled data. * The key assertion is that the "Text" views in the "List" display the expected names from the mock data. * In a real scenario, you'd use "XCTWaiter" for more robust asynchronous UI updates. ### 3.3. Testing Navigation Testing navigation flows in SwiftUI applications is crucial for ensuring a smooth user experience. **Do This:** Verify that navigation links and sheet presentations work as expected, and that data is passed correctly between views. **Don't Do This:** Overlook edge cases in navigation, such as handling back gestures or dismissing sheets. """swift // Second View struct DetailView: View { let item: String var body: some View { Text("Detail View for \(item)") } } // Modified SwiftUI View struct ContentView: View { @StateObject var viewModel: ViewModel var body: some View { NavigationView { List(viewModel.data, id: \.self) { item in NavigationLink(destination: DetailView(item: item)) { Text(item) } } .navigationTitle("Items") .task { await viewModel.loadData() } } } } """ **Integration Test:** """swift import XCTest import SwiftUI @testable import YourAppName final class NavigationIntegrationTests: XCTestCase { func testNavigationLink_NavigatesToDetailView() throws { // Arrange let mockDataService = MockDataService() let viewModel = ViewModel(dataService: mockDataService) let contentView = ContentView(viewModel: viewModel) let app = XCUIApplication() app.launch() sleep(3) // Short delay for the UI to load // Act app.staticTexts["Mock Item 1"].tap() sleep(3) // Assert XCTAssertTrue(app.staticTexts["Detail View for Mock Item 1"].exists) } } """ **Explanation:** * The test taps on a "NavigationLink" in the "ContentView" to navigate to the "DetailView". * It then asserts that the "DetailView" is displayed with the correct data. * Again, consider using "XCTWaiter" to improve the reliability of the tests. ## 4. UI Testing in SwiftUI UI testing involves simulating user interactions with your app and verifying that the UI behaves as expected. This is the highest-level testing and is crucial for ensuring the end-to-end functionality of your app. ### 4.1. Key Principles for UI Tests * **Simulate User Interactions:** UI tests should mimic real user actions as closely as possible. * **Verify UI State:** Assert that the UI elements are displayed correctly and that the app responds appropriately to user input. * **Record and Replay:** Xcode's UI testing framework allows you to record user interactions and generate test code automatically, but always refactor into meaningful tests. * **Accessibility Identifiers:** Use accessibility identifiers to target UI elements reliably. * **Avoid Sleep Statements:** Use "XCTWaiter" for asynchronous UI updates to avoid flaky tests. ### 4.2. Setting Accessibility Identifiers Accessibility identifiers are crucial for writing robust UI tests. **Do This:** Set accessibility identifiers for all interactive UI elements, such as buttons, text fields, and lists. **Don't Do This:** Use generic or ambiguous identifiers that make it difficult to target specific elements. """swift // SwiftUI View struct ContentView: View { @StateObject var viewModel: ViewModel var body: some View { List(viewModel.data, id: \.self) { item in NavigationLink(destination: DetailView(item: item)) { Text(item) .accessibilityIdentifier("itemText_\(item)") // Unique identifier for each item } } .navigationTitle("Items") .accessibilityIdentifier("itemList") // Identifier for the entire list .task { await viewModel.loadData() } } } """ ### 4.3. Example UI Test """swift import XCTest final class UITests: XCTestCase { func testListItemTap_NavigatesToDetailView() throws { // Arrange let app = XCUIApplication() app.launch() // Need to wait for content to load before attempting to tap on the items let item1 = app.staticTexts["itemText_Mock Item 1"] expectation(for: NSPredicate(format: "exists == 1"), evaluatedWith: item1, handler: nil) waitForExpectations(timeout: 5, handler: nil) // Act item1.tap() // Assert XCTAssertTrue(app.staticTexts["Detail View for Mock Item 1"].exists) } } """ **Explanation:** * We launch the app using "XCUIApplication()". * We use "app.staticTexts["itemText_Mock Item 1"]" to target the "Text" view with a specific accessibility identifier. * We then simulate a tap on the element using ".tap()". * Finally, we assert that the navigation to the "DetailView" was successful by checking for the presence of a specific text element. * "expectation" and "waitForExpectations" are used to avoid premature assertions before UI updates. * Tests should be named with specificity to denote exactly what is being tested. ### 4.4. Handling Alerts and Sheets SwiftUI apps often use alerts and sheets to display important information or gather user input. UI tests need to handle these elements correctly. **Do This:** Use "app.alerts" and "app.sheets" to access alerts and sheets in your UI tests. **Don't Do This:** Assume that alerts or sheets will always be present or that their titles will always be the same. ## 5. Test Driven Development (TDD) in SwiftUI TDD is a development process where you write tests before you write the code. This helps to ensure that your code is testable and that you are only writing the code that is necessary to pass the tests. ### 5.1 Implementing TDD * **Red-Green-Refactor:** Write a failing test (Red), write the minimal amount of code to pass the test (Green), and then refactor the code to improve its design and maintainability. * **Start Small:** Begin with small, focused tests and gradually increase the complexity of your tests as you develop the code. * **Focus on Behavior:** Write tests that focus on the behavior of the code, rather than the implementation details. ## 6. Continuous Integration Integrating testing into a Continuous Integration (CI) pipeline is crucial for automating the testing process and ensuring that code changes don't introduce regressions. **Do This:** Set up a CI system, such as Jenkins, CircleCI, or GitHub Actions, to run your tests automatically whenever code is committed or pulled requests are created. **Don't Do This:** Rely on manual testing alone, as it can be time-consuming and error-prone. """yaml # Example .github/workflows/ci.yml for GitHub Actions name: CI on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: build: runs-on: macos-latest steps: - uses: actions/checkout@v3 - name: Select Xcode run: sudo xcode-select -switch /Applications/Xcode_15.2.app # Or whatever version - name: Build and Test run: xcodebuild clean build test -project YourProject.xcodeproj -scheme YourScheme -destination 'platform=iOS Simulator,name=iPhone 15' """ ## 7. Performance Testing While functional testing ensures your app works correctly, performance testing ensures it does so efficiently. **Do This:** Employ Instruments to identify performance bottlenecks in your SwiftUI app. Focus on areas like rendering performance, memory usage, and energy consumption. **Don't Do This:** Neglect performance testing until late in the development cycle. ## 8. Security Testing Security testing is an essential part of the development process. **Do This:** Follow secure coding practices to prevent common vulnerabilities, such as data breaches or unauthorized access. ## 9. Conclusion By adhering to these coding standards for SwiftUI testing methodologies, you can develop robust, maintainable, and reliable iOS applications. Remember to adapt these guidelines to the specific needs of your projects and to stay up-to-date with the latest SwiftUI features and best practices.
# Component Design Standards for SwiftUI This document outlines component design standards for SwiftUI development, focusing on creating reusable, maintainable, and performant UI elements. Adhering to these standards will promote consistency, readability, and scalability across projects. ## 1. Component Architecture and Principles ### 1.1. Single Responsibility Principle (SRP) **Standard:** Each SwiftUI view (component) should have one, and only one, reason to change. **Why:** SRP enhances reusability, testability, and maintainability. Views that handle multiple responsibilities become tightly coupled and difficult to modify without introducing unintended side effects. **Do This:** Break down complex views into smaller, focused components. **Don't Do This:** Create monolithic views that handle data fetching, business logic, and UI rendering. **Example:** """swift // Good: Separating concerns struct UserProfileView: View { let userID: String var body: some View { VStack { UserHeaderView(userID: userID) UserDetailView(userID: userID) } } } struct UserHeaderView: View { @StateObject var viewModel: UserHeaderViewModel init(userID: String) { _viewModel = StateObject(wrappedValue: UserHeaderViewModel(userID: userID)) } var body: some View { HStack { AsyncImage(url: viewModel.user?.profileImageURL) { image in image .resizable() .scaledToFit() .frame(width: 50, height: 50) } placeholder: { ProgressView() .frame(width: 50, height: 50) } Text(viewModel.user?.name ?? "Loading...") } .onAppear{ viewModel.loadUser() } } } struct UserDetailView: View { @StateObject var viewModel: UserDetailViewModel init(userID: String) { _viewModel = StateObject(wrappedValue: UserDetailViewModel(userID: userID)) } var body: some View { VStack(alignment: .leading) { Text("Email: \(viewModel.user?.email ?? "Loading...")") Text("Location: \(viewModel.user?.location ?? "Loading...")") } .onAppear{ viewModel.loadUser() } } } """ """swift //MARK: View Models import Foundation class UserHeaderViewModel: ObservableObject { @Published var user: User? let userID: String init(userID: String) { self.userID = userID } func loadUser() { // Simulate an API call DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.user = User(id: self.userID, name: "John Doe", email: "john.doe@example.com", location: "New York", profileImageURL: URL(string: "https://example.com/johndoe.jpg")) } } } class UserDetailViewModel: ObservableObject { @Published var user: User? let userID: String init(userID: String) { self.userID = userID } func loadUser() { // Simulate an API call DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.user = User(id: self.userID, name: "John Doe", email: "john.doe@example.com", location: "New York", profileImageURL: URL(string: "https://example.com/johndoe.jpg")) } } } //MARK: Data Model struct User: Identifiable { let id: String let name: String let email: String let location: String let profileImageURL: URL? } """ ### 1.2. Abstraction **Standard:** Create abstract components that can be customized through parameters, modifiers, and content closures. **Why:** Abstraction promotes code reuse and reduces duplication, making it easier to maintain and evolve the UI. **Do This:** Parameterize views with data and callbacks, using environment objects for global state. **Don't Do This:** Hardcode specific values or behaviors within a component that should be configurable. **Example:** """swift // Good: Abstract Button struct CustomButton<Content: View>: View { let action: () -> Void let label: Content let isDisabled: Bool init(action: @escaping () -> Void, isDisabled: Bool = false, @ViewBuilder label: () -> Content) { self.action = action self.label = label() self.isDisabled = isDisabled } var body: some View { Button(action: action) { label .padding() .background(isDisabled ? Color.gray : Color.blue) .foregroundColor(.white) .cornerRadius(10) } .disabled(isDisabled) } } // Usage struct ContentView: View { @State private var counter = 0 var body: some View { VStack { Text("Counter: \(counter)") CustomButton(action: { counter += 1 }) { Text("Increment") } CustomButton(action: { counter = 0 }, isDisabled: counter == 0) { Text("Reset") } } } } """ ### 1.3. Composition **Standard:** Build complex UIs by composing smaller, reusable views. **Why:** Composition simplifies the structure of the UI, makes components more testable, and allows for greater flexibility in design. **Do This:** Use "ViewBuilder" to compose flexible layouts within components. **Don't Do This:** Embed deeply nested logic within a single view. **Example:** """swift // Good: Compositional layout struct ProductCard: View { let product: Product var body: some View { VStack(alignment: .leading) { ProductImage(url: product.imageURL) ProductDetails(name: product.name, price: product.price) } .padding() .background(Color.white) .cornerRadius(10) .shadow(radius: 3) } } struct ProductImage: View { let url: URL? var body: some View { AsyncImage(url: url) { image in image.resizable().scaledToFit() } placeholder: { Color.gray.opacity(0.3) } .frame(height: 150) } } struct ProductDetails: View { let name: String let price: Double var body: some View { VStack(alignment: .leading) { Text(name) .font(.headline) Text("Price: $\(price, specifier: "%.2f")") .font(.subheadline) } } } struct Product: Identifiable { let id = UUID() let name: String let price: Double let imageURL: URL? } """ ### 1.4. Separation of Concerns (SoC) **Standard:** Separate UI presentation from data handling and business logic. **Why:** SoC significantly improves code organization, testability, and maintainability. **Do This:** Use ViewModels (ObservableObjects) to manage data and state. **Don't Do This:** Perform network requests, data transformations, or complex calculations directly within the view's "body". **Example:** """swift // Good: Using ViewModel class ArticleViewModel: ObservableObject { @Published var article: Article? @Published var isLoading = false private let articleID: String init(articleID: String) { self.articleID = articleID } func loadArticle() { isLoading = true // Simulate API call DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.article = Article(title: "Example Article", content: "This is example content.", author: "Jane Doe") self.isLoading = false } } } struct ArticleView: View { @StateObject var viewModel: ArticleViewModel init(articleID: String) { _viewModel = StateObject(wrappedValue: ArticleViewModel(articleID: articleID)) } var body: some View { VStack { if viewModel.isLoading { ProgressView() } else if let article = viewModel.article { Text(article.title) .font(.title) Text(article.content) .padding() Text("By: \(article.author)") .font(.subheadline) } else { Text("Failed to load article") } } .onAppear { viewModel.loadArticle() } } } struct Article { let title: String let content: String let author: String } """ ## 2. SwiftUI Specific Standards ### 2.1. Using "@State", "@Binding", "@ObservedObject", "@StateObject", and "@EnvironmentObject" **Standard:** Choose the correct state management property wrapper based on the scope and ownership of data. **Why:** Proper state management is crucial for predictable UI behavior and performance. * "@State": Use for simple, private state within a single view. * "@Binding": Use to create a two-way connection to a "@State" or "@Binding" property in a parent view. * "@ObservedObject": Use to observe changes from an external ObservableObject (e.g., ViewModel). The observed object should be passed down from another View. * "@StateObject": Use to create and own an ObservableObject within a view's lifecycle. This ensures the ViewModel is only created *once* for each instance of the View. * "@EnvironmentObject": Use to access shared data across multiple views via the environment. **Do This:** * Use "@StateObject" to instantiate the ViewModel in the parent, and "@ObservedObject" to receive it in child views. Inject only the required properties in the children. * Pass data down using "@Binding" for direct modifications from child views and reading the data from parent views. * Prefer using "@EnvironmentObject" for app-wide configurations and less for specific data transfer between views. **Don't Do This:** * Use "@State" for complex data models or data that needs to be shared across views. * Create a new instance of the ViewModel every time the view initializes (e.g. in the "body"). **Example:** """swift // Good: Correct State Management class CounterViewModel: ObservableObject { @Published var count = 0 func increment() { count += 1 } } struct CounterView: View { @StateObject var viewModel = CounterViewModel() var body: some View { VStack { Text("Counter: \(viewModel.count)") IncrementButton(viewModel: viewModel) ResetButton(counter: $viewModel.count) // Pass down count as a Binding } .padding() } } struct IncrementButton: View { @ObservedObject var viewModel: CounterViewModel var body: some View { Button("Increment") { viewModel.increment() } } } struct ResetButton: View { @Binding var counter: Int var body: some View { Button("Reset") { counter = 0 } } } // Imagine MyApp is the top-level app @main struct MyApp: App { @StateObject private var appSettings = AppSettings() // App-level config var body: some Scene { WindowGroup { ContentView() // CounterView would exist somewhere down this tree .environmentObject(appSettings) } } } class AppSettings: ObservableObject { @Published var theme = "light" // Example app-wide setting } struct ContentView: View { @EnvironmentObject var appSettings: AppSettings var body: some View { VStack { Text("Theme: \(appSettings.theme)") // Access global theme CounterView() } } } """ ### 2.2. View Composition with "ViewBuilder" **Standard:** Use "ViewBuilder" to create flexible and reusable container views. **Why:** "ViewBuilder" simplifies the creation of conditional and dynamic view hierarchies. **Do This:** * Utilize "@ViewBuilder" to create custom layouts (e.g., stacks, grids) that accept arbitrary content. * Create separate modifiers for styling common visual attributes. **Don't Do This:** * Embed complex conditional logic directly within the "body" when composition can be used instead. **Example:** """swift // Good: Using ViewBuilder for flexible layout struct CardView<Content: View>: View { let title: String @ViewBuilder let content: () -> Content var body: some View { VStack(alignment: .leading) { Text(title) .font(.headline) .padding(.bottom, 5) content() } .padding() .background(Color.gray.opacity(0.1)) .cornerRadius(10) } } struct ContentView: View { var body: some View { CardView(title: "User Profile") { Text("Name: John Doe") Text("Email: john.doe@example.com") } } } """ ### 2.3. Custom Modifiers **Standard:** Encapsulate reusable view modifications into custom modifiers. **Why:** Custom modifiers promote code reuse and make UI code more readable. **Do This:** * Create extension on "View" to add new view modifiers. **Don't Do This:** * Apply the same long chain of modifiers repeatedly throughout codebase. **Example:** """swift // Good: Custom Modifier struct ShadowModifier: ViewModifier { func body(content: Content) -> some View { content .shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 2) } } extension View { func withShadow() -> some View { modifier(ShadowModifier()) } } struct ContentView: View { var body: some View { Text("Hello, world!") .padding() .background(Color.white) .withShadow() } } """ ### 2.4 Accessibility **Standard:** Ensure all custom components are fully accessible. **Why:** Ensures that app is usable by everyone, including users with disabilities. **Do This:** * Use "accessibilityLabel", "accessibilityHint", and "accessibilityAdjustableAction" appropriately. * Test accessibility thoroughly using VoiceOver and other accessibility tools. **Don't Do This:** * Rely solely on visual cues without providing alternative text descriptions. **Example:** """swift // Accessibility Example struct AccessibleButton: View { let action: () -> Void let label: String var body: some View { Button(action: action) { Text(label) .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(10) } .accessibilityLabel(label) .accessibilityHint("Taps to perform action.") //Describe what the button does } } """ ### 2.5. PreviewProvider **Standard:** Provide previews for all custom components for easy iteration. **Why:** Previews streamline UI development by allowing developers to see live changes without building and running the app on a device or simulator. **Do This:** * Create simple preview providers for the component * Include multiple previews to showcase different states or configurations for the component. **Don't Do This:** * Skip previews. **Example:** """swift struct CustomText: View { var text: String = "Default Text" //Default value for the preview var body: some View { Text(text).font(.title) } } struct CustomText_Previews: PreviewProvider { static var previews: some View { Group { CustomText(text: "Custom Text here") //Initial state of the component CustomText()//Default state of the component }.previewLayout(.sizeThatFits) } } """ ## 3. Performance Optimization ### 3.1. Avoiding Unnecessary View Updates **Standard:** Minimize unnecessary view updates to improve performance. **Why:** Frequent view updates can lead to performance bottlenecks, especially in complex UIs. **Do This:** * Use "Equatable" conformance and "onChange" modifiers to control view updates based on specific data changes. * Use SwiftUI's "isEqual" to ensure views that are logically identical aren't redrawn. **Don't Do This:** * Force views to re-render unnecessarily with methods like "objectWillChange.send()". **Example:** """swift // Good: Preventing unnecessary updates struct ProductView: View, Equatable { let product: Product // Ensure Product conforms to Equatable static func == (lhs: ProductView, rhs: ProductView) -> Bool { return lhs.product == rhs.product } var body: some View { Text(product.name) .onChange(of: product) { newProduct in print("Product changed: \(newProduct.name)") } } } struct Product: Equatable { let id = UUID() let name: String } """ ### 3.2. Lazy Loading **Standard:** Load content only when it's needed or visible. **Why:** Improves startup time and reduces memory usage, especially in lists and complex layouts. **Do This:** * Use "LazyVStack" and "LazyHStack" for views that contain many children or load content based on user-triggered actions. * Use "AsyncImage" in order to load them in the background. Show a "ProgressView" until the image loads. **Don't Do This:** * Load all content upfront, especially if it's not immediately visible. **Example:** """swift // Good: Lazy Loading struct ProductListView: View { let products: [Product] = Array(repeating: Product(name: "Product"), count: 100) var body: some View { ScrollView { LazyVStack { ForEach(products.indices, id: \.self) { index in ProductRow(product: products[index]) // Each product gets its own row } } } } } struct ProductRow: View { let product: Product var body: some View { Text("Row \(product.name) \(index)") .padding() } } """ ### 3.3. Reducing View Hierarchy Depth **Standard:** Minimize the depth of the view hierarchy. **Why:** Deep view hierarchies can negatively impact rendering performance. **Do This:** * Flatten layouts where possible by using techniques such as absolute positioning or custom layout containers introduced in later SwiftUI versions. * Make sure there is no extra padding that can be added to the parent for example. **Don't Do This:** * Nest unnecessary "VStack"s, "HStack"s, and "ZStack"s. **Example:** """swift //Good - Flatter Hierarchy struct FlatterLayout: View { var body: some View { ZStack(alignment: .topLeading) { Rectangle() .fill(.gray.opacity(0.3)) .frame(width: 200, height: 100) .zIndex(0) Text("Overlayed Text 1") .padding(10) .background(.white) .zIndex(1) .offset(x: 10, y: 10) Text("Overlayed Text 2") .padding(10) .background(.white) .zIndex(1) .offset(x: 20, y: 50) } } } """ ### 3.4. Image Optimization **Standard:** Optimize images for size and resolution. **Why:** Large images can consume excessive memory and increase loading times. **Do This:** * Use appropriately sized images for their display dimensions. * Use image compression tools to reduce file sizes. * Use Asset Catalogs to manage different image resolutions. **Don't Do This:** * Use excessively large images, especially for assets displayed at smaller sizes. ## 4. Security Considerations ### 4.1. Secure Data Handling **Standard:** Handle sensitive data securely. **Why:** Protecting user data is crucial for security and privacy. **Do This:** * Use Keychain Services to store sensitive information like passwords and API keys. * Encrypt sensitive data both in transit and at rest. **Don't Do This:** * Store sensitive data in plain text, UserDefaults, or directly within the app's code. ### 4.2. Input Validation **Standard:** Validate user input to prevent injection attacks and data corruption. **Why:** Input validation is essential for preventing security vulnerabilities and ensuring data integrity. **Do This:** * Use secure coding practices for handling user inputs and validating the data. * Sanitize the data before sending it to the backend, so only the correct data is sent over **Don't Do This:** * Trust user input without validation. **Example:** """swift // Input Validation Example struct ValidatedTextField: View { @State private var text: String = "" @State private var isValid: Bool = true var body: some View { VStack { TextField("Enter Email", text: $text) .onChange(of: text) { newValue in isValid = isValidEmail(newValue) } if !isValid { Text("Invalid email format").foregroundColor(.red) } } } func isValidEmail(_ email: String) -> Bool { let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" return NSPredicate(format:"SELF MATCHES %@", emailRegex).evaluate(with: email) } } """ ## 5. Naming Conventions **Standard:** Follow consistent naming conventions for components, properties, and methods. **Why:** Consistent naming improves code readability and maintainability. **Do This:** * Use descriptive names that clearly indicate the purpose and function of the component. * Use TitleCase for struct names, camelCase for variable names, and PascalCase for functions/methods. **Don't Do This:** * Use cryptic or abbreviated names that are difficult to understand. ## 6. Error Handling **Standard:** Implement robust error handling to gracefully manage unexpected situations. **Why:** Proper error handling prevents crashes and enhances the user experience. **Do This:** * Use "Result" types and "try/catch" blocks to handle potential errors. * Display informative error messages to the user. **Don't Do This:** * Ignore errors or allow the app to crash silently. ## 7. Testing **Standard:** Write unit and UI tests to ensure the correctness and reliability of components. **Why:** Tests help prevent bugs and ensure that components behave as expected. **Do This:** * Write unit tests for individual views and ViewModels. * Use UI tests to verify the behavior of the UI. **Don't Do This:** * Skip testing components altogether. By following these guidelines, teams can deliver high-quality, maintainable, and performant SwiftUI applications ensuring maximum productivity both alone and when collaborating with AI code-generation tools.
# State Management Standards for SwiftUI This document outlines the coding standards for state management in SwiftUI applications. It aims to provide clear, actionable guidelines for managing application state, data flow, and reactivity to ensure maintainable, performant, and secure code. These standards are based on the latest version of SwiftUI and incorporate modern best practices. ## 1. Introduction to State Management in SwiftUI SwiftUI offers several mechanisms for managing state, enabling dynamic UI updates and data flow between views. Selecting the appropriate state management technique depends on the scope and complexity of the data being managed. Poorly managed state can lead to performance issues, unexpected behavior, and difficult-to-debug code. * **Standard:** Choose the simplest state management approach that meets the needs of the specific use case. Avoid over-complicating state management for trivial scenarios. * **Why:** Simplifies code, reduces potential for bugs, and improves performance. ## 2. State and Binding "@State" and "@Binding" are fundamental property wrappers for managing simple, local state within views. ### 2.1. Using "@State" "@State" manages mutable data within a single view. It's ideal for UI controls and temporary data that doesn't need to persist between view reloads or be shared with other views. * **Do This:** Use "@State" for simple, view-local state, such as whether a button is toggled or the text in a TextField. * **Don't Do This:** Don't use "@State" to manage data that needs to persist across view recreations or be shared among different views. **Example:** """swift import SwiftUI struct ToggleExampleView: View { @State private var isToggled: Bool = false var body: some View { VStack { Toggle(isOn: $isToggled) { Text("Toggle Me") } Text(isToggled ? "Toggled On" : "Toggled Off") } .padding() } } """ * **Why:** "@State" is optimized for fast, local updates. Misusing it can create unnecessary data duplication and make debugging difficult. ### 2.2. Using "@Binding" "@Binding" creates a two-way connection between a view and its parent, allowing the child view to modify the parent's state directly. * **Do This:** Use "@Binding" to allow child views to control or modify state owned by a parent view. * **Don't Do This:** Avoid creating deep chains of bindings (passing a "@Binding" down multiple levels). This can make data flow hard to trace and debug. **Example:** """swift import SwiftUI struct ParentView: View { @State private var message: String = "Hello" var body: some View { VStack { Text("Parent View: \(message)") ChildView(message: $message) } .padding() } } struct ChildView: View { @Binding var message: String var body: some View { TextField("Enter message", text: $message) .padding() } } """ * **Why:** "@Binding" enables clear data flow between related views, but excessive use can lead to tangled dependencies. It promotes data accessibility and UI coordination. ### 2.3 Anti-Pattern: Prop Drilling with Bindings * **Don't Do This:** Passing "@Binding" properties down multiple layers of views (prop drilling). This creates tight coupling between unrelated views. It is a sign that "EnvironmentObject" or "ObservedObject" should be considered. * **Why:** Prop drilling makes code harder to maintain and understand, particularly as the application grows in complexity. ## 3. Observed Objects and State Objects "@ObservedObject" and "@StateObject" are essential for managing complex data models within SwiftUI. ### 3.1. Using "@ObservedObject" "@ObservedObject" is used to subscribe to an external object that conforms to the "ObservableObject" protocol. When the object's "@Published" properties change, the view updates automatically. * **Do This:** Use "@ObservedObject" when the lifetime of the observable object is managed *outside* the view, such as when passed in from another view, or when the view needs to respond to shared global state. * **Don't Do This:** Don't create the observable object *inside* the view using "@ObservedObject", or you will recreate the object every time the view redraws. **Example:** """swift import SwiftUI import Combine class UserSettings: ObservableObject { @Published var username: String = "Guest" } struct UserProfileView: View { @ObservedObject var settings: UserSettings var body: some View { Text("Username: \(settings.username)") } } //Usage in another view: struct ContentView: View { @StateObject var userSettings = UserSettings() var body: some View { UserProfileView(settings: userSettings) } } """ * **Why:** "@ObservedObject" is used to represent externally managed mutable state. ### 3.2. Using "@StateObject" "@StateObject" is similar to "@ObservedObject", but it ensures that the observable object is created and managed *by* the view. The object persists across view updates. This is the right method when initializing the observable object within your view. * **Do This:** Use "@StateObject" to create and manage the lifecycle of an observable object within a specific view. Typically best when the object is private to the view. * **Don't Do This:** Don't use "@StateObject" to manage data that should be shared globally. Use "@EnvironmentObject" for that. **Example:** """swift import SwiftUI import Combine class Counter: ObservableObject { @Published var count: Int = 0 func increment() { count += 1 } } struct CounterView: View { @StateObject var counter = Counter() var body: some View { VStack { Text("Count: \(counter.count)") Button("Increment") { counter.increment() } } .padding() } } """ * **Why:** "@StateObject" prevents the observable object from being re-initialized when the view is redrawn, ensuring that the state persists correctly. ### 3.3. Publishing Data "@Published" is a property wrapper that signals changes to the observable object to which it belongs. SwiftUI views observing this object will automatically update when a "@Published" property changes. * **Do This:** Declare properties that SwiftUI views should observe with "@Published". * **Don't Do This:** Don't overuse "@Published" on properties that don't directly affect the view. This can lead to unnecessary redraws and hurt performance. **Example:** """swift import SwiftUI import Combine class DataModel: ObservableObject { @Published var data: String = "" func fetchData() { //Simulate fetching data DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.data = "Data fetched!" } } } struct DataView: View { @StateObject var dataModel = DataModel() var body: some View { Text(dataModel.data) .onAppear { dataModel.fetchData() } } } """ * **Why:** "@Published" ensures that the UI remains synchronized with the underlying data. ### 3.4. Anti-Pattern: Forgetting to Conform to "ObservableObject" * **Don't Do This**: Neglecting to make a class conform to the "ObservableObject" protocol when intending to use it with "@ObservedObject" or "@StateObject". * **Why**: SwiftUI won't be able to track changes within the class, resulting in the UI not updating as expected. ## 4. Environment Objects "@EnvironmentObject" facilitates sharing data across an entire view hierarchy without explicitly passing it down through each view. It works well for application settings, user preferences, and other global state. * **Do This:** Use "@EnvironmentObject" to inject globally accessible data into the view hierarchy. * **Don't Do This:** Don't pass data as an environment object if only a few views need it. Consider "ObservedObject" or "Binding" instead. **Example:** """swift import SwiftUI import Combine class AppSettings: ObservableObject { @Published var theme: String = "Light" } struct ThemeView: View { @EnvironmentObject var settings: AppSettings var body: some View { Text("Current Theme: \(settings.theme)") } } struct ContentView: View { @StateObject var appSettings = AppSettings() var body: some View { ThemeView() .environmentObject(appSettings) } } """ * **Why:** "@EnvironmentObject" avoids prop drilling, creating more modular and maintainable code. But only use it for truly global state. ### 4.1. Supplying the Environment Object It is critical that an "EnvironmentObject" is injected into the view hierarchy *above* where it is used. Failing to supply the "EnvironmentObject" will cause a runtime crash! * **Do This:** Ensure any view requiring an "@EnvironmentObject" has the object provided using the ".environmentObject()" modifier on a parent view. * **Don't Do This:** Forget to inject the environment object. This results in a runtime error. ### 4.2. Anti-Pattern: Overuse of Environment Objects * **Don't Do This**: Using "@EnvironmentObject" for localized state only used by one or two views. * **Why**: Overusing "@EnvironmentObject" makes data flow less explicit and can harm performance due to unnecessary updates across the view hierarchy. It is important to reserve its usage for data truly used throughout the application. ## 5. AppStorage "@AppStorage" provides a simple way to persist data to "UserDefaults", allowing data to be saved between app launches. * **Do This:** Use "@AppStorage" for simple settings or user preferences that need to persist. * **Don't Do This:** Don't store large or complex data structures in "AppStorage" due to its limitations and performance implications. Consider Core Data or other persistence solutions for complex data. **Example:** """swift import SwiftUI struct SettingsView: View { @AppStorage("isDarkMode") private var isDarkMode: Bool = false var body: some View { Toggle(isOn: $isDarkMode) { Text("Dark Mode") } .padding() } } """ * **Why:** "@AppStorage" simplifies persistence for basic data. ### 5.1 Considerations for Data Types * **Do This:** Ensure the data types you use with "@AppStorage" are compatible with "UserDefaults". Common types like "Bool", "Int", "Double", "String", "URL", and "Data" are appropriate. For custom types, consider encoding and decoding them as "Data" using "Codable". * **Don't Do This:** Try to directly store complex data types in "@AppStorage" without proper encoding, or store very large data, particularly when using value types that are copied on write. ## 6. Data Flow Patterns SwiftUI embraces unidirectional data flow. State changes trigger view updates, but views should not directly modify external state except through bindings or predefined APIs. ### 6.1. Unidirectional Data Flow * **Do This:** Ensure that data flows in a single direction: from the source of truth (e.g., observable object) to the views. Views can signal changes through callbacks or bindings, but the source of truth remains in control. * **Don't Do This:** Don't allow views to directly modify the state of unrelated views. Use bindings, observable objects, or environment objects to manage data flow. ### 6.2 State Restoration * **Do This:** When the app returns to the foreground, restore the app to the state when the app was moved to the background. * **Don't Do This:** If the app did not implement state restoration, the user may start back at the beginning. **Example:** """swift import SwiftUI struct ContentView: View { @State private var counter: Int = 0 var body: some View { VStack { Text("Counter: \(counter)") Button("Increment") { counter += 1 } } .onAppear { // Load saved state if let savedCounter = UserDefaults.standard.value(forKey: "counter") as? Int { counter = savedCounter } } .onDisappear { // Save state UserDefaults.standard.set(counter, forKey: "counter") } .padding() } } """ * **Why:** Unidirectional data flow enhances predictability and simplifies debugging since changes originate from a single, well-defined location. ## 7. Combining State Management Techniques Many SwiftUI applications require a mix of state management techniques to handle different scenarios and complexities. It is important to choose each technique wisely. ### 7.1. Example: Using "@State", "@ObservedObject", and "@EnvironmentObject" """swift import SwiftUI import Combine // Global app settings class AppSettings: ObservableObject { @Published var theme: String = "Light" } // Data model for a specific view class ItemViewModel: ObservableObject { @Published var isSelected: Bool = false let itemName: String init(itemName: String) { self.itemName = itemName } } struct ItemView: View { @ObservedObject var item: ItemViewModel @State private var quantity: Int = 1 var body: some View { HStack { Text("\(item.itemName) (Quantity: \(quantity))") Spacer() Button(action: { quantity += 1 }) { Image(systemName: "plus") } Toggle(isOn: $item.isSelected) { Text("Select") } } .padding() } } struct ContentView: View { @EnvironmentObject var appSettings: AppSettings @StateObject var settings = AppSettings() @State private var numberOfItems: Int = 5 let items = [ItemViewModel(itemName: "Item 1"), ItemViewModel(itemName: "Item 2"), ItemViewModel(itemName: "Item 3")] var body: some View { VStack { Text("Theme: \(appSettings.theme)") TextField("Number of Items", value: $numberOfItems, format: .number) .padding() ForEach(items) { item in ItemView(item: item) } } .padding() } } @main struct MyApp: App { @StateObject var appSettings = AppSettings() var body: some Scene { WindowGroup { ContentView() .environmentObject(appSettings) } } } """ In this example: * "AppSettings" is shared globally as an "@EnvironmentObject". * "ItemViewModel" is a view-specific data model observed using "@ObservedObject". * "quantity" within "ItemView" is managed locally using "@State". "ContentView" uses the "@EnvironmentObject" to display theme info. ### 7.2 When to use what? In summary, here’s a quick guide to choosing the right state management tool in SwiftUI: * **Use "@State"** for: Simple, localized state (simple types) in a view. It’s the easiest way to manage basic UI state (e.g., managing whether a button is toggled). * **Use "@Binding"** for: Sharing state and data between parent and child views (especially for two-way data flow). Commonly used for passing data to custom controls or UI components. * **Use "@StateObject"** for: Managing the lifecycle and state of an entire class instance (particularly when you create an instance inside a view). The state persists the lifetime of a view instance. Useful when you want to encapsulate an object graph within a view. *Only initialize the model once.* * **Use "@ObservedObject"** for: Observing state in external objects (i.e., objects you did *not* initialize in the view). It’s used when an external source owns the data, such as another view or a parent object. The state updates when the observed object changes but *does not* control the object's creation and lifecycle. * **Use "@EnvironmentObject"** for: Sharing data between views without prop drilling. It’s useful when a particular object or model should be accessible virtually anywhere in a view. ## 8. Performance Considerations Efficient state management is crucial for SwiftUI performance. Frequent, unnecessary view updates can lead to sluggish UI. * **Standard:** Minimize unnecessary view updates by ensuring that only the relevant parts of the UI are redrawn when state changes. * **Why:** Improves app responsiveness and battery life. ### 8.1. "Equatable" Conformance * **Do This:** Make sure your data models conform to the "Equatable" protocol when appropriate. SwiftUI can use this to avoid redrawing views when the underlying data hasn't actually changed. Also consider using "==" to check if the data is equal. * **Don't Do This:** Avoid relying solely on SwiftUI's identity comparison, as it can sometimes lead to unnecessary view updates. ### 8.3 "onChange" Modifier (SwiftUI 14 and later) * **Do This:** Use the ".onChange" modifier in SwiftUI 14 to respond to changes in specific state variables and perform specific actions. * **Don't Do This:** Redraw the whole view body when changes arise, leading to performance issues. **Example** """swift import SwiftUI struct MyView: View { @State private var textValue: String = "" var body: some View { TextField("Enter text:", text: $textValue) .onChange(of: textValue) { newValue in print("Text changed to: \(newValue)") // Actions here only executed when textValue changes! } } } """ ### 8.4. "withAnimation" * **Do This**: Use "withAnimation" when you want to animate the change of a SwiftUI View. * **Don't Do This**: Leaving out "withAnimation" will make it a difficult user experience. **Example** """swift import SwiftUI struct RectangleExample: View { @State private var changeRectangle: Bool = false var body: some View { RoundedRectangle(cornerRadius: changeRectangle ? 20 : 10) .fill(changeRectangle ? .red : .blue) .frame(width: changeRectangle ? 200 : 100, height: changeRectangle ? 300: 100) .rotationEffect(Angle(degrees: changeRectangle ? 360 : 0)) .animation(.easeInOut, value: changeRectangle) // Older method .onTapGesture { withAnimation(.spring()) { changeRectangle.toggle() } } } } """ * **Why:** The "withAnimation" modifier allows for smooth transitions instead of jarring ones. ## 9. Testing Testing is crucial for verifying the correctness and reliability of SwiftUI applications. * **Standard:** Write unit tests that validate state changes occur as expected and UI updates correctly in response to those changes. Use SwiftUI previews for visual validation. * **Why:** Ensures the application behaves predictably and reduces the likelihood of bugs. Especially important for state management, where improper handling can lead to many hard-to-debug bugs. ### 9.1. Unit Testing Observable Objects * **Do This:** Mock dependencies and external data sources when testing observable objects to ensure that the tests are isolated and deterministic. * **Don't Do This:** Rely on live data or external services in unit tests, as this can lead to flaky tests that are difficult to reproduce. ## 10. Security Considerations While not always directly related to state management, security should be considered when handling user-sensitive data. * **Standard:** Avoid storing sensitive information in "@AppStorage" or other persistent storage mechanisms without proper encryption. Protect user data by following security best practices. * **Why:** Prevents unauthorized access to sensitive information. ## 11. Conclusion Effective state management is essential for building robust and maintainable SwiftUI applications. By following these coding standards, developers can create code that is easier to understand, debug, and scale. SwiftUI continues to evolve, so it's important to stay up-to-date with the latest best practices and techniques.
# Performance Optimization Standards for SwiftUI This document outlines coding standards focused solely on performance optimization in SwiftUI applications. These standards aim to improve application speed, responsiveness, and resource usage by leveraging the latest SwiftUI features and best practices. This document will be used as context for AI coding assistants. ## 1. Data Management and State Updates Efficient data management and state updates are crucial for SwiftUI performance. Unnecessary updates trigger view re-renders, leading to performance bottlenecks. ### 1.1. Use "@State", "@Binding", "@ObservedObject", "@StateObject", and "@EnvironmentObject" Appropriately * **Standard:** Choose the correct state management property wrapper based on the scope and lifetime of the data. * **Why:** Incorrect usage leads to unnecessary view updates and potential memory leaks. * **Do This:** * Use "@State" for simple, single-view state. * Use "@Binding" to create a two-way connection to a state owned higher up in the view hierarchy. * Use "@ObservedObject" for external, mutable reference type objects that are already created elsewhere. * Use "@StateObject" for creating and managing the lifecycle of external, mutable reference type objects tied to the view's lifetime. Avoid re-creation when SwiftUI re-renders a "View". * Use "@EnvironmentObject" inject shared data into the view hierarchy. * **Don't Do This:** * Use "@ObservedObject" for creating and managing the lifecycle of an object – prefer "@StateObject". * Pass large data structures directly as "@State" or "@Binding" unless strictly necessary. Consider "EnvironmentObject" or "ObservedObject" (while managing the object instantiation carefully) for shared data that is expensive to copy. * **Code Example:** """swift import SwiftUI class UserSettings: ObservableObject { @Published var volume: Double = 0.5 } struct ContentView: View { @StateObject var settings = UserSettings() var body: some View { VStack { Slider(value: $settings.volume, in: 0...1) // Binding to settings property DetailView() } .environmentObject(settings) // Inject settings into the environment } } struct DetailView: View { @EnvironmentObject var settings: UserSettings // Consume settings from the environment var body: some View { Text("Volume: \(settings.volume)") } } """ ### 1.2. Minimize View Updates with "Equatable" and "Identifiable" * **Standard:** Make data models "Equatable" and use "Identifiable" for collections in "ForEach" loops. * **Why:** SwiftUI compares data before re-rendering. Implementing these protocols allows SwiftUI to optimize updates by only re-rendering views based on actual changes. * **Do This:** """swift import SwiftUI struct Task: Identifiable, Equatable { let id = UUID() var title: String var isCompleted: Bool static func == (lhs: Task, rhs: Task) -> Bool { return lhs.id == rhs.id && lhs.title == rhs.title && lhs.isCompleted == rhs.isCompleted } } struct TaskListView: View { @State var tasks: [Task] = [ Task(title: "Buy groceries", isCompleted: false), Task(title: "Walk the dog", isCompleted: true) ] var body: some View { List { ForEach(tasks) { task in TaskRow(task: task) .id(task.id) // Explicit ID assignment for ForEach optimization } } } } struct TaskRow: View { let task: Task var body: some View { HStack { Text(task.title) Spacer() Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") } } } """ * **Don't Do This:** * Omit "Equatable" conformance for data models, forcing SwiftUI to assume all changes trigger re-renders. * Use "ForEach(0..<tasks.count)" without "id: \.self" and no explicit id assignment in the displayed view. This forces list views to refresh all items with any change. ### 1.3. Use "onChange" Correctly * **Standard:** Use the "onChange" modifier to react efficiently to state changes. * **Why:** "onChange" provides a controlled way to execute code only when a specific value changes. * **Do This:** """swift import SwiftUI struct ContentView: View { @State private var text: String = "" @State private var processedText: String = "" var body: some View { VStack { TextField("Enter text", text: $text) .padding() Text("Processed Text: \(processedText)") .padding() } .onChange(of: text) { newValue in // Perform an expensive operation only when 'text' changes processedText = expensiveTextProcessing(text: newValue) } } func expensiveTextProcessing(text: String) -> String { // Simulate an expensive operation Thread.sleep(forTimeInterval: 0.5) return text.uppercased() } } """ * **Don't Do This:** * Perform operations directly within the "body" that should have been done using ".onChange". This will rerun any time the view is re-rendered even if the data wasn't updated. * Abuse "onChange" by performing computationally expensive operations that could be handled on a background thread. ### 1.4. Debouncing State Updates * **Standard:** Use debouncing for state updates from rapidly changing inputs (e.g., search bars). * **Why:** Minimizes the number of state update calls, reducing view re-renders. * **Do This:** """swift import SwiftUI import Combine class SearchViewModel: ObservableObject { @Published var searchText: String = "" @Published var results: [String] = [] private var cancellables: Set<AnyCancellable> = [] init() { $searchText .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .removeDuplicates() .sink { [weak self] term in self?.performSearch(term: term) } .store(in: &cancellables) } func performSearch(term: String) { // Simulate network request or data processing DispatchQueue.global().async { Thread.sleep(forTimeInterval: 0.5) let fakeResults = (1...5).map { "\(term)-\($0)" } DispatchQueue.main.async { self.results = fakeResults } } } } struct SearchView: View { @StateObject var viewModel = SearchViewModel() var body: some View { VStack { TextField("Search", text: $viewModel.searchText) .padding() List(viewModel.results, id: \.self) { result in Text(result) } } } } """ * **Don't Do This:** Directly make network calls or complex operations from TextField input changes without debouncing. UI will lag due to rate limiting. ## 2. View Composition and Rendering Optimizing how views are composed and rendered significantly impacts performance. ### 2.1. Use "Group" and "ViewBuilder" to Reduce View Hierarchy Depth * **Standard:** Use "Group" and "@ViewBuilder" to flatten view hierarchies without introducing extra overhead. * **Why:** Deep view hierarchies impact rendering performance. These constructs help structure code while minimizing the impact on the view tree. * **Do This:** """swift import SwiftUI struct ComplexView: View { var body: some View { VStack { HeaderView() ContentView() FooterView() } } } struct HeaderView: View { var body: some View { Text("Header") .font(.largeTitle) .padding() } } struct ContentView: View { var body: some View { VStack { // Wrapping content to satisfy "one view" requirement Text("Content Part 1") Text("Content Part 2") } .padding() } } struct FooterView: View { var body: some View { Text("Footer") .font(.footnote) .padding() } } """ * **Don't Do This:** Over-nest views unnecessarily, creating deeply nested hierarchies that can be avoided with grouping or ViewBuilder. ### 2.2. Avoid Complex Logic in "body" * **Standard:** Move complex logic and computations out of the "body" property into separate functions or computed properties. * **Why:** The "body" should primarily describe the view structure. Complex logic makes the "body" harder to read and can lead to performance issues when the view is re-rendered. * **Do This:** """swift import SwiftUI struct ProductView: View { let product: Product var formattedPrice: String { return String(format: "$%.2f", product.price) // Move formatting logic outside body } var body: some View { VStack { Text(product.name) .font(.title) Text(formattedPrice) .font(.headline) } } } struct Product { let name: String let price: Double } """ * **Don't Do This:** Perform complex calculations directly within the "body". This will severely impact performance as it will run frequently. ### 2.3. Use "LazyVStack" and "LazyHStack" for Large Lists * **Standard:** Employ "LazyVStack" and "LazyHStack" for scrollable lists with many items. * **Why:** These lazy stacks load views only as they come into view, improving initial load time and memory usage for very large datasets. * **Do This:** """swift import SwiftUI struct ItemListView: View { let items = Array(1...1000).map { "Item \($0)" } // Simulate a large dataset var body: some View { ScrollView { LazyVStack { ForEach(items, id: \.self) { item in Text(item) .padding() Divider() } } } } } """ * **Don't Do This:** Use "VStack" or "HStack" for large lists, as they load all views at once, degrading performance. ### 2.4. Utilize "redacted(reason:)" and "shimmering()" for Placeholder UI * **Standard:** Employ the "redacted(reason:)" and custom "shimmering()" effect modifiers to create placeholder UI while content loads. * **Why:** Improves the user experience when loading data by indicating to the user that the data is on its way instead of showing a blank page. * **Do This:** """swift import SwiftUI struct RedactedView: View { @State private var isLoading = true var body: some View { VStack { Text("This is redacted while loading") .redacted(reason: isLoading ? .placeholder : []) Text("And this will Shimmer") .shimmering() Button("Toggle Loading") { isLoading.toggle() } } } } extension View { func shimmering(duration: Double = 1.5) -> some View { modifier(Shimmer(duration: duration)) } } struct Shimmer: ViewModifier { let duration: Double @State private var shimmering = false func body(content: Content) -> some View { content .opacity(shimmering ? 0.5 : 1) .animation(Animation.linear(duration: duration).repeatForever(autoreverses: true), value: shimmering) .onAppear { shimmering = true } } } """ * **Don't Do This:** Show a blank screen without any indication of loading while fetching data. ## 3. Image Handling Efficient image handling is essential for preventing memory issues and improving load times. ### 3.1. Use "AsyncImage" for Remote Images * **Standard:** Use "AsyncImage" for fetching and displaying remote images. * **Why:** "AsyncImage" handles image loading asynchronously and provides built-in caching, reducing the need for manual image loading and caching logic and avoiding blocking the main thread. * **Do This:** """swift import SwiftUI struct RemoteImageView: View { let url: URL var body: some View { AsyncImage(url: url) { phase in switch phase { case .empty: ProgressView() case .success(let image): image .resizable() .scaledToFit() case .failure: Image(systemName: "xmark.circle.fill") .foregroundColor(.red) @unknown default: EmptyView() } } } } """ * **Don't Do This:** Manually load images using "UIImage(data:)" on the main thread, blocking the UI, or implement custom caching solutions when "AsyncImage" provides these features. ### 3.2. Optimize Image Sizes * **Standard:** Resize images to the appropriate dimensions before displaying them. * **Why:** Displaying large images at smaller sizes wastes memory and processing power. Right-sizing your images for display results in better performance and memory profile for free.. * **Do This:** Use asset catalogs to provide different image resolutions for different devices. Alternatively, use code to resize images. * **Example using ".resizable()" and ".scaledToFit()" """swift import SwiftUI struct OptimizedImageView: View { let imageName: String var body: some View { Image(imageName) .resizable() .scaledToFit() .frame(width: 100, height: 100) // Adjust dimensions as needed } } """ ### 3.3. Image Caching Strategies * **Standard:** Implement a caching strategy for frequently accessed images to avoid repeated downloads. * **Why:** Reduces network overhead and improves the user experience by providing faster image loading. "AsyncImage" has default caching. For more control, you can use a custom caching solution. * **Example using URLCache:** """swift import SwiftUI class ImageCache { private let cache = URLCache.shared func cachedImage(url: URL) -> UIImage? { let request = URLRequest(url: url) if let cachedResponse = cache.cachedResponse(for: request) { return UIImage(data: cachedResponse.data) } return nil } func cacheImage(url: URL, image: UIImage) { guard let data = image.jpegData(compressionQuality: 0.8) else { return } let response = URLResponse(url: url, mimeType: "image/jpeg", expectedContentLength: data.count, textEncodingName: nil) let cachedResponse = CachedURLResponse(response: response, data: data) let request = URLRequest(url: url) cache.storeCachedResponse(cachedResponse, for: request) } } """ ## 4. Background Tasks and Concurrency Offloading long-running tasks to background threads is crucial for maintaining UI responsiveness. ### 4.1. Use "DispatchQueue" for Background Tasks * **Standard:** Perform network requests, data processing, and other long-running tasks on background threads using "DispatchQueue". * **Why:** Prevents blocking the main thread, ensuring the UI remains responsive. * **Do This:** """swift import SwiftUI struct ContentView: View { @State private var data: String = "Loading..." var body: some View { Text(data) .onAppear { loadData() } } func loadData() { DispatchQueue.global().async { // Simulate a long-running task Thread.sleep(forTimeInterval: 2) let loadedData = "Data loaded from background" DispatchQueue.main.async { data = loadedData // Update UI on the main thread } } } } """ * **Don't Do This:** Perform time intensive operations on the main thread directly. This will freeze the UI. ### 4.2. Leverage "async" / "await" * **Standard:** Employ Swift's "async" and "await" for asynchronous operations to simplify asynchronous code. * **Why:** Results in more readable and maintainable asynchronous code than traditional completion handlers. * **Do This:** """swift import SwiftUI struct ContentView: View { @State private var data: String = "Loading..." var body: some View { Text(data) .onAppear { Task { await loadData() } } } func loadData() async { // Simulate a long-running task do { let loadedData = try await fetchData() data = loadedData // Update UI on the main thread } catch { data = "Error loading data" } } func fetchData() async throws -> String { try await Task.sleep(nanoseconds: 2_000_000_000) // Simulate async delay return "Data loaded asynchronously" } } """ * **Don't Do This:** Nest completion handlers excessively, leading to "callback hell." Favor "async" / "await". ### 4.3. Be Mindful of Thread Safety * **Standard:** Ensure thread safety when accessing and modifying shared data from multiple threads. * **Why:** Prevents race conditions and data corruption, ensuring data consistency. * **Do This:** Use "DispatchQueue.main.async" to update UI elements. Use actors or locks for data mutation. * **Example using an Actor:** """swift import SwiftUI actor Counter { private var count = 0 func increment() -> Int { count += 1 return count } func getCount() -> Int { return count } } struct ContentView: View { @State private var count: Int = 0 let counter = Counter() var body: some View { Text("Count: \(count)") .onAppear { Task { for _ in 1...1000 { let newCount = await counter.increment() // Await actor function DispatchQueue.main.async { count = newCount } } } } } } """ * **Don't Do This:** Directly modify UI elements or shared data from background threads without proper synchronization. ## 5. Performance Profiling and Debugging Regular profiling and debugging are essential for identifying and resolving performance bottlenecks. ### 5.1. Use Instruments for Performance Analysis * **Standard:** Use Instruments to profile the application and identify performance bottlenecks. * **Why:** Instruments provides detailed insights into CPU usage, memory allocation, and drawing performance. * **Do This:** * Use the Time Profiler to identify CPU-intensive tasks. * Use the Allocations instrument to track memory usage and identify memory leaks. * Use the Core Animation instrument to analyze rendering performance and identify drawing bottlenecks. * **Don't Do This:** Neglect Instruments and rely solely on guesswork for performance optimization. ### 5.2. Use SwiftUI Previews Efficiently * **Standard:** Use SwiftUI previews for rapid iteration but limit the scope of previews to individual components. * **Why:** Overly complex previews slow down the development process. * **Do This:** * Create focused previews for individual views. * Use conditional compilation ("#if DEBUG") to include mock data or simplified logic in previews. * **Don't Do This:** Create all-encompassing previews that attempt to render the entire application. ### 5.3. Implement Logging and Monitoring * **Standard:** Add logging and monitoring to track performance metrics in production. * **Why:** Provides insights into performance issues users are experiencing and helps prioritize optimization efforts. * **Do This:** * Use "os_log" for structured logging. * Integrate with analytics platforms to track performance metrics. * Implement custom performance monitors to track specific areas of the application. * **Don't Do This:** Rely solely on manual testing and neglect production performance monitoring. By adhering to these performance optimization standards, developers can create SwiftUI applications that are not only visually appealing but also performant and responsive, providing a better user experience.
# API Integration Standards for SwiftUI This document outlines coding standards for integrating APIs into SwiftUI applications. It focuses on patterns, best practices, and modern approaches for creating robust, maintainable, and performant applications that consume backend services and external APIs. ## 1. Architectural Considerations for API Integration ### 1.1. Model-View-ViewModel (MVVM) with Repository Pattern **Standard:** Employ the MVVM architecture with a Repository pattern to decouple the SwiftUI view layer from the data access layer. * **Do This:** Isolate data fetching and processing logic within dedicated "Repository" classes. * **Don't Do This:** Directly perform API calls within SwiftUI views or view models. **Why:** * **Maintainability:** Decoupling data fetching makes the code easier to test, modify, and understand. * **Testability:** Repositories can be easily mocked for unit testing view models. * **Reusability:** Repositories can be used across multiple view models and even different parts of the application. **Code Example:** """swift // Data Model struct Article: Identifiable, Decodable { let id: Int let title: String let body: String } // Repository Protocol protocol ArticleRepository { func fetchArticles() async throws -> [Article] } // Concrete Repository Implementation class ArticleRepositoryImpl: ArticleRepository { private let baseURL = "https://example.com/api" private let session: URLSession init(session: URLSession = .shared) { self.session = session } func fetchArticles() async throws -> [Article] { guard let url = URL(string: "\(baseURL)/articles") else { throw APIError.invalidURL } let (data, response) = try await session.data(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw APIError.invalidResponse } do { let decoder = JSONDecoder() return try decoder.decode([Article].self, from: data) } catch { throw APIError.decodingError(error) } } // Define custom API errors for better error handling enum APIError: Error { case invalidURL case invalidResponse case decodingError(Error) } } // Example of Unit Testing a Repository (using XCTest) #if DEBUG class MockArticleRepository: ArticleRepository { var articles: [Article] = [ Article(id: 1, title: "Test Article 1", body: "This is a test article."), Article(id: 2, title: "Test Article 2", body: "This is another test article.") ] func fetchArticles() async throws -> [Article] { return articles } } import XCTest class ArticleRepositoryTests: XCTestCase { func testFetchArticlesSuccess() async throws { // Mock URLSession for controlled testing let mockData = "[{\"id\":1,\"title\":\"Test Article\",\"body\":\"Test Body\"}]".data(using: .utf8)! let mockResponse = HTTPURLResponse(url: URL(string: "https://example.com/api/articles")!, statusCode: 200, httpVersion: nil, headerFields: nil)! let mockSession = MockURLSession(data: mockData, response: mockResponse, error: nil) let repository = ArticleRepositoryImpl(session: mockSession) let articles = try await repository.fetchArticles() XCTAssertEqual(articles.count, 1) XCTAssertEqual(articles[0].title, "Test Article") } func testFetchArticlesInvalidURL() async throws { let repository = ArticleRepositoryImpl(session: .shared) do { _ = try await repository.fetchArticles() // This will trigger an exception later on with the real API. XCTFail("Expected an error to be thrown.") } catch { XCTAssertTrue(error is ArticleRepositoryImpl.APIError) } } } // Mock URLSession Class for testing class MockURLSession: URLSession { var data: Data? var response: URLResponse? var error: Error? init(data: Data?, response: URLResponse?, error: Error?) { self.data = data self.response = response self.error = error } override func data(from url: URL) async throws -> (Data, URLResponse) { if let error = error { throw error } return (data!, response!) } } #endif // View Model class ArticleViewModel: ObservableObject { @Published var articles: [Article] = [] @Published var isLoading: Bool = false @Published var errorMessage: String? private let articleRepository: ArticleRepository init(articleRepository: ArticleRepository) { self.articleRepository = articleRepository } func fetchArticles() { isLoading = true errorMessage = nil Task { do { let fetchedArticles = try await articleRepository.fetchArticles() await MainActor.run { self.articles = fetchedArticles self.isLoading = false } } catch { await MainActor.run { self.errorMessage = "Failed to fetch articles: \(error.localizedDescription)" self.isLoading = false } } } } } // SwiftUI View struct ArticleListView: View { @ObservedObject var viewModel: ArticleViewModel var body: some View { VStack { if viewModel.isLoading { ProgressView("Loading Articles...") } else if let errorMessage = viewModel.errorMessage { Text("Error: \(errorMessage)") .foregroundColor(.red) } else { List(viewModel.articles) { article in Text(article.title) } } } .onAppear { viewModel.fetchArticles() } } } """ ### 1.2. Dependency Injection **Standard:** Use dependency injection to provide the "Repository" instance to the "ViewModel". * **Do This:** Initialize the "ViewModel" with a "Repository" instance passed as a parameter. * **Don't Do This:** Create the "Repository" instance directly within the "ViewModel". **Why:** * **Testability:** Allows you to inject mock repositories for testing purposes. * **Flexibility:** Enables swapping different repository implementations (e.g., local vs. remote) without modifying the view model. **Code Example:** """swift // In the SwiftUI View: struct ArticleListView: View { @ObservedObject var viewModel: ArticleViewModel init(viewModel: ArticleViewModel) { self.viewModel = viewModel } var body: some View { // ... (rest of the view implementation) } } // Usage: let repository = ArticleRepositoryImpl() let viewModel = ArticleViewModel(articleRepository: repository) let contentView = ArticleListView(viewModel: viewModel) """ ### 1.3. Error Handling **Standard:** Implement comprehensive error handling that covers the entire API integration process. * **Do This:** Use a custom "APIError" enum to represent specific error scenarios (e.g., invalid URL, network error, decoding error). Handle errors gracefully in the view model and display meaningful error messages to the user. * **Don't Do This:** Ignore errors or display generic error messages. **Why:** * **User Experience:** Provides informative feedback to the user when something goes wrong. * **Debugging:** Simplifies debugging by providing specific error information. **Code Example:** (See the "ArticleRepositoryImpl" class in section 1.1 for an example of a custom "APIError" enum). ## 2. API Request Standards ### 2.1. URLSession Configuration **Standard:** Configure "URLSession" with appropriate timeouts and caching policies. * **Do This:** Use a custom "URLSessionConfiguration" to set "timeoutIntervalForRequest" and "urlCache". * **Don't Do This:** Use the default "URLSession.shared" without any configuration for production environments. **Why:** * **Performance:** Improves application responsiveness by setting reasonable timeouts. * **Efficiency:** Reduces network traffic by leveraging URL caching. **Code Example:** """swift let configuration = URLSessionConfiguration.default configuration.timeoutIntervalForRequest = 15 // seconds configuration.urlCache = URLCache.shared let session = URLSession(configuration: configuration) let repository = ArticleRepositoryImpl(session: session) """ ### 2.2. Request Methods **Standard:** Use the appropriate HTTP request methods (GET, POST, PUT, DELETE) according to RESTful principles. * **Do This:** Use "GET" for retrieving data, "POST" for creating new resources, "PUT" for updating existing resources, and "DELETE" for deleting resources. * **Don't Do This:** Use "GET" requests to perform actions that modify data. ### 2.3. Request Headers **Standard:** Include necessary request headers such as "Content-Type" and "Authorization". * **Do This:** Set the "Content-Type" header to "application/json" when sending JSON data. Use the "Authorization" header for authentication. * **Don't Do This:** Expose sensitive information in request headers unnecessarily. **Code Example:** """swift func createNewArticle(article: Article) async throws { guard let url = URL(string: "\(baseURL)/articles") else { throw APIError.invalidURL } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") // Example: Adding an authorization header if required request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization") let encoder = JSONEncoder() request.httpBody = try encoder.encode(article) let (_, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 201 else { // 201 Created is the expected response for a successful POST throw APIError.invalidResponse } // Handle successful creation (e.g., parse response if needed) } """ ### 2.4. Asynchronous Operations **Standard:** Always perform API requests asynchronously to avoid blocking the main thread. * **Do This:** Use "async/await" or Combine to handle asynchronous API requests. Update the UI on the main thread using "@MainActor". * **Don't Do This:** Perform synchronous API requests on the main thread. ## 3. Data Handling Standards ### 3.1. JSON Parsing **Standard:** Use "JSONDecoder" and "JSONEncoder" to parse and serialize JSON data. * **Do This:** Define data models using "Codable" conformance. Handle potential decoding errors gracefully. * **Don't Do This:** Manually parse JSON data using "JSONSerialization". **Code Example:** (See the "Article" struct and the JSON decoding logic in "ArticleRepositoryImpl" in section 1.1). ### 3.2. Data Validation **Standard:** Validate API responses before using the data in your application. * **Do This:** Check for required fields and data types. Handle unexpected data formats gracefully. * **Don't Do This:** Assume that the API response always contains the expected data. **Code Example:** """swift struct User: Decodable { let id: Int let name: String? // Make optional as the API might return null let email: String // Custom decoding to handle potential nil values or incorrect types init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(Int.self, forKey: .id) name = try container.decodeIfPresent(String.self, forKey: .name) // Use decodeIfPresent email = try container.decode(String.self, forKey: .email) } enum CodingKeys: String, CodingKey { case id, name, email } } """ ### 3.3. Secure Data Storage **Standard:** Use secure storage mechanisms for sensitive data such as API keys and user credentials. * **Do This:** Use the Keychain to store sensitive data. * **Don't Do This:** Store sensitive data in plain text in code or in "UserDefaults". **Code Example:** (Implementation of the Keychain storage is beyond the scope of this document, but there are numerous libraries available for this purpose, such as "SwiftKeychainWrapper"). ## 4. SwiftUI-Specific Considerations ### 4.1. State Management **Standard:** Leverage SwiftUI's state management features ("@State", "@ObservedObject", "@EnvironmentObject") to manage data flow between the view model and the view. * **Do This:** Use "@ObservedObject" for the view model, "@State" for local view state, and "@EnvironmentObject" for shared data across the application. * **Don't Do This:** Directly modify the view model's state from the view. **Code Example:** (See the "ArticleListView" in section 1.1 for an example of using "@ObservedObject"). ### 4.2. Data Binding **Standard:** Use data binding to keep the UI synchronized with the view model. * **Do This:** Use "@State" variables and "TextField" bindings to update the view model when the user interacts with the UI. * **Don't Do This:** Manually update the UI elements when the view model changes. **Code Example:** """swift struct EditArticleView: View { @ObservedObject var viewModel: ArticleViewModel @State private var articleTitle: String // local state for editing var body: some View { VStack { TextField("Title", text: $articleTitle) // Binding to local state .padding() Button("Update Title") { viewModel.updateArticleTitle(newTitle: articleTitle) // Call VM to update } }.onAppear { // Initialize the local state articleTitle = viewModel.currentArticle.title } } } class ArticleViewModel: ObservableObject { @Published var currentArticle: Article // ... other code init(article: Article) { self.currentArticle = article } func updateArticleTitle(newTitle: String) { // Business logic to update the article title // Potentially make an API call to persist update currentArticle.title = newTitle } } """ ### 4.3. List Performance **Standard:** Optimize the performance of lists that display data fetched from APIs. * **Do This:** Use "Identifiable" protocol conformance to provide stable identities for list items. Use "LazyVStack" and "LazyHStack" for large lists. Employ pagination where appropriate. * **Don't Do This:** Display large lists without any optimization. **Code Example:** """swift // Ensure your data Model conforms to Identifiable struct Article: Identifiable, Decodable { let id: Int let title: String let body: String } // Using LazyVStack for efficient rendering of a large list: struct ArticleListView: View { @ObservedObject var viewModel: ArticleViewModel var body: some View { ScrollView { // Wrap in ScrollView for lazy loading LazyVStack { ForEach(viewModel.articles) { article in ArticleRow(article: article) } } } .onAppear { viewModel.fetchArticles() } } } """ ### 4.4. Handling Loading States **Standard:** Provide clear visual feedback to the user while data is loading from the API. * **Do This:** Display a "ProgressView" while loading data. Disable UI elements that are not available until the data is loaded. Use a "placeholder" view. * **Don't Do This:** Leave the user in the dark while data is loading. **Code Example:** (See the "ArticleListView" in section 1.1 for an example of displaying a "ProgressView"). ## 5. Security Best Practices ### 5.1. HTTPS **Standard:** Always use HTTPS for all API requests. * **Do This:** Ensure that all API endpoints use HTTPS. * **Don't Do This:** Use HTTP for sensitive data or authentication. ### 5.2. Input Validation **Standard:** Validate all user input before sending it to the API. * **Do This:** Check for invalid characters, malicious code, and potential security vulnerabilities. * **Don't Do This:** Trust user input without validation. ### 5.3. Output Encoding **Standard:** Encode all data received from the API before displaying it in the UI. * **Do This:** Use appropriate encoding techniques to prevent cross-site scripting (XSS) and other security vulnerabilities. * **Don't Do This:** Display raw data from the API without encoding. ## 6. Testing ### 6.1. Unit Tests **Standard:** Write unit tests for the "Repository" and "ViewModel" layers. * **Do This:** Mock the "URLSession" or the "Repository" to isolate the code under test. Verify that the code behaves as expected in different scenarios (e.g., successful response, error response, invalid data). * **Don't Do This:** Skip unit tests for the data access and business logic layers. ### 6.2. UI Tests **Standard:** Write UI tests to verify that the API integration works correctly from the user's perspective. * **Do This:** Simulate user interactions and verify that the UI displays the correct data. Mock API responses to test different scenarios. * **Don't Do This:** Rely solely on manual testing to verify the API integration. ## 7. Common Anti-Patterns ### 7.1. God Object View Models **Anti-Pattern:** Creating view models that handle too many responsibilities, including API requests, data parsing, and UI updates. **Solution:** Follow the MVVM pattern strictly. Delegate data fetching to a dedicated "Repository". ### 7.2. Massive View Controllers (in UIKit interop) **Anti-Pattern:** Placing all the logic for API integration directly inside SwiftUI views, or if using UIKit, inside UIViewControllers. **Solution:** Adhere to MVVM. Move API calls and data processing into the view model and repository. ### 7.3. Ignoring Errors **Anti-Pattern:** Not handling errors properly and displaying generic error messages to the user. **Solution:** Implement comprehensive error handling with custom "APIError" types and display informative error messages. ## 8. Performance Optimization ### 8.1. Caching Strategies **Standard:** Implement layers of caching from HTTP Cache, Memory Cache to Disk Cache depends on the data criticality. * **Do This:** Apply "URLCache" for HTTP caching. Store in memory frequently using data with "NSCache" * **Don't Do This:** Neglect using cache or aggressively cache information when the data is highly dynamic. ### 8.2. Image Loading Optimizations **Standard:** Optimize image loading, especially if you're retrieving images from an API. * **Do This**: Utilize "AsyncImage" which automatically handles the loading and caching. * **Don't Do This**: Load images synchrounously directly in the main thread. """swift import SwiftUI struct AsyncImageView: View { let imageURL: String var body: some View { AsyncImage(url: URL(string: imageURL)) { phase in switch phase { case .empty: ProgressView() // Show a progress indicator case .success(let image): image .resizable() .scaledToFit() case .failure: Image(systemName: "photo") // Show a placeholder image for errors .foregroundColor(.red) @unknown default: EmptyView() } } } } """ ## 9. Conclusion By following these coding standards, you can create robust, maintainable, and performant SwiftUI applications that integrate with APIs effectively. This will result in a better user experience, easier debugging, and more efficient development process. Remember to regularly review and update these standards as SwiftUI evolves and new best practices emerge.