This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Build (Debug)
xcodebuild -project AppThinnerAnalyzer/AppThinnerAnalyzer.xcodeproj \
-scheme AppThinnerAnalyzer -destination 'platform=macOS' -configuration Debug build
# Build (Release)
xcodebuild -project AppThinnerAnalyzer/AppThinnerAnalyzer.xcodeproj \
-scheme AppThinnerAnalyzer -destination 'platform=macOS' -configuration Release build
# Run all tests
xcodebuild test -project AppThinnerAnalyzer/AppThinnerAnalyzer.xcodeproj \
-scheme AppThinnerAnalyzer -destination 'platform=macOS'The project has no linter configuration (no .swiftlint.yml). Swift package dependencies (MachOKit, MachOObjCSection, etc.) are resolved automatically by Xcode.
This is a macOS SwiftUI app (macOS 14.0+) for analyzing iOS app package sizes. It follows MVVM with a protocol-based dependency injection container.
The entire application is built around fusing three independent data sources into a single file tree keyed by project-relative paths:
| Source | Parser | Output | Use |
|---|---|---|---|
LinkMap .txt |
LinkmapAnalyzer |
CodeSizeInfo[] — per-.o code size mapped to project path |
codeSize on each tree node |
IPA / .app |
PackageParser |
PackageFileInfo[] — package-relative paths + sizes |
resourceSize / frameworkSize |
| Project directory | ProjectResourceScanner |
ProjectFileEntry[] — all source & resource files |
Tree skeleton; path index for LinkMap mapping |
The bridge key between all three is the project-relative path (relativePath). Every matching step ultimately resolves to this shared key.
Phase 1 (concurrent): IPA parse + project scan → then LinkMap (depends on project entries)
Phase 2: buildIntegratedDataFromProjectEntries → IntegratedAnalysisData (codeSize + resourceSize + frameworkSize per file)
Phase 3: buildUnusedContentResults → BinaryUnusedCodeAnalyzer (Mach-O) + UnusedScanService (resources)
Phase 4: createAnalysisProject → CoreData write (AnalysisProject + AnalysisResult rows)
AnalysisService is the sole orchestrator. All other services are called from it and should not call each other directly.
LinkMap object file paths (absolute, machine-specific) are resolved to project-relative paths in priority order:
- Static lib (
.a(Obj.o)or.a[N](Obj.o)) →staticLibLookup["pathComponent|baseName"]orstaticLibNameToPath[libName] - Framework (
.framework/Name(Obj.o)) →frameworkNameToPath[fwName]+ append originalfileNameto produce a virtual.opath likePods/X.framework/X(Foo.o) - Base name →
projectFileIndex[baseName] - Fallback → stripped linkmap path (logged as unmapped)
Step 2 preserves the parenthesized filename so that AnalysisService.isVirtualObjectPath() can recognize it and create per-compilation-unit virtual nodes in the tree, instead of collapsing everything to the .framework directory. This is the key mechanism for showing .framework contents at .o granularity.
When a LinkMap path resolves via the framework step, codeSizeInfo.relativePath looks like Pods/SDWebImage/SDWebImage.framework/SDWebImage(SDImageCache.o). In buildIntegratedDataFromProjectEntries:
isVirtualObjectPath(path)detects these (contains(and ends with.o))- They are split from real source entries and become independent
IntegratedFileInfonodes - The
.frameworkcontainer'sframeworkSizeis zeroed to avoid double-counting - In
createAnalysisProject,isUnusedCodefor virtual nodes is determined byvirtualNodeContainsUnusedClass()(matching the.obase name against the unused class name set), not by path lookup
- Unused code:
BinaryUnusedCodeAnalyzerparses the main binary's__objc_classlist(defined classes) minus__objc_classrefs+superClassName+ category host classes → candidates filtered by a whitelist of runtime-dynamic suffixes (ViewController,Delegate,Cell, etc.) - Unused resources:
UnusedScanServicecollects resource name references from source files via regex, then checks each resource file against this set - Both run inside
UnusedScanService.runLocalUnusedScan, called fromAnalysisService
The MachOKit and MachOObjCSection dependencies are optional; both are guarded with #if canImport(MachOKit). Without them the binary analysis step is skipped silently.
Two main entities under AnalysisProject (1-to-many):
AnalysisResult: one row per file/virtual node —relativePath,codeSize,resourceSize,frameworkSize,isUnusedCode,isUnusedResourceExternalUnusedData: imported external unused lists
AnalysisProject.analysisResultsArray sorts by relativePath and is the sole input to TreemapGenerator.
TreemapGenerator.buildDirectoryStructure splits AnalysisResult rows into a flat [directoryPath: DirectoryNode] map using path.components(separatedBy: "/").dropLast(). Virtual .o paths (Pods/X.framework/X(Foo.o)) correctly land under Pods/X.framework as leaf FileNode entries — no special handling needed in the generator.
createFileNode forces isUnused = false for dynamic library nodes (frameworkSize > 0 && codeSize == 0) to prevent third-party dynamic frameworks from being wrongly flagged.
DependencyContainer.shared (in Services/DependencyContainer.swift) holds all service singletons and exposes factory methods (makeMainViewModel(), makeTreemapViewModel()). All services are protocol-typed (*Protocol), enabling test substitution.
| Concern | File |
|---|---|
| Full analysis orchestration | Services/AnalysisService.swift |
| LinkMap parsing + path resolution | Services/LinkmapAnalyzer.swift |
IPA / .app parsing |
Services/PackageParser.swift |
| Project directory scan | Services/ProjectResourceScanner.swift |
| Mach-O unused class detection | Services/BinaryUnusedCodeAnalyzer.swift |
| Unused resource scan | Services/UnusedScanService.swift |
| Squarified treemap layout | Services/TreemapGenerator.swift |
| All value-type data structures | Models/DataModels.swift |
| CoreData entity extensions | CoreData/ |
- All path comparisons are exact string equality on project-relative paths. If unused detection mismatches the tree, the root cause is almost always a path normalization inconsistency between
UnusedCode.filePath/UnusedResource.relativePathandIntegratedFileInfo.relativePath. ProjectFileEntrynever includes.h/.hppfiles — they don't produce.ocompilation units and would pollute theprojectFileIndex..frameworkdirectories are added as singleProjectFileEntrynodes (withskipDescendants) so the framework binary name can be matched viaframeworkNameToPath, while their internal files are not separately indexed.aggregateFrameworkObjectPathsToBinaryis only applied torealCodeEntries(non-virtual paths). Virtual.opaths are handled separately and must not pass through this function.- The whitelist in
BinaryUnusedCodeAnalyzer(ViewController,Controller,Cell,View,Button,Delegate,DataSource,AppDelegate,SceneDelegate,Module,Router,Coordinator) must be maintained when adding new runtime-dynamic class patterns.