AGENTS.MD
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
IceCubesApp is a multiplatform Mastodon client built entirely in SwiftUI. It's an open-source native Apple application that runs on iOS, iPadOS, macOS, and visionOS.
To build IceCubesApp for iPhone Air simulator:
mcp__XcodeBuildMCP__build_sim_name_proj projectPath: "/Users/thomas/Documents/Dev/Open Source/IceCubesApp/IceCubesApp.xcodeproj" scheme: "IceCubesApp" simulatorName: "iPhone Air"
# Set defaults once per session
mcp__XcodeBuildMCP__session-set-defaults projectPath: "/Users/thomas/Documents/Dev/Open Source/IceCubesApp/IceCubesApp.xcodeproj" simulatorName: "iPhone Air"
# Then run any package test scheme
mcp__XcodeBuildMCP__test_sim scheme: "AccountTests"
mcp__XcodeBuildMCP__test_sim scheme: "ModelsTests"
mcp__XcodeBuildMCP__test_sim scheme: "NetworkTests"
mcp__XcodeBuildMCP__test_sim scheme: "TimelineTests"
mcp__XcodeBuildMCP__test_sim scheme: "EnvTests"
The project uses SwiftFormat with 2-space indentation. Configuration is in .swiftformat.
The app is organized into Swift Packages under /Packages/:
The codebase contains legacy MVVM patterns, but new features should NOT use ViewModels.
@Observable for services injected via EnvironmentAppAccountsManager with secure storageUse SwiftUI's built-in property wrappers appropriately:
@State - Local, ephemeral view state@Binding - Two-way data flow between views@Observable - Shared state (preferred for new code)@Environment - Dependency injection for app-wide concernsExample:
struct TimelineView: View {
@Environment(Client.self) private var client
@State private var viewState: ViewState = .loading
enum ViewState {
case loading
case loaded(statuses: [Status])
case error(Error)
}
var body: some View {
Group {
switch viewState {
case .loading:
ProgressView()
case .loaded(let statuses):
StatusList(statuses: statuses)
case .error(let error):
ErrorView(error: error)
}
}
.task {
await loadTimeline()
}
}
private func loadTimeline() async {
do {
let statuses = try await client.getHomeTimeline()
viewState = .loaded(statuses: statuses)
} catch {
viewState = .error(error)
}
}
}
async/await as the default for asynchronous operations.task modifier for lifecycle-aware async workIMPORTANT: When editing code, you MUST:
Example workflow:
# Build the main app
mcp__XcodeBuildMCP__build_mac_proj projectPath: "/path/to/IceCubesApp.xcodeproj" scheme: "IceCubesApp"
# Or for iOS simulator
mcp__XcodeBuildMCP__build_ios_sim_name_proj projectPath: "/path/to/IceCubesApp.xcodeproj" scheme: "IceCubesApp" simulatorName: "iPhone Air"
@Observable
class AppAccountsManager {
var currentAccount: Account?
var availableAccounts: [Account] = []
func switchAccount(_ account: Account) {
currentAccount = account
// Handle account switching
}
}
// In App file
struct IceCubesApp: App {
@State private var accountManager = AppAccountsManager()
var body: some Scene {
WindowGroup {
ContentView()
.environment(accountManager)
}
}
}
struct NotificationsView: View {
@Environment(Client.self) private var client
@State private var notifications: [Notification] = []
@State private var isLoading = false
@State private var error: Error?
var body: some View {
List(notifications) { notification in
NotificationRow(notification: notification)
}
.overlay {
if isLoading {
ProgressView()
}
}
.task {
await loadNotifications()
}
.refreshable {
await loadNotifications()
}
}
private func loadNotifications() async {
isLoading = true
defer { isLoading = false }
do {
notifications = try await client.getNotifications()
} catch {
self.error = error
}
}
}