Files
yattee/AGENTS.md
Arkadiusz Fal a2a4691957 Integrate Sparkle auto-updates for macOS Developer ID builds
New Release-DeveloperID configuration gates Sparkle behind a SPARKLE
compile flag so the App Store Release build stays Sparkle-free. Adds
SPUStandardUpdaterController wrapper, Check for Updates menu command,
Advanced Settings section with beta channel toggle, and a Ruby script
plus GitHub Actions job that signs each release and publishes the
appcast to gh-pages for consumption by Sparkle and Homebrew cask.
2026-04-23 04:51:00 +02:00

3.8 KiB

Yattee Development Guide for AI Agents

Deployment Targets

iOS: 18.0+ | macOS: 15.0+ | tvOS: 18.0+

This project targets the latest OS versions only - use newest APIs freely without availability checks.

Build & Test Commands

Build: xcodebuild -scheme Yattee -configuration Debug
Test (all): xcodebuild test -scheme Yattee -destination 'platform=macOS'
Test (single): xcodebuild test -scheme Yattee -destination 'platform=macOS' -only-testing:YatteeTests/TestSuiteName/testMethodName
Lint: periphery scan (config: .periphery.yml)

Build Configurations

Three configurations exist, mapped to distribution channels:

Configuration Sparkle (#if SPARKLE) Used for
Debug off local development, tests
Release off App Store / TestFlight (fastlane mac beta) — must stay Sparkle-free, App Review rejects auto-update frameworks
Release-DeveloperID on Developer ID notarized build (fastlane mac build_and_notarize), distributed via GitHub Releases + Homebrew cask, receives Sparkle updates

All Sparkle-dependent code must be wrapped in #if SPARKLE ... #endif so the Release variant links zero Sparkle symbols. When adding new Sparkle features, test both configs build clean on macOS.

Code Style

Language: Swift 5.0+ with strict concurrency (Swift 6 mode enabled)
UI: SwiftUI with @Observable macro for view models (not ObservableObject)
Concurrency: Use actor for services, @MainActor for UI-related code, async/await everywhere
Testing: Swift Testing framework (@Test, @Suite, #expect) - NOT XCTest

Imports & Organization

Import order: Foundation first, then SwiftUI, then @testable imports
File headers: Include // FileName.swift, // Yattee, and brief comment describing purpose
MARK comments: Use // MARK: - Section Name to organize code sections
Sendable: All models, errors, and actors must conform to Sendable

Types & Naming

Models: Immutable structs with Codable, Hashable, Sendable conformance
Services: Use actor for thread-safe services, final class for @Observable view models
Enums: Use associated values for typed errors (see APIError.swift)
Optionals: Prefer guard-let unwrapping; use if let for simple cases
Naming: camelCase for variables/functions, PascalCase for types, clear descriptive names

Error Handling

Errors: Define typed enum errors conforming to Error, LocalizedError, Equatable, Sendable
Async throws: All async network/IO operations should throw typed errors
Logging: Use LoggingService.shared for all logging (see HTTPClient.swift for patterns)
User feedback: Provide localized error descriptions via errorDescription

Testing & Debugging

Add logging/visual clues (borders, backgrounds) when debugging issues - then ask user for results
If first fix doesn't work: Add debug code before second attempt to understand the issue better

UI Testing (Ruby/RSpec with AXe CLI)

Run UI tests: ./bin/ui-test --skip-build --keep-simulator
Run single spec: SKIP_BUILD=1 KEEP_SIMULATOR=1 bundle exec rspec spec/ui/smoke/search_spec.rb

Accessibility labels vs identifiers: On iOS 26+, .accessibilityIdentifier() doesn't work reliably on Group, ScrollView, and some container views (AXUniqueId comes back empty). Use .accessibilityLabel() instead, which maps to AXLabel and can be detected via AXe's text_visible?() method.

iOS 26 TabView search: The search field is integrated into the bottom tab bar with Tab(role: .search). Typing \n doesn't submit - use hardware key press via press_return (AXe key 40).

ScrollView children: Video rows inside LazyVStack/ScrollView aren't exposed in the accessibility tree. Use coordinate-based tapping instead.