Skip to content

EmilAxme/CleanMil

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CleanMil

A commercial-grade iOS photo-library cleaner — finds duplicates, similar shots, screenshots, large videos (free) plus blurry photos, selfies, video compression and duplicate-contact merging (Premium), monetised with auto-renewable subscriptions.

  • Swift, UIKit in code (no Storyboards except LaunchScreen), iOS 16+
  • MVVM + Coordinators, clean Presentation / Domain / Data / Services split
  • async/await concurrency, @MainActor UI
  • On-device Vision analysis (feature prints, Laplacian blur, face/selfie)
  • Apphud + StoreKit, Firebase (Analytics / Crashlytics / Remote Config), AppsFlyer — all behind protocols, so the app builds & runs with zero keys

Out of the box the project compiles and runs against mock providers (no SPM packages, no keys). Adding the real SDKs + keys upgrades it to production without touching feature code.


1. Requirements

Tool Version
Xcode 16+ (built & tested on Xcode 26)
iOS deployment target 16.0
XcodeGen brew install xcodegen

2. Getting started (runs immediately, no keys)

brew install xcodegen
cd CleanMil
xcodegen generate          # creates CleanMil.xcodeproj from project.yml
open CleanMil.xcodeproj
# select the CleanMil scheme + a simulator → Run

Run the tests:

xcodebuild test -project CleanMil.xcodeproj -scheme CleanMil \
  -destination 'platform=iOS Simulator,name=iPhone 16'

Because no SDKs are linked, you get: MockPurchasesProvider, ConsoleAnalytics, NoopCrashReporter, NoopAttributionProvider, InMemoryRemoteConfigProvider. The full funnel (onboarding → scan → review/swipe → paywall → "purchase" → premium unlock) works.

3. Project structure

CleanMil/
├─ project.yml                 # XcodeGen spec (regenerate the .xcodeproj)
├─ codemagic.yaml              # CI: build+test, and TestFlight workflows
├─ Config/                     # xcconfig + Secrets (keys never committed)
│  ├─ Shared/Debug/Release.xcconfig
│  └─ Secrets.example.xcconfig  → copy to Secrets.xcconfig
├─ CleanMil/
│  ├─ App/                     # AppDelegate, SceneDelegate
│  ├─ Application/             # Coordinator protocol, AppCoordinator
│  ├─ DI/                      # AppContainer (composition root), AppSecrets, Firebase bootstrap
│  ├─ Domain/                  # framework-free Models, Protocols, UseCases
│  ├─ Data/                    # Photos, Vision dedup, blur/selfie, video, contacts
│  ├─ Services/                # quota, subscription, analytics, remote config, crash, attribution
│  ├─ Presentation/            # Onboarding, Home, Review (+Swipe), Paywall, Settings, Common
│  └─ Resources/               # Info.plist, Assets, en/ru.lproj, LaunchScreen
└─ CleanMilTests/              # quota limiter, dedup clustering, subscription, remote-config mapping

Layering rules

  • Domain imports only Foundation/CoreGraphics — no UIKit/Photos/Vision/SDKs.
  • Data implements Domain protocols using Apple frameworks.
  • Services implement Domain protocols using third-party SDKs (behind #if canImport), with mock fallbacks.
  • Presentation depends on Domain protocols + the small service facades.
  • AppContainer wires everything and vends view models.

4. Adding the real SDKs (SPM)

Every adapter is wrapped in #if canImport(<Module>), so just adding the package makes the real implementation light up; remove it and you fall back to mocks.

In project.yml, uncomment the packages: block and the matching dependencies: entries (already stubbed), or in Xcode use File ▸ Add Package Dependencies:

SDK Package URL Products to add
Apphud https://github.com/apphud/ApphudSDK ApphudSDK
Firebase https://github.com/firebase/firebase-ios-sdk FirebaseAnalytics, FirebaseCrashlytics, FirebaseRemoteConfig
AppsFlyer https://github.com/AppsFlyerSDK/AppsFlyerFramework AppsFlyerLib

Then xcodegen generate again (if you edited project.yml).

The Apphud adapter targets Apphud 3.5+; spots that vary across SDK versions are flagged with // Apphud-API:.

5. Where the keys go

5.1 xcconfig secrets (Apphud + AppsFlyer)

cp Config/Secrets.example.xcconfig Config/Secrets.xcconfig   # git-ignored

Fill in:

APPHUD_API_KEY    = app_xxxxxxxxxxxxxxxxxxxx     # Apphud ▸ App Settings ▸ API Key
APPSFLYER_DEV_KEY = xxxxxxxxxxxxxxxxxxxxxx       # AppsFlyer ▸ App Settings ▸ Dev Key
APPSFLYER_APP_ID  = 1234567890                   # numeric App Store app id

These flow into Info.plist via $(KEY) and are read by AppSecrets. Empty/placeholder values are ignored, so partial configuration still runs.

5.2 Firebase

Drop GoogleService-Info.plist into CleanMil/Resources/ (git-ignored). FirebaseConfigurator calls FirebaseApp.configure() only when the plist and the SDK are present — otherwise Firebase stays off and analytics go to the console.

Enable Crashlytics in the Firebase console; add a Run-Script build phase that uploads dSYMs if you want symbolicated crashes (see Firebase docs).

6. App Store Connect — subscriptions

  1. Apps ▸ your app ▸ Subscriptions → create a Subscription Group (e.g. CleanMil Premium).
  2. Add two auto-renewable products and use these IDs (or change them in MockPurchasesProvider / Apphud):
    • com.cleanmil.weekly — Weekly, with a 3-day free trial (Introductory Offer).
    • com.cleanmil.yearly — Yearly.
  3. Add localized display names, prices, and a review screenshot.
  4. Fill the App Privacy section and add your Terms / Privacy URLs (also set CMTermsURL / CMPrivacyURL in Info.plist).
  5. In Apphud: connect your App Store Connect API key, import the products, and build a Paywall/Placement containing them. availableProducts() reads from Apphud Placements; pricing comes from StoreKit.
  6. Test purchases with a Sandbox Apple ID.

7. Remote Config (A/B without a release)

Seed these keys in Firebase Remote Config (defaults are seeded in-app and live in RemoteConfigValues.defaults). Mapping/validation is in RemoteConfigMapper (unit-tested):

Key Type Default Purpose
paywall_variant string control control / trialFirst / valueProps
trial_days int 3 Trial length shown on the paywall
free_daily_delete_limit int 50 Free-tier deletions/day (0 = unlimited)
hard_paywall bool false Force paywall after onboarding (delayed close)
large_video_threshold_mb int 50 "Large video" cutoff
duplicate_distance_threshold double 0.10 Vision distance for duplicates
similar_distance_threshold double 0.35 Vision distance for similar

8. Analytics funnel

Emitted via AnalyticsTracking (Firebase + console). Names are GA4 snake_case:

onboarding_completed, photo_permission, scan_started, scan_finished (findings, potential bytes), paywall_shown (variant, source), paywall_purchase_tapped, subscription_activated, purchase_failed, restore_completed, cleanup_done (category, count, freed_bytes), quota_reached.

On subscription_activated the price/revenue is also sent to AppsFlyer (af_purchase); the AppsFlyer UID is forwarded to Apphud for revenue attribution.

9. CI/CD — Codemagic

codemagic.yaml defines two workflows:

  • ios-build-test — every push/PR: installs XcodeGen, generates the project, resolves SPM, runs unit tests on a simulator (no signing). Caches SPM.
  • ios-testflight — on v* tags: code-signs, bumps build number, builds the .ipa, and uploads to TestFlight.

Fill the placeholders marked « … »: the App Store Connect API key integration name, bundle id, numeric app id, and notification email. Connect the repo in Codemagic and add the App Store Connect integration under Team ▸ Integrations.

10. Testing

CleanMilTests covers the pure, high-value logic:

  • DeletionQuotaManagerTests — daily limit, counting, midnight rollover (injected DateProvider), unlimited kill-switch.
  • SimilarityClustererTests — single-linkage clustering, transitivity, canCompare pre-filter, inclusive threshold.
  • SubscriptionServiceTests — purchase activates + fires analytics/attribution, statusUpdates() multicast stream, restore.
  • RemoteConfigMapperTests — typed mapping, defaults fallback, clamping, string-encoded values.

11. Privacy & notes

  • All photo/video analysis runs on device; nothing is uploaded.
  • fileSize is read from PHAssetResource via KVC (App-Store-safe), with a pixel/duration-based fallback.
  • Deletions go through PHPhotoLibrary.performChanges (system confirmation); user-cancellation is handled silently.
  • Dark theme is forced app-wide; SF Symbols throughout; RU/EN localized with a runtime language switch in Settings.

12. License

Proprietary — © CleanMil. Replace with your license of choice before shipping.

About

Коммерческий iOS-чистильщик фотогалереи (дубликаты, похожие, скриншоты, большие видео) с подписочной монетизацией. UIKit + MVVM/Coordinator, Vision, Apphud/Firebase/AppsFlyer за протоколами.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages