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"- All tests: Run through Xcode's Test navigator
- Specific package tests:
xcodebuild -scheme AccountTests test xcodebuild -scheme ModelsTests test xcodebuild -scheme NetworkTests test xcodebuild -scheme TimelineTests test xcodebuild -scheme EnvTests test
- Swift Package Manager (for individual packages):
cd Packages/[PackageName] swift test
The project uses SwiftFormat with 2-space indentation. Configuration is in .swiftformat.
The app is organized into Swift Packages under /Packages/:
- Models: Data models and API structures for Mastodon entities
- Network: API client implementation with support for Mastodon, DeepL, and OpenAI APIs
- Env: Environment objects, app-wide state, and dependency injection
- DesignSystem: Theming, colors, fonts, and reusable UI components
- Account: User profile views and account management
- Timeline: Timeline views, filtering, and unread status tracking
- StatusKit: Status/post composition and display components
- Notifications: Notification views and handling
- MediaUI: Media viewing with zoom, video playback, and sharing
The codebase contains legacy MVVM patterns, but new features should NOT use ViewModels.
- Legacy: Some older views still use ViewModels (being phased out)
- Modern Approach: Views as pure state expressions using SwiftUI primitives
- Environment Objects: Used for dependency injection (Router, CurrentAccount, Theme, etc.)
- Swift Concurrency: Async/await throughout for API calls
- Observation Framework: Uses
@Observablefor services injected via Environment
- NotificationService: Handles push notification decryption and formatting
- ShareExtension: Enables sharing content to the app
- ActionExtension: Quick actions from share sheet
- WidgetsExtension: Home screen widgets for timeline, mentions, and accounts
- Multi-account: Managed through
AppAccountsManagerwith secure storage - Push Notifications: Custom proxy server implementation for privacy
- Theme System: Extensive customization with 40+ app icons
- Translation: Supports DeepL API and instance-provided translations
- AI Features: OpenAI integration for alt text generation
- SwiftUI is the default UI paradigm - embrace its declarative nature
- Avoid legacy UIKit patterns and unnecessary abstractions
- Focus on simplicity, clarity, and native data flow
- Let SwiftUI handle the complexity - don't fight the framework
- No ViewModels - Use native SwiftUI data flow patterns
Use 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 concerns
- Views own their local state unless sharing is required
- State flows down, actions flow up
- Keep state as close to where it's used as possible
- Extract shared state only when multiple views need it
Example:
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)
}
}
}- Use
async/awaitas the default for asynchronous operations - Leverage
.taskmodifier for lifecycle-aware async work - Handle errors gracefully with try/catch
- Avoid Combine unless absolutely necessary
- Build UI with small, focused views
- Extract reusable components naturally
- Use view modifiers to encapsulate common styling
- Prefer composition over inheritance
- Organize by feature (e.g., Timeline/, Account/, Settings/)
- Keep related code together in the same file when appropriate
- Use extensions to organize large files
- Follow Swift naming conventions consistently
IMPORTANT: When editing code, you MUST:
- Build the project after making changes using XcodeBuildMCP commands
- Fix any compilation errors before proceeding
- Run relevant tests if modifying existing functionality
- Ensure code follows modern SwiftUI patterns
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
}
}
}- Write self-contained views when possible
- Use property wrappers as intended by Apple
- Test logic in isolation, preview UI visually
- Handle loading and error states explicitly
- Keep views focused on presentation
- Use Swift's type system for safety
- Trust SwiftUI's update mechanism
- Create ViewModels for every view
- Move state out of views unnecessarily
- Add abstraction layers without clear benefit
- Use Combine for simple async operations
- Fight SwiftUI's update mechanism
- Overcomplicate simple features
- Nest @Observable objects within other @Observable objects - This breaks SwiftUI's observation system. Initialize services at the view level instead.
- Unit test business logic in services/clients
- Use SwiftUI Previews for visual testing
- Test @Observable classes independently
- Keep tests simple and focused
- Don't sacrifice code clarity for testability
- Maintain existing patterns in legacy code
- New features use modern patterns exclusively
- Prefer composition over inheritance
- Keep views focused and single-purpose
- Use descriptive names for state enums
- Write SwiftUI code that looks and feels like SwiftUI
- Minimum Swift 6.0
- iOS 26 SDK (June 2025)
- Minimum deployment: iOS 18.0, visionOS 1.0
- Xcode 16.0 or later with iOS 26 SDK
- Apple Developer account for device testing
IMPORTANT: The project now supports iOS 26 SDK (June 2025) while maintaining iOS 18 as the minimum deployment target. Use #available checks when adopting iOS 26+ APIs.
glassEffect(_:in:isEnabled:)- Apply Liquid Glass effects to viewsbuttonStyle(.glass)- Apply Liquid Glass styling to buttonsToolbarSpacer- Create visual breaks in toolbars with Liquid Glass
Example:
Button("Post", action: postStatus)
.buttonStyle(.glass)
.glassEffect(.thin, in: .rect(cornerRadius: 12))scrollEdgeEffectStyle(_:for:)- Configure scroll edge effectsbackgroundExtensionEffect()- Duplicate, mirror, and blur views around edges
tabBarMinimizeBehavior(_:)- Control tab bar minimization behavior- Search role for tabs with search field replacing tab bar
TabViewBottomAccessoryPlacement- Adjust accessory view content based on placement
WebViewandWebPage- Full control over browsing experience
draggable(_:_:)- Drag multiple itemsdragContainer(for:id:in:selection:_:)- Container for draggable views
@Animatablemacro - SwiftUI synthesizes custom animatable data properties
Sliderwith automatic tick marks when using step parameterwindowResizeAnchor(_:)- Set window anchor point for resizing
TextEditornow supportsAttributedStringAttributedTextSelection- Handle text selection with attributed textAttributedTextFormattingDefinition- Define text styling in specific contextsFindContext- Create find navigator in text editing views
AssistiveAccess- Support Assistive Access in iOS/iPadOS scenes
Color.ResolvedHDR- RGBA values with HDR headroom information
UIHostingSceneDelegate- Host and present SwiftUI scenes in UIKitNSHostingSceneRepresentation- Host SwiftUI scenes in AppKitNSGestureRecognizerRepresentable- Incorporate gesture recognizers from AppKit
manipulable(coordinateSpace:operations:inertia:isEnabled:onChanged:)- Hand gesture manipulationSurfaceSnappingInfo- Snap volumes and windows to surfacesRemoteImmersiveSpace- Render stereo content from Mac to Apple Vision ProSpatialContainer- 3D layout container- Depth-based modifiers:
aspectRatio3D(_:contentMode:),rotation3DLayout(_:),depthAlignment(_:)
- Use
#available(iOS 26, *)for iOS 26-only features - Replace legacy implementations with iOS 26 APIs where appropriate
- Leverage Liquid Glass effects for modern UI aesthetics in timeline and status views
- Use enhanced text capabilities for the status composer
- Apply new drag-and-drop APIs for media and status interactions