swift-ios-development
SKILL.md
Swift iOS Development Standards
Professional guidelines for building production-ready iOS applications with Swift and SwiftUI.
Core Principles
- Swift Best Practices: Modern Swift syntax, protocols, and type safety
- MVVM Architecture: Separation of UI (View), logic (ViewModel), and data (Model)
- SwiftUI Declarative: Leverage SwiftUI's declarative paradigm
- Async/Await: Use modern concurrency for network and async operations
- Error Handling: Comprehensive error handling with proper user feedback
Standard Project Structure
YourApp/
├── App/
│ └── YourAppApp.swift # App entry point
├── Models/ # Data models
├── ViewModels/ # Business logic and state
├── Views/ # SwiftUI views
├── Services/ # Networking, persistence, etc.
├── Config/ # Configuration files
└── Utils/ # Helper functions and extensions
MVVM Architecture Pattern
Model (Data Layer)
// Models/User.swift
import Foundation
struct User: Codable, Identifiable {
let id: String
let name: String
let email: String
let createdAt: Date
}
// Response wrapper for API
struct APIResponse<T: Codable>: Codable {
let data: T
}
struct ErrorResponse: Codable {
let error: ErrorDetail
}
struct ErrorDetail: Codable {
let message: String
let code: String?
}
ViewModel (Business Logic)
// ViewModels/UsersViewModel.swift
import Foundation
@MainActor
class UsersViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@Published var errorMessage: String?
private let apiService: APIService
init(apiService: APIService = APIService.shared) {
self.apiService = apiService
}
func fetchUsers() async {
isLoading = true
errorMessage = nil
do {
users = try await apiService.fetchUsers()
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
func deleteUser(_ user: User) async {
do {
try await apiService.deleteUser(id: user.id)
users.removeAll { $0.id == user.id }
} catch {
errorMessage = "Failed to delete user: \(error.localizedDescription)"
}
}
}
View (UI Layer)
// Views/UsersView.swift
import SwiftUI
struct UsersView: View {
@StateObject private var viewModel = UsersViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
ProgressView("Loading users...")
} else if let error = viewModel.errorMessage {
ErrorView(message: error) {
Task { await viewModel.fetchUsers() }
}
} else {
usersList
}
}
.navigationTitle("Users")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Add", systemImage: "plus") {
// Show add user sheet
}
}
}
.task {
await viewModel.fetchUsers()
}
}
}
private var usersList: some View {
List {
ForEach(viewModel.users) { user in
NavigationLink(value: user) {
UserRow(user: user)
}
}
.onDelete { indexSet in
for index in indexSet {
Task {
await viewModel.deleteUser(viewModel.users[index])
}
}
}
}
.navigationDestination(for: User.self) { user in
UserDetailView(user: user)
}
}
}
// Reusable error view
struct ErrorView: View {
let message: String
let retry: () -> Void
var body: some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.largeTitle)
.foregroundStyle(.red)
Text(message)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
Button("Retry", action: retry)
.buttonStyle(.borderedProminent)
}
.padding()
}
}
Networking with APIService
API Service Pattern
// Services/APIService.swift
import Foundation
enum APIError: LocalizedError {
case invalidURL
case invalidResponse
case unauthorized
case serverError(String)
case decodingError
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL"
case .invalidResponse: return "Invalid response from server"
case .unauthorized: return "Please log in again"
case .serverError(let message): return message
case .decodingError: return "Failed to process server response"
}
}
}
class APIService {
static let shared = APIService()
private let baseURL: String
private let session: URLSession
init(baseURL: String = "https://api.example.com",
session: URLSession = .shared) {
self.baseURL = baseURL
self.session = session
}
// MARK: - Generic Request Method
private func request<T: Decodable>(
_ endpoint: String,
method: HTTPMethod = .get,
body: (any Encodable)? = nil,
headers: [String: String] = [:]
) async throws -> T {
guard let url = URL(string: baseURL + endpoint) else {
throw APIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Add authentication token
if let token = await AuthService.shared.getToken() {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
// Add custom headers
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
// Add body
if let body = body {
request.httpBody = try JSONEncoder().encode(body)
}
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
print("Decoding error: \(error)")
throw APIError.decodingError
}
case 401:
throw APIError.unauthorized
default:
if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: data) {
throw APIError.serverError(errorResponse.error.message)
}
throw APIError.serverError("Request failed with status \(httpResponse.statusCode)")
}
}
// MARK: - API Methods
func fetchUsers() async throws -> [User] {
let response: APIResponse<[User]> = try await request("/api/users")
return response.data
}
func createUser(_ user: CreateUserRequest) async throws -> User {
let response: APIResponse<User> = try await request(
"/api/users",
method: .post,
body: user
)
return response.data
}
func updateUser(id: String, _ user: UpdateUserRequest) async throws -> User {
let response: APIResponse<User> = try await request(
"/api/users/\(id)",
method: .patch,
body: user
)
return response.data
}
func deleteUser(id: String) async throws {
let _: EmptyResponse = try await request(
"/api/users/\(id)",
method: .delete
)
}
}
// MARK: - Supporting Types
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
struct EmptyResponse: Decodable {}
struct CreateUserRequest: Encodable {
let name: String
let email: String
}
struct UpdateUserRequest: Encodable {
let name: String?
let email: String?
}
Configuration Management
// Config/APIConfig.swift
import Foundation
enum Environment {
case development
case staging
case production
var baseURL: String {
switch self {
case .development: return "http://localhost:3000"
case .staging: return "https://staging-api.example.com"
case .production: return "https://api.example.com"
}
}
}
struct APIConfig {
static let environment: Environment = {
#if DEBUG
return .development
#else
return .production
#endif
}()
static let baseURL = environment.baseURL
}
SwiftUI Best Practices
View Composition
// Break down large views into smaller components
struct ProductCard: View {
let product: Product
var body: some View {
VStack(alignment: .leading, spacing: 8) {
ProductImage(url: product.imageURL)
ProductInfo(product: product)
ProductPrice(price: product.price)
}
.padding()
.background(.background)
.cornerRadius(12)
.shadow(radius: 2)
}
}
// Use ViewBuilders for complex logic
@ViewBuilder
func statusBadge(for status: Status) -> some View {
switch status {
case .active:
Label("Active", systemImage: "checkmark.circle.fill")
.foregroundStyle(.green)
case .pending:
Label("Pending", systemImage: "clock.fill")
.foregroundStyle(.orange)
case .inactive:
Label("Inactive", systemImage: "xmark.circle.fill")
.foregroundStyle(.gray)
}
}
State Management
// @State for local view state
@State private var isSheetPresented = false
@State private var searchText = ""
// @StateObject for view model ownership
@StateObject private var viewModel = MyViewModel()
// @ObservedObject for passed-in view models
@ObservedObject var viewModel: MyViewModel
// @EnvironmentObject for app-wide state
@EnvironmentObject var appState: AppState
// @AppStorage for UserDefaults
@AppStorage("hasSeenOnboarding") private var hasSeenOnboarding = false
Performance Optimization
// Use Identifiable for list performance
struct Item: Identifiable {
let id: UUID
let name: String
}
// Lazy loading for large lists
LazyVStack {
ForEach(items) { item in
ItemRow(item: item)
}
}
// Task lifecycle
.task {
// Runs when view appears, cancels when disappears
await viewModel.load()
}
// Prevent unnecessary redraws
.equatable() // Only redraw when properties change
Error Handling Patterns
// Custom error types
enum ValidationError: LocalizedError {
case emptyField(String)
case invalidEmail
case passwordTooShort
var errorDescription: String? {
switch self {
case .emptyField(let field):
return "\(field) cannot be empty"
case .invalidEmail:
return "Please enter a valid email address"
case .passwordTooShort:
return "Password must be at least 8 characters"
}
}
}
// Error display in UI
struct FormView: View {
@State private var errorMessage: String?
var body: some View {
Form {
// Form fields...
if let error = errorMessage {
Text(error)
.foregroundStyle(.red)
.font(.caption)
}
}
}
}
Testing Patterns
Unit Testing ViewModels
import XCTest
@testable import YourApp
@MainActor
class UsersViewModelTests: XCTestCase {
var viewModel: UsersViewModel!
var mockAPIService: MockAPIService!
override func setUp() {
super.setUp()
mockAPIService = MockAPIService()
viewModel = UsersViewModel(apiService: mockAPIService)
}
func testFetchUsersSuccess() async {
// Given
let expectedUsers = [
User(id: "1", name: "John", email: "john@example.com", createdAt: Date())
]
mockAPIService.usersToReturn = expectedUsers
// When
await viewModel.fetchUsers()
// Then
XCTAssertEqual(viewModel.users, expectedUsers)
XCTAssertNil(viewModel.errorMessage)
XCTAssertFalse(viewModel.isLoading)
}
func testFetchUsersFailure() async {
// Given
mockAPIService.shouldThrowError = true
// When
await viewModel.fetchUsers()
// Then
XCTAssertTrue(viewModel.users.isEmpty)
XCTAssertNotNil(viewModel.errorMessage)
XCTAssertFalse(viewModel.isLoading)
}
}
class MockAPIService: APIService {
var usersToReturn: [User] = []
var shouldThrowError = false
override func fetchUsers() async throws -> [User] {
if shouldThrowError {
throw APIError.serverError("Mock error")
}
return usersToReturn
}
}
Code Organization Best Practices
- One model/view/viewmodel per file
- Group related files in folders
- Use extensions to organize code by functionality
- Keep files under 300 lines
- Use meaningful variable names
- Add comments only when logic isn't obvious
Swift Naming Conventions
// Types: UpperCamelCase
class UserManager { }
struct Product { }
enum Status { }
// Variables, functions: lowerCamelCase
var userName: String
func fetchUserData() { }
// Constants: lowerCamelCase
let maxRetryCount = 3
// Private properties: prefix with underscore (optional)
private var _internalState: Int
// Boolean names: use is/has/should prefix
var isLoading: Bool
var hasSeenOnboarding: Bool
func shouldShowAlert() -> Bool
Common SwiftUI Modifiers
// Layout
.padding()
.frame(width: 100, height: 50)
.background(.blue)
.cornerRadius(8)
// Navigation
.navigationTitle("Title")
.navigationBarTitleDisplayMode(.inline)
.toolbar { }
// Presentation
.sheet(isPresented: $showSheet) { }
.alert("Title", isPresented: $showAlert) { }
// Lists
.listStyle(.insetGrouped)
.swipeActions { }
.onDelete { }
// Performance
.task { }
.refreshable { }
Security Best Practices
- Never hardcode API keys or secrets
- Use Keychain for sensitive data
- Validate all user input
- Use HTTPS for all network requests
- Implement certificate pinning for production
- Obfuscate sensitive data in logs
- Handle authentication token expiry
Quick Reference
# Run iOS app
Cmd+R in Xcode
# Run tests
Cmd+U in Xcode
# Clean build
Cmd+Shift+K
# Format code
Ctrl+I
Additional Resources
Weekly Installs
3
Repository
miicolas/applyFirst Seen
Feb 13, 2026
Security Audits
Installed on
opencode3
gemini-cli3
antigravity2
replit2
claude-code2
github-copilot2