This guide provides comprehensive information for developers working on ProcessReporter, a macOS application that monitors and reports user activity.
- Development Environment Setup
- Code Organization and Conventions
- Building and Debugging
- Testing Strategies
- Common Development Tasks
- Performance Profiling
- Release Process
- Troubleshooting Development Issues
- macOS 15.0+ (Sequoia or later)
- Xcode 15.0+ with Swift 5.9+
- Swift Package Manager (included with Xcode)
- Git for version control
- CocoaPods (optional, if migrating from older versions)
-
Clone the repository
git clone https://github.com/yourusername/ProcessReporter.git cd ProcessReporter -
Open in Xcode
open ProcessReporter.xcodeproj
-
Configure signing
- Select the project in Xcode navigator
- Go to "Signing & Capabilities" tab
- Select your development team
- Xcode will automatically manage provisioning profiles
-
Grant permissions
- Build and run the app
- Grant accessibility permissions when prompted
- Enable screen recording if needed for window title capture
For better code intelligence in editors like VS Code:
-
Install xcode-build-server:
brew install xcode-build-server
-
Generate build server configuration:
xcode-build-server config -workspace . -scheme ProcessReporter
ProcessReporter/
├── main.swift # Application entry point
├── AppDelegate.swift # App lifecycle management
├── Core/ # Core business logic
│ ├── Reporter/ # Reporting system
│ ├── Database/ # Data persistence
│ ├── MediaInfoManager/ # Media tracking
│ └── Utilities/ # Helper classes
├── Preferences/ # Settings UI
│ ├── Controllers/ # View controllers
│ ├── DataModels/ # Preference models
│ └── Views/ # Custom views
├── Extensions/ # Swift extensions
├── Resources/ # Assets and resources
└── Supporting Files/ # Info.plist, entitlements
-
Naming
- Use descriptive names:
updateReportingIntervalnotupdateRI - Classes/Structs: PascalCase (
ReportModel) - Methods/Variables: camelCase (
sendReport()) - Constants: camelCase with
let(let maxRetryCount = 3)
- Use descriptive names:
-
Code Organization
class MyClass { // MARK: - Properties private let reporter: Reporter private var isRunning = false // MARK: - Initialization init(reporter: Reporter) { self.reporter = reporter } // MARK: - Public Methods func start() { // Implementation } // MARK: - Private Methods private func processData() { // Implementation } }
-
Error Handling
enum ReporterError: LocalizedError { case networkUnavailable case invalidConfiguration var errorDescription: String? { switch self { case .networkUnavailable: return "Network connection is unavailable" case .invalidConfiguration: return "Reporter configuration is invalid" } } }
-
Async/Await Pattern
func fetchData() async throws -> ReportModel { // Prefer async/await over completion handlers let data = try await networkClient.fetch() return ReportModel(from: data) }
- Keep files under 500 lines
- One primary type per file
- Group related functionality using
// MARK: -comments - Place extensions in separate files when they're substantial
- Select scheme: ProcessReporter
- Select destination: My Mac
- Build: ⌘B
- Run: ⌘R
# Debug build
xcodebuild -project ProcessReporter.xcodeproj \
-scheme ProcessReporter \
-configuration Debug \
build
# Release build
xcodebuild -project ProcessReporter.xcodeproj \
-scheme ProcessReporter \
-configuration Release \
build
# Archive for distribution
xcodebuild -project ProcessReporter.xcodeproj \
-scheme ProcessReporter \
-configuration Release \
archive \
-archivePath ./build/ProcessReporter.xcarchive// Use os_log for production logging
import os.log
private let logger = Logger(subsystem: "com.processreporter", category: "Reporter")
func debugMethod() {
logger.debug("Starting report generation")
logger.info("Report sent successfully")
logger.error("Failed to send report: \(error)")
}- Conditional breakpoints: Right-click breakpoint → Edit Breakpoint
- Symbolic breakpoints: Debug → Breakpoints → + → Symbolic Breakpoint
- Exception breakpoints: For catching thrown errors
Add debug menu items for testing:
#if DEBUG
menuItem.submenu?.addItem(NSMenuItem.separator())
menuItem.submenu?.addItem(NSMenuItem(
title: "Debug: Trigger Report",
action: #selector(debugTriggerReport),
keyEquivalent: ""
))
#endifSet in Xcode scheme editor:
PROCESSREPORTER_DEBUG=1- Enable debug loggingPROCESSREPORTER_MOCK_DATA=1- Use mock data sources
- Enable Malloc Stack Logging: Product → Scheme → Edit Scheme → Diagnostics
- Use Instruments: Product → Profile → Leaks/Allocations
- Debug Memory Graph: Debug bar → Debug Memory Graph button
import XCTest
@testable import ProcessReporter
class ReporterTests: XCTestCase {
var reporter: Reporter!
override func setUp() {
super.setUp()
reporter = Reporter(configuration: .mock)
}
override func tearDown() {
reporter = nil
super.tearDown()
}
func testReportGeneration() async throws {
// Given
let mockData = createMockWindowInfo()
// When
let report = try await reporter.generateReport(from: mockData)
// Then
XCTAssertNotNil(report)
XCTAssertEqual(report.applicationName, "TestApp")
}
}class MockReporterExtension: ReporterExtension {
var runCalled = false
var lastReport: ReportModel?
func canRun() -> Bool { true }
func run(report: ReportModel) async throws {
runCalled = true
lastReport = report
}
}Test actual integrations with timeouts:
func testMixSpaceIntegration() async throws {
let expectation = XCTestExpectation(description: "MixSpace report sent")
let extension = MixSpaceExtension(config: testConfig)
try await extension.run(report: testReport)
expectation.fulfill()
await fulfillment(of: [expectation], timeout: 10.0)
}class PreferencesUITests: XCTestCase {
let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
app.launch()
}
func testPreferencesWindow() {
app.menuBarItems["ProcessReporter"].click()
app.menuItems["Preferences..."].click()
XCTAssertTrue(app.windows["Preferences"].exists)
}
}- Create extension class:
// File: ProcessReporter/Core/Reporter/Extensions/MyServiceExtension.swift
class MyServiceExtension: ReporterExtension {
private let apiKey: String
init(apiKey: String) {
self.apiKey = apiKey
}
func canRun() -> Bool {
return !apiKey.isEmpty
}
func run(report: ReportModel) async throws {
// Implement async reporting logic
}
func runSync(report: ReportModel) throws {
// Implement sync reporting logic
}
}- Add to Reporter initialization:
// In Reporter.swift
private func setupExtensions() {
if let apiKey = preferences.myServiceApiKey {
extensions.append(MyServiceExtension(apiKey: apiKey))
}
}- Create preferences UI:
- Add view controller in
Preferences/Controllers/ - Update
PreferencesWindowControllerto include new tab - Add data model properties in
PreferencesDataModel
- Add view controller in
- Update ApplicationMonitor:
// Add new tracking capability
private func trackAdditionalInfo(_ app: NSRunningApplication) {
// Implementation
}- Extend FocusedWindowInfo:
struct FocusedWindowInfo {
// Existing properties...
let additionalData: String? // New property
}// In StatusItemManager.swift
private func createMenuItem(title: String, action: Selector?) -> NSMenuItem {
let item = NSMenuItem(
title: title,
action: action,
keyEquivalent: ""
)
item.target = self
return item
}- Update schema version in
Database.swift - Add migration:
private func migrateToVersion2() throws {
try db.execute("""
ALTER TABLE history
ADD COLUMN media_info TEXT
""")
}-
Time Profiler:
# Profile CPU usage Product → Profile → Time Profiler -
Allocations:
- Track memory allocations
- Identify retain cycles
- Monitor memory growth
-
System Trace:
- Analyze system calls
- Track file I/O
- Monitor network activity
- Minimize main thread work:
Task.detached(priority: .background) {
let processedData = await self.processHeavyData()
await MainActor.run {
self.updateUI(with: processedData)
}
}- Cache expensive operations:
private let imageCache = NSCache<NSString, NSImage>()
func getAppIcon(for bundleID: String) -> NSImage? {
if let cached = imageCache.object(forKey: bundleID as NSString) {
return cached
}
// Load and cache icon
}- Batch database operations:
try database.transaction { db in
for record in records {
try db.insert(record)
}
}-
Update version number:
Info.plist: CFBundleShortVersionStringInfo.plist: CFBundleVersion (build number)
-
Test on clean system:
- Remove app support files
- Test first-run experience
- Verify permissions requests
-
Update documentation:
- README.md with new features
- CHANGELOG.md with version notes
- API documentation if changed
-
Archive the app:
xcodebuild -project ProcessReporter.xcodeproj \ -scheme ProcessReporter \ -configuration Release \ clean archive \ -archivePath ./build/ProcessReporter.xcarchive -
Export for distribution:
xcodebuild -exportArchive \ -archivePath ./build/ProcessReporter.xcarchive \ -exportPath ./build \ -exportOptionsPlist ExportOptions.plist -
Notarize the app:
xcrun notarytool submit ProcessReporter.zip \ --apple-id "your-apple-id" \ --team-id "your-team-id" \ --wait -
Staple the ticket:
xcrun stapler staple ProcessReporter.app
# Create DMG for distribution
create-dmg \
--volname "ProcessReporter" \
--window-pos 200 120 \
--window-size 600 400 \
--icon-size 100 \
--icon "ProcessReporter.app" 150 150 \
--hide-extension "ProcessReporter.app" \
--app-drop-link 450 150 \
"ProcessReporter.dmg" \
"build/"Problem: Window titles are nil Solution:
- Check System Settings → Privacy & Security → Accessibility
- Remove and re-add ProcessReporter
- Restart the app
Problem: "ProcessReporter" can't be opened Solution:
# Check code signature
codesign -dv --verbose=4 ProcessReporter.app
# Re-sign if needed
codesign --force --deep --sign "Developer ID" ProcessReporter.appProblem: Package resolution failed Solution:
- File → Packages → Reset Package Caches
- Delete
~/Library/Developer/Xcode/DerivedData - Clean build folder: ⇧⌘K
Problem: SQLite errors on startup Solution:
# Check database integrity
sqlite3 ~/Library/Application\ Support/ProcessReporter/database.db "PRAGMA integrity_check;"
# Reset database (data loss!)
rm ~/Library/Application\ Support/ProcessReporter/database.db- Enable verbose logging:
UserDefaults.standard.set(true, forKey: "VerboseLogging")-
Check Console.app for system logs:
- Filter by "ProcessReporter"
- Look for crash reports
-
Use lldb commands:
(lldb) po self.reporter.extensions
(lldb) expr self.isRunning = false
(lldb) thread backtrace
- Check existing issues on GitHub
- Review code comments and documentation
- Use Xcode's documentation viewer: ⌥-click on symbols
- Profile the app to understand performance issues
For additional help or to report issues, please visit the project's issue tracker.