moai-lang-swift

Swift 6+ development specialist covering SwiftUI, Combine, Swift Concurrency, and iOS patterns. Use when building iOS apps, macOS apps, or Apple platform applications.

$ Installer

git clone https://github.com/junseokandylee/RallyApp /tmp/RallyApp && cp -r /tmp/RallyApp/.claude/skills/moai-lang-swift ~/.claude/skills/RallyApp

// tip: Run this command in your terminal to install the skill


name: moai-lang-swift description: Swift 6+ development specialist covering SwiftUI, Combine, Swift Concurrency, and iOS patterns. Use when building iOS apps, macOS apps, or Apple platform applications. version: 1.0.0 category: language tags: [swift, swiftui, ios, macos, combine, concurrency] context7-libraries: [/apple/swift, /apple/swift-evolution] related-skills: [moai-lang-kotlin, moai-lang-flutter] updated: 2025-12-07 status: active

Quick Reference (30 seconds)

Swift 6+ Development Expert - iOS/macOS with SwiftUI, Combine, and Swift Concurrency.

Auto-Triggers: Swift files (.swift), iOS/macOS projects, Xcode workspaces

Core Capabilities:

  • Swift 6.0: Typed throws, actors, data-race safety by default
  • SwiftUI 6: @Observable, NavigationStack, modern declarative UI
  • Combine: Reactive programming with publishers and subscribers
  • Swift Concurrency: async/await, actors, TaskGroup
  • XCTest: Unit testing, UI testing, async test support
  • Swift Package Manager: Dependency management

Version Requirements:

  • Swift: 6.0+
  • Xcode: 16.0+
  • iOS: 17.0+ (recommended), minimum 15.0
  • macOS: 14.0+ (recommended)

Implementation Guide (5 minutes)

Swift 6.0 Core Features

Typed Throws (Error Type Specification):

enum NetworkError: Error {
    case invalidURL
    case requestFailed(statusCode: Int)
    case decodingFailed
}

func fetchData() throws(NetworkError) -> Data {
    guard let url = URL(string: "https://api.example.com") else {
        throw .invalidURL
    }
    // Implementation
}

// Caller knows exact error types
do {
    let data = try fetchData()
} catch .invalidURL {
    print("Invalid URL")
} catch .requestFailed(let code) {
    print("Request failed: \(code)")
} catch .decodingFailed {
    print("Decoding failed")
}

Complete Concurrency (Data-Race Safety):

// Swift 6 enforces data-race safety by default
actor UserCache {
    private var cache: [String: User] = [:]

    func get(_ id: String) -> User? { cache[id] }
    func set(_ id: String, user: User) { cache[id] = user }
    func clear() { cache.removeAll() }
}

// Sendable conformance required for cross-actor data
struct User: Codable, Identifiable, Sendable {
    let id: String
    let name: String
    let email: String
}

// MainActor for UI-related code
@MainActor
final class UserViewModel: ObservableObject {
    @Published private(set) var user: User?
    private let cache = UserCache()

    func loadUser(_ id: String) async throws {
        if let cached = await cache.get(id) {
            self.user = cached
            return
        }
        let user = try await api.fetchUser(id)
        await cache.set(id, user: user)
        self.user = user
    }
}

SwiftUI 6 Patterns

@Observable Macro (iOS 17+):

import Observation

@Observable
class ProfileViewModel {
    var user: User?
    var isLoading = false
    var errorMessage: String?

    func loadProfile() async {
        isLoading = true
        defer { isLoading = false }

        do {
            user = try await api.fetchCurrentUser()
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

struct ProfileView: View {
    @State private var viewModel = ProfileViewModel()

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.isLoading {
                    ProgressView()
                } else if let user = viewModel.user {
                    UserDetailView(user: user)
                } else if let error = viewModel.errorMessage {
                    ErrorView(message: error)
                }
            }
            .task { await viewModel.loadProfile() }
            .navigationTitle("Profile")
        }
    }
}

Modern Navigation (NavigationStack):

@Observable
class NavigationRouter {
    var path = NavigationPath()

    func push<D: Hashable>(_ destination: D) {
        path.append(destination)
    }

    func pop() {
        guard !path.isEmpty else { return }
        path.removeLast()
    }

    func popToRoot() {
        path.removeLast(path.count)
    }
}

struct ContentView: View {
    @State private var router = NavigationRouter()

    var body: some View {
        NavigationStack(path: $router.path) {
            HomeView()
                .navigationDestination(for: User.self) { user in
                    UserDetailView(user: user)
                }
                .navigationDestination(for: Post.self) { post in
                    PostDetailView(post: post)
                }
        }
        .environment(router)
    }
}

Swift Concurrency Patterns

Async/Await with Error Handling:

protocol UserServiceProtocol: Sendable {
    func fetchUser(_ id: String) async throws(NetworkError) -> User
    func updateUser(_ user: User) async throws(NetworkError) -> User
}

actor UserService: UserServiceProtocol {
    private let session: URLSession
    private let decoder: JSONDecoder

    init(session: URLSession = .shared) {
        self.session = session
        self.decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
    }

    func fetchUser(_ id: String) async throws(NetworkError) -> User {
        let url = URL(string: "https://api.example.com/users/\(id)")!

        do {
            let (data, response) = try await session.data(from: url)
            guard let httpResponse = response as? HTTPURLResponse,
                  200..<300 ~= httpResponse.statusCode else {
                throw NetworkError.requestFailed(statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0)
            }
            return try decoder.decode(User.self, from: data)
        } catch is DecodingError {
            throw NetworkError.decodingFailed
        } catch let error as NetworkError {
            throw error
        } catch {
            throw NetworkError.requestFailed(statusCode: 0)
        }
    }
}

TaskGroup for Parallel Execution:

func loadDashboard() async throws -> Dashboard {
    // Parallel async let for fixed number of tasks
    async let user = api.fetchUser()
    async let posts = api.fetchPosts()
    async let notifications = api.fetchNotifications()

    return try await Dashboard(
        user: user,
        posts: posts,
        notifications: notifications
    )
}

// TaskGroup for dynamic parallelism
func loadAllUsers(_ ids: [String]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask { try await api.fetchUser(id) }
        }
        return try await group.reduce(into: []) { $0.append($1) }
    }
}

Actor Isolation for Thread-Safe Caching:

actor ImageCache {
    private var cache: [URL: UIImage] = [:]
    private var inProgress: [URL: Task<UIImage, Error>] = [:]

    func image(for url: URL) async throws -> UIImage {
        // Return cached image if available
        if let cached = cache[url] { return cached }

        // Return in-progress task if already downloading
        if let task = inProgress[url] {
            return try await task.value
        }

        // Start new download task
        let task = Task { try await downloadImage(url) }
        inProgress[url] = task

        do {
            let image = try await task.value
            cache[url] = image
            inProgress[url] = nil
            return image
        } catch {
            inProgress[url] = nil
            throw error
        }
    }

    private func downloadImage(_ url: URL) async throws -> UIImage {
        let (data, _) = try await URLSession.shared.data(from: url)
        guard let image = UIImage(data: data) else {
            throw ImageError.invalidData
        }
        return image
    }
}

Combine Framework

Publisher and Subscriber Patterns:

import Combine

class SearchViewModel: ObservableObject {
    @Published var searchText = ""
    @Published private(set) var results: [SearchResult] = []
    @Published private(set) var isSearching = false

    private var cancellables = Set<AnyCancellable>()
    private let searchService: SearchServiceProtocol

    init(searchService: SearchServiceProtocol) {
        self.searchService = searchService
        setupSearchPipeline()
    }

    private func setupSearchPipeline() {
        $searchText
            .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
            .removeDuplicates()
            .filter { $0.count >= 2 }
            .handleEvents(receiveOutput: { [weak self] _ in
                self?.isSearching = true
            })
            .flatMap { [searchService] query in
                searchService.search(query)
                    .catch { _ in Just([]) }
            }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] results in
                self?.results = results
                self?.isSearching = false
            }
            .store(in: &cancellables)
    }
}

// Publisher extension for async/await bridge
extension Publisher {
    func async() async throws -> Output where Failure == Error {
        try await withCheckedThrowingContinuation { continuation in
            var cancellable: AnyCancellable?
            cancellable = first()
                .sink(
                    receiveCompletion: { completion in
                        if case .failure(let error) = completion {
                            continuation.resume(throwing: error)
                        }
                        cancellable?.cancel()
                    },
                    receiveValue: { value in
                        continuation.resume(returning: value)
                    }
                )
        }
    }
}

XCTest Unit Testing

Async Test with MainActor:

@MainActor
final class UserViewModelTests: XCTestCase {
    var sut: UserViewModel!
    var mockAPI: MockUserAPI!

    override func setUp() {
        mockAPI = MockUserAPI()
        sut = UserViewModel(api: mockAPI)
    }

    override func tearDown() {
        sut = nil
        mockAPI = nil
    }

    func testLoadUserSuccess() async throws {
        // Given
        let expectedUser = User(id: "1", name: "Test User", email: "test@example.com")
        mockAPI.mockUser = expectedUser

        // When
        try await sut.loadUser("1")

        // Then
        XCTAssertEqual(sut.user?.id, expectedUser.id)
        XCTAssertEqual(sut.user?.name, expectedUser.name)
        XCTAssertFalse(sut.isLoading)
        XCTAssertNil(sut.errorMessage)
    }

    func testLoadUserNetworkError() async {
        // Given
        mockAPI.error = NetworkError.requestFailed(statusCode: 500)

        // When
        do {
            try await sut.loadUser("1")
            XCTFail("Expected error to be thrown")
        } catch {
            // Then
            XCTAssertNil(sut.user)
            XCTAssertNotNil(sut.errorMessage)
        }
    }
}

// Mock implementation
class MockUserAPI: UserAPIProtocol {
    var mockUser: User?
    var error: Error?

    func fetchUser(_ id: String) async throws -> User {
        if let error = error { throw error }
        guard let user = mockUser else {
            throw NetworkError.requestFailed(statusCode: 404)
        }
        return user
    }
}

Swift Package Manager

Package.swift Configuration:

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "MyApp",
    platforms: [
        .iOS(.v17),
        .macOS(.v14)
    ],
    products: [
        .library(name: "MyAppCore", targets: ["MyAppCore"]),
        .executable(name: "MyAppCLI", targets: ["MyAppCLI"])
    ],
    dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.9.0"),
        .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.12.0"),
        .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.15.0")
    ],
    targets: [
        .target(
            name: "MyAppCore",
            dependencies: [
                "Alamofire",
                "Kingfisher",
                .product(name: "ComposableArchitecture", package: "swift-composable-architecture")
            ],
            swiftSettings: [
                .enableExperimentalFeature("StrictConcurrency")
            ]
        ),
        .testTarget(
            name: "MyAppCoreTests",
            dependencies: ["MyAppCore"]
        )
    ]
)

Advanced Patterns

For comprehensive coverage including:

  • The Composable Architecture (TCA) patterns
  • Advanced actor patterns and custom executors
  • Keychain and secure storage
  • Core Data and SwiftData integration
  • Network layer with retry and caching
  • CI/CD with Xcode Cloud and Fastlane

See: reference.md and examples.md

Context7 Library Mappings

Core Swift:

  • /apple/swift - Swift language and standard library
  • /apple/swift-evolution - Swift evolution proposals
  • /apple/swift-package-manager - SwiftPM documentation

Popular Libraries:

  • /Alamofire/Alamofire - HTTP networking
  • /onevcat/Kingfisher - Image downloading and caching
  • /realm/realm-swift - Mobile database
  • /pointfreeco/swift-composable-architecture - TCA architecture
  • /Quick/Quick - BDD testing framework
  • /Quick/Nimble - Matcher framework

Works Well With

  • moai-lang-kotlin - Android counterpart for cross-platform projects
  • moai-lang-flutter - Flutter/Dart for cross-platform mobile
  • moai-domain-backend - API integration and backend communication
  • moai-quality-security - iOS security best practices
  • moai-essentials-debug - Xcode debugging and profiling

Version: 1.0.0 Last Updated: 2025-12-07 Status: Production Ready