Repository: quran/quran-ios
Branch: main
Commit: 91948a747adf
Files: 825
Total size: 4.4 MB
Directory structure:
gitextract_dc9d8v7i/
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .swift-version
├── .swiftformat
├── .swiftlint.yml
├── .swiftpm/
│ └── xcode/
│ └── xcshareddata/
│ └── xcschemes/
│ ├── AllTargetsTests.xcscheme
│ ├── AnnotationsService.xcscheme
│ ├── AppStructureFeature.xcscheme
│ ├── BatchDownloader.xcscheme
│ ├── BookmarksFeature.xcscheme
│ ├── Caching.xcscheme
│ ├── Crashing.xcscheme
│ ├── HomeFeature.xcscheme
│ ├── LastPagePersistence.xcscheme
│ ├── Localization.xcscheme
│ ├── MoreMenuFeature.xcscheme
│ ├── NetworkSupport.xcscheme
│ ├── NoorUI.xcscheme
│ ├── NotePersistence.xcscheme
│ ├── NotesFeature.xcscheme
│ ├── OAuthServiceAppAuthImpl.xcscheme
│ ├── PageBookmarkPersistence.xcscheme
│ ├── Preferences.xcscheme
│ ├── QuranAudioKit.xcscheme
│ ├── QuranEngine-Package.xcscheme
│ ├── QuranImageFeature.xcscheme
│ ├── QuranKit.xcscheme
│ ├── QuranPagesFeature.xcscheme
│ ├── QuranTextKit.xcscheme
│ ├── QuranTextKitTests.xcscheme
│ ├── QuranTranslationFeature.xcscheme
│ ├── ReadingSelectorFeature.xcscheme
│ ├── ReadingService.xcscheme
│ ├── ReciterListFeature.xcscheme
│ ├── SQLitePersistence.xcscheme
│ ├── SearchFeature.xcscheme
│ ├── SettingsFeature.xcscheme
│ ├── SystemDependencies.xcscheme
│ ├── SystemDependenciesFake.xcscheme
│ ├── Timing.xcscheme
│ ├── TranslationService.xcscheme
│ ├── TranslationsFeature.xcscheme
│ ├── UIx.xcscheme
│ ├── Utilities.xcscheme
│ ├── UtilitiesTests.xcscheme
│ ├── VLogging.xcscheme
│ └── VersionUpdater.xcscheme
├── AllTargetsTests/
│ └── Empty.swift
├── Core/
│ ├── Analytics/
│ │ └── AnalyticsLibrary.swift
│ ├── AppMigrator/
│ │ ├── Sources/
│ │ │ ├── AppMigrator.swift
│ │ │ └── AppVersionUpdater.swift
│ │ └── Tests/
│ │ └── AppMigratorTests.swift
│ ├── AsyncUtilitiesForTesting/
│ │ ├── AsyncAlgorithms++.swift
│ │ ├── AsyncAsserts.swift
│ │ ├── PublisherCollector.swift
│ │ ├── XCTestCase+PromiseKit.swift
│ │ └── XCTestCase+Publisher.swift
│ ├── Caching/
│ │ ├── Sources/
│ │ │ ├── Cache.swift
│ │ │ ├── OperationCacheableService.swift
│ │ │ └── PagesCacheableService.swift
│ │ └── Tests/
│ │ ├── CacheTests.swift
│ │ ├── OperationCacheableServiceTests.swift
│ │ └── PagesCacheableServiceTests.swift
│ ├── Crashing/
│ │ ├── Crasher.swift
│ │ └── Global.swift
│ ├── Localization/
│ │ ├── Localizations.swift
│ │ ├── NumberFormatter+Extension.swift
│ │ ├── Resources/
│ │ │ ├── ar.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ ├── Readers.strings
│ │ │ │ └── Suras.strings
│ │ │ ├── de.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ ├── Readers.strings
│ │ │ │ └── Suras.strings
│ │ │ ├── en.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ ├── Readers.strings
│ │ │ │ └── Suras.strings
│ │ │ ├── es.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ ├── Readers.strings
│ │ │ │ └── Suras.strings
│ │ │ ├── fa.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ ├── Readers.strings
│ │ │ │ └── Suras.strings
│ │ │ ├── fr.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ ├── Readers.strings
│ │ │ │ └── Suras.strings
│ │ │ ├── kk.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ ├── Readers.strings
│ │ │ │ └── Suras.strings
│ │ │ ├── ms.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ └── Readers.strings
│ │ │ ├── nl.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ ├── Readers.strings
│ │ │ │ └── Suras.strings
│ │ │ ├── pt.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ └── Readers.strings
│ │ │ ├── ru.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ ├── Readers.strings
│ │ │ │ └── Suras.strings
│ │ │ ├── tr.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ └── Suras.strings
│ │ │ ├── ug.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ ├── Readers.strings
│ │ │ │ └── Suras.strings
│ │ │ ├── uz.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ ├── Readers.strings
│ │ │ │ └── Suras.strings
│ │ │ ├── vi.lproj/
│ │ │ │ ├── Android.strings
│ │ │ │ ├── Android.stringsdict
│ │ │ │ ├── Localizable.strings
│ │ │ │ ├── Readers.strings
│ │ │ │ └── Suras.strings
│ │ │ └── zh.lproj/
│ │ │ ├── Android.strings
│ │ │ ├── Android.stringsdict
│ │ │ └── Localizable.strings
│ │ └── resource_bundle.swift
│ ├── Locking/
│ │ ├── NSLocking+Extension.swift
│ │ └── Protected.swift
│ ├── OAuthService/
│ │ └── OAuthService.swift
│ ├── OAuthServiceAppAuthImpl/
│ │ └── OAuthServiceAppAuthImpl.swift
│ ├── OAuthServiceFake/
│ │ └── OAuthServiceFake.swift
│ ├── Preferences/
│ │ ├── Preference.swift
│ │ ├── PreferenceKey.swift
│ │ ├── PreferenceTransformer.swift
│ │ └── Preferences.swift
│ ├── QueuePlayer/
│ │ ├── AudioInterruptionMonitor.swift
│ │ ├── AudioPlayer.swift
│ │ ├── AudioPlaying.swift
│ │ ├── AudioRequest.swift
│ │ ├── NowPlayingUpdater.swift
│ │ ├── Player.swift
│ │ ├── PlayerItemInfo.swift
│ │ ├── QueuePlayer.swift
│ │ └── Runs.swift
│ ├── SecurePersistence/
│ │ └── SecurePersistence.swift
│ ├── SystemDependencies/
│ │ ├── EventObserver.swift
│ │ ├── FileSystem.swift
│ │ ├── KeychainAccess.swift
│ │ ├── PersistentHistoryTransaction.swift
│ │ ├── SystemBundle.swift
│ │ ├── SystemTime.swift
│ │ └── Zipper.swift
│ ├── SystemDependenciesFake/
│ │ ├── AsyncChannelEventObserver.swift
│ │ ├── FileSystemFake.swift
│ │ ├── KeychainAccessFake.swift
│ │ ├── PersistentHistoryTransactionFake.swift
│ │ ├── SystemBundleFake.swift
│ │ ├── SystemTimeFake.swift
│ │ └── ZipperFake.swift
│ ├── Timing/
│ │ └── Timer.swift
│ ├── Utilities/
│ │ ├── Sources/
│ │ │ ├── Extensions/
│ │ │ │ ├── Array+Extension.swift
│ │ │ │ ├── Error+Extension.swift
│ │ │ │ ├── FileManager+Extension.swift
│ │ │ │ ├── Int+Extension.swift
│ │ │ │ ├── Result+Extension.swift
│ │ │ │ ├── Sequence+Extension.swift
│ │ │ │ ├── String+Chunking.swift
│ │ │ │ ├── String+Extension.swift
│ │ │ │ ├── Task+Extension.swift
│ │ │ │ └── URL+Extension.swift
│ │ │ └── Features/
│ │ │ ├── Address.swift
│ │ │ ├── AsyncInitializer.swift
│ │ │ ├── AsyncPublisher.swift
│ │ │ ├── AsyncThrowingPublisher.swift
│ │ │ ├── Attempt.swift
│ │ │ ├── Locking.swift
│ │ │ ├── MultiPredicateComparer.swift
│ │ │ ├── MulticastContinuation.swift
│ │ │ ├── Pair.swift
│ │ │ └── RelativeFilePath.swift
│ │ └── Tests/
│ │ ├── AsyncPublisherTests.swift
│ │ ├── AsyncThrowingPublisherTests.swift
│ │ ├── String+ChunkingTests.swift
│ │ └── String+ExtensionTests.swift
│ ├── VLogging/
│ │ └── Logger.swift
│ └── WeakSet/
│ ├── UnsafeWeakSet.swift
│ └── WeakSet.swift
├── Data/
│ ├── AudioTimingPersistence/
│ │ ├── AyahTimingPersistence.swift
│ │ └── GRDBAyahTimingPersistence.swift
│ ├── AuthenticationClient/
│ │ ├── Sources/
│ │ │ ├── AuthenticationClient.swift
│ │ │ ├── AuthenticationClientMobileSyncImpl.swift
│ │ │ └── AuthentincationClientImpl.swift
│ │ └── Tests/
│ │ └── AuthenticationClientTests.swift
│ ├── BatchDownloader/
│ │ ├── Sources/
│ │ │ ├── DownloadProgress.swift
│ │ │ ├── Downloader/
│ │ │ │ ├── DownloadBatchDataController.swift
│ │ │ │ ├── DownloadBatchResponse.swift
│ │ │ │ ├── DownloadManager.swift
│ │ │ │ ├── DownloadSessionDelegate.swift
│ │ │ │ ├── DownloadsObserver.swift
│ │ │ │ ├── DownloadsPersistence.swift
│ │ │ │ └── GRDBDownloadsPersistence.swift
│ │ │ ├── Entities/
│ │ │ │ ├── Download.swift
│ │ │ │ └── DownloadRequest.swift
│ │ │ └── Errors/
│ │ │ └── FileSystemError.swift
│ │ └── Tests/
│ │ ├── DownloadManagerTests.swift
│ │ └── HistoryProgressListener.swift
│ ├── BatchDownloaderFake/
│ │ └── BatchDownloaderFake.swift
│ ├── CoreDataModel/
│ │ ├── Quran.xcdatamodeld/
│ │ │ └── Quran.xcdatamodel/
│ │ │ └── contents
│ │ ├── Resources.swift
│ │ └── Schema.swift
│ ├── CoreDataPersistence/
│ │ ├── Sources/
│ │ │ ├── CoreDataPersistentHistoryProcessor.swift
│ │ │ ├── CoreDataPublisher.swift
│ │ │ ├── CoreDataStack.swift
│ │ │ ├── CoreDataTypes.swift
│ │ │ ├── NSManagedObjectContext+Extensions.swift
│ │ │ └── merging/
│ │ │ ├── CoreDataEntityUniquifier.swift
│ │ │ ├── CoreDataInsertedEntitiesRetriever.swift
│ │ │ ├── CoreDataPersistentHistoryTransactionsMerger.swift
│ │ │ └── SimpleCoreDataEntityUniquifier.swift
│ │ └── Tests/
│ │ ├── CoreDataInsertedEntitiesRetrieverTests.swift
│ │ ├── CoreDataPublisherTests.swift
│ │ ├── CoreDataStackTests.swift
│ │ └── SimpleCoreDataEntityUniquifierTests.swift
│ ├── CoreDataPersistenceTestSupport/
│ │ ├── CoreDataStack+Extensions.swift
│ │ ├── LastPage+++.swift
│ │ ├── Note+++.swift
│ │ └── PageBookmark+++.swift
│ ├── LastPagePersistence/
│ │ ├── Sources/
│ │ │ ├── CoreDataLastPageOverflowHandler.swift
│ │ │ ├── CoreDataLastPagePersistence.swift
│ │ │ ├── CoreDataLastPageUniquifier.swift
│ │ │ ├── LastPagePersistence.swift
│ │ │ └── LastPagePersistenceModel.swift
│ │ └── Tests/
│ │ ├── CoreDataLastPagePersistenceTests.swift
│ │ └── CoreDataLastPageUniquifierTests.swift
│ ├── LinePagePersistence/
│ │ ├── Sources/
│ │ │ ├── GRDBLinePagePersistence.swift
│ │ │ ├── LinePageModels.swift
│ │ │ └── LinePagePersistence.swift
│ │ └── Tests/
│ │ └── LinePagePersistenceTests.swift
│ ├── NetworkSupport/
│ │ ├── Sources/
│ │ │ ├── NetworkError.swift
│ │ │ ├── NetworkManager.swift
│ │ │ └── NetworkSession.swift
│ │ └── Tests/
│ │ └── NetworkManagerTests.swift
│ ├── NetworkSupportFake/
│ │ └── NetworkSessionFake.swift
│ ├── NotePersistence/
│ │ ├── Sources/
│ │ │ ├── CoreDataNotePersistence.swift
│ │ │ ├── CoreDataNoteUniquifier.swift
│ │ │ ├── NotePersistence.swift
│ │ │ └── NotePersistenceModel.swift
│ │ └── Tests/
│ │ ├── CoreDataNotePersistenceTests.swift
│ │ └── CoreDataNoteUniquifierTests.swift
│ ├── PageBookmarkPersistence/
│ │ ├── Sources/
│ │ │ ├── CoreDataPageBookmarkPersistence.swift
│ │ │ ├── CoreDataPageBookmarkUniquifier.swift
│ │ │ ├── MobileSyncPageBookmarkPersistence.swift
│ │ │ ├── PageBookmarkPersistence.swift
│ │ │ └── PageBookmarkPersistenceModel.swift
│ │ └── Tests/
│ │ └── CoreDataPageBookmarkPersistenceTests.swift
│ ├── SQLitePersistence/
│ │ ├── Sources/
│ │ │ ├── DatabaseConnection.swift
│ │ │ └── PersistenceError.swift
│ │ └── Tests/
│ │ └── DatabaseConnectionTests.swift
│ ├── SyncedPageBookmarkPersistence/
│ │ ├── Sources/
│ │ │ ├── GRDBSyncedPageBookmarkPersistence.swift
│ │ │ ├── SyncedPageBookmarkPersistence.swift
│ │ │ └── SyncedPageBookmarkPersistenceModel.swift
│ │ └── Tests/
│ │ └── GRDBSyncedPageBookmarkPersistenceTests.swift
│ ├── TranslationPersistence/
│ │ ├── ActiveTranslationsPersistence.swift
│ │ └── GRDBActiveTranslationsPersistence.swift
│ ├── VerseTextPersistence/
│ │ ├── DatabaseVersionPersistence.swift
│ │ ├── GRDBDatabaseVersionPersistence.swift
│ │ ├── GRDBVerseTextPersistence.swift
│ │ └── VerseTextPersistence.swift
│ ├── WordFramePersistence/
│ │ ├── GRDBWordFramePersistence.swift
│ │ └── WordFramePersistence.swift
│ └── WordTextPersistence/
│ ├── GRDBWordTextPersistence.swift
│ └── WordTextPersistence.swift
├── Documentation/
│ └── MobileSyncLocalDevelopment.md
├── Domain/
│ ├── AnnotationsService/
│ │ ├── Sources/
│ │ │ ├── AnalyticsLibrary+Events.swift
│ │ │ ├── LastPageService.swift
│ │ │ ├── LastPageUpdater.swift
│ │ │ ├── NoteService.swift
│ │ │ ├── PageBookmarkService.swift
│ │ │ └── QuranHighlightsService.swift
│ │ └── Tests/
│ │ └── EmptyTests.swift
│ ├── AudioTimingService/
│ │ └── ReciterTimingRetriever.swift
│ ├── AudioUpdater/
│ │ ├── Sources/
│ │ │ ├── AudioUpdate.swift
│ │ │ ├── AudioUpdatePreferences.swift
│ │ │ ├── AudioUpdater.swift
│ │ │ ├── AudioUpdatesNetworkManager.swift
│ │ │ └── MD5Calculator.swift
│ │ └── Tests/
│ │ └── AudioUpdaterTests.swift
│ ├── ImageService/
│ │ ├── Sources/
│ │ │ ├── ImageDataService.swift
│ │ │ ├── LinePageAssetService.swift
│ │ │ ├── LinePageGeometry.swift
│ │ │ └── LinePageWordFrameAdapter.swift
│ │ └── Tests/
│ │ ├── ImageDataServiceTests.swift
│ │ ├── LinePageAssetServiceTests.swift
│ │ ├── LinePageGeometryTests.swift
│ │ ├── LinePageWordFrameAdapterTests.swift
│ │ ├── WordFrameTests.swift
│ │ └── __Snapshots__/
│ │ └── ImageDataServiceTests/
│ │ ├── testGettingImageAtPage1.2.json
│ │ ├── testGettingImageAtPage3.2.json
│ │ └── testGettingImageAtPage604.2.json
│ ├── QuranAudioKit/
│ │ ├── Sources/
│ │ │ ├── AudioPlayer/
│ │ │ │ ├── GaplessAudioRequestBuilder.swift
│ │ │ │ ├── GappedAudioRequestBuilder.swift
│ │ │ │ ├── QuranAudioPlayer.swift
│ │ │ │ └── QuranAudioRequestBuilder.swift
│ │ │ ├── Dependencies/
│ │ │ │ └── QueuingPlayer.swift
│ │ │ ├── Downloads/
│ │ │ │ ├── Download+Types.swift
│ │ │ │ └── QuranAudioDownloader.swift
│ │ │ └── Preferences/
│ │ │ ├── AudioEnd+Localization.swift
│ │ │ ├── AudioPreferences.swift
│ │ │ └── PreferencesLastAyahFinder.swift
│ │ └── Tests/
│ │ ├── AudioRequest+Extension.swift
│ │ ├── GaplessAudioRequestBuilderTests.swift
│ │ ├── GappedAudioRequestBuilderTests.swift
│ │ ├── QueuePlayerFake.swift
│ │ ├── QuranAudioDownloaderTests.swift
│ │ ├── QuranAudioPlayerDelegateClosures.swift
│ │ ├── QuranAudioPlayerTests.swift
│ │ └── __Snapshots__/
│ │ └── QuranAudioPlayerTests/
│ │ ├── testPlayingDownloadedGaplessReciter1FullSura.1.json
│ │ ├── testPlayingDownloadedGaplessReciter1SuraEndsEarly.1.json
│ │ ├── testPlayingDownloadedGaplessReciter1SurasHasEndTimestamp.1.json
│ │ ├── testPlayingDownloadedGaplessReciter1SurasHasEndTimestampStopBeforeEnd.1.json
│ │ ├── testPlayingDownloadedGaplessReciter2Suras1stSuraHasEndTimestamp.1.json
│ │ ├── testPlayingDownloadedGaplessReciter3FullSura.1.json
│ │ ├── testPlayingDownloadedGappedReciter1FullSura.1.json
│ │ ├── testPlayingDownloadedGappedReciter1SuraEndsEarly.1.json
│ │ ├── testPlayingDownloadedGappedReciter3FullSura.1.json
│ │ └── testPlayingDownloadedGappedReciterAtTawbah.1.json
│ ├── QuranResources/
│ │ └── QuranResources.swift
│ ├── QuranTextKit/
│ │ ├── Sources/
│ │ │ ├── Localization/
│ │ │ │ └── QuranKit+Localization.swift
│ │ │ ├── Preferences/
│ │ │ │ ├── FontSizePreferences.swift
│ │ │ │ └── QuranContentStatePreferences.swift
│ │ │ ├── Search/
│ │ │ │ ├── Recents/
│ │ │ │ │ └── SearchRecentsService.swift
│ │ │ │ └── Searchers/
│ │ │ │ ├── CompositeSearcher.swift
│ │ │ │ ├── NumberSearcher.swift
│ │ │ │ ├── PersistenceSearcher.swift
│ │ │ │ ├── SearchTerm.swift
│ │ │ │ ├── Searcher.swift
│ │ │ │ ├── SuraSearcher.swift
│ │ │ │ └── TranslationSearcher.swift
│ │ │ ├── ShareableText/
│ │ │ │ └── ShareableVerseTextRetriever.swift
│ │ │ ├── TranslationText/
│ │ │ │ └── QuranTextDataService.swift
│ │ │ └── TwoPages/
│ │ │ └── TwoPagesUtils.swift
│ │ └── Tests/
│ │ ├── CompositeSearcherTests.swift
│ │ ├── Encoding.swift
│ │ ├── QuartersDataRetrieverTests.swift
│ │ ├── QuranTextDataServiceTests.swift
│ │ ├── SearchRecentsServiceTests.swift
│ │ ├── ShareableVerseTextRetrieverTests.swift
│ │ ├── TestData.swift
│ │ ├── TwoPagesUtilsTests.swift
│ │ └── __Snapshots__/
│ │ ├── CompositeSearcherTests/
│ │ │ ├── testMatchArabicQuran.1.json
│ │ │ ├── testMatchArabicQuran.2.json
│ │ │ ├── testMatchArabicSuraName.1.json
│ │ │ ├── testMatchArabicSuraName.2.json
│ │ │ ├── testMatchMultipleSuras.1.json
│ │ │ ├── testMatchMultipleSuras.2.json
│ │ │ ├── testMatchOneSura.1.json
│ │ │ ├── testMatchOneSura.2.json
│ │ │ ├── testMatchSuraAndQuran.1.json
│ │ │ ├── testMatchSuraAndQuran.2.json
│ │ │ ├── testMatchSuraAndQuran.3.json
│ │ │ ├── testMatchSuraAndQuran.4.json
│ │ │ ├── testMatchSuraAndQuran.5.json
│ │ │ ├── testMatchSuraAndQuran.6.json
│ │ │ ├── testMatchSuraAndQuranWithIncorrectTashkeel.1.json
│ │ │ ├── testMatchSuraAndQuranWithIncorrectTashkeel.2.json
│ │ │ ├── testMatchTranslation.1.json
│ │ │ ├── testMatchTranslation.2.json
│ │ │ ├── testNumbers.1.json
│ │ │ ├── testNumbers.2.json
│ │ │ └── testNumbers.3.json
│ │ └── SearchRecentsServiceTests/
│ │ ├── testPopularTerms.1.json
│ │ └── testPopularTerms.2.json
│ ├── ReadingService/
│ │ ├── Sources/
│ │ │ ├── ReadingPreferences.swift
│ │ │ ├── ReadingRemoteResources.swift
│ │ │ ├── ReadingResourceDownloader.swift
│ │ │ └── ReadingResourcesService.swift
│ │ └── Tests/
│ │ ├── ReadingRemoteResourcesFake.swift
│ │ └── ReadingResourcesServiceTests.swift
│ ├── ReciterService/
│ │ ├── Sources/
│ │ │ ├── AudioFileListRetriever.swift
│ │ │ ├── AudioUnzipper.swift
│ │ │ ├── DownloadedRecitersService.swift
│ │ │ ├── RecentRecitersService.swift
│ │ │ ├── Reciter+Localization.swift
│ │ │ ├── ReciterAudioDeleter.swift
│ │ │ ├── ReciterDataRetriever.swift
│ │ │ ├── ReciterPreferences.swift
│ │ │ └── ReciterSizeInfoRetriever.swift
│ │ └── Tests/
│ │ ├── DownloadedRecitersServiceTests.swift
│ │ ├── RecentRecitersServiceTests.swift
│ │ └── ReciterSizeInfoRetrieverTests.swift
│ ├── ReciterServiceFake/
│ │ ├── Reciter+Fixture.swift
│ │ └── Reciter+Preparation.swift
│ ├── SettingsService/
│ │ ├── ReviewPersistence.swift
│ │ └── ReviewService.swift
│ ├── TestResources/
│ │ └── TestResources.swift
│ ├── TranslationService/
│ │ ├── Sources/
│ │ │ ├── LocalTranslationsRetriever.swift
│ │ │ ├── SelectedTranslationsPreferences.swift
│ │ │ ├── TranslationDeleter.swift
│ │ │ ├── TranslationNetworkManager.swift
│ │ │ ├── TranslationUnzipper.swift
│ │ │ ├── TranslationsDownloader.swift
│ │ │ ├── TranslationsParser.swift
│ │ │ ├── TranslationsRepository.swift
│ │ │ └── TranslationsVersionUpdater.swift
│ │ └── Tests/
│ │ ├── LocalTranslationsRetrieverTests.swift
│ │ ├── SelectedTranslationsPreferencesTests.swift
│ │ ├── TranslationDeleterTests.swift
│ │ ├── TranslationsDownloaderTests.swift
│ │ └── TranslationsRepositoryTests.swift
│ ├── TranslationServiceFake/
│ │ ├── LocalTranslationsFake.swift
│ │ └── TranslationTestData.swift
│ ├── WordFrameService/
│ │ ├── WordFrame+Extension.swift
│ │ └── WordFrameProcessor.swift
│ └── WordTextService/
│ ├── Sources/
│ │ ├── WordTextPreferences.swift
│ │ └── WordTextService.swift
│ └── Tests/
│ └── WordTextServiceTests.swift
├── Example/
│ ├── QuranEngineApp/
│ │ ├── Classes/
│ │ │ ├── Analytics.swift
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Container.swift
│ │ │ ├── QuranEngineApp-Bridging-Header.h
│ │ │ └── SceneDelegate.swift
│ │ ├── Info.plist
│ │ └── Resources/
│ │ ├── Assets.xcassets/
│ │ │ ├── AccentColor.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── app-image.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── quran-engine2.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── readings/
│ │ │ ├── Contents.json
│ │ │ ├── hafs_1405.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── hafs_1421.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── hafs_1439.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── hafs_1440.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── hafs_1441.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── tajweed.imageset/
│ │ │ └── Contents.json
│ │ ├── Base.lproj/
│ │ │ └── LaunchScreen.storyboard
│ │ └── reciters.plist
│ └── QuranEngineApp.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata/
│ └── xcschemes/
│ └── QuranEngineApp.xcscheme
├── Features/
│ ├── AdvancedAudioOptionsFeature/
│ │ ├── AdvancedAudioOptions.swift
│ │ ├── AdvancedAudioOptionsBuilder.swift
│ │ ├── AdvancedAudioOptionsView.swift
│ │ ├── AdvancedAudioOptionsViewModel.swift
│ │ ├── AdvancedAudioVersesViewController.swift
│ │ └── Runs++.swift
│ ├── AppDependencies/
│ │ └── AppDependencies.swift
│ ├── AppMigrationFeature/
│ │ ├── FileSystemMigrator.swift
│ │ ├── MigrationView.swift
│ │ ├── MigrationViewController.swift
│ │ └── RecitersPathMigrator.swift
│ ├── AppStructureFeature/
│ │ ├── App/
│ │ │ ├── AppBuilder.swift
│ │ │ ├── AppInteractor.swift
│ │ │ └── AppViewController.swift
│ │ ├── Common/
│ │ │ ├── TabBuilder.swift
│ │ │ ├── TabInteractor.swift
│ │ │ └── TabViewController.swift
│ │ ├── Launch/
│ │ │ ├── LaunchBuilder.swift
│ │ │ └── LaunchStartup.swift
│ │ └── Tabs/
│ │ ├── BookmarksTab.swift
│ │ ├── HomeTab.swift
│ │ ├── NotesTab.swift
│ │ ├── SearchTab.swift
│ │ └── SettingsTab.swift
│ ├── AudioBannerFeature/
│ │ ├── AudioBannerBuilder.swift
│ │ ├── AudioBannerView.swift
│ │ ├── AudioBannerViewModel.swift
│ │ └── RemoteCommandsHandler.swift
│ ├── AudioDownloadsFeature/
│ │ ├── AudioDownloadItem.swift
│ │ ├── AudioDownloadsBuilder.swift
│ │ ├── AudioDownloadsView.swift
│ │ ├── AudioDownloadsViewController.swift
│ │ └── AudioDownloadsViewModel.swift
│ ├── AyahMenuFeature/
│ │ ├── AyahMenuBuilder.swift
│ │ ├── AyahMenuViewController.swift
│ │ └── AyahMenuViewModel.swift
│ ├── BookmarksFeature/
│ │ ├── Sources/
│ │ │ ├── BookmarksBuilder.swift
│ │ │ ├── BookmarksPreferences.swift
│ │ │ ├── BookmarksView.swift
│ │ │ ├── BookmarksViewController.swift
│ │ │ └── BookmarksViewModel.swift
│ │ └── Tests/
│ │ └── BookmarksViewModelTests.swift
│ ├── FeaturesSupport/
│ │ ├── Analytics.Screen.swift
│ │ ├── CommonAnalytics.swift
│ │ └── QuranNavigator.swift
│ ├── HomeFeature/
│ │ ├── HomeBuilder.swift
│ │ ├── HomePreferences.swift
│ │ ├── HomeView.swift
│ │ ├── HomeViewController.swift
│ │ ├── HomeViewModel.swift
│ │ └── QuarterItem.swift
│ ├── MoreMenuFeature/
│ │ ├── MoreMenuBuilder.swift
│ │ ├── MoreMenuController.swift
│ │ ├── MoreMenuModel.swift
│ │ ├── MoreMenuViewModel.swift
│ │ └── views/
│ │ ├── FontSizeStepper.swift
│ │ ├── MoreMenuDeviceRotation.swift
│ │ ├── MoreMenuEmpty.swift
│ │ ├── MoreMenuFontSize.swift
│ │ ├── MoreMenuModeSelector.swift
│ │ ├── MoreMenuThemeSettings.swift
│ │ ├── MoreMenuThemeSettingsViewModel.swift
│ │ ├── MoreMenuTranslationSelector.swift
│ │ ├── MoreMenuTwoPages.swift
│ │ ├── MoreMenuVerticalScrolling.swift
│ │ ├── MoreMenuView.swift
│ │ ├── MoreMenuWordPointer.swift
│ │ └── MoreMenuWordPointerType.swift
│ ├── NoteEditorFeature/
│ │ ├── NoteEditorBuilder.swift
│ │ ├── NoteEditorInteractor.swift
│ │ └── NoteEditorViewController.swift
│ ├── NotesFeature/
│ │ ├── NoteItem.swift
│ │ ├── NotesBuilder.swift
│ │ ├── NotesView.swift
│ │ ├── NotesViewController.swift
│ │ └── NotesViewModel.swift
│ ├── QuranContentFeature/
│ │ ├── ContentBuilder.swift
│ │ ├── ContentViewController.swift
│ │ ├── ContentViewModel.swift
│ │ ├── PagesView.swift
│ │ └── QuranInput.swift
│ ├── QuranImageFeature/
│ │ ├── ContentImageBuilder.swift
│ │ ├── ContentImageView.swift
│ │ ├── ContentImageViewModel.swift
│ │ ├── ContentLineView.swift
│ │ └── ContentLineViewModel.swift
│ ├── QuranPagesFeature/
│ │ ├── Page+Localization.swift
│ │ ├── PageGeometryActions.swift
│ │ └── QuranPaginationView.swift
│ ├── QuranTranslationFeature/
│ │ ├── ContentTranslationBuilder.swift
│ │ ├── ContentTranslationView.swift
│ │ ├── ContentTranslationViewModel.swift
│ │ ├── Translation+UI.swift
│ │ ├── TranslationFootnote.swift
│ │ ├── TranslationItem+View.swift
│ │ ├── TranslationItem.swift
│ │ └── TranslationURL.swift
│ ├── QuranViewFeature/
│ │ ├── QuranBuilder.swift
│ │ ├── QuranInteractor.swift
│ │ ├── QuranView.swift
│ │ └── QuranViewController.swift
│ ├── ReadingSelectorFeature/
│ │ ├── ReadingSelectorBuilder.swift
│ │ ├── ReadingSelectorViewController.swift
│ │ ├── ReadingSelectorViewModel.swift
│ │ └── View/
│ │ ├── Reading+Resources.swift
│ │ ├── ReadingDetails.swift
│ │ ├── ReadingImage.swift
│ │ ├── ReadingImageView.swift
│ │ ├── ReadingInfo.swift
│ │ ├── ReadingItem.swift
│ │ └── ReadingSelector.swift
│ ├── ReciterListFeature/
│ │ ├── ReciterListBuilder.swift
│ │ ├── ReciterListView.swift
│ │ ├── ReciterListViewController.swift
│ │ └── ReciterListViewModel.swift
│ ├── SearchFeature/
│ │ ├── SearchBuilder.swift
│ │ ├── SearchTypes.swift
│ │ ├── SearchView.swift
│ │ ├── SearchViewController.swift
│ │ └── SearchViewModel.swift
│ ├── SettingsFeature/
│ │ ├── Sources/
│ │ │ ├── ContactUsService.swift
│ │ │ ├── Diagnostics/
│ │ │ │ ├── DiagnosticsBuilder.swift
│ │ │ │ ├── DiagnosticsService.swift
│ │ │ │ ├── DiagnosticsView.swift
│ │ │ │ └── DiagnosticsViewModel.swift
│ │ │ ├── SettingsBuilder.swift
│ │ │ ├── SettingsRootView.swift
│ │ │ ├── SettingsRootViewModel.swift
│ │ │ └── UIViewController+Share.swift
│ │ └── Tests/
│ │ └── SettingsRootViewModelTests.swift
│ ├── TranslationVerseFeature/
│ │ ├── TranslationVerseBuilder.swift
│ │ ├── TranslationVerseView.swift
│ │ ├── TranslationVerseViewController.swift
│ │ └── TranslationVerseViewModel.swift
│ ├── TranslationsFeature/
│ │ ├── TranslationItem.swift
│ │ ├── TranslationsListBuilder.swift
│ │ ├── TranslationsListView.swift
│ │ ├── TranslationsListViewModel.swift
│ │ └── TranslationsViewController.swift
│ ├── WhatsNewFeature/
│ │ ├── AppWhatsNew.swift
│ │ ├── AppWhatsNewController.swift
│ │ ├── AppWhatsNewVersionStore.swift
│ │ └── whats-new.plist
│ └── WordPointerFeature/
│ ├── WordPointerBuilder.swift
│ ├── WordPointerViewController.swift
│ └── WordPointerViewModel.swift
├── LICENSE
├── Makefile
├── Model/
│ ├── QuranAnnotations/
│ │ ├── LastPage.swift
│ │ ├── Note.swift
│ │ ├── PageBookmark.swift
│ │ └── QuranHighlights.swift
│ ├── QuranAudio/
│ │ ├── AudioDownloadedSize.swift
│ │ ├── AudioEnd.swift
│ │ ├── AyahTiming.swift
│ │ ├── RangeTiming.swift
│ │ ├── Reciter+URLs.swift
│ │ ├── Reciter.swift
│ │ ├── SuraTiming.swift
│ │ └── Timing.swift
│ ├── QuranGeometry/
│ │ ├── AyahNumberLocation.swift
│ │ ├── ImagePage.swift
│ │ ├── SuraHeaderLocation.swift
│ │ ├── WordFrame.swift
│ │ ├── WordFrameCollection.swift
│ │ ├── WordFrameLine.swift
│ │ └── WordFrameScale.swift
│ ├── QuranKit/
│ │ ├── Sources/
│ │ │ ├── AyahNumber.swift
│ │ │ ├── Hizb.swift
│ │ │ ├── Juz.swift
│ │ │ ├── LastAyahFinder/
│ │ │ │ ├── JuzBasedLastAyahFinder.swift
│ │ │ │ ├── LastAyahFinder.swift
│ │ │ │ ├── PageBasedLastAyahFinder.swift
│ │ │ │ ├── QuranBasedLastAyahFinder.swift
│ │ │ │ └── SuraBasedLastAyahFinder.swift
│ │ │ ├── LazyAtomic.swift
│ │ │ ├── Navigatable.swift
│ │ │ ├── Page.swift
│ │ │ ├── Quarter.swift
│ │ │ ├── Quran.swift
│ │ │ ├── QuranGroup.swift
│ │ │ ├── QuranValueStorage.swift
│ │ │ ├── Reading.swift
│ │ │ ├── ReadingInfo/
│ │ │ │ ├── Madani1405QuranReadingInfoRawData.swift
│ │ │ │ ├── Madani1440QuranReadingInfoRawData.swift
│ │ │ │ └── QuranReadingInfoRawData.swift
│ │ │ ├── Sura.swift
│ │ │ ├── Util.swift
│ │ │ └── Word.swift
│ │ └── Tests/
│ │ ├── AyahNumberTests.swift
│ │ ├── HizbTests.swift
│ │ ├── JuzTests.swift
│ │ ├── PageTests.swift
│ │ ├── QuarterTests.swift
│ │ └── SuraTests.swift
│ └── QuranText/
│ ├── FontSize.swift
│ ├── QuranMode.swift
│ ├── SearchResults.swift
│ ├── TranslatedVerses.swift
│ ├── Translation+URLs.swift
│ ├── Translation.swift
│ └── WordTextType.swift
├── Package.resolved
├── Package.swift
├── QuranEngine-Package.xctestplan
├── README.md
├── UI/
│ ├── NoorFont/
│ │ └── FontName.swift
│ ├── NoorUI/
│ │ ├── BaseControllers/
│ │ │ ├── BaseNavigationController.swift
│ │ │ ├── BaseViewController.swift
│ │ │ └── UIViewController+Error.swift
│ │ ├── Colors/
│ │ │ ├── Color+extension.swift
│ │ │ └── Colors.xcassets/
│ │ │ ├── Contents.json
│ │ │ ├── appTint.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── theme-calm-bg.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── theme-calm-text.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── theme-focus-bg.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── theme-focus-text.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── theme-original-bg.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── theme-original-text.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── theme-paper-bg.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── theme-paper-text.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── theme-quiet-bg.colorset/
│ │ │ │ └── Contents.json
│ │ │ └── theme-quiet-text.colorset/
│ │ │ └── Contents.json
│ │ ├── Components/
│ │ │ ├── ActiveRoundedButton.swift
│ │ │ ├── AppStoreDownloadButton.swift
│ │ │ ├── AppearanceModeSelector.swift
│ │ │ ├── ChoicesView.swift
│ │ │ ├── DataUnavailableView.swift
│ │ │ ├── DisclosureIndicator.swift
│ │ │ ├── DropdownButton.swift
│ │ │ ├── ErrorAlertModifier.swift
│ │ │ ├── List/
│ │ │ │ ├── NoorList.swift
│ │ │ │ ├── NoorListItem.swift
│ │ │ │ └── NoorSection.swift
│ │ │ ├── LoadingView.swift
│ │ │ ├── MultipartText.swift
│ │ │ ├── ProminentRoundedButton.swift
│ │ │ └── ThemeStyleSelector.swift
│ │ ├── Features/
│ │ │ ├── AudioBanner/
│ │ │ │ └── AudioBannerViewUI.swift
│ │ │ ├── AyahMenu/
│ │ │ │ ├── AyahMenuUI.swift
│ │ │ │ └── AyahMenuView.swift
│ │ │ ├── Content/
│ │ │ │ └── ContentStatusView.swift
│ │ │ ├── Note/
│ │ │ │ ├── EditableNote.swift
│ │ │ │ ├── Note.Color++.swift
│ │ │ │ ├── NoteCircle.swift
│ │ │ │ ├── NoteEditorView.swift
│ │ │ │ └── UIViewController+Note.swift
│ │ │ └── Quran/
│ │ │ ├── AdaptiveImageScrollView.swift
│ │ │ ├── AdaptiveQuranScrollView.swift
│ │ │ ├── ImageDecorationsView.swift
│ │ │ ├── QuranArabicText.swift
│ │ │ ├── QuranPageFooter.swift
│ │ │ ├── QuranPageHeader.swift
│ │ │ ├── QuranPageSeparators.swift
│ │ │ ├── QuranScrollingViewModifier.swift
│ │ │ ├── QuranSuraName.swift
│ │ │ ├── QuranThemedImage.swift
│ │ │ ├── QuranTranslationReferenceVerse.swift
│ │ │ ├── QuranTranslationTextChunk.swift
│ │ │ ├── QuranTranslatorName.swift
│ │ │ └── QuranVerseSeparator.swift
│ │ ├── Font/
│ │ │ ├── FontName++.swift
│ │ │ └── FontSize++.swift
│ │ ├── Formatters/
│ │ │ └── TimeAgo.swift
│ │ ├── Images/
│ │ │ ├── Images.xcassets/
│ │ │ │ ├── Contents.json
│ │ │ │ ├── ayah-end-marker.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── pointer-25.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── rotate_to_landscape-25.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── rotate_to_portrait-25.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── settings-25.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── settings_filled-25.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── sura_header.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── NoorImage.swift
│ │ │ └── NoorSystemImage.swift
│ │ ├── Miscellaneous/
│ │ │ ├── ContentDimension.swift
│ │ │ ├── Dimensions.swift
│ │ │ ├── ReadableInsetsViewModifier.swift
│ │ │ ├── TestResource+Path.swift
│ │ │ └── ThemedColorInvert.swift
│ │ ├── Pager/
│ │ │ └── PageViewController.swift
│ │ ├── Shapes/
│ │ │ └── Arc.swift
│ │ └── Theme/
│ │ ├── AppearanceModeViews.swift
│ │ ├── QuranHighlights+Theme.swift
│ │ ├── ThemeService.swift
│ │ └── ThemeStyleViews.swift
│ ├── UIx/
│ │ ├── SwiftUI/
│ │ │ ├── CollectionView/
│ │ │ │ ├── CollectionView.swift
│ │ │ │ ├── CollectionViewController.swift
│ │ │ │ ├── CollectionViewDataSource.swift
│ │ │ │ ├── CollectionViewReader.swift
│ │ │ │ ├── CollectionViewScroller.swift
│ │ │ │ ├── HostingCollectionViewCell.swift
│ │ │ │ └── ListSection.swift
│ │ │ ├── Epoxy/
│ │ │ │ ├── CollectionViewScrollToItemHelper.swift
│ │ │ │ ├── DataIDProviding.swift
│ │ │ │ ├── EpoxyIntrinsicContentSizeInvalidator.swift
│ │ │ │ ├── EpoxySwiftUIHostingController.swift
│ │ │ │ ├── EpoxySwiftUIHostingView.swift
│ │ │ │ ├── EpoxySwiftUILayoutMargins.swift
│ │ │ │ ├── README.md
│ │ │ │ └── _Compatibility.swift
│ │ │ ├── Miscellaneous/
│ │ │ │ ├── AsyncAction.swift
│ │ │ │ ├── BackgroundHighlightingStyle.swift
│ │ │ │ ├── CustomButtonStyle.swift
│ │ │ │ ├── EdgeInsets++.swift
│ │ │ │ ├── EditController.swift
│ │ │ │ ├── SwiftUIColor+extension.swift
│ │ │ │ ├── UIKitNavigator.swift
│ │ │ │ ├── View+Task.swift
│ │ │ │ ├── View+URL.swift
│ │ │ │ ├── View+onSizeChange.swift
│ │ │ │ └── WrappingHStack.swift
│ │ │ ├── Mutate.swift
│ │ │ ├── SingleChoice/
│ │ │ │ ├── SingleChoiceRow.swift
│ │ │ │ └── SingleChoiceSelector.swift
│ │ │ ├── Toast/
│ │ │ │ ├── Toast.swift
│ │ │ │ └── ToastEnvironmentKey.swift
│ │ │ └── Views/
│ │ │ ├── AttributedString++.swift
│ │ │ ├── AutoSizingHostingController.swift
│ │ │ ├── AutoUpdatingPreferredContentSizeHostingController.swift
│ │ │ ├── CloseButton.swift
│ │ │ ├── CocoaNavigationBar.swift
│ │ │ ├── CocoaNavigationView.swift
│ │ │ ├── CollectionTracker.swift
│ │ │ ├── HostingCell.swift
│ │ │ ├── InvertInDarkModeModifier.swift
│ │ │ ├── PopoverNavigationController.swift
│ │ │ ├── PreferredContentSizeMatchesScrollView.swift
│ │ │ ├── SheetPresentationDetents.swift
│ │ │ ├── SingleAxisGeometryReader.swift
│ │ │ ├── StaticViewControllerRepresentable.swift
│ │ │ ├── TextAlignmentModifier.swift
│ │ │ ├── TextView.swift
│ │ │ ├── UIViewControllerReader.swift
│ │ │ └── WindowSafeAreaInsetsReaderViewModifier.swift
│ │ └── UIKit/
│ │ ├── DataSources/
│ │ │ ├── DefaultSection.swift
│ │ │ └── NSDiffableDataSourceSnapshot++.swift
│ │ ├── Extensions/
│ │ │ ├── CALayer+Extension.swift
│ │ │ ├── SegmentedControl+Extension.swift
│ │ │ ├── String+Size.swift
│ │ │ ├── UIBezierPath+Extension.swift
│ │ │ ├── UIColor+Extension.swift
│ │ │ ├── UIImage+Extension.swift
│ │ │ ├── UITableView+Extension.swift
│ │ │ ├── UIView+AutoLayout.swift
│ │ │ ├── UIView+Extension.swift
│ │ │ ├── UIViewController+Extensions.swift
│ │ │ └── UIWIndow+Extensions.swift
│ │ ├── Miscellaneous/
│ │ │ ├── NSDirectionalEdgeInsets++.swift
│ │ │ ├── PresentationsMonitor.swift
│ │ │ └── ScrollViewPageBehavior.swift
│ │ ├── Popover/
│ │ │ ├── PhonePopoverPresenter.swift
│ │ │ └── PopoverPresenter.swift
│ │ └── Views/
│ │ ├── BackgroundColorButton.swift
│ │ ├── ByPassTouchesView.swift
│ │ ├── CircleView.swift
│ │ ├── CircularView.swift
│ │ ├── GradientView.swift
│ │ ├── MagnifyingGlass.swift
│ │ ├── RoundedShadowView.swift
│ │ ├── ScrollViewController.swift
│ │ ├── SearchControllerWithNoCancelButton.swift
│ │ └── TwoLineNavigationTitleView.swift
│ └── ViewConstrainer/
│ ├── GroupConstrainer.swift
│ ├── SingleConstrainer.swift
│ ├── UIView+Const.swift
│ └── ViewConstrainer.swift
├── agents.md
└── codecov.yml
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- main
pull_request:
jobs:
Package:
runs-on: macos-26
steps:
- uses: actions/checkout@v2
- name: Setting up Xcode
run: sudo xcode-select -s "/Applications/Xcode_26.2.app"
- name: Install xcbeautify
run: brew install xcbeautify
- name: Run tests
run: make test
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
ExampleApp:
runs-on: macos-26
steps:
- uses: actions/checkout@v2
- name: Setting up Xcode
run: sudo xcode-select -s "/Applications/Xcode_26.2.app"
- name: Install xcbeautify
run: brew install xcbeautify
- name: Build
run: make build-example
SwiftFormat:
runs-on: macos-26
steps:
- uses: actions/checkout@v2
- name: Setting up Xcode
run: sudo xcode-select -s "/Applications/Xcode_26.2.app"
- name: SwiftFormat
run: make format-lint
================================================
FILE: .gitignore
================================================
# Xcode
#
build/
.DS_Store
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
!default.xcworkspace
xcuserdata
#Breakpoints_v2.xcbkptlist
upload_dsym_results
xcschememanagement.plist
*.xccheckout
profile
*.moved-aside
DerivedData
.idea/
*.hmap
*.ipa
*.xcuserstate
package.json
# Swift Package Manager. See: https://github.com/apple/swift-package-manager/blob/main/Sources/Workspace/InitPackage.swift#L381
.DS_Store
.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
/BuildTools/
================================================
FILE: .swift-version
================================================
5.10
================================================
FILE: .swiftformat
================================================
# Don't format
--exclude .build,UI/UIx/SwiftUI/Epoxy,BuildTools
# Options
--importgrouping testable-bottom # sortedImports
--wraparguments before-first # wrapArguments
--wrapparameters before-first # wrapArguments
--funcattributes prev-line # wrapAttributes
--typeattributes prev-line # wrapAttributes
--beforemarks typealias,struct # organizeDeclarations
--structthreshold 70
--classthreshold 70
--enumthreshold 70
# Disabled
--disable andOperator
--disable emptyBraces
--disable extensionAccessControl
--disable hoistAwait
--disable hoistPatternLet
--disable hoistTry
--disable redundantType
--disable unusedArguments
--disable redundantReturn
# Enabled
--enable blankLineAfterImports
--enable blankLinesBetweenImports
--enable blockComments
--enable isEmpty
--enable organizeDeclarations
================================================
FILE: .swiftlint.yml
================================================
excluded:
- .build
- Tests/QuranTextKitTests/Mocks.swift
disabled_rules:
- colon
- for_where
- syntactic_sugar
- nesting
- opening_brace
- trailing_comma # should be useful for git
- multiple_closures_with_trailing_closure
- todo
opt_in_rules:
#NOT NEEDED - anonymous_argument_in_multiline_closure
- array_init
#NOT NEEDED - attributes
#NOT NEEDED - balanced_xctest_lifecycle
#NOT NEEDED - closure_end_indentation
- closure_spacing
#NOT NEEDED - collection_alignment
#NOT NEEDED - conditional_returns_on_newline
- contains_over_filter_count
- contains_over_filter_is_empty
- contains_over_first_not_nil
- contains_over_range_nil_comparison
#NOT NEEDED - convenience_type
#NOT NEEDED - discouraged_assert
#NOT NEEDED - discouraged_none_name
- discarded_notification_center_observer
#NOT NEEDED - discouraged_object_literal
- discouraged_optional_boolean
#NOT NEEDED for now - discouraged_optional_collection
- empty_count
- empty_string
- empty_xctest_method
- enum_case_associated_values_count
#NOT NEEDED - expiring_todo
#NOT NEEDED - explicit_acl
#NOT NEEDED - explicit_enum_raw_value
#NOT NEEDED - explicit_self
#NOT NEEDED - explicit_top_level_acl
#NOT NEEDED - explicit_type_interface
- explicit_init
#NOT NEEDED - explicit_type_interface
#NOT NEEDED - extension_access_modifier
#NOT NEEDED - fallthrough
- fatal_error_message
#- file_header
#NOT NEEDED - file_name
- file_name_no_space
#NOT NEEDED - file_types_order
- flatmap_over_map_reduce
- first_where
# Frequently used - force_unwrapping
#NOT NEEDED - function_default_parameter_at_end
#NOT NEEDED - implicit_return
- ibinspectable_in_extension
- identical_operands
- implicit_return
- implicitly_unwrapped_optional
#NOT NEEDED - indentation_width
#NOT NEEDED - joined_default_parameter
- last_where
#NOT NEEDED - legacy_multiple
#NOT NEEDED - legacy_objc_type
- legacy_random
#NOT NEEDED - let_var_whitespace
- literal_expression_end_indentation
#NOT NEEDED - lower_acl_than_parent
#NOT NEEDED - missing_docs
- modifier_order
#NOT NEEDED - multiline_arguments
#NOT NEEDED - multiline_arguments_brackets
- multiline_function_chains
#NOT NEEDED - multiline_literal_brackets
- multiline_parameters
#NOT NEEDED - multiline_parameters_brackets
- multiple_closures_with_trailing_closure
- nesting
- nimble_operator
#NOT NEEDED - no_extension_access_modifier
#NOT NEEDED - no_grouping_extension
- notification_center_detachment
#NOT NEEDED - nslocalizedstring_key
#NOT NEEDED - nslocalizedstring_require_bundle
#NOT NEEDED - number_separator
- object_literal
#NOT NEEDED - opening_brace
#NOT NEEDED - operator_usage_whitespace
- optional_enum_case_matching
- overridden_super_call
- override_in_extension
- pattern_matching_keywords
#NOT NEEDED - prefer_nimble
#NOT NEEDED - prefer_self_in_static_references
#NOT NEEDED - prefer_self_type_over_type_of_self
- prefer_zero_over_explicit_init
#NOT NEEDED - prefixed_toplevel_constant
#NOT NEEDED - private_action
#NOT NEEDED - private_outlet
- private_subject
# // TODO: uncomment - prohibited_interface_builder
- prohibited_super_call
- quick_discouraged_call
- quick_discouraged_focused_test
- quick_discouraged_pending_test
#NOT NEEDED - raw_value_for_camel_cased_codable_enum
- reduce_into
- redundant_nil_coalescing
#NOT NEEDED - redundant_type_annotation
#NOT NEEDED - required_deinit
#NOT CONFIGURED - required_enum_case
- single_test_class
- sorted_first_last
- sorted_imports
- static_operator
- strict_fileprivate
#NOT NEEDED - strong_iboutlet
#NOT NEEDED - switch_case_on_newline
- syntactic_sugar
#NOT NEEDED - test_case_accessibility
#NOT NEEDED - toggle_bool
#NOT NEEDED - trailing_closure
#NOT NEEDED - trailing_comma
#NOT NEEDED - unavailable_function
#NOT NEEDED - unneeded_parentheses_in_closure_argument
- unowned_variable_capture
- untyped_error_in_catch
- vertical_parameter_alignment_on_call
#NOT NEEDED - vertical_whitespace_between_cases
#NOT NEEDED - vertical_whitespace_closing_braces
#NOT NEEDED - vertical_whitespace_opening_braces
#NOT NEEDED - xct_specific_matcher
#NOT NEEDED - yoda_condition
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
- capture_variable
# - explicit_self
- unused_declaration
- unused_import
type_name:
min_length: 2
max_length: 60
identifier_name:
min_length: 1
max_length: 60
file_length: 600
generic_type_name:
max_length: 30
line_length: 150 # Needs to configure it correctly!
type_body_length: 300
function_parameter_count: 10
function_body_length: 80
large_tuple: 4
object_literal:
color_literal: false
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/AllTargetsTests.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/AnnotationsService.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/AppStructureFeature.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/BatchDownloader.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/BookmarksFeature.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/Caching.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/Crashing.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/HomeFeature.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/LastPagePersistence.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/Localization.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/MoreMenuFeature.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/NetworkSupport.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/NoorUI.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/NotePersistence.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/NotesFeature.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/OAuthServiceAppAuthImpl.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/PageBookmarkPersistence.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/Preferences.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/QuranAudioKit.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/QuranEngine-Package.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/QuranImageFeature.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/QuranKit.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/QuranPagesFeature.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/QuranTextKit.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/QuranTextKitTests.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/QuranTranslationFeature.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/ReadingSelectorFeature.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/ReadingService.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/ReciterListFeature.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/SQLitePersistence.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/SearchFeature.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/SettingsFeature.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/SystemDependencies.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/SystemDependenciesFake.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/Timing.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/TranslationService.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/TranslationsFeature.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/UIx.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/Utilities.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/UtilitiesTests.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/VLogging.xcscheme
================================================
================================================
FILE: .swiftpm/xcode/xcshareddata/xcschemes/VersionUpdater.xcscheme
================================================
================================================
FILE: AllTargetsTests/Empty.swift
================================================
//
// Empty.swift
//
//
// Created by Mohamed Afifi on 2023-06-17.
//
import XCTest
/// Empty test class to help ensure we have at least one test case.
///
/// AllTargetsTests is a test target to link all other targets to ensure we produce coverage info for all code.
final class Empty: XCTestCase {
func testEmpty() throws {
}
}
================================================
FILE: Core/Analytics/AnalyticsLibrary.swift
================================================
//
// AnalyticsLibrary.swift
//
//
// Created by Mohamed Afifi on 2023-06-12.
//
public protocol AnalyticsLibrary: Sendable {
func logEvent(_ name: String, value: String)
}
================================================
FILE: Core/AppMigrator/Sources/AppMigrator.swift
================================================
//
// AppMigrator.swift
// Quran
//
// Created by Mohamed Afifi on 9/10/18.
//
// Quran for iOS is a Quran reading application for iOS.
// Copyright (C) 2018 Quran.com
//
import Foundation
import SystemDependencies
import VLogging
public protocol Migrator {
var blocksUI: Bool { get }
var uiTitle: String? { get }
func execute(update: LaunchVersionUpdate) async
}
public enum MigrationStatus: Equatable {
case noMigration
case migrate(blocksUI: Bool, titles: Set)
}
public final class AppMigrator {
// MARK: Lifecycle
public convenience init() {
self.init(bundle: DefaultSystemBundle())
}
public init(bundle: SystemBundle) {
updater = AppVersionUpdater(bundle: bundle)
}
// MARK: Public
public var launchVersion: LaunchVersionUpdate { updater.launchVersion() }
public func register(migrator: Migrator, for version: AppVersion) {
migrators.append((version, migrator))
}
public func migrationStatus() -> MigrationStatus {
let updaters = versionUpdaters()
if updaters.isEmpty {
updater.commitUpdates()
return .noMigration
} else {
let blocksUI = updaters.contains { $0.blocksUI }
let titles = Set(updaters.compactMap(\.uiTitle))
return .migrate(blocksUI: blocksUI, titles: titles)
}
}
public func migrate() async {
let launchVersion = updater.launchVersion()
logger.notice("Version Update: \(launchVersion)")
await withTaskGroup(of: Void.self) { taskGroup in
let updaters = versionUpdaters()
for updater in updaters {
taskGroup.addTask {
await updater.execute(update: launchVersion)
}
}
}
updater.commitUpdates()
}
// MARK: Private
private var migrators: [(AppVersion, Migrator)] = []
private let updater: AppVersionUpdater
private func versionUpdaters() -> [Migrator] {
switch launchVersion {
case .update(let old, _), .firstLaunch(version: let old), .sameVersion(version: let old):
return updaters(for: old)
}
}
private func updaters(for version: String) -> [Migrator] {
migrators // Returns updaters where: oldVersion < updaters.version.
.filter { version.compare($0.0, options: .numeric) == .orderedAscending }
.map { $1 }
}
}
================================================
FILE: Core/AppMigrator/Sources/AppVersionUpdater.swift
================================================
//
// AppVersionUpdater.swift
// Quran
//
// Created by Mohamed Afifi on 5/2/17.
//
// Quran for iOS is a Quran reading application for iOS.
// Copyright (C) 2017 Quran.com
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
import Preferences
import SystemDependencies
public typealias AppVersion = String
public enum LaunchVersionUpdate {
case sameVersion(version: AppVersion)
case firstLaunch(version: AppVersion)
case update(from: AppVersion, to: AppVersion)
}
struct AppVersionPreferences {
// MARK: Lifecycle
private init() {}
// MARK: Internal
static let shared = AppVersionPreferences()
@Preference(appVersion)
var appVersion: String?
static func reset() {
Preferences.shared.removeValueForKey(appVersion)
}
// MARK: Private
private static let appVersion = PreferenceKey(key: "appVersion", defaultValue: nil)
}
struct AppVersionUpdater {
// MARK: Lifecycle
init(bundle: SystemBundle) {
self.bundle = bundle
}
// MARK: Internal
func launchVersion() -> LaunchVersionUpdate {
let current = current
let previous = preferences.appVersion
if let previous {
if previous == current {
return .sameVersion(version: current)
} else {
return .update(from: previous, to: current)
}
} else {
return .firstLaunch(version: current)
}
}
/// eventually we should update the app version
func commitUpdates() {
preferences.appVersion = current
}
// MARK: Private
private let bundle: SystemBundle
private let preferences = AppVersionPreferences.shared
private var current: String {
guard let version = bundle.infoValue(forKey: "CFBundleShortVersionString") as? String else {
fatalError("CFBundleShortVersionString should be set in your main bundle.")
}
return version
}
}
================================================
FILE: Core/AppMigrator/Tests/AppMigratorTests.swift
================================================
//
// AppMigratorTests.swift
//
//
// Created by Mohamed Afifi on 2023-05-17.
//
import SystemDependenciesFake
import XCTest
@testable import AppMigrator
final class AppMigratorTests: XCTestCase {
// MARK: Internal
override func setUp() async throws {
try await super.setUp()
bundle = SystemBundleFake()
service = AppMigrator(bundle: bundle)
worker1 = MigratorTester(blocksUI: true, uiTitle: "Worker 1")
worker2 = MigratorTester(blocksUI: true, uiTitle: "Worker 2")
nonBlockingWorker = MigratorTester(blocksUI: false, uiTitle: nil)
service.register(migrator: worker1, for: "1.16.0")
service.register(migrator: worker2, for: "1.17.0")
service.register(migrator: nonBlockingWorker, for: "1.18.0")
bundle.info["CFBundleShortVersionString"] = "1.18.0"
}
override func tearDown() async throws {
try await super.tearDown()
AppVersionPreferences.reset()
}
func test_futureNewInstallation() async {
await verifyNoMigration()
}
func test_currentNewInstallation() async {
bundle.info["CFBundleShortVersionString"] = "1.18.0"
await verifyNoMigration()
}
func test_sameVersion() async {
preferences.appVersion = "1.18.0"
bundle.info["CFBundleShortVersionString"] = preferences.appVersion
await verifyNoMigration()
}
func test_upgrade_noUpdater() async {
preferences.appVersion = "1.18.0"
bundle.info["CFBundleShortVersionString"] = "1.19.0"
await verifyNoMigration()
}
func test_upgrade_runLastUpdater() async {
preferences.appVersion = "1.17.0"
let status = service.migrationStatus()
XCTAssertEqual(status, .migrate(blocksUI: false, titles: []))
await service.migrate()
XCTAssertNil(worker1.update)
XCTAssertNil(worker2.update)
XCTAssertNotNil(nonBlockingWorker.update)
}
func test_upgrade_runLastTwoUpdaters() async {
preferences.appVersion = "1.16.0"
let status = service.migrationStatus()
XCTAssertEqual(status, .migrate(blocksUI: true, titles: ["Worker 2"]))
await service.migrate()
XCTAssertNil(worker1.update)
XCTAssertNotNil(worker2.update)
XCTAssertNotNil(nonBlockingWorker.update)
}
func test_upgrade_runAllUpdaters() async {
preferences.appVersion = "1.15.0"
let status = service.migrationStatus()
XCTAssertEqual(status, .migrate(blocksUI: true, titles: ["Worker 1", "Worker 2"]))
await service.migrate()
XCTAssertNotNil(worker1.update)
XCTAssertNotNil(worker2.update)
XCTAssertNotNil(nonBlockingWorker.update)
}
// MARK: Private
private var service: AppMigrator!
private let preferences = AppVersionPreferences.shared
private var bundle: SystemBundleFake!
private var worker1: MigratorTester!
private var worker2: MigratorTester!
private var nonBlockingWorker: MigratorTester!
// MARK: - Helpers
private func verifyNoMigration(file: StaticString = #filePath, line: UInt = #line) async {
let status = service.migrationStatus()
XCTAssertEqual(status, .noMigration)
await service.migrate()
XCTAssertNil(worker1.update, file: file, line: line)
XCTAssertNil(worker2.update, file: file, line: line)
XCTAssertNil(nonBlockingWorker.update, file: file, line: line)
}
}
private final class MigratorTester: Migrator {
// MARK: Lifecycle
init(blocksUI: Bool, uiTitle: String?) {
self.blocksUI = blocksUI
self.uiTitle = uiTitle
}
// MARK: Internal
let blocksUI: Bool
let uiTitle: String?
var update: LaunchVersionUpdate?
func execute(update: LaunchVersionUpdate) async {
self.update = update
}
}
================================================
FILE: Core/AsyncUtilitiesForTesting/AsyncAlgorithms++.swift
================================================
//
// AsyncAlgorithms++.swift
//
//
// Created by Mohamed Afifi on 2023-05-28.
//
import AsyncAlgorithms
extension AsyncChannel {
public func next() async -> Element? {
var iterator = makeAsyncIterator()
return await iterator.next()
}
}
extension AsyncChannel where Element == Void {
public func send() async {
await send(())
}
}
================================================
FILE: Core/AsyncUtilitiesForTesting/AsyncAsserts.swift
================================================
//
// AsyncAsserts.swift
//
//
// Created by Mohamed Afifi on 2023-06-03.
//
import Foundation
import XCTest
// Credits to @pointfreeco
// https://github.com/pointfreeco/combine-schedulers
extension Task where Success == Failure, Failure == Never {
public static func megaYield(count: Int = 10) async {
for _ in 1 ... count {
await Task.detached(priority: .background) { await Task.yield() }.value
}
}
}
public func AsyncAssertEqual(
_ expression1: @autoclosure () async throws -> T,
_ expression2: @autoclosure () async throws -> T,
_ message: @autoclosure () -> String = "",
file: StaticString = #filePath,
line: UInt = #line
) async rethrows where T: Equatable {
let e1 = try await expression1()
let e2 = try await expression2()
XCTAssertEqual(e1, e2, message(), file: file, line: line)
}
public func AsyncAssertThrows(
_ expression: @autoclosure () async throws -> Void,
_ expectedError: NSError?,
_ message: @autoclosure () -> String = "Didn't throw",
file: StaticString = #filePath,
line: UInt = #line
) async {
do {
try await expression()
XCTFail(message(), file: file, line: line)
} catch {
if let expectedError {
XCTAssertEqual(error as NSError?, expectedError, message(), file: file, line: line)
}
}
}
public func AsyncUnwrap(
_ expression: @autoclosure () async throws -> T?,
_ message: @autoclosure () -> String = "",
file: StaticString = #filePath,
line: UInt = #line
) async throws -> T {
let value = try await expression()
return try XCTUnwrap(value, message(), file: file, line: line)
}
================================================
FILE: Core/AsyncUtilitiesForTesting/PublisherCollector.swift
================================================
//
// PublisherCollector.swift
//
//
// Created by Mohamed Afifi on 2023-05-30.
//
import Combine
public final class PublisherCollector {
// MARK: Lifecycle
public init(_ publisher: P) where P.Output == T, P.Failure == Never {
cancellable = publisher.sink(receiveValue: { [weak self] item in
self?.items.append(item)
})
}
// MARK: Public
public var cancellable: AnyCancellable?
public var items: [T] = []
}
================================================
FILE: Core/AsyncUtilitiesForTesting/XCTestCase+PromiseKit.swift
================================================
//
// XCTestCase+PromiseKit.swift
//
//
// Created by Mohamed Afifi on 2021-12-19.
//
import XCTest
extension XCTestCase {
public static let defaultTimeout: TimeInterval = 5
@nonobjc
public func wait(for queue: DispatchQueue, timeout: TimeInterval = defaultTimeout) {
let expectation = expectation(description: "DispatchQueue")
queue.async(flags: .barrier) {
expectation.fulfill()
}
wait(for: [expectation], timeout: timeout)
}
// From: https://www.swiftbysundell.com/articles/testing-error-code-paths-in-swift/
public func assert(
_ expression: @autoclosure () throws -> some Any,
throws error: E,
in file: StaticString = #file,
line: UInt = #line
) {
var thrownError: Error?
XCTAssertThrowsError(
try expression(),
file: file,
line: line
) {
thrownError = $0
}
XCTAssertTrue(
thrownError is E,
"Unexpected error type: \(type(of: thrownError))",
file: file, line: line
)
XCTAssertEqual(
thrownError as? E, error,
file: file, line: line
)
}
}
================================================
FILE: Core/AsyncUtilitiesForTesting/XCTestCase+Publisher.swift
================================================
//
// XCTestCase+Publisher.swift
//
//
// Created by Mohamed Afifi on 2023-02-20.
//
import Combine
import XCTest
extension XCTestCase {
public func awaitPublisher(
_ publisher: T,
timeout: TimeInterval = 10,
file: StaticString = #file,
line: UInt = #line
) throws -> T.Output {
// This time, we use Swift's Result type to keep track
// of the result of our Combine pipeline:
var result: Result?
let expectation = expectation(description: "Awaiting publisher")
let cancellable = publisher.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
result = .failure(error)
case .finished:
break
}
expectation.fulfill()
},
receiveValue: { value in
result = .success(value)
}
)
// Just like before, we await the expectation that we
// created at the top of our test, and once done, we
// also cancel our cancellable to avoid getting any
// unused variable warnings:
waitForExpectations(timeout: timeout)
cancellable.cancel()
// Here we pass the original file and line number that
// our utility was called at, to tell XCTest to report
// any encountered errors at that original call site:
let unwrappedResult = try XCTUnwrap(
result,
"Awaited publisher did not produce any output",
file: file,
line: line
)
return try unwrappedResult.get()
}
public func awaitPublisher(
_ publisher: T,
numberOfElements: Int,
timeout: TimeInterval = 10,
file: StaticString = #file,
line: UInt = #line
) throws -> [T.Output]
where T.Failure == Never
{
var elements: [T.Output] = []
let expectation = expectation(description: "Awaiting publisher")
let cancellable = publisher.sink { value in
elements.append(value)
if elements.count == numberOfElements {
expectation.fulfill()
}
}
waitForExpectations(timeout: timeout)
cancellable.cancel()
if elements.count < numberOfElements {
XCTFail("Received less than \(numberOfElements) elements \(elements)")
}
return elements
}
public func awaitSingleItemPublisher(
_ publisher: T,
timeout: TimeInterval = 10,
file: StaticString = #file,
line: UInt = #line
) throws -> T.Output
where T.Failure == Never
{
let elements = try awaitPublisher(
publisher,
numberOfElements: 1,
timeout: timeout,
file: file, line: line
)
return elements[0]
}
}
================================================
FILE: Core/Caching/Sources/Cache.swift
================================================
//
// Cache.swift
// Quran
//
// Created by Mohamed Afifi on 11/1/16.
//
// Quran for iOS is a Quran reading application for iOS.
// Copyright (C) 2017 Quran.com
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
import Foundation
import UIKit
private class ObjectWrapper {
// MARK: Lifecycle
init(_ value: Any) {
self.value = value
}
// MARK: Internal
let value: Any
}
private class KeyWrapper: NSObject {
// MARK: Lifecycle
init(_ key: KeyType) {
self.key = key
}
// MARK: Internal
let key: KeyType
override var hash: Int {
key.hashValue
}
override func isEqual(_ object: Any?) -> Bool {
guard let other = object as? KeyWrapper else {
return false
}
return key == other.key
}
}
public final class Cache: Sendable {
// MARK: Lifecycle
public init(lowMemoryAware: Bool = true) {
guard lowMemoryAware else { return }
NotificationCenter.default.addObserver(
self,
selector: #selector(onLowMemory),
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: Public
public var name: String {
get { cache.name }
set { cache.name = newValue }
}
public weak var delegate: NSCacheDelegate? {
get { cache.delegate }
set { cache.delegate = newValue }
}
public var countLimit: Int {
get { cache.countLimit }
set { cache.countLimit = newValue }
}
public func object(forKey key: KeyType) -> ObjectType? {
cache.object(forKey: KeyWrapper(key))?.value as? ObjectType
}
public func setObject(_ obj: ObjectType, forKey key: KeyType) { // 0 cost
cache.setObject(ObjectWrapper(obj), forKey: KeyWrapper(key))
}
public func removeObject(forKey key: KeyType) {
cache.removeObject(forKey: KeyWrapper(key))
}
public func removeAllObjects() {
cache.removeAllObjects()
}
// MARK: Private
private let cache: NSCache, ObjectWrapper> = NSCache()
@objc
private func onLowMemory() {
removeAllObjects()
}
}
extension NSCache: @retroactive @unchecked Sendable {}
================================================
FILE: Core/Caching/Sources/OperationCacheableService.swift
================================================
//
// OperationCacheableService.swift
// Quran
//
// Created by Mohamed Afifi on 3/28/17.
//
// Quran for iOS is a Quran reading application for iOS.
// Copyright (C) 2017 Quran.com
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
import Utilities
public typealias CacheableOperation = @Sendable (Input) async throws -> Output
final class OperationCacheableService: Sendable {
private struct State: Sendable {
let cache: Cache
var inProgressOperations: [Input: MulticastContinuation