diff --git a/.gitignore b/.gitignore index 330d167..ba1ff08 100644 --- a/.gitignore +++ b/.gitignore @@ -87,4 +87,4 @@ fastlane/test_output # After new code Injection tools there's a generated folder /iOSInjectionProject # https://github.com/johnno1962/injectionforxcode -iOSInjectionProject/ +iOSInjectionProject/ \ No newline at end of file diff --git a/GithubTrending.xcodeproj/project.pbxproj b/GithubTrending.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1c5ad7b --- /dev/null +++ b/GithubTrending.xcodeproj/project.pbxproj @@ -0,0 +1,1168 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + F4C7477B290D5BA900A8C02E /* MockNetworkingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7471E290D5BA900A8C02E /* MockNetworkingManager.swift */; }; + F4C7477C290D5BA900A8C02E /* Mockable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7471F290D5BA900A8C02E /* Mockable.swift */; }; + F4C7477D290D5BA900A8C02E /* RepoResponse_ashawkey_stable-dreamfusion.json in Resources */ = {isa = PBXBuildFile; fileRef = F4C74721290D5BA900A8C02E /* RepoResponse_ashawkey_stable-dreamfusion.json */; }; + F4C7477E290D5BA900A8C02E /* RepoReadMeResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F4C74722290D5BA900A8C02E /* RepoReadMeResponse.json */; }; + F4C7477F290D5BA900A8C02E /* ResponseAvatarImageSample.png in Resources */ = {isa = PBXBuildFile; fileRef = F4C74723290D5BA900A8C02E /* ResponseAvatarImageSample.png */; }; + F4C74780290D5BA900A8C02E /* RepoResponse_huggingface_datasets.json in Resources */ = {isa = PBXBuildFile; fileRef = F4C74724290D5BA900A8C02E /* RepoResponse_huggingface_datasets.json */; }; + F4C74781290D5BA900A8C02E /* ResponseReadMeSample.md in Resources */ = {isa = PBXBuildFile; fileRef = F4C74725290D5BA900A8C02E /* ResponseReadMeSample.md */; }; + F4C74782290D5BA900A8C02E /* MockReadMeFetchingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74727290D5BA900A8C02E /* MockReadMeFetchingService.swift */; }; + F4C74783290D5BA900A8C02E /* MockTrendingScraperService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74728290D5BA900A8C02E /* MockTrendingScraperService.swift */; }; + F4C74784290D5BA900A8C02E /* MockImageFetchingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74729290D5BA900A8C02E /* MockImageFetchingService.swift */; }; + F4C74785290D5BA900A8C02E /* UITestingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7472B290D5BA900A8C02E /* UITestingHelper.swift */; }; + F4C74786290D5BA900A8C02E /* BaseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7472D290D5BA900A8C02E /* BaseViewModel.swift */; }; + F4C74787290D5BA900A8C02E /* SplashScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74730290D5BA900A8C02E /* SplashScreenView.swift */; }; + F4C74788290D5BA900A8C02E /* SplashAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74731290D5BA900A8C02E /* SplashAnimationView.swift */; }; + F4C74789290D5BA900A8C02E /* LaunchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74732290D5BA900A8C02E /* LaunchView.swift */; }; + F4C7478A290D5BA900A8C02E /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F4C74733290D5BA900A8C02E /* Launch Screen.storyboard */; }; + F4C7478B290D5BA900A8C02E /* RepoAvatarImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74737290D5BA900A8C02E /* RepoAvatarImageViewModel.swift */; }; + F4C7478C290D5BA900A8C02E /* RepoAvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74739290D5BA900A8C02E /* RepoAvatarImageView.swift */; }; + F4C7478D290D5BA900A8C02E /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7473A290D5BA900A8C02E /* WebView.swift */; }; + F4C7478E290D5BA900A8C02E /* RepoRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7473B290D5BA900A8C02E /* RepoRowView.swift */; }; + F4C7478F290D5BA900A8C02E /* LoadingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7473C290D5BA900A8C02E /* LoadingIndicator.swift */; }; + F4C74790290D5BA900A8C02E /* LargeTitleTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7473E290D5BA900A8C02E /* LargeTitleTextView.swift */; }; + F4C74791290D5BA900A8C02E /* BodyTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7473F290D5BA900A8C02E /* BodyTextView.swift */; }; + F4C74792290D5BA900A8C02E /* UnderlinedButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74741290D5BA900A8C02E /* UnderlinedButtonView.swift */; }; + F4C74793290D5BA900A8C02E /* RoundedButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74742290D5BA900A8C02E /* RoundedButtonView.swift */; }; + F4C74794290D5BA900A8C02E /* RepoDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74746290D5BA900A8C02E /* RepoDetailViewModel.swift */; }; + F4C74795290D5BA900A8C02E /* RepoDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74748290D5BA900A8C02E /* RepoDetailView.swift */; }; + F4C74796290D5BA900A8C02E /* TrendingRepoListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7474C290D5BA900A8C02E /* TrendingRepoListViewModel.swift */; }; + F4C74797290D5BA900A8C02E /* TrendingRepoListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7474E290D5BA900A8C02E /* TrendingRepoListView.swift */; }; + F4C74798290D5BA900A8C02E /* ListUIRefreshControlModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74751290D5BA900A8C02E /* ListUIRefreshControlModifier.swift */; }; + F4C74799290D5BA900A8C02E /* ListBackgroundModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74752290D5BA900A8C02E /* ListBackgroundModifier.swift */; }; + F4C7479A290D5BA900A8C02E /* NavigationBarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74753290D5BA900A8C02E /* NavigationBarModifier.swift */; }; + F4C7479B290D5BA900A8C02E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4C74754290D5BA900A8C02E /* Assets.xcassets */; }; + F4C7479C290D5BA900A8C02E /* RepoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74756290D5BA900A8C02E /* RepoModel.swift */; }; + F4C7479D290D5BA900A8C02E /* ReadMeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74757290D5BA900A8C02E /* ReadMeModel.swift */; }; + F4C7479E290D5BA900A8C02E /* ScrapedRepoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74758290D5BA900A8C02E /* ScrapedRepoModel.swift */; }; + F4C7479F290D5BA900A8C02E /* OwnerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74759290D5BA900A8C02E /* OwnerModel.swift */; }; + F4C747A0290D5BA900A8C02E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4C7475B290D5BA900A8C02E /* Preview Assets.xcassets */; }; + F4C747A1290D5BA900A8C02E /* HapticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7475D290D5BA900A8C02E /* HapticManager.swift */; }; + F4C747A2290D5BA900A8C02E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F4C7475F290D5BA900A8C02E /* Localizable.strings */; }; + F4C747A4290D5BA900A8C02E /* GithubTrendingApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74764290D5BA900A8C02E /* GithubTrendingApp.swift */; }; + F4C747A6290D5BA900A8C02E /* LocalizedAlertError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74767290D5BA900A8C02E /* LocalizedAlertError.swift */; }; + F4C747A7290D5BA900A8C02E /* NetworkEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74769290D5BA900A8C02E /* NetworkEnvironment.swift */; }; + F4C747A8290D5BA900A8C02E /* NetworkingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7476A290D5BA900A8C02E /* NetworkingError.swift */; }; + F4C747A9290D5BA900A8C02E /* NetworkingManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7476B290D5BA900A8C02E /* NetworkingManagerProtocol.swift */; }; + F4C747AA290D5BA900A8C02E /* NetworkingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7476C290D5BA900A8C02E /* NetworkingManager.swift */; }; + F4C747AB290D5BA900A8C02E /* EndpointItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7476D290D5BA900A8C02E /* EndpointItem.swift */; }; + F4C747AC290D5BA900A8C02E /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7476F290D5BA900A8C02E /* View.swift */; }; + F4C747AD290D5BA900A8C02E /* PreviewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74770290D5BA900A8C02E /* PreviewProvider.swift */; }; + F4C747AE290D5BA900A8C02E /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74771290D5BA900A8C02E /* String.swift */; }; + F4C747AF290D5BA900A8C02E /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74772290D5BA900A8C02E /* Color.swift */; }; + F4C747B0290D5BA900A8C02E /* TrendingScraperService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74774290D5BA900A8C02E /* TrendingScraperService.swift */; }; + F4C747B1290D5BA900A8C02E /* ReadMeFetchingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74775290D5BA900A8C02E /* ReadMeFetchingService.swift */; }; + F4C747B2290D5BA900A8C02E /* ScraperServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74777290D5BA900A8C02E /* ScraperServiceProtocol.swift */; }; + F4C747B3290D5BA900A8C02E /* FileFetchingServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74778290D5BA900A8C02E /* FileFetchingServiceProtocol.swift */; }; + F4C747B4290D5BA900A8C02E /* ImageFetchingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C74779290D5BA900A8C02E /* ImageFetchingService.swift */; }; + F4C747B8290D5C2F00A8C02E /* MarkdownView in Frameworks */ = {isa = PBXBuildFile; productRef = F4C747B7290D5C2F00A8C02E /* MarkdownView */; }; + F4C747BB290D5C7E00A8C02E /* GithubTrendingAPI in Frameworks */ = {isa = PBXBuildFile; productRef = F4C747BA290D5C7E00A8C02E /* GithubTrendingAPI */; }; + F4C747C0290D61D000A8C02E /* RepoAvatarImageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C747BD290D61D000A8C02E /* RepoAvatarImageViewModelTests.swift */; }; + F4C747C1290D61D000A8C02E /* RepoDetailViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C747BE290D61D000A8C02E /* RepoDetailViewModelTests.swift */; }; + F4C747C2290D61D000A8C02E /* TrendingRepoListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C747BF290D61D000A8C02E /* TrendingRepoListViewModelTests.swift */; }; + F4C747CA290D61E200A8C02E /* RepoDetailViewFailureUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C747C4290D61E200A8C02E /* RepoDetailViewFailureUITests.swift */; }; + F4C747CB290D61E200A8C02E /* RepoDetailViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C747C5290D61E200A8C02E /* RepoDetailViewUITests.swift */; }; + F4C747CC290D61E200A8C02E /* SplashAnimationViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C747C6290D61E200A8C02E /* SplashAnimationViewUITests.swift */; }; + F4C747CD290D61E200A8C02E /* TrendingRepoListViewFailureUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C747C8290D61E200A8C02E /* TrendingRepoListViewFailureUITests.swift */; }; + F4C747CE290D61E200A8C02E /* TrendingRepoListViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C747C9290D61E200A8C02E /* TrendingRepoListViewUITests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F4C74700290D5B2500A8C02E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F4C746E7290D5B2300A8C02E /* Project object */; + proxyType = 1; + remoteGlobalIDString = F4C746EE290D5B2300A8C02E; + remoteInfo = GithubTrending; + }; + F4C7470A290D5B2500A8C02E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F4C746E7290D5B2300A8C02E /* Project object */; + proxyType = 1; + remoteGlobalIDString = F4C746EE290D5B2300A8C02E; + remoteInfo = GithubTrending; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + F4C746EF290D5B2300A8C02E /* GithubTrending.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GithubTrending.app; sourceTree = BUILT_PRODUCTS_DIR; }; + F4C746FF290D5B2500A8C02E /* GithubTrendingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GithubTrendingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F4C74709290D5B2500A8C02E /* GithubTrendingUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GithubTrendingUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F4C7471E290D5BA900A8C02E /* MockNetworkingManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockNetworkingManager.swift; sourceTree = ""; }; + F4C7471F290D5BA900A8C02E /* Mockable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mockable.swift; sourceTree = ""; }; + F4C74721290D5BA900A8C02E /* RepoResponse_ashawkey_stable-dreamfusion.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "RepoResponse_ashawkey_stable-dreamfusion.json"; sourceTree = ""; }; + F4C74722290D5BA900A8C02E /* RepoReadMeResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = RepoReadMeResponse.json; sourceTree = ""; }; + F4C74723290D5BA900A8C02E /* ResponseAvatarImageSample.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ResponseAvatarImageSample.png; sourceTree = ""; }; + F4C74724290D5BA900A8C02E /* RepoResponse_huggingface_datasets.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = RepoResponse_huggingface_datasets.json; sourceTree = ""; }; + F4C74725290D5BA900A8C02E /* ResponseReadMeSample.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = ResponseReadMeSample.md; sourceTree = ""; }; + F4C74727290D5BA900A8C02E /* MockReadMeFetchingService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockReadMeFetchingService.swift; sourceTree = ""; }; + F4C74728290D5BA900A8C02E /* MockTrendingScraperService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockTrendingScraperService.swift; sourceTree = ""; }; + F4C74729290D5BA900A8C02E /* MockImageFetchingService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockImageFetchingService.swift; sourceTree = ""; }; + F4C7472B290D5BA900A8C02E /* UITestingHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITestingHelper.swift; sourceTree = ""; }; + F4C7472D290D5BA900A8C02E /* BaseViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseViewModel.swift; sourceTree = ""; }; + F4C74730290D5BA900A8C02E /* SplashScreenView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashScreenView.swift; sourceTree = ""; }; + F4C74731290D5BA900A8C02E /* SplashAnimationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashAnimationView.swift; sourceTree = ""; }; + F4C74732290D5BA900A8C02E /* LaunchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchView.swift; sourceTree = ""; }; + F4C74733290D5BA900A8C02E /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; + F4C74737290D5BA900A8C02E /* RepoAvatarImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoAvatarImageViewModel.swift; sourceTree = ""; }; + F4C74739290D5BA900A8C02E /* RepoAvatarImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoAvatarImageView.swift; sourceTree = ""; }; + F4C7473A290D5BA900A8C02E /* WebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; + F4C7473B290D5BA900A8C02E /* RepoRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoRowView.swift; sourceTree = ""; }; + F4C7473C290D5BA900A8C02E /* LoadingIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingIndicator.swift; sourceTree = ""; }; + F4C7473E290D5BA900A8C02E /* LargeTitleTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LargeTitleTextView.swift; sourceTree = ""; }; + F4C7473F290D5BA900A8C02E /* BodyTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BodyTextView.swift; sourceTree = ""; }; + F4C74741290D5BA900A8C02E /* UnderlinedButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnderlinedButtonView.swift; sourceTree = ""; }; + F4C74742290D5BA900A8C02E /* RoundedButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedButtonView.swift; sourceTree = ""; }; + F4C74746290D5BA900A8C02E /* RepoDetailViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoDetailViewModel.swift; sourceTree = ""; }; + F4C74748290D5BA900A8C02E /* RepoDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoDetailView.swift; sourceTree = ""; }; + F4C7474C290D5BA900A8C02E /* TrendingRepoListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrendingRepoListViewModel.swift; sourceTree = ""; }; + F4C7474E290D5BA900A8C02E /* TrendingRepoListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrendingRepoListView.swift; sourceTree = ""; }; + F4C74751290D5BA900A8C02E /* ListUIRefreshControlModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListUIRefreshControlModifier.swift; sourceTree = ""; }; + F4C74752290D5BA900A8C02E /* ListBackgroundModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListBackgroundModifier.swift; sourceTree = ""; }; + F4C74753290D5BA900A8C02E /* NavigationBarModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBarModifier.swift; sourceTree = ""; }; + F4C74754290D5BA900A8C02E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F4C74756290D5BA900A8C02E /* RepoModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoModel.swift; sourceTree = ""; }; + F4C74757290D5BA900A8C02E /* ReadMeModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadMeModel.swift; sourceTree = ""; }; + F4C74758290D5BA900A8C02E /* ScrapedRepoModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapedRepoModel.swift; sourceTree = ""; }; + F4C74759290D5BA900A8C02E /* OwnerModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OwnerModel.swift; sourceTree = ""; }; + F4C7475B290D5BA900A8C02E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + F4C7475D290D5BA900A8C02E /* HapticManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = ""; }; + F4C74760290D5BA900A8C02E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + F4C74763290D5BA900A8C02E /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + F4C74764290D5BA900A8C02E /* GithubTrendingApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GithubTrendingApp.swift; sourceTree = ""; }; + F4C74767290D5BA900A8C02E /* LocalizedAlertError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizedAlertError.swift; sourceTree = ""; }; + F4C74769290D5BA900A8C02E /* NetworkEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkEnvironment.swift; sourceTree = ""; }; + F4C7476A290D5BA900A8C02E /* NetworkingError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkingError.swift; sourceTree = ""; }; + F4C7476B290D5BA900A8C02E /* NetworkingManagerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkingManagerProtocol.swift; sourceTree = ""; }; + F4C7476C290D5BA900A8C02E /* NetworkingManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkingManager.swift; sourceTree = ""; }; + F4C7476D290D5BA900A8C02E /* EndpointItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EndpointItem.swift; sourceTree = ""; }; + F4C7476F290D5BA900A8C02E /* View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; + F4C74770290D5BA900A8C02E /* PreviewProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewProvider.swift; sourceTree = ""; }; + F4C74771290D5BA900A8C02E /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; + F4C74772290D5BA900A8C02E /* Color.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; + F4C74774290D5BA900A8C02E /* TrendingScraperService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrendingScraperService.swift; sourceTree = ""; }; + F4C74775290D5BA900A8C02E /* ReadMeFetchingService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadMeFetchingService.swift; sourceTree = ""; }; + F4C74777290D5BA900A8C02E /* ScraperServiceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScraperServiceProtocol.swift; sourceTree = ""; }; + F4C74778290D5BA900A8C02E /* FileFetchingServiceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileFetchingServiceProtocol.swift; sourceTree = ""; }; + F4C74779290D5BA900A8C02E /* ImageFetchingService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageFetchingService.swift; sourceTree = ""; }; + F4C747BD290D61D000A8C02E /* RepoAvatarImageViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoAvatarImageViewModelTests.swift; sourceTree = ""; }; + F4C747BE290D61D000A8C02E /* RepoDetailViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoDetailViewModelTests.swift; sourceTree = ""; }; + F4C747BF290D61D000A8C02E /* TrendingRepoListViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrendingRepoListViewModelTests.swift; sourceTree = ""; }; + F4C747C4290D61E200A8C02E /* RepoDetailViewFailureUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoDetailViewFailureUITests.swift; sourceTree = ""; }; + F4C747C5290D61E200A8C02E /* RepoDetailViewUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoDetailViewUITests.swift; sourceTree = ""; }; + F4C747C6290D61E200A8C02E /* SplashAnimationViewUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashAnimationViewUITests.swift; sourceTree = ""; }; + F4C747C8290D61E200A8C02E /* TrendingRepoListViewFailureUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrendingRepoListViewFailureUITests.swift; sourceTree = ""; }; + F4C747C9290D61E200A8C02E /* TrendingRepoListViewUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrendingRepoListViewUITests.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F4C746EC290D5B2300A8C02E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F4C747B8290D5C2F00A8C02E /* MarkdownView in Frameworks */, + F4C747BB290D5C7E00A8C02E /* GithubTrendingAPI in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4C746FC290D5B2500A8C02E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4C74706290D5B2500A8C02E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + F4C746E6290D5B2300A8C02E = { + isa = PBXGroup; + children = ( + F4C7471C290D5BA900A8C02E /* GithubTrending */, + F4C74702290D5B2500A8C02E /* GithubTrendingTests */, + F4C7470C290D5B2500A8C02E /* GithubTrendingUITests */, + F4C746F0290D5B2300A8C02E /* Products */, + ); + sourceTree = ""; + }; + F4C746F0290D5B2300A8C02E /* Products */ = { + isa = PBXGroup; + children = ( + F4C746EF290D5B2300A8C02E /* GithubTrending.app */, + F4C746FF290D5B2500A8C02E /* GithubTrendingTests.xctest */, + F4C74709290D5B2500A8C02E /* GithubTrendingUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + F4C74702290D5B2500A8C02E /* GithubTrendingTests */ = { + isa = PBXGroup; + children = ( + F4C747BC290D61D000A8C02E /* ViewModelTests */, + ); + path = GithubTrendingTests; + sourceTree = ""; + }; + F4C7470C290D5B2500A8C02E /* GithubTrendingUITests */ = { + isa = PBXGroup; + children = ( + F4C747C3290D61E200A8C02E /* RepoDetailView */, + F4C747C7290D61E200A8C02E /* TrendingRepoListView */, + F4C747C6290D61E200A8C02E /* SplashAnimationViewUITests.swift */, + ); + path = GithubTrendingUITests; + sourceTree = ""; + }; + F4C7471C290D5BA900A8C02E /* GithubTrending */ = { + isa = PBXGroup; + children = ( + F4C7471D290D5BA900A8C02E /* Mocks */, + F4C7475C290D5BA900A8C02E /* Utilities */, + F4C74766290D5BA900A8C02E /* Helpers */, + F4C74755290D5BA900A8C02E /* Models */, + F4C7472C290D5BA900A8C02E /* Core */, + F4C74764290D5BA900A8C02E /* GithubTrendingApp.swift */, + F4C74754290D5BA900A8C02E /* Assets.xcassets */, + F4C7475A290D5BA900A8C02E /* Preview Content */, + ); + path = GithubTrending; + sourceTree = ""; + }; + F4C7471D290D5BA900A8C02E /* Mocks */ = { + isa = PBXGroup; + children = ( + F4C7472A290D5BA900A8C02E /* UITesting */, + F4C74726290D5BA900A8C02E /* MockServices */, + F4C74720290D5BA900A8C02E /* MockedResponses */, + F4C7471E290D5BA900A8C02E /* MockNetworkingManager.swift */, + F4C7471F290D5BA900A8C02E /* Mockable.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + F4C74720290D5BA900A8C02E /* MockedResponses */ = { + isa = PBXGroup; + children = ( + F4C74721290D5BA900A8C02E /* RepoResponse_ashawkey_stable-dreamfusion.json */, + F4C74722290D5BA900A8C02E /* RepoReadMeResponse.json */, + F4C74723290D5BA900A8C02E /* ResponseAvatarImageSample.png */, + F4C74724290D5BA900A8C02E /* RepoResponse_huggingface_datasets.json */, + F4C74725290D5BA900A8C02E /* ResponseReadMeSample.md */, + ); + path = MockedResponses; + sourceTree = ""; + }; + F4C74726290D5BA900A8C02E /* MockServices */ = { + isa = PBXGroup; + children = ( + F4C74727290D5BA900A8C02E /* MockReadMeFetchingService.swift */, + F4C74728290D5BA900A8C02E /* MockTrendingScraperService.swift */, + F4C74729290D5BA900A8C02E /* MockImageFetchingService.swift */, + ); + path = MockServices; + sourceTree = ""; + }; + F4C7472A290D5BA900A8C02E /* UITesting */ = { + isa = PBXGroup; + children = ( + F4C7472B290D5BA900A8C02E /* UITestingHelper.swift */, + ); + path = UITesting; + sourceTree = ""; + }; + F4C7472C290D5BA900A8C02E /* Core */ = { + isa = PBXGroup; + children = ( + F4C7472D290D5BA900A8C02E /* BaseViewModel.swift */, + F4C7474F290D5BA900A8C02E /* ViewModifiers */, + F4C74734290D5BA900A8C02E /* Components */, + F4C7472E290D5BA900A8C02E /* Launch */, + F4C74743290D5BA900A8C02E /* Trending */, + ); + path = Core; + sourceTree = ""; + }; + F4C7472E290D5BA900A8C02E /* Launch */ = { + isa = PBXGroup; + children = ( + F4C7472F290D5BA900A8C02E /* Views */, + ); + path = Launch; + sourceTree = ""; + }; + F4C7472F290D5BA900A8C02E /* Views */ = { + isa = PBXGroup; + children = ( + F4C74730290D5BA900A8C02E /* SplashScreenView.swift */, + F4C74731290D5BA900A8C02E /* SplashAnimationView.swift */, + F4C74732290D5BA900A8C02E /* LaunchView.swift */, + F4C74733290D5BA900A8C02E /* Launch Screen.storyboard */, + ); + path = Views; + sourceTree = ""; + }; + F4C74734290D5BA900A8C02E /* Components */ = { + isa = PBXGroup; + children = ( + F4C74735290D5BA900A8C02E /* RepoAvatarImage */, + F4C7473A290D5BA900A8C02E /* WebView.swift */, + F4C7473B290D5BA900A8C02E /* RepoRowView.swift */, + F4C7473C290D5BA900A8C02E /* LoadingIndicator.swift */, + F4C7473D290D5BA900A8C02E /* TextViews */, + F4C74740290D5BA900A8C02E /* ButtonGroups */, + ); + path = Components; + sourceTree = ""; + }; + F4C74735290D5BA900A8C02E /* RepoAvatarImage */ = { + isa = PBXGroup; + children = ( + F4C74736290D5BA900A8C02E /* ViewModels */, + F4C74738290D5BA900A8C02E /* Views */, + ); + path = RepoAvatarImage; + sourceTree = ""; + }; + F4C74736290D5BA900A8C02E /* ViewModels */ = { + isa = PBXGroup; + children = ( + F4C74737290D5BA900A8C02E /* RepoAvatarImageViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + F4C74738290D5BA900A8C02E /* Views */ = { + isa = PBXGroup; + children = ( + F4C74739290D5BA900A8C02E /* RepoAvatarImageView.swift */, + ); + path = Views; + sourceTree = ""; + }; + F4C7473D290D5BA900A8C02E /* TextViews */ = { + isa = PBXGroup; + children = ( + F4C7473E290D5BA900A8C02E /* LargeTitleTextView.swift */, + F4C7473F290D5BA900A8C02E /* BodyTextView.swift */, + ); + path = TextViews; + sourceTree = ""; + }; + F4C74740290D5BA900A8C02E /* ButtonGroups */ = { + isa = PBXGroup; + children = ( + F4C74741290D5BA900A8C02E /* UnderlinedButtonView.swift */, + F4C74742290D5BA900A8C02E /* RoundedButtonView.swift */, + ); + path = ButtonGroups; + sourceTree = ""; + }; + F4C74743290D5BA900A8C02E /* Trending */ = { + isa = PBXGroup; + children = ( + F4C74744290D5BA900A8C02E /* RepoDetail */, + F4C7474A290D5BA900A8C02E /* TrendingRepoList */, + ); + path = Trending; + sourceTree = ""; + }; + F4C74744290D5BA900A8C02E /* RepoDetail */ = { + isa = PBXGroup; + children = ( + F4C74745290D5BA900A8C02E /* ViewModels */, + F4C74747290D5BA900A8C02E /* Views */, + ); + path = RepoDetail; + sourceTree = ""; + }; + F4C74745290D5BA900A8C02E /* ViewModels */ = { + isa = PBXGroup; + children = ( + F4C74746290D5BA900A8C02E /* RepoDetailViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + F4C74747290D5BA900A8C02E /* Views */ = { + isa = PBXGroup; + children = ( + F4C74748290D5BA900A8C02E /* RepoDetailView.swift */, + F4C74749290D5BA900A8C02E /* MarkdownView */, + ); + path = Views; + sourceTree = ""; + }; + F4C74749290D5BA900A8C02E /* MarkdownView */ = { + isa = PBXGroup; + children = ( + ); + path = MarkdownView; + sourceTree = ""; + }; + F4C7474A290D5BA900A8C02E /* TrendingRepoList */ = { + isa = PBXGroup; + children = ( + F4C7474B290D5BA900A8C02E /* ViewModels */, + F4C7474D290D5BA900A8C02E /* Views */, + ); + path = TrendingRepoList; + sourceTree = ""; + }; + F4C7474B290D5BA900A8C02E /* ViewModels */ = { + isa = PBXGroup; + children = ( + F4C7474C290D5BA900A8C02E /* TrendingRepoListViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + F4C7474D290D5BA900A8C02E /* Views */ = { + isa = PBXGroup; + children = ( + F4C7474E290D5BA900A8C02E /* TrendingRepoListView.swift */, + ); + path = Views; + sourceTree = ""; + }; + F4C7474F290D5BA900A8C02E /* ViewModifiers */ = { + isa = PBXGroup; + children = ( + F4C74750290D5BA900A8C02E /* List */, + F4C74753290D5BA900A8C02E /* NavigationBarModifier.swift */, + ); + path = ViewModifiers; + sourceTree = ""; + }; + F4C74750290D5BA900A8C02E /* List */ = { + isa = PBXGroup; + children = ( + F4C74751290D5BA900A8C02E /* ListUIRefreshControlModifier.swift */, + F4C74752290D5BA900A8C02E /* ListBackgroundModifier.swift */, + ); + path = List; + sourceTree = ""; + }; + F4C74755290D5BA900A8C02E /* Models */ = { + isa = PBXGroup; + children = ( + F4C74756290D5BA900A8C02E /* RepoModel.swift */, + F4C74758290D5BA900A8C02E /* ScrapedRepoModel.swift */, + F4C74759290D5BA900A8C02E /* OwnerModel.swift */, + F4C74757290D5BA900A8C02E /* ReadMeModel.swift */, + ); + path = Models; + sourceTree = ""; + }; + F4C7475A290D5BA900A8C02E /* Preview Content */ = { + isa = PBXGroup; + children = ( + F4C7475B290D5BA900A8C02E /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + F4C7475C290D5BA900A8C02E /* Utilities */ = { + isa = PBXGroup; + children = ( + F4C7475E290D5BA900A8C02E /* Localization */, + F4C7475D290D5BA900A8C02E /* HapticManager.swift */, + ); + path = Utilities; + sourceTree = ""; + }; + F4C7475E290D5BA900A8C02E /* Localization */ = { + isa = PBXGroup; + children = ( + F4C7475F290D5BA900A8C02E /* Localizable.strings */, + ); + path = Localization; + sourceTree = ""; + }; + F4C74766290D5BA900A8C02E /* Helpers */ = { + isa = PBXGroup; + children = ( + F4C74768290D5BA900A8C02E /* Network */, + F4C74773290D5BA900A8C02E /* Services */, + F4C7476E290D5BA900A8C02E /* Extensions */, + F4C74767290D5BA900A8C02E /* LocalizedAlertError.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + F4C74768290D5BA900A8C02E /* Network */ = { + isa = PBXGroup; + children = ( + F4C74769290D5BA900A8C02E /* NetworkEnvironment.swift */, + F4C7476A290D5BA900A8C02E /* NetworkingError.swift */, + F4C7476B290D5BA900A8C02E /* NetworkingManagerProtocol.swift */, + F4C7476C290D5BA900A8C02E /* NetworkingManager.swift */, + F4C7476D290D5BA900A8C02E /* EndpointItem.swift */, + ); + path = Network; + sourceTree = ""; + }; + F4C7476E290D5BA900A8C02E /* Extensions */ = { + isa = PBXGroup; + children = ( + F4C7476F290D5BA900A8C02E /* View.swift */, + F4C74770290D5BA900A8C02E /* PreviewProvider.swift */, + F4C74771290D5BA900A8C02E /* String.swift */, + F4C74772290D5BA900A8C02E /* Color.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + F4C74773290D5BA900A8C02E /* Services */ = { + isa = PBXGroup; + children = ( + F4C74776290D5BA900A8C02E /* Protocols */, + F4C74774290D5BA900A8C02E /* TrendingScraperService.swift */, + F4C74775290D5BA900A8C02E /* ReadMeFetchingService.swift */, + F4C74779290D5BA900A8C02E /* ImageFetchingService.swift */, + ); + path = Services; + sourceTree = ""; + }; + F4C74776290D5BA900A8C02E /* Protocols */ = { + isa = PBXGroup; + children = ( + F4C74777290D5BA900A8C02E /* ScraperServiceProtocol.swift */, + F4C74778290D5BA900A8C02E /* FileFetchingServiceProtocol.swift */, + ); + path = Protocols; + sourceTree = ""; + }; + F4C747BC290D61D000A8C02E /* ViewModelTests */ = { + isa = PBXGroup; + children = ( + F4C747BD290D61D000A8C02E /* RepoAvatarImageViewModelTests.swift */, + F4C747BE290D61D000A8C02E /* RepoDetailViewModelTests.swift */, + F4C747BF290D61D000A8C02E /* TrendingRepoListViewModelTests.swift */, + ); + path = ViewModelTests; + sourceTree = ""; + }; + F4C747C3290D61E200A8C02E /* RepoDetailView */ = { + isa = PBXGroup; + children = ( + F4C747C4290D61E200A8C02E /* RepoDetailViewFailureUITests.swift */, + F4C747C5290D61E200A8C02E /* RepoDetailViewUITests.swift */, + ); + path = RepoDetailView; + sourceTree = ""; + }; + F4C747C7290D61E200A8C02E /* TrendingRepoListView */ = { + isa = PBXGroup; + children = ( + F4C747C8290D61E200A8C02E /* TrendingRepoListViewFailureUITests.swift */, + F4C747C9290D61E200A8C02E /* TrendingRepoListViewUITests.swift */, + ); + path = TrendingRepoListView; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F4C746EE290D5B2300A8C02E /* GithubTrending */ = { + isa = PBXNativeTarget; + buildConfigurationList = F4C74713290D5B2500A8C02E /* Build configuration list for PBXNativeTarget "GithubTrending" */; + buildPhases = ( + F4C746EB290D5B2300A8C02E /* Sources */, + F4C746EC290D5B2300A8C02E /* Frameworks */, + F4C746ED290D5B2300A8C02E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = GithubTrending; + packageProductDependencies = ( + F4C747B7290D5C2F00A8C02E /* MarkdownView */, + F4C747BA290D5C7E00A8C02E /* GithubTrendingAPI */, + ); + productName = GithubTrending; + productReference = F4C746EF290D5B2300A8C02E /* GithubTrending.app */; + productType = "com.apple.product-type.application"; + }; + F4C746FE290D5B2500A8C02E /* GithubTrendingTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F4C74716290D5B2500A8C02E /* Build configuration list for PBXNativeTarget "GithubTrendingTests" */; + buildPhases = ( + F4C746FB290D5B2500A8C02E /* Sources */, + F4C746FC290D5B2500A8C02E /* Frameworks */, + F4C746FD290D5B2500A8C02E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F4C74701290D5B2500A8C02E /* PBXTargetDependency */, + ); + name = GithubTrendingTests; + productName = GithubTrendingTests; + productReference = F4C746FF290D5B2500A8C02E /* GithubTrendingTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F4C74708290D5B2500A8C02E /* GithubTrendingUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F4C74719290D5B2500A8C02E /* Build configuration list for PBXNativeTarget "GithubTrendingUITests" */; + buildPhases = ( + F4C74705290D5B2500A8C02E /* Sources */, + F4C74706290D5B2500A8C02E /* Frameworks */, + F4C74707290D5B2500A8C02E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F4C7470B290D5B2500A8C02E /* PBXTargetDependency */, + ); + name = GithubTrendingUITests; + productName = GithubTrendingUITests; + productReference = F4C74709290D5B2500A8C02E /* GithubTrendingUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F4C746E7290D5B2300A8C02E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1400; + LastUpgradeCheck = 1400; + TargetAttributes = { + F4C746EE290D5B2300A8C02E = { + CreatedOnToolsVersion = 14.0.1; + }; + F4C746FE290D5B2500A8C02E = { + CreatedOnToolsVersion = 14.0.1; + TestTargetID = F4C746EE290D5B2300A8C02E; + }; + F4C74708290D5B2500A8C02E = { + CreatedOnToolsVersion = 14.0.1; + LastSwiftMigration = 1400; + TestTargetID = F4C746EE290D5B2300A8C02E; + }; + }; + }; + buildConfigurationList = F4C746EA290D5B2300A8C02E /* Build configuration list for PBXProject "GithubTrending" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + hu, + ); + mainGroup = F4C746E6290D5B2300A8C02E; + packageReferences = ( + F4C747B6290D5C2F00A8C02E /* XCRemoteSwiftPackageReference "MarkdownView" */, + F4C747B9290D5C7E00A8C02E /* XCRemoteSwiftPackageReference "GithubTrendingAPI" */, + ); + productRefGroup = F4C746F0290D5B2300A8C02E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F4C746EE290D5B2300A8C02E /* GithubTrending */, + F4C746FE290D5B2500A8C02E /* GithubTrendingTests */, + F4C74708290D5B2500A8C02E /* GithubTrendingUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F4C746ED290D5B2300A8C02E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F4C74781290D5BA900A8C02E /* ResponseReadMeSample.md in Resources */, + F4C74780290D5BA900A8C02E /* RepoResponse_huggingface_datasets.json in Resources */, + F4C747A2290D5BA900A8C02E /* Localizable.strings in Resources */, + F4C747A0290D5BA900A8C02E /* Preview Assets.xcassets in Resources */, + F4C7478A290D5BA900A8C02E /* Launch Screen.storyboard in Resources */, + F4C7477D290D5BA900A8C02E /* RepoResponse_ashawkey_stable-dreamfusion.json in Resources */, + F4C7477F290D5BA900A8C02E /* ResponseAvatarImageSample.png in Resources */, + F4C7477E290D5BA900A8C02E /* RepoReadMeResponse.json in Resources */, + F4C7479B290D5BA900A8C02E /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4C746FD290D5B2500A8C02E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4C74707290D5B2500A8C02E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F4C746EB290D5B2300A8C02E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F4C74790290D5BA900A8C02E /* LargeTitleTextView.swift in Sources */, + F4C74783290D5BA900A8C02E /* MockTrendingScraperService.swift in Sources */, + F4C74792290D5BA900A8C02E /* UnderlinedButtonView.swift in Sources */, + F4C747AB290D5BA900A8C02E /* EndpointItem.swift in Sources */, + F4C74796290D5BA900A8C02E /* TrendingRepoListViewModel.swift in Sources */, + F4C74789290D5BA900A8C02E /* LaunchView.swift in Sources */, + F4C747B3290D5BA900A8C02E /* FileFetchingServiceProtocol.swift in Sources */, + F4C747B4290D5BA900A8C02E /* ImageFetchingService.swift in Sources */, + F4C747AD290D5BA900A8C02E /* PreviewProvider.swift in Sources */, + F4C747A4290D5BA900A8C02E /* GithubTrendingApp.swift in Sources */, + F4C747AF290D5BA900A8C02E /* Color.swift in Sources */, + F4C747B1290D5BA900A8C02E /* ReadMeFetchingService.swift in Sources */, + F4C74784290D5BA900A8C02E /* MockImageFetchingService.swift in Sources */, + F4C74788290D5BA900A8C02E /* SplashAnimationView.swift in Sources */, + F4C747A8290D5BA900A8C02E /* NetworkingError.swift in Sources */, + F4C7479F290D5BA900A8C02E /* OwnerModel.swift in Sources */, + F4C747B0290D5BA900A8C02E /* TrendingScraperService.swift in Sources */, + F4C747A6290D5BA900A8C02E /* LocalizedAlertError.swift in Sources */, + F4C747A9290D5BA900A8C02E /* NetworkingManagerProtocol.swift in Sources */, + F4C747AA290D5BA900A8C02E /* NetworkingManager.swift in Sources */, + F4C7479C290D5BA900A8C02E /* RepoModel.swift in Sources */, + F4C7478F290D5BA900A8C02E /* LoadingIndicator.swift in Sources */, + F4C7478E290D5BA900A8C02E /* RepoRowView.swift in Sources */, + F4C7478B290D5BA900A8C02E /* RepoAvatarImageViewModel.swift in Sources */, + F4C74795290D5BA900A8C02E /* RepoDetailView.swift in Sources */, + F4C7478D290D5BA900A8C02E /* WebView.swift in Sources */, + F4C74791290D5BA900A8C02E /* BodyTextView.swift in Sources */, + F4C7479E290D5BA900A8C02E /* ScrapedRepoModel.swift in Sources */, + F4C74785290D5BA900A8C02E /* UITestingHelper.swift in Sources */, + F4C747AC290D5BA900A8C02E /* View.swift in Sources */, + F4C747A1290D5BA900A8C02E /* HapticManager.swift in Sources */, + F4C74798290D5BA900A8C02E /* ListUIRefreshControlModifier.swift in Sources */, + F4C74786290D5BA900A8C02E /* BaseViewModel.swift in Sources */, + F4C747A7290D5BA900A8C02E /* NetworkEnvironment.swift in Sources */, + F4C7478C290D5BA900A8C02E /* RepoAvatarImageView.swift in Sources */, + F4C7477B290D5BA900A8C02E /* MockNetworkingManager.swift in Sources */, + F4C747AE290D5BA900A8C02E /* String.swift in Sources */, + F4C7479D290D5BA900A8C02E /* ReadMeModel.swift in Sources */, + F4C74787290D5BA900A8C02E /* SplashScreenView.swift in Sources */, + F4C74793290D5BA900A8C02E /* RoundedButtonView.swift in Sources */, + F4C7479A290D5BA900A8C02E /* NavigationBarModifier.swift in Sources */, + F4C747B2290D5BA900A8C02E /* ScraperServiceProtocol.swift in Sources */, + F4C74794290D5BA900A8C02E /* RepoDetailViewModel.swift in Sources */, + F4C74799290D5BA900A8C02E /* ListBackgroundModifier.swift in Sources */, + F4C74782290D5BA900A8C02E /* MockReadMeFetchingService.swift in Sources */, + F4C7477C290D5BA900A8C02E /* Mockable.swift in Sources */, + F4C74797290D5BA900A8C02E /* TrendingRepoListView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4C746FB290D5B2500A8C02E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F4C747C2290D61D000A8C02E /* TrendingRepoListViewModelTests.swift in Sources */, + F4C747C0290D61D000A8C02E /* RepoAvatarImageViewModelTests.swift in Sources */, + F4C747C1290D61D000A8C02E /* RepoDetailViewModelTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4C74705290D5B2500A8C02E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F4C747CD290D61E200A8C02E /* TrendingRepoListViewFailureUITests.swift in Sources */, + F4C747CA290D61E200A8C02E /* RepoDetailViewFailureUITests.swift in Sources */, + F4C747CB290D61E200A8C02E /* RepoDetailViewUITests.swift in Sources */, + F4C747CC290D61E200A8C02E /* SplashAnimationViewUITests.swift in Sources */, + F4C747CE290D61E200A8C02E /* TrendingRepoListViewUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F4C74701290D5B2500A8C02E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F4C746EE290D5B2300A8C02E /* GithubTrending */; + targetProxy = F4C74700290D5B2500A8C02E /* PBXContainerItemProxy */; + }; + F4C7470B290D5B2500A8C02E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F4C746EE290D5B2300A8C02E /* GithubTrending */; + targetProxy = F4C7470A290D5B2500A8C02E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + F4C7475F290D5BA900A8C02E /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + F4C74760290D5BA900A8C02E /* en */, + F4C74763290D5BA900A8C02E /* hu */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + F4C74711290D5B2500A8C02E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + F4C74712290D5B2500A8C02E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + F4C74714290D5B2500A8C02E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 0; + DEVELOPMENT_ASSET_PATHS = "\"GithubTrending/Preview Content\""; + DEVELOPMENT_TEAM = JZG66JS6WW; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen.storyboard"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationPortrait; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = borsosbe.GithubTrending; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + F4C74715290D5B2500A8C02E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 0; + DEVELOPMENT_ASSET_PATHS = "\"GithubTrending/Preview Content\""; + DEVELOPMENT_TEAM = JZG66JS6WW; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen.storyboard"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationPortrait; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = borsosbe.GithubTrending; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + F4C74717290D5B2500A8C02E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JZG66JS6WW; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = borsosbe.GithubTrendingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GithubTrending.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GithubTrending"; + }; + name = Debug; + }; + F4C74718290D5B2500A8C02E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JZG66JS6WW; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = borsosbe.GithubTrendingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GithubTrending.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GithubTrending"; + }; + name = Release; + }; + F4C7471A290D5B2500A8C02E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JZG66JS6WW; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = borsosbe.GithubTrendingUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = GithubTrending; + }; + name = Debug; + }; + F4C7471B290D5B2500A8C02E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JZG66JS6WW; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = borsosbe.GithubTrendingUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = GithubTrending; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F4C746EA290D5B2300A8C02E /* Build configuration list for PBXProject "GithubTrending" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F4C74711290D5B2500A8C02E /* Debug */, + F4C74712290D5B2500A8C02E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F4C74713290D5B2500A8C02E /* Build configuration list for PBXNativeTarget "GithubTrending" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F4C74714290D5B2500A8C02E /* Debug */, + F4C74715290D5B2500A8C02E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F4C74716290D5B2500A8C02E /* Build configuration list for PBXNativeTarget "GithubTrendingTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F4C74717290D5B2500A8C02E /* Debug */, + F4C74718290D5B2500A8C02E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F4C74719290D5B2500A8C02E /* Build configuration list for PBXNativeTarget "GithubTrendingUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F4C7471A290D5B2500A8C02E /* Debug */, + F4C7471B290D5B2500A8C02E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + F4C747B6290D5C2F00A8C02E /* XCRemoteSwiftPackageReference "MarkdownView" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/borsosbe/MarkdownView"; + requirement = { + branch = bugfix/ios_16; + kind = branch; + }; + }; + F4C747B9290D5C7E00A8C02E /* XCRemoteSwiftPackageReference "GithubTrendingAPI" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/elkoiko/GithubTrendingAPI"; + requirement = { + branch = main; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + F4C747B7290D5C2F00A8C02E /* MarkdownView */ = { + isa = XCSwiftPackageProductDependency; + package = F4C747B6290D5C2F00A8C02E /* XCRemoteSwiftPackageReference "MarkdownView" */; + productName = MarkdownView; + }; + F4C747BA290D5C7E00A8C02E /* GithubTrendingAPI */ = { + isa = XCSwiftPackageProductDependency; + package = F4C747B9290D5C7E00A8C02E /* XCRemoteSwiftPackageReference "GithubTrendingAPI" */; + productName = GithubTrendingAPI; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = F4C746E7290D5B2300A8C02E /* Project object */; +} diff --git a/GithubTrending.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/GithubTrending.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/GithubTrending.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/GithubTrending.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/GithubTrending.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/GithubTrending.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/GithubTrending.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/GithubTrending.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..af496c4 --- /dev/null +++ b/GithubTrending.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,32 @@ +{ + "pins" : [ + { + "identity" : "githubtrendingapi", + "kind" : "remoteSourceControl", + "location" : "https://github.com/elkoiko/GithubTrendingAPI", + "state" : { + "branch" : "main", + "revision" : "8a76fd8aa64ba2d14e4d9a1ad095e7d6801b46dd" + } + }, + { + "identity" : "markdownview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/borsosbe/MarkdownView", + "state" : { + "branch" : "bugfix/ios_16", + "revision" : "599f4df2f9fdb7e1e94f3cb910b638ad9a3e1070" + } + }, + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup.git", + "state" : { + "revision" : "11da8c685ddde2d519fad04a7daf8485bbbc773e", + "version" : "1.7.5" + } + } + ], + "version" : 2 +} diff --git a/GithubTrending.xcodeproj/xcshareddata/xcschemes/GithubTrending.xcscheme b/GithubTrending.xcodeproj/xcshareddata/xcschemes/GithubTrending.xcscheme new file mode 100644 index 0000000..fde9ed1 --- /dev/null +++ b/GithubTrending.xcodeproj/xcshareddata/xcschemes/GithubTrending.xcscheme @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GithubTrending/Assets.xcassets/AppIcon.appiconset/Contents.json b/GithubTrending/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..cb87235 --- /dev/null +++ b/GithubTrending/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "Fill 52-2.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GithubTrending/Assets.xcassets/AppIcon.appiconset/Fill 52-2.png b/GithubTrending/Assets.xcassets/AppIcon.appiconset/Fill 52-2.png new file mode 100644 index 0000000..35d26af Binary files /dev/null and b/GithubTrending/Assets.xcassets/AppIcon.appiconset/Fill 52-2.png differ diff --git a/GithubTrending/Assets.xcassets/Contents.json b/GithubTrending/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/GithubTrending/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GithubTrending/Assets.xcassets/Images/Contents.json b/GithubTrending/Assets.xcassets/Images/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/GithubTrending/Assets.xcassets/Images/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Contents.json b/GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Contents.json new file mode 100644 index 0000000..8e6944a --- /dev/null +++ b/GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Fill 52.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Fill 52@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Fill 52@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Fill 52.png b/GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Fill 52.png new file mode 100644 index 0000000..1ff30ff Binary files /dev/null and b/GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Fill 52.png differ diff --git a/GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Fill 52@2x.png b/GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Fill 52@2x.png new file mode 100644 index 0000000..59a70ab Binary files /dev/null and b/GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Fill 52@2x.png differ diff --git a/GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Fill 52@3x.png b/GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Fill 52@3x.png new file mode 100644 index 0000000..e15354d Binary files /dev/null and b/GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Fill 52@3x.png differ diff --git a/GithubTrending/Assets.xcassets/ThemeColors/AccentColor.colorset/Contents.json b/GithubTrending/Assets.xcassets/ThemeColors/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..b953c8f --- /dev/null +++ b/GithubTrending/Assets.xcassets/ThemeColors/AccentColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.294", + "green" : "0.392", + "red" : "0.988" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.294", + "green" : "0.392", + "red" : "0.988" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GithubTrending/Assets.xcassets/ThemeColors/BackgroundColor.colorset/Contents.json b/GithubTrending/Assets.xcassets/ThemeColors/BackgroundColor.colorset/Contents.json new file mode 100644 index 0000000..abc86a1 --- /dev/null +++ b/GithubTrending/Assets.xcassets/ThemeColors/BackgroundColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.078", + "green" : "0.082", + "red" : "0.086" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.078", + "green" : "0.082", + "red" : "0.086" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GithubTrending/Assets.xcassets/ThemeColors/Contents.json b/GithubTrending/Assets.xcassets/ThemeColors/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/GithubTrending/Assets.xcassets/ThemeColors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GithubTrending/Assets.xcassets/ThemeColors/LightBackgroundColor.colorset/Contents.json b/GithubTrending/Assets.xcassets/ThemeColors/LightBackgroundColor.colorset/Contents.json new file mode 100644 index 0000000..c9aa129 --- /dev/null +++ b/GithubTrending/Assets.xcassets/ThemeColors/LightBackgroundColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.953", + "green" : "0.933", + "red" : "0.937" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.953", + "green" : "0.933", + "red" : "0.937" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GithubTrending/Assets.xcassets/ThemeColors/SecondaryColor.colorset/Contents.json b/GithubTrending/Assets.xcassets/ThemeColors/SecondaryColor.colorset/Contents.json new file mode 100644 index 0000000..8f84549 --- /dev/null +++ b/GithubTrending/Assets.xcassets/ThemeColors/SecondaryColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "61", + "green" : "54", + "red" : "48" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "61", + "green" : "54", + "red" : "48" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GithubTrending/Assets.xcassets/ThemeColors/TextColor.colorset/Contents.json b/GithubTrending/Assets.xcassets/ThemeColors/TextColor.colorset/Contents.json new file mode 100644 index 0000000..c60cea5 --- /dev/null +++ b/GithubTrending/Assets.xcassets/ThemeColors/TextColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "255", + "red" : "255" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "255", + "red" : "255" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GithubTrending/Assets.xcassets/ThemeColors/UnderlineTextButtonColor.colorset/Contents.json b/GithubTrending/Assets.xcassets/ThemeColors/UnderlineTextButtonColor.colorset/Contents.json new file mode 100644 index 0000000..6eaef74 --- /dev/null +++ b/GithubTrending/Assets.xcassets/ThemeColors/UnderlineTextButtonColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.875", + "green" : "0.867", + "red" : "0.863" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.875", + "green" : "0.867", + "red" : "0.863" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GithubTrending/Core/BaseViewModel.swift b/GithubTrending/Core/BaseViewModel.swift new file mode 100644 index 0000000..d1b7235 --- /dev/null +++ b/GithubTrending/Core/BaseViewModel.swift @@ -0,0 +1,27 @@ +// +// BaseViewModel.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 11.. +// + +import Foundation +import Combine + +class BaseViewModel: ObservableObject { + @Published var error: Error? + var onErrorCompletion: ((Subscribers.Completion) -> Void)? + + init(error: Error? = nil) { + self.error = nil + self.onErrorCompletion = { completion in + switch completion { + case .finished: + break + case .failure(let error): + print("🔥 failure: \(error)") + self.error = error + } + } + } +} diff --git a/GithubTrending/Core/Components/ButtonGroups/RoundedButtonView.swift b/GithubTrending/Core/Components/ButtonGroups/RoundedButtonView.swift new file mode 100644 index 0000000..6901024 --- /dev/null +++ b/GithubTrending/Core/Components/ButtonGroups/RoundedButtonView.swift @@ -0,0 +1,29 @@ +// +// RoundedButtonView.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 05.. +// + +import SwiftUI + +struct RoundedButtonView: View { + let title: String + + var body: some View { + Text(title) + .fontWeight(.semibold) + .frame(maxWidth: .infinity) + .padding() + .background(Color.theme.accent) + .foregroundColor(.white) + .clipShape(Capsule()) + } +} + +struct RoundedButtonView_Previews: PreviewProvider { + static var previews: some View { + RoundedButtonView(title: "Round Me") + .previewLayout(.sizeThatFits) + } +} diff --git a/GithubTrending/Core/Components/ButtonGroups/UnderlinedButtonView.swift b/GithubTrending/Core/Components/ButtonGroups/UnderlinedButtonView.swift new file mode 100644 index 0000000..b650b6b --- /dev/null +++ b/GithubTrending/Core/Components/ButtonGroups/UnderlinedButtonView.swift @@ -0,0 +1,23 @@ +// +// UnderlinedButtonView.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 05.. +// + +import SwiftUI + +struct UnderlinedButtonView: View { + let title: String + var body: some View { + Text(title) + .foregroundColor(Color.theme.underlineButton) + .underline() + } +} + +struct UnderlinedButtonView_Previews: PreviewProvider { + static var previews: some View { + UnderlinedButtonView(title: "Privacy Policy") + } +} diff --git a/GithubTrending/Core/Components/LoadingIndicator.swift b/GithubTrending/Core/Components/LoadingIndicator.swift new file mode 100644 index 0000000..66e4bf0 --- /dev/null +++ b/GithubTrending/Core/Components/LoadingIndicator.swift @@ -0,0 +1,25 @@ +// +// LoadingIndicator.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// + +import SwiftUI + +struct LoadingIndicator: View { + var body: some View { + ProgressView() + .tint(Color.theme.accent) + .scaleEffect(x: 1.5, y: 1.5, anchor: .center) + .padding(10) + .background(Color.theme.lightBackgroundColor) + .cornerRadius(5) + } +} + +struct LoadingIndicator_Previews: PreviewProvider { + static var previews: some View { + LoadingIndicator() + } +} diff --git a/GithubTrending/Core/Components/RepoAvatarImage/ViewModels/RepoAvatarImageViewModel.swift b/GithubTrending/Core/Components/RepoAvatarImage/ViewModels/RepoAvatarImageViewModel.swift new file mode 100644 index 0000000..9e155b9 --- /dev/null +++ b/GithubTrending/Core/Components/RepoAvatarImage/ViewModels/RepoAvatarImageViewModel.swift @@ -0,0 +1,36 @@ +// +// RepoAvatarImageViewModel.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// +// +import Foundation +import Combine + +final class RepoAvatarImageViewModel: ObservableObject { + @Published private(set) var imageData: Data? = nil + @Published private(set) var isLoading: Bool = false + private let imageFetchingService: FileFetchingServiceProtocol + private let url: URL? + + init(url: URL?, imageFetchingService: FileFetchingServiceProtocol) { + self.imageFetchingService = imageFetchingService + self.url = url + } + + func fetchImage() async { + DispatchQueue.main.async { + self.isLoading.toggle() + } + if let url = url { + let data: Data? = try? await imageFetchingService.asyncDownload(url: url) + await MainActor.run { + self.imageData = data + self.isLoading.toggle() + } + } else { + print("Bad url") + } + } +} diff --git a/GithubTrending/Core/Components/RepoAvatarImage/Views/RepoAvatarImageView.swift b/GithubTrending/Core/Components/RepoAvatarImage/Views/RepoAvatarImageView.swift new file mode 100644 index 0000000..dbda5f7 --- /dev/null +++ b/GithubTrending/Core/Components/RepoAvatarImage/Views/RepoAvatarImageView.swift @@ -0,0 +1,49 @@ +// +// RepoAvatarImageView.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// + +import SwiftUI + +struct RepoAvatarImageView: View { + @StateObject private var vm: RepoAvatarImageViewModel + let width: CGFloat + let height: CGFloat + init(imageFetchingService: FileFetchingServiceProtocol, url: URL?, width: CGFloat, height: CGFloat) { + _vm = StateObject(wrappedValue: RepoAvatarImageViewModel(url: url, imageFetchingService: imageFetchingService)) + self.width = width + self.height = height + } + + var body: some View { + ZStack { + if let imageData = vm.imageData { + Image(uiImage: UIImage(data: imageData)!) + .resizable() + .scaledToFit() + .frame(width: width, height: height) + } else if vm.isLoading { + LoadingIndicator() + } else { + Image(systemName: "questionmark") + .foregroundColor(Color.theme.accent) + } + } + .onAppear { + Task { + await vm.fetchImage() + } + } + } +} + +struct RepoAvatarImageView_Previews: PreviewProvider { + static let imageFetchingService = ImageFetchingService() + static let url = URL(string: "https://avatars.githubusercontent.com/u/25720743?v=4") + + static var previews: some View { + RepoAvatarImageView(imageFetchingService: imageFetchingService, url: url, width: 50, height: 50) + } +} diff --git a/GithubTrending/Core/Components/RepoRowView.swift b/GithubTrending/Core/Components/RepoRowView.swift new file mode 100644 index 0000000..dedc71e --- /dev/null +++ b/GithubTrending/Core/Components/RepoRowView.swift @@ -0,0 +1,64 @@ +// +// RepoRowView.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 06.. +// + +import SwiftUI + +struct RepoRowView: View { + let repo: RepoModel + + var body: some View { + VStack(alignment: .leading) { + VStack { + HStack { + RepoAvatarImageView(imageFetchingService: ImageFetchingService(), url: repo.owner.avatarURL, width: 50, height: 50) + VStack(alignment: .leading) { + BodyTextView(text: repo.owner.name) + BodyTextView(text: repo.name, fontWeight: .bold, textColor: Color.theme.accent) + } + Spacer() + } + if (!(repo.description?.isEmpty ?? true)) { + BodyTextView(text: repo.description ?? "") + .padding(EdgeInsets(top: 5, leading: 0, bottom: 10, trailing: 0)) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + } + HStack { + HStack { + Image(systemName: "star") + .foregroundColor(Color.theme.text) + BodyTextView(text: String(repo.stargazersCount)) + } + .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 10)) + HStack { + Image(systemName: "eye") + .foregroundColor(Color.theme.text) + BodyTextView(text: String(repo.subscribersCount)) + BodyTextView(text: repo.language ?? "", textColor: Color.theme.accent) + .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 0)) + } + Spacer() + } + .padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)) + } + .padding(10) + } + .background(Color.theme.secondary) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(.clear, lineWidth: 0) + ) + .dynamicTypeSize(.small ... .xLarge) + } +} + +struct RepoRowView_Previews: PreviewProvider { + static var previews: some View { + RepoRowView(repo: dev.repo) + } +} diff --git a/GithubTrending/Core/Components/TextViews/BodyTextView.swift b/GithubTrending/Core/Components/TextViews/BodyTextView.swift new file mode 100644 index 0000000..3dd992d --- /dev/null +++ b/GithubTrending/Core/Components/TextViews/BodyTextView.swift @@ -0,0 +1,30 @@ +// +// BodyTextView.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// + +import SwiftUI + +struct BodyTextView: View { + let text: String + var fontWeight: Font.Weight = .regular + var textColor: Color = Color.theme.text + + var body: some View { + Text(text) + .font(.body) + .fontWeight(fontWeight) + .foregroundColor(textColor) + } +} + +struct BodyTextView_Previews: PreviewProvider { + static var previews: some View { + ZStack { + Color.theme.background.ignoresSafeArea() + LargeTitleTextView(text: "Github Trending") + } + } +} diff --git a/GithubTrending/Core/Components/TextViews/LargeTitleTextView.swift b/GithubTrending/Core/Components/TextViews/LargeTitleTextView.swift new file mode 100644 index 0000000..52aaf23 --- /dev/null +++ b/GithubTrending/Core/Components/TextViews/LargeTitleTextView.swift @@ -0,0 +1,29 @@ +// +// LargeTitleTextView.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// + +import SwiftUI + +struct LargeTitleTextView: View { + let text: String + + var body: some View { + Text(text) + .font(.largeTitle) + .fontWeight(.bold) + .foregroundColor(Color.theme.text) + } +} + +struct LargeTitleTextView_Previews: PreviewProvider { + static var previews: some View { + ZStack { + Color.theme.background.ignoresSafeArea() + LargeTitleTextView(text: "Github Trending") + } + } +} + diff --git a/GithubTrending/Core/Components/WebView.swift b/GithubTrending/Core/Components/WebView.swift new file mode 100644 index 0000000..3083327 --- /dev/null +++ b/GithubTrending/Core/Components/WebView.swift @@ -0,0 +1,21 @@ +// +// WebView.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 05.. +// + +import SwiftUI +import SafariServices + +struct WebView: UIViewControllerRepresentable { + let url: URL + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> SFSafariViewController { + return SFSafariViewController(url: url) + } + + func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext) { + } + +} diff --git a/GithubTrending/Core/Launch/Views/Launch Screen.storyboard b/GithubTrending/Core/Launch/Views/Launch Screen.storyboard new file mode 100644 index 0000000..6fa8ea3 --- /dev/null +++ b/GithubTrending/Core/Launch/Views/Launch Screen.storyboard @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GithubTrending/Core/Launch/Views/LaunchView.swift b/GithubTrending/Core/Launch/Views/LaunchView.swift new file mode 100644 index 0000000..334faf8 --- /dev/null +++ b/GithubTrending/Core/Launch/Views/LaunchView.swift @@ -0,0 +1,127 @@ +// +// LaunchView.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 05.. +// + +import SwiftUI + +struct LaunchView: View { + @State private var showWebView = false + @Binding var showTrendingList: Bool + private let linkText: LocalizedStringKey = "launch_github_link" + private let title: LocalizedStringKey = "launch_welcome" + private let info: LocalizedStringKey = "launch_info" + private let desc: LocalizedStringKey = "launch_desc" + private let buttonText: LocalizedStringKey = "launch_enter_button" + private let pp: LocalizedStringKey = "launch_pp" + private let terms: LocalizedStringKey = "launch_terms" + private let and: LocalizedStringKey = "and" + private let githubUrl: LocalizedStringKey = "launch_github_url" + private let githubPpUrl: LocalizedStringKey = "launch_github_pp_url" + private let githubTermsUrl: LocalizedStringKey = "launch_github_terms_url" + + var body: some View { + ZStack { + Color.theme.background.ignoresSafeArea() + GeometryReader { geo in + VStack (alignment: .center) { + launchHeader + Spacer() + launchBodyImage + .frame(height: geo.size.width * 0.34) + launchBodyText + .frame(width: geo.size.width * 0.9) + .transaction { transaction in + transaction.animation = nil + } + Spacer() + launchFooterButton + .frame(width: geo.size.width * 0.7) + .padding(EdgeInsets(top: 0, leading: 0, bottom: geo.size.height * 0.04, trailing: 0)) + launchFooterTextButtons + .frame(width: geo.size.width * 0.9) + }.dynamicTypeSize(.small ... .xLarge) + } + } + } +} + +struct LaunchView_Previews: PreviewProvider { + static var previews: some View { + LaunchView(showTrendingList: .constant(false)) + } +} + +extension LaunchView { + private var launchHeader: some View { + HStack { + Spacer() + ZStack { + Link(linkText, destination: URL(string: githubUrl.localized())!) + .font(.system(size: 16, weight: .bold)) + .padding(.top, 5) + }.fixedSize(horizontal: true, vertical: true) + .foregroundColor(Color.theme.text) + .padding() + } + } + + private var launchBodyImage: some View { + Image("GithubLogo") + .resizable() + .aspectRatio(contentMode: .fit) + .padding(10) + } + + private var launchBodyText: some View { + VStack { + LargeTitleTextView(text: title.localized()) + .allowsTightening(true) + .multilineTextAlignment(.center) + .padding(10) + BodyTextView(text: info.localized()) + BodyTextView(text: desc.localized()) + .padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20)) + .multilineTextAlignment(.center) + } + } + + private var launchFooterButton: some View { + VStack { + Button(action: { + showTrendingList.toggle() + }, label: { + RoundedButtonView(title: buttonText.localized()) + .accessibilityIdentifier("enterButton") + }) + } + } + + private var launchFooterTextButtons: some View { + VStack { + HStack(alignment: .center, spacing: 4, content: { + Spacer() + Button(action: { + showWebView.toggle() + }, label: { + UnderlinedButtonView(title: pp.localized()) + }).sheet(isPresented: $showWebView) { + WebView(url: URL(string: githubPpUrl.localized())!) + } + Text(and) + .foregroundColor(Color.theme.underlineButton) + Button(action: { + showWebView.toggle() + }, label: { + UnderlinedButtonView(title: terms.localized()) + }).sheet(isPresented: $showWebView) { + WebView(url: URL(string: githubTermsUrl.localized())!) + } + Spacer() + }) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 25, trailing: 0)) + } + } +} diff --git a/GithubTrending/Core/Launch/Views/SplashAnimationView.swift b/GithubTrending/Core/Launch/Views/SplashAnimationView.swift new file mode 100644 index 0000000..2b3992a --- /dev/null +++ b/GithubTrending/Core/Launch/Views/SplashAnimationView.swift @@ -0,0 +1,40 @@ +// +// SplashAnimationView.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 08.. +// + +import SwiftUI + +struct SplashAnimationView: View { + @State var linkText = "Github" + @Binding var showTrendingList: Bool + private let githubUrl: LocalizedStringKey = "launch_github_url" + + var body: some View { + GeometryReader { geo in + VStack { + SplashScreenView(linkText: $linkText) { + LaunchView(showTrendingList: $showTrendingList) + } logoView: { + Image("GithubLogo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 91) + } linkView: { + Link(linkText, destination: URL(string: githubUrl.localized())!) + .padding(.top, geo.size.height * 0.04) + .foregroundColor(Color.theme.text) + .frame(width: 128) + } + } + } + } +} + +struct SplashAnimationView_Previews: PreviewProvider { + static var previews: some View { + SplashAnimationView(showTrendingList: .constant(false)) + } +} diff --git a/GithubTrending/Core/Launch/Views/SplashScreenView.swift b/GithubTrending/Core/Launch/Views/SplashScreenView.swift new file mode 100644 index 0000000..a8c9575 --- /dev/null +++ b/GithubTrending/Core/Launch/Views/SplashScreenView.swift @@ -0,0 +1,105 @@ +// +// SplashScreenView.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 08.. +// + +import SwiftUI + +struct SplashScreenView: View { + private let linkText: Binding + private let content: Content + private let logoView: Logo + private let linkView: Link + private let linkTextKey: LocalizedStringKey = "launch_github_link" + + init(linkText: Binding, @ViewBuilder content: @escaping () -> Content, @ViewBuilder logoView: @escaping () -> Logo, @ViewBuilder linkView: @escaping () -> Link) { + self.content = content() + self.logoView = logoView() + self.linkView = linkView() + self.linkText = linkText + } + + @State var animating = false + @State var endAnimation = false + @Namespace var animation + + var body: some View { + VStack(spacing: 0) { + GeometryReader { geo in + ZStack { + Color.theme.background.ignoresSafeArea() + .overlay( + ZStack{ + if !endAnimation{ + logoView + .matchedGeometryEffect(id: "LOGO", in: animation) + .opacity(animating ? 0 : 1) + linkView + .matchedGeometryEffect(id: "LINK", in: animation) + .font(.system(size: animating ? 16: 40, weight: .bold)) + .offset(y: 110) + } + } + ) + .overlay( + VStack { + HStack{ + Spacer() + if endAnimation { + if #available(iOS 16.0, *) { + // iOS 15 version does not look great with iOS 16 + linkView + .matchedGeometryEffect(id: "LINK", in: animation) + .fontWeight(.bold) + } else { + linkView + .matchedGeometryEffect(id: "LINK", in: animation) + .font(.system(size: 16, weight: .bold)) + } + } + }.padding(.horizontal) + Spacer() + if endAnimation { + logoView + .matchedGeometryEffect(id: "LOGO", in: animation) + .opacity(0) + } + } + ) + } + .frame(height: endAnimation ? 60 : nil) + .zIndex(1) + content + .frame(height: endAnimation ? nil : 0) + .zIndex(0) + } + } + .background(Color.theme.background) + .frame(maxHeight: .infinity, alignment: .top) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.64) { + withAnimation(.spring()) { + animating.toggle() + } + withAnimation( + Animation.easeIn(duration: 0.8)) { + endAnimation.toggle() + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + withAnimation( + Animation.spring()) { + self.linkText.wrappedValue = linkTextKey.localized() + } + } + } + } + } +} + +struct SplashScreen_Previews: PreviewProvider { + static var previews: some View { + LaunchView(showTrendingList: .constant(false)) + } +} diff --git a/GithubTrending/Core/Trending/RepoDetail/ViewModels/RepoDetailViewModel.swift b/GithubTrending/Core/Trending/RepoDetail/ViewModels/RepoDetailViewModel.swift new file mode 100644 index 0000000..3b020d5 --- /dev/null +++ b/GithubTrending/Core/Trending/RepoDetail/ViewModels/RepoDetailViewModel.swift @@ -0,0 +1,59 @@ +// +// RepoDetailViewModel.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// + +import Foundation +import Combine + +final class RepoDetailViewModel: BaseViewModel { + @Published private(set) var repo: RepoModel + @Published private(set) var readMeMarkDown: String? + @Published private(set) var markDownURL: URL? + private let networkingManager: NetworkingManagerProtocol + private let readMeFetchingService: FileFetchingServiceProtocol + var cancellables = Set() + + init(repo: RepoModel, networkingManager: NetworkingManagerProtocol, readMeFetchingService: FileFetchingServiceProtocol) { + self.repo = repo + self.networkingManager = networkingManager + self.readMeFetchingService = readMeFetchingService + super.init(error: nil) + } + + func onViewModelAppear() { + downloadRepoReadMe() + $markDownURL + .receive(on: DispatchQueue.main) + .sink { value in + Task { [weak self] in + await self?.getReadMeContentFromURL(markDownURL: value) + } + } + .store(in: &cancellables) + } + + private func downloadRepoReadMe() { + networkingManager.download(type: EndpointItem.getRepoReadMe(repo.owner.name, repo.name)) + .decode(type: ReadMeModel.self, decoder: JSONDecoder()) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: networkingManager.handleCompletion, receiveValue: { [weak self] (returnedReadMe) in + self?.markDownURL = returnedReadMe.downloadUrl + }) + .store(in: &cancellables) + } + + private func getReadMeContentFromURL(markDownURL: URL?) async { + guard let readMeUrl = markDownURL else { return } + let string: String? = try? await readMeFetchingService.asyncDownload(url: readMeUrl) + await MainActor.run { + if let str = string { + self.readMeMarkDown = str + } else { + self.markDownURL = nil + } + } + } +} diff --git a/GithubTrending/Core/Trending/RepoDetail/Views/RepoDetailView.swift b/GithubTrending/Core/Trending/RepoDetail/Views/RepoDetailView.swift new file mode 100644 index 0000000..bc78280 --- /dev/null +++ b/GithubTrending/Core/Trending/RepoDetail/Views/RepoDetailView.swift @@ -0,0 +1,104 @@ +// +// RepoDetailView.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// + +import SwiftUI +import MarkdownView + +struct RepoDetailLoadingView: View { + @Binding var repo: RepoModel? + + var body: some View { + ZStack { + if let repo = repo { + RepoDetailView(repo: repo, networkingManager: NetworkingManager(), readMeFetchingService: ReadMeFetchingService()) + } + } + } +} + +struct RepoDetailView: View { + @StateObject private var vm: RepoDetailViewModel + @State var markDownRendered: Bool = false + private let navTitle: LocalizedStringKey = "repo_detail_nav_title" + + init(repo: RepoModel, networkingManager: NetworkingManagerProtocol, readMeFetchingService: FileFetchingServiceProtocol ) { +#if DEBUG + if UITestingHelper.isUITesting { + let mockNetworkingManager = MockNetworkingManager() + let mockReadMeFetchingService = MockReadMeFetchingService() + if !UITestingHelper.isRepoDetailedViewNetworkingSuccessful { + mockReadMeFetchingService.mockNetworkFailure = true + } + _vm = StateObject(wrappedValue: RepoDetailViewModel(repo: repo, networkingManager: mockNetworkingManager, readMeFetchingService: mockReadMeFetchingService)) + } else { + _vm = StateObject(wrappedValue: RepoDetailViewModel(repo: repo, networkingManager: networkingManager, readMeFetchingService: readMeFetchingService)) + } +#else + _vm = StateObject(wrappedValue: RepoDetailViewModel(repo: repo, networkingManager: networkingManager, readMeFetchingService: readMeFetchingService)) +#endif + } + + var body: some View { + ZStack { + Color.theme.background.ignoresSafeArea() + ScrollView { + RepoRowView(repo: vm.repo) + .accessibilityIdentifier("repoRow") + .navigationTitle(navTitle) + .padding(10) + if (vm.markDownURL != nil) { + readMeView + .accessibilityIdentifier("readMe") + } + } + } + .onAppear { + vm.onViewModelAppear() + } + } +} + +struct RepoDetailView_Previews: PreviewProvider { + static let networkingManager = NetworkingManager(networkEnviroment: .dev) + static let readMeFetchingService = ReadMeFetchingService() + static var previews: some View { + NavigationView { + RepoDetailView(repo: dev.repo, networkingManager: networkingManager, readMeFetchingService: readMeFetchingService) + }.background(Color.theme.background) + .modifier(NavigationBarModifier(backgroundColor: UIColor(Color.theme.background), titleColor: UIColor(Color.theme.text))) + } +} + +extension RepoDetailView { + private var readMeView: some View { + VStack { + GroupBox { + if (!markDownRendered) { + LoadingIndicator() + } + if (vm.readMeMarkDown != nil) { + MarkdownUI(body: vm.readMeMarkDown) + .onRendered { _ in + if !markDownRendered { + markDownRendered.toggle() + } + } + .onTouchLink { link in + if let url = link.url { + UIApplication.shared.open(url) + } + return false + } + .background(Color.theme.lightBackgroundColor) + } + } + .padding(EdgeInsets(top: 0, leading: 10, bottom: 10, trailing: 10)) + } + .colorScheme(.light) + } +} + diff --git a/GithubTrending/Core/Trending/TrendingRepoList/ViewModels/TrendingRepoListViewModel.swift b/GithubTrending/Core/Trending/TrendingRepoList/ViewModels/TrendingRepoListViewModel.swift new file mode 100644 index 0000000..19c5d7a --- /dev/null +++ b/GithubTrending/Core/Trending/TrendingRepoList/ViewModels/TrendingRepoListViewModel.swift @@ -0,0 +1,83 @@ +// +// TrendingRepoListViewModel.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 06.. +// + +import Foundation +import Combine +import GithubTrendingAPI + +final class TrendingRepoListViewModel: BaseViewModel { + @Published private(set) var repos: [RepoModel] = [] + @Published private(set) var refreshInProgess: Bool = false + private let scraperService: ScraperServiceProtocol + private let networkingManager: NetworkingManagerProtocol + private var cancellables = Set() + + init(scraperService: ScraperServiceProtocol, networkingManager: NetworkingManagerProtocol) { + self.scraperService = scraperService + self.networkingManager = networkingManager + super.init(error: nil) + self.onErrorCompletion = { completion in + switch completion { + case .finished: + break + case .failure(let error): + print("🔥 failure: \(error)") + DispatchQueue.main.async { [weak self] in + self?.refreshInProgess = false + self?.error = error + } + } + } + } + + func onViewModelAppear() { + if !refreshInProgess && repos.isEmpty { + loadScrapedData() + } + } + + func reload() { + HapticManager.notification(type: .success) + if !refreshInProgess { + loadScrapedData() + } + } + + private func loadScrapedData() { + refreshInProgess.toggle() + scraperService.scrapData() + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: onErrorCompletion!, receiveValue: { [weak self] result in + self?.downloadRepos(scrapedRepos: result) + }) + .store(in: &cancellables) + } + + private func downloadRepos(scrapedRepos: [ScrapedRepoModel]?) { + guard scrapedRepos != nil else { + return + } + var count = 0 + var tempRepos: [RepoModel] = [] + for scrapedRepo in scrapedRepos! { + networkingManager.download(type: EndpointItem.getRepo(scrapedRepo.ownerName, scrapedRepo.repoName)) + .decode(type: RepoModel.self, decoder: JSONDecoder()) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: onErrorCompletion!, receiveValue: { [weak self] (returnedRepo) in + tempRepos.append(returnedRepo) + count += 1 + if count == scrapedRepos!.count { + self?.repos = tempRepos + self?.refreshInProgess.toggle() + } + }) + .store(in: &cancellables) + } + } +} + + diff --git a/GithubTrending/Core/Trending/TrendingRepoList/Views/TrendingRepoListView.swift b/GithubTrending/Core/Trending/TrendingRepoList/Views/TrendingRepoListView.swift new file mode 100644 index 0000000..111336f --- /dev/null +++ b/GithubTrending/Core/Trending/TrendingRepoList/Views/TrendingRepoListView.swift @@ -0,0 +1,110 @@ +// +// TrendingRepoListView.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 06.. +// + +import SwiftUI + +struct TrendingRepoListView: View { + @StateObject private var vm: TrendingRepoListViewModel + @State private var showDetailView: Bool = false + @State private var selectedRepo: RepoModel? = nil + private let navTitle: LocalizedStringKey = "trending_repo_list_nav_title" + + init(scraperService: ScraperServiceProtocol, networkingManager: NetworkingManagerProtocol) { +#if DEBUG + if UITestingHelper.isUITesting { + let mockTrendingScraperService = MockTrendingScraperService() + let mockNetworkingManager = MockNetworkingManager() + if UITestingHelper.isTrendingRepoListViewNetworkingSuccessful { + mockTrendingScraperService.fillUpscrapedRepoModelsResults() + } else { + mockTrendingScraperService.mockNetworkFailure = true + } + _vm = StateObject(wrappedValue: TrendingRepoListViewModel(scraperService: mockTrendingScraperService, networkingManager: mockNetworkingManager)) + } else { + _vm = StateObject(wrappedValue: TrendingRepoListViewModel(scraperService: scraperService, networkingManager: networkingManager)) + } +#else + _vm = StateObject(wrappedValue: TrendingRepoListViewModel(scraperService: scraperService, networkingManager: networkingManager)) +#endif + } + + var body: some View { + ZStack { + Color.theme.background.ignoresSafeArea() + VStack { + if vm.refreshInProgess { + LoadingIndicator() + .accessibilityIdentifier("loadingIndicator") + } + if !vm.refreshInProgess && !vm.repos.isEmpty { + repoList + .background( + NavigationLink( + destination: RepoDetailLoadingView(repo: $selectedRepo), + isActive: $showDetailView, + label: { EmptyView() })) + } + } + } + .errorAlert(error: $vm.error) + .navigationTitle(navTitle) + .toolbar { + Button(action: { + vm.reload() + }, label: { + Image(systemName: "goforward") + }) + .accessibilityIdentifier("refreshButton") + } + .onAppear { + vm.onViewModelAppear() + } + } +} + +struct TrendingRepoListView_Previews: PreviewProvider { + static let trendingScraperService = TrendingScraperService() + static let networkingManager = NetworkingManager(networkEnviroment: .dev) + + static var previews: some View { + NavigationView { + TrendingRepoListView(scraperService: trendingScraperService, networkingManager: networkingManager) + } + .background(Color.theme.background) + .modifier(NavigationBarModifier(backgroundColor: UIColor(Color.theme.background), titleColor: UIColor(Color.theme.text))) + } +} + +extension TrendingRepoListView { + private var repoList: some View { + List { + ForEach(vm.repos) { repo in + RepoRowView(repo: repo) + .accessibilityIdentifier("item_\(repo.id)") + .listRowInsets( + .init(top: 10, leading: 10, bottom: 10, trailing: 10)) + .listRowSeparator(.hidden) + .listRowBackground(Color.theme.background) + .onTapGesture { + segue(repo: repo) + } + } + } + .accessibilityIdentifier("trendingRepoList") + .tint(Color.theme.accent) + .listStyle(.plain) + .modifier(ListBackgroundModifier()) + .modifier(ListUIRefreshControlModifier(tintColor: UIColor(Color.theme.accent))) + } + + private func segue(repo: RepoModel) { + selectedRepo = repo + showDetailView.toggle() + } +} + + diff --git a/GithubTrending/Core/ViewModifiers/List/ListBackgroundModifier.swift b/GithubTrending/Core/ViewModifiers/List/ListBackgroundModifier.swift new file mode 100644 index 0000000..1d6c4d1 --- /dev/null +++ b/GithubTrending/Core/ViewModifiers/List/ListBackgroundModifier.swift @@ -0,0 +1,21 @@ +// +// ListBackgroundModifier.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// + +import SwiftUI + +struct ListBackgroundModifier: ViewModifier { + @ViewBuilder + func body(content: Content) -> some View { + if #available(iOS 16.0, *) { + content + .scrollContentBackground(.hidden) + } else { + content + } + } +} + diff --git a/GithubTrending/Core/ViewModifiers/List/ListUIRefreshControlModifier.swift b/GithubTrending/Core/ViewModifiers/List/ListUIRefreshControlModifier.swift new file mode 100644 index 0000000..5aa74c9 --- /dev/null +++ b/GithubTrending/Core/ViewModifiers/List/ListUIRefreshControlModifier.swift @@ -0,0 +1,21 @@ +// +// ListUIRefreshControlModifier.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// + +import SwiftUI + +struct ListUIRefreshControlModifier: ViewModifier { + private var tintColor: UIColor? + + init(tintColor: UIColor?) { + UIRefreshControl.appearance().tintColor = tintColor ?? .white + } + + @ViewBuilder + func body(content: Content) -> some View { + content + } +} diff --git a/GithubTrending/Core/ViewModifiers/NavigationBarModifier.swift b/GithubTrending/Core/ViewModifiers/NavigationBarModifier.swift new file mode 100644 index 0000000..269f9bd --- /dev/null +++ b/GithubTrending/Core/ViewModifiers/NavigationBarModifier.swift @@ -0,0 +1,41 @@ +// +// NavigationBarModifier.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// + +import SwiftUI + +struct NavigationBarModifier: ViewModifier { + private var backgroundColor: UIColor? + private var titleColor: UIColor? + + init(backgroundColor: UIColor?, titleColor: UIColor?) { + self.backgroundColor = backgroundColor + let coloredAppearance = UINavigationBarAppearance() + coloredAppearance.configureWithTransparentBackground() + coloredAppearance.backgroundColor = backgroundColor + coloredAppearance.titleTextAttributes = [.foregroundColor: titleColor ?? .white] + coloredAppearance.largeTitleTextAttributes = [.foregroundColor: titleColor ?? .white] + + UINavigationBar.appearance().standardAppearance = coloredAppearance + UINavigationBar.appearance().compactAppearance = coloredAppearance + UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance + } + + func body(content: Content) -> some View { + ZStack{ + content + VStack { + GeometryReader { geometry in + Color(self.backgroundColor ?? .clear) + .frame(height: geometry.safeAreaInsets.top) + .edgesIgnoringSafeArea(.top) + Spacer() + } + } + } + } +} + diff --git a/GithubTrending/GithubTrendingApp.swift b/GithubTrending/GithubTrendingApp.swift new file mode 100644 index 0000000..8941180 --- /dev/null +++ b/GithubTrending/GithubTrendingApp.swift @@ -0,0 +1,41 @@ +// +// GithubTrendingApp.swift +// GithubTrendingApp +// +// Created by Bence Borsos on 2022. 10. 05.. +// + +import SwiftUI + +@main +struct GithubTrendingApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @State private var showTrendingList: Bool = false + + var body: some Scene { + WindowGroup { + if !showTrendingList { + SplashAnimationView(showTrendingList: $showTrendingList) + .preferredColorScheme(.dark) + } else { + NavigationView { + TrendingRepoListView(scraperService: TrendingScraperService(), networkingManager: NetworkingManager()) + } + .background(Color.theme.background) + .modifier(NavigationBarModifier(backgroundColor: UIColor(Color.theme.background), titleColor: UIColor(Color.theme.text))) + .preferredColorScheme(.dark) + .navigationBarHidden(true) + .navigationViewStyle(.stack) + } + } + } +} + +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + #if DEBUG + print("👷🏾‍♂️ Is UI Test Running: \(UITestingHelper.isUITesting)") + #endif + return true + } +} diff --git a/GithubTrending/Helpers/Extensions/Color.swift b/GithubTrending/Helpers/Extensions/Color.swift new file mode 100644 index 0000000..03a3f8a --- /dev/null +++ b/GithubTrending/Helpers/Extensions/Color.swift @@ -0,0 +1,22 @@ +// +// Color.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 05.. +// + +import Foundation +import SwiftUI + +extension Color { + static let theme = ColorTheme() +} + +struct ColorTheme { + let accent = Color("AccentColor") + let secondary = Color("SecondaryColor") + let background = Color("BackgroundColor") + let text = Color("TextColor") + let underlineButton = Color("UnderlineTextButtonColor") + let lightBackgroundColor = Color("LightBackgroundColor") +} diff --git a/GithubTrending/Helpers/Extensions/PreviewProvider.swift b/GithubTrending/Helpers/Extensions/PreviewProvider.swift new file mode 100644 index 0000000..eb5aad4 --- /dev/null +++ b/GithubTrending/Helpers/Extensions/PreviewProvider.swift @@ -0,0 +1,22 @@ +// +// PreviewProvider.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// + +import Foundation +import SwiftUI + +extension PreviewProvider { + static var dev: DeveloperPreview { + return DeveloperPreview.instance + } +} + +class DeveloperPreview { + static let instance = DeveloperPreview() + private init() { } + + let repo = RepoModel(id: 0, name: "datasets", owner: OwnerModel(id: 0, name: "huggingface", githubURL: URL(string: "https://github.com/huggingface")!, avatarURL: URL(string: "https://avatars.githubusercontent.com/u/25720743?v=4")!), description: "🤗 The largest hub of ready-to-use datasets for ML models with fast, easy-to-use and efficient data manipulation tools", stargazersCount: 79, subscribersCount: 60, language: "Python") +} diff --git a/GithubTrending/Helpers/Extensions/String.swift b/GithubTrending/Helpers/Extensions/String.swift new file mode 100644 index 0000000..2857f07 --- /dev/null +++ b/GithubTrending/Helpers/Extensions/String.swift @@ -0,0 +1,39 @@ +// +// String.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 06.. +// + +import Foundation +import SwiftUI + +extension String { + func slice(from: String, to: String) -> String? { + return (range(of: from)?.upperBound).flatMap { substringFrom in + (range(of: to, range: substringFrom.. String { + let language = locale.languageCode + let path = Bundle.main.path(forResource: language, ofType: "lproj")! + let bundle = Bundle(path: path)! + let localizedString = NSLocalizedString(key, bundle: bundle, comment: "") + return localizedString + } +} + +extension LocalizedStringKey { + func localized(locale: Locale = .current) -> String { + return .localizedString(for: self.stringKey ?? "", locale: locale) + } +} + +extension LocalizedStringKey { + var stringKey: String? { + Mirror(reflecting: self).children.first(where: { $0.label == "key" })?.value as? String + } +} diff --git a/GithubTrending/Helpers/Extensions/View.swift b/GithubTrending/Helpers/Extensions/View.swift new file mode 100644 index 0000000..f51b1be --- /dev/null +++ b/GithubTrending/Helpers/Extensions/View.swift @@ -0,0 +1,22 @@ +// +// View.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 11.. +// + +import Foundation +import SwiftUI + +extension View { + func errorAlert(error: Binding, buttonTitle: String = "OK") -> some View { + let localizedAlertError = LocalizedAlertError(error: error.wrappedValue) + return alert(isPresented: .constant(localizedAlertError != nil), error: localizedAlertError) { _ in + Button(buttonTitle) { + error.wrappedValue = nil + } + } message: { error in + Text(error.recoverySuggestion ?? "") + } + } +} diff --git a/GithubTrending/Helpers/LocalizedAlertError.swift b/GithubTrending/Helpers/LocalizedAlertError.swift new file mode 100644 index 0000000..2a67e03 --- /dev/null +++ b/GithubTrending/Helpers/LocalizedAlertError.swift @@ -0,0 +1,23 @@ +// +// LocalizedAlertError.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 11.. +// + +import Foundation + +struct LocalizedAlertError: LocalizedError { + let underlyingError: LocalizedError + var errorDescription: String? { + underlyingError.errorDescription + } + var recoverySuggestion: String? { + underlyingError.recoverySuggestion + } + + init?(error: Error?) { + guard let localizedError = error as? LocalizedError else { return nil } + underlyingError = localizedError + } +} diff --git a/GithubTrending/Helpers/Network/EndpointItem.swift b/GithubTrending/Helpers/Network/EndpointItem.swift new file mode 100644 index 0000000..5cf6d4a --- /dev/null +++ b/GithubTrending/Helpers/Network/EndpointItem.swift @@ -0,0 +1,53 @@ +// +// EndpointItem.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 06.. +// + +import Foundation + +enum EndpointItem { + case getRepo(_: String, _: String) + case getRepoReadMe(_: String, _: String) + + func value() -> Any? { + switch self { + case .getRepo(let str1, let str2): + return [str1, str2] + case .getRepoReadMe(let str1, let str2): + return [str1, str2] + } + } +} + +protocol EndPointType { + var baseURL: String { get } + var path: String { get } + var url: URL { get } +} + +extension EndpointItem: EndPointType { + var baseURL: String { + switch NetworkingManager.networkEnviroment { + case .dev: return "https://api.github.com/" + case .production: return "https://api.github.com/" + } + } + + var path: String { + switch self { + case .getRepo(let ownerName, let repoName): + return "repos/\(ownerName)/\(repoName)" + case .getRepoReadMe(let ownerName, let repoName): + return "repos/\(ownerName)/\(repoName)/contents/README.md" + } + } + + var url: URL { + switch self { + default: + return URL(string: self.baseURL + self.path)! + } + } +} diff --git a/GithubTrending/Helpers/Network/NetworkEnvironment.swift b/GithubTrending/Helpers/Network/NetworkEnvironment.swift new file mode 100644 index 0000000..ad9e049 --- /dev/null +++ b/GithubTrending/Helpers/Network/NetworkEnvironment.swift @@ -0,0 +1,13 @@ +// +// NetworkEnvironment.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 06.. +// + +import Foundation + +enum NetworkEnvironment { + case dev + case production +} diff --git a/GithubTrending/Helpers/Network/NetworkingError.swift b/GithubTrending/Helpers/Network/NetworkingError.swift new file mode 100644 index 0000000..bf986e8 --- /dev/null +++ b/GithubTrending/Helpers/Network/NetworkingError.swift @@ -0,0 +1,20 @@ +// +// NetworkingError.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 11.. +// + +import Foundation + +enum NetworkingError: LocalizedError { + case badURLResponse(url: URL) + case unknown + + var errorDescription: String? { + switch self { + case .badURLResponse(url: let url): return "[🔥] Bad response from URL: \(url)" + case .unknown: return "[⚠️] Unknown error occured" + } + } +} diff --git a/GithubTrending/Helpers/Network/NetworkingManager.swift b/GithubTrending/Helpers/Network/NetworkingManager.swift new file mode 100644 index 0000000..0816fba --- /dev/null +++ b/GithubTrending/Helpers/Network/NetworkingManager.swift @@ -0,0 +1,49 @@ +// +// NetworkingManager.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 06.. +// + +import Foundation +import Combine + +class NetworkingManager: NetworkingManagerProtocol { + static var networkEnviroment: NetworkEnvironment = .dev + + // for testing + init(networkEnviroment: NetworkEnvironment? = nil) { + NetworkingManager.networkEnviroment = networkEnviroment ?? .dev + } + + func download(type: EndPointType) -> AnyPublisher { + var urlRequest = URLRequest(url: type.url) + let token = "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" + urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type") + urlRequest.addValue("application/json", forHTTPHeaderField: "Accept") + urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + return URLSession.shared.dataTaskPublisher(for: urlRequest) + .tryMap({ [weak self] in + try self?.handleURLResponse(output: $0, url: type.url) ?? Data() + }) + .retry(1) + .eraseToAnyPublisher() + } + + func handleURLResponse(output: URLSession.DataTaskPublisher.Output, url: URL) throws -> Data { + guard let response = output.response as? HTTPURLResponse, + response.statusCode >= 200 && response.statusCode < 300 else { + throw NetworkingError.badURLResponse(url: url) + } + return output.data + } + + func handleCompletion(completion: Subscribers.Completion) { + switch completion { + case .finished: + break + case .failure(let error): + print(error.localizedDescription) + } + } +} diff --git a/GithubTrending/Helpers/Network/NetworkingManagerProtocol.swift b/GithubTrending/Helpers/Network/NetworkingManagerProtocol.swift new file mode 100644 index 0000000..9048c7f --- /dev/null +++ b/GithubTrending/Helpers/Network/NetworkingManagerProtocol.swift @@ -0,0 +1,16 @@ +// +// NetworkingManagerProtocol.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 12.. +// + +import Foundation +import Combine + +protocol NetworkingManagerProtocol { + static var networkEnviroment: NetworkEnvironment { get } + func download(type: EndPointType) -> AnyPublisher + func handleURLResponse(output: URLSession.DataTaskPublisher.Output, url: URL) throws -> Data + func handleCompletion(completion: Subscribers.Completion) +} diff --git a/GithubTrending/Helpers/Services/ImageFetchingService.swift b/GithubTrending/Helpers/Services/ImageFetchingService.swift new file mode 100644 index 0000000..a03aae3 --- /dev/null +++ b/GithubTrending/Helpers/Services/ImageFetchingService.swift @@ -0,0 +1,29 @@ +// +// ImageFetchingService.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// + +import Foundation + +final class ImageFetchingService: FileFetchingServiceProtocol { + func asyncDownload(url: URL) async throws -> T? { + do { + let (data, response) = try await URLSession.shared.data(from: url, delegate: nil) + return handleResponse(data: data, response: response) + } catch { + throw error + } + } + + func handleResponse(data: Data?, response: URLResponse?) -> T? { + guard + let imgData = data as? T, + let response = response as? HTTPURLResponse, + response.statusCode >= 200 && response.statusCode < 300 else { + return nil + } + return imgData + } +} diff --git a/GithubTrending/Helpers/Services/Protocols/FileFetchingServiceProtocol.swift b/GithubTrending/Helpers/Services/Protocols/FileFetchingServiceProtocol.swift new file mode 100644 index 0000000..c15e338 --- /dev/null +++ b/GithubTrending/Helpers/Services/Protocols/FileFetchingServiceProtocol.swift @@ -0,0 +1,13 @@ +// +// FileFetchingServiceProtocol.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 08.. +// + +import Foundation + +protocol FileFetchingServiceProtocol { + func asyncDownload(url: URL) async throws -> T? + func handleResponse(data: Data?, response: URLResponse?) -> T? +} diff --git a/GithubTrending/Helpers/Services/Protocols/ScraperServiceProtocol.swift b/GithubTrending/Helpers/Services/Protocols/ScraperServiceProtocol.swift new file mode 100644 index 0000000..4535687 --- /dev/null +++ b/GithubTrending/Helpers/Services/Protocols/ScraperServiceProtocol.swift @@ -0,0 +1,13 @@ +// +// ScraperServiceProtocol.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 08.. +// + +import Foundation +import Combine + +protocol ScraperServiceProtocol { + func scrapData() -> Future<[T], Error> +} diff --git a/GithubTrending/Helpers/Services/ReadMeFetchingService.swift b/GithubTrending/Helpers/Services/ReadMeFetchingService.swift new file mode 100644 index 0000000..b4bf5b2 --- /dev/null +++ b/GithubTrending/Helpers/Services/ReadMeFetchingService.swift @@ -0,0 +1,30 @@ +// +// ReadMeFetchingService.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 08.. +// + +import Foundation + +final class ReadMeFetchingService: FileFetchingServiceProtocol { + func asyncDownload(url: URL) async throws -> T? { + do { + let (data, response) = try await URLSession.shared.data(from: url, delegate: nil) + return handleResponse(data: data, response: response) + } catch { + throw error + } + } + + func handleResponse(data: Data?, response: URLResponse?) -> T? { + guard + let data = data, + let string = String(data: data, encoding: .utf8) as? T, + let response = response as? HTTPURLResponse, + response.statusCode >= 200 && response.statusCode < 300 else { + return nil + } + return string + } +} diff --git a/GithubTrending/Helpers/Services/TrendingScraperService.swift b/GithubTrending/Helpers/Services/TrendingScraperService.swift new file mode 100644 index 0000000..77ee7cf --- /dev/null +++ b/GithubTrending/Helpers/Services/TrendingScraperService.swift @@ -0,0 +1,45 @@ +// +// ScraperService.swift +// GithubTrending +// The scraper package depandancy is not the best, the Repository model properties are inaccessible due to 'internal' protection level but still printable so we slice out the neccassery information from it +// Created by Bence Borsos on 2022. 10. 06.. +// + +import Foundation +import GithubTrendingAPI +import Combine + +final class TrendingScraperService: ScraperServiceProtocol { + let slicePartBeginning: String = "https://github.com/" + let slicePartEnding: String = "), description:" + let seperator: String = "/" + + func scrapData() -> Future<[T], Error> { + return Future { promise in + DispatchQueue.global().async { [weak self] in + let res: [Repository] = GithubTrendingAPI.getRepositories() + if !res.isEmpty { + promise(.success(self?.decodeScrapedData(trendingRepositories: res) as? [T] ?? [])) + } else { + promise(.failure(NetworkingError.unknown)) + } + } + } + } + + private func decodeScrapedData(trendingRepositories: [Repository]) -> [ScrapedRepoModel] { + var scrapedRepos: [ScrapedRepoModel] = [] + for trendingRepository in trendingRepositories { + let scrapedString = "\(trendingRepository)".slice(from: slicePartBeginning, to: slicePartEnding) + if let scrapedComponents: [String] = scrapedString?.components(separatedBy: seperator) { + // example: microsoft/PowerToys + if scrapedComponents.count == 2 { + scrapedRepos.append(ScrapedRepoModel(ownerName: scrapedComponents[0], repoName: scrapedComponents[1])) + } + } + } + return scrapedRepos + } +} + + diff --git a/GithubTrending/Mocks/MockNetworkingManager.swift b/GithubTrending/Mocks/MockNetworkingManager.swift new file mode 100644 index 0000000..e16d5e2 --- /dev/null +++ b/GithubTrending/Mocks/MockNetworkingManager.swift @@ -0,0 +1,39 @@ +// +// MockNetworkingManager.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 10.. +// + +#if DEBUG +import Foundation +import Combine + +final class MockNetworkingManager: NetworkingManagerProtocol, Mockable { + static var networkEnviroment: NetworkEnvironment = .dev + var responseData: Data? + + init(networkEnviroment: NetworkEnvironment? = nil) { + NetworkingManager.networkEnviroment = networkEnviroment ?? .dev + } + + func download(type: EndPointType) -> AnyPublisher { + let repoData = loadJSONDataForEndpoint(endpoint: type as! EndpointItem) + return CurrentValueSubject(responseData != nil ? responseData! : repoData) + .eraseToAnyPublisher() + } + + func handleURLResponse(output: URLSession.DataTaskPublisher.Output, url: URL) throws -> Data { + return Data() + } + + func handleCompletion(completion: Subscribers.Completion) { + switch completion { + case .finished: + break + case .failure(let error): + print(error.localizedDescription) + } + } +} +#endif diff --git a/GithubTrending/Mocks/MockServices/MockImageFetchingService.swift b/GithubTrending/Mocks/MockServices/MockImageFetchingService.swift new file mode 100644 index 0000000..57e68e9 --- /dev/null +++ b/GithubTrending/Mocks/MockServices/MockImageFetchingService.swift @@ -0,0 +1,20 @@ +// +// MockImageFetchingService.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 11.. +// + +#if DEBUG +import Foundation + +final class MockImageFetchingService: FileFetchingServiceProtocol, Mockable { + func asyncDownload(url: URL) async throws -> T? { + return handleResponse(data: loadAvatarImageSample(), response: .none) + } + + func handleResponse(data: Data?, response: URLResponse?) -> T? { + return data as? T + } +} +#endif diff --git a/GithubTrending/Mocks/MockServices/MockReadMeFetchingService.swift b/GithubTrending/Mocks/MockServices/MockReadMeFetchingService.swift new file mode 100644 index 0000000..f48e6a9 --- /dev/null +++ b/GithubTrending/Mocks/MockServices/MockReadMeFetchingService.swift @@ -0,0 +1,29 @@ +// +// MockReadMeFetchingService.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 10.. +// + +#if DEBUG +import Foundation + +final class MockReadMeFetchingService: FileFetchingServiceProtocol, Mockable { + var mockNetworkFailure: Bool = false + func asyncDownload(url: URL) async throws -> T? { + if mockNetworkFailure { + throw NetworkingError.unknown + } + return handleResponse(data: loadReadMeSample(), response: .none) + } + + func handleResponse(data: Data?, response: URLResponse?) -> T? { + guard + let data = data, + let string = String(data: data, encoding: .utf8) as? T else { + fatalError("Failed to decode .md data to String.") + } + return string + } +} +#endif diff --git a/GithubTrending/Mocks/MockServices/MockTrendingScraperService.swift b/GithubTrending/Mocks/MockServices/MockTrendingScraperService.swift new file mode 100644 index 0000000..cce1886 --- /dev/null +++ b/GithubTrending/Mocks/MockServices/MockTrendingScraperService.swift @@ -0,0 +1,37 @@ +// +// MockTrendingScraperService.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 10.. +// + +#if DEBUG +import Combine +import Foundation + +final class MockTrendingScraperService: ScraperServiceProtocol { + var scrapedRepoModelsResults: [ScrapedRepoModel] = [] + var mockNetworkFailure: Bool = false + + func scrapData() -> Future<[T], Error> { + return Future { promise in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + if self?.mockNetworkFailure ?? false { + promise(.failure(NetworkingError.unknown)) + } else { + promise(.success(self?.scrapedRepoModelsResults as? [T] ?? [])) + } + } + } + } + + func fillUpscrapedRepoModelsResults() { + let firstItem = ScrapedRepoModel(ownerName: "huggingface", repoName: "datasets") + let lastItem = ScrapedRepoModel(ownerName: "ashawkey", repoName: "stable-dreamfusion") + for _ in 1...5 { + scrapedRepoModelsResults.append(firstItem) + } + scrapedRepoModelsResults.append(lastItem) + } +} +#endif diff --git a/GithubTrending/Mocks/Mockable.swift b/GithubTrending/Mocks/Mockable.swift new file mode 100644 index 0000000..1189dfe --- /dev/null +++ b/GithubTrending/Mocks/Mockable.swift @@ -0,0 +1,71 @@ +// +// Mockable.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 10.. +// + +#if DEBUG +import Foundation + +protocol Mockable: AnyObject { + var bundle: Bundle { get } + func loadJSONDataForEndpoint(endpoint: EndpointItem) -> Data + func loadReadMeSample() -> Data? +} + +extension Mockable { + var bundle: Bundle { + return Bundle(for: type(of: self)) + } + + private func jsonForEndpoint(endpoint: EndpointItem) -> String { + switch endpoint { + case .getRepo(_, _): + let strings = endpoint.value() as? [String] ?? [] + return "RepoResponse_\(strings[0])_\(strings[1])" + case .getRepoReadMe: + return "RepoReadMeResponse" + } + } + + func loadJSONDataForEndpoint(endpoint: EndpointItem) -> Data { + guard let path = bundle.url(forResource: jsonForEndpoint(endpoint: endpoint), withExtension: "json") else { + fatalError("❌ Failed to load JSON file: RepoResponse_\(endpoint).") + } + do { + let data = try Data(contentsOf: path) + return data + } catch { + print("❌ \(error)") + fatalError("Failed to convert json file to Data with endpoint: \(endpoint).") + } + } + + func loadReadMeSample() -> Data? { + guard let path = bundle.url(forResource: "ResponseReadMeSample", withExtension: "md") else { + fatalError("❌ Failed to load .md file.") + } + do { + let data = try Data(contentsOf: path) + return data + } catch { + print("❌ \(error)") + fatalError("❌ Failed to convert .md file to Data.") + } + } + + func loadAvatarImageSample() -> Data? { + guard let path = bundle.url(forResource: "ResponseAvatarImageSample", withExtension: "png") else { + fatalError("❌ Failed to load .png file.") + } + do { + let data = try Data(contentsOf: path) + return data + } catch { + print("❌ \(error)") + fatalError("❌ Failed to convert .png file to Data.") + } + } +} +#endif diff --git a/GithubTrending/Mocks/MockedResponses/RepoReadMeResponse.json b/GithubTrending/Mocks/MockedResponses/RepoReadMeResponse.json new file mode 100644 index 0000000..1d7853a --- /dev/null +++ b/GithubTrending/Mocks/MockedResponses/RepoReadMeResponse.json @@ -0,0 +1,18 @@ +{ + "name": "README.md", + "path": "README.md", + "sha": "348c7bc54646230ea64bf3e22629f06f4c6f4b71", + "size": 12174, + "url": "https://api.github.com/repos/huggingface/datasets/contents/README.md?ref=main", + "html_url": "https://github.com/huggingface/datasets/blob/main/README.md", + "git_url": "https://api.github.com/repos/huggingface/datasets/git/blobs/348c7bc54646230ea64bf3e22629f06f4c6f4b71", + "download_url": "https://raw.githubusercontent.com/huggingface/datasets/main/README.md", + "type": "file", + "content": "PHAgYWxpZ249ImNlbnRlciI+CiAgICA8YnI+CiAgICA8aW1nIHNyYz0iaHR0\ncHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2h1Z2dpbmdmYWNlL2Rh\ndGFzZXRzL21haW4vZG9jcy9zb3VyY2UvaW1ncy9kYXRhc2V0c19sb2dvX25h\nbWUuanBnIiB3aWR0aD0iNDAwIi8+CiAgICA8YnI+CjxwPgo8cCBhbGlnbj0i\nY2VudGVyIj4KICAgIDxhIGhyZWY9Imh0dHBzOi8vZ2l0aHViLmNvbS9odWdn\naW5nZmFjZS9kYXRhc2V0cy9hY3Rpb25zL3dvcmtmbG93cy9jaS55bWw/cXVl\ncnk9YnJhbmNoJTNBbWFpbiI+CiAgICAgICAgPGltZyBhbHQ9IkJ1aWxkIiBz\ncmM9Imh0dHBzOi8vZ2l0aHViLmNvbS9odWdnaW5nZmFjZS9kYXRhc2V0cy9h\nY3Rpb25zL3dvcmtmbG93cy9jaS55bWwvYmFkZ2Uuc3ZnP2JyYW5jaD1tYWlu\nIj4KICAgIDwvYT4KICAgIDxhIGhyZWY9Imh0dHBzOi8vZ2l0aHViLmNvbS9o\ndWdnaW5nZmFjZS9kYXRhc2V0cy9ibG9iL21haW4vTElDRU5TRSI+CiAgICAg\nICAgPGltZyBhbHQ9IkdpdEh1YiIgc3JjPSJodHRwczovL2ltZy5zaGllbGRz\nLmlvL2dpdGh1Yi9saWNlbnNlL2h1Z2dpbmdmYWNlL2RhdGFzZXRzLnN2Zz9j\nb2xvcj1ibHVlIj4KICAgIDwvYT4KICAgIDxhIGhyZWY9Imh0dHBzOi8vaHVn\nZ2luZ2ZhY2UuY28vZG9jcy9kYXRhc2V0cy9pbmRleC5odG1sIj4KICAgICAg\nICA8aW1nIGFsdD0iRG9jdW1lbnRhdGlvbiIgc3JjPSJodHRwczovL2ltZy5z\naGllbGRzLmlvL3dlYnNpdGUvaHR0cC9odWdnaW5nZmFjZS5jby9kb2NzL2Rh\ndGFzZXRzL2luZGV4Lmh0bWwuc3ZnP2Rvd25fY29sb3I9cmVkJmRvd25fbWVz\nc2FnZT1vZmZsaW5lJnVwX21lc3NhZ2U9b25saW5lIj4KICAgIDwvYT4KICAg\nIDxhIGhyZWY9Imh0dHBzOi8vZ2l0aHViLmNvbS9odWdnaW5nZmFjZS9kYXRh\nc2V0cy9yZWxlYXNlcyI+CiAgICAgICAgPGltZyBhbHQ9IkdpdEh1YiByZWxl\nYXNlIiBzcmM9Imh0dHBzOi8vaW1nLnNoaWVsZHMuaW8vZ2l0aHViL3JlbGVh\nc2UvaHVnZ2luZ2ZhY2UvZGF0YXNldHMuc3ZnIj4KICAgIDwvYT4KICAgIDxh\nIGhyZWY9Imh0dHBzOi8vaHVnZ2luZ2ZhY2UuY28vZGF0YXNldHMvIj4KICAg\nICAgICA8aW1nIGFsdD0iTnVtYmVyIG9mIGRhdGFzZXRzIiBzcmM9Imh0dHBz\nOi8vaW1nLnNoaWVsZHMuaW8vZW5kcG9pbnQ/dXJsPWh0dHBzOi8vaHVnZ2lu\nZ2ZhY2UuY28vYXBpL3NoaWVsZHMvZGF0YXNldHMmY29sb3I9YnJpZ2h0Z3Jl\nZW4iPgogICAgPC9hPgogICAgPGEgaHJlZj0iQ09ERV9PRl9DT05EVUNULm1k\nIj4KICAgICAgICA8aW1nIGFsdD0iQ29udHJpYnV0b3IgQ292ZW5hbnQiIHNy\nYz0iaHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS9Db250cmlidXRvciUy\nMENvdmVuYW50LTIuMC00YmFhYWEuc3ZnIj4KICAgIDwvYT4KICAgIDxhIGhy\nZWY9Imh0dHBzOi8vemVub2RvLm9yZy9iYWRnZS9sYXRlc3Rkb2kvMjUwMjEz\nMjg2Ij48aW1nIHNyYz0iaHR0cHM6Ly96ZW5vZG8ub3JnL2JhZGdlLzI1MDIx\nMzI4Ni5zdmciIGFsdD0iRE9JIj48L2E+CjwvcD4KCvCfpJcgRGF0YXNldHMg\naXMgYSBsaWdodHdlaWdodCBsaWJyYXJ5IHByb3ZpZGluZyAqKnR3byoqIG1h\naW4gZmVhdHVyZXM6CgotICoqb25lLWxpbmUgZGF0YWxvYWRlcnMgZm9yIG1h\nbnkgcHVibGljIGRhdGFzZXRzKio6IG9uZS1saW5lcnMgdG8gZG93bmxvYWQg\nYW5kIHByZS1wcm9jZXNzIGFueSBvZiB0aGUgIVtudW1iZXIgb2YgZGF0YXNl\ndHNdKGh0dHBzOi8vaW1nLnNoaWVsZHMuaW8vZW5kcG9pbnQ/dXJsPWh0dHBz\nOi8vaHVnZ2luZ2ZhY2UuY28vYXBpL3NoaWVsZHMvZGF0YXNldHMmY29sb3I9\nYnJpZ2h0Z3JlZW4pIG1ham9yIHB1YmxpYyBkYXRhc2V0cyAodGV4dCBkYXRh\nc2V0cyBpbiA0NjcgbGFuZ3VhZ2VzIGFuZCBkaWFsZWN0cywgaW1hZ2UgZGF0\nYXNldHMsIGF1ZGlvIGRhdGFzZXRzLCBldGMuKSBwcm92aWRlZCBvbiB0aGUg\nW0h1Z2dpbmdGYWNlIERhdGFzZXRzIEh1Yl0oaHR0cHM6Ly9odWdnaW5nZmFj\nZS5jby9kYXRhc2V0cykuIFdpdGggYSBzaW1wbGUgY29tbWFuZCBsaWtlIGBz\ncXVhZF9kYXRhc2V0ID0gbG9hZF9kYXRhc2V0KCJzcXVhZCIpYCwgZ2V0IGFu\neSBvZiB0aGVzZSBkYXRhc2V0cyByZWFkeSB0byB1c2UgaW4gYSBkYXRhbG9h\nZGVyIGZvciB0cmFpbmluZy9ldmFsdWF0aW5nIGEgTUwgbW9kZWwgKE51bXB5\nL1BhbmRhcy9QeVRvcmNoL1RlbnNvckZsb3cvSkFYKSwKLSAqKmVmZmljaWVu\ndCBkYXRhIHByZS1wcm9jZXNzaW5nKio6IHNpbXBsZSwgZmFzdCBhbmQgcmVw\ncm9kdWNpYmxlIGRhdGEgcHJlLXByb2Nlc3NpbmcgZm9yIHRoZSBhYm92ZSBw\ndWJsaWMgZGF0YXNldHMgYXMgd2VsbCBhcyB5b3VyIG93biBsb2NhbCBkYXRh\nc2V0cyBpbiBDU1YvSlNPTi90ZXh0L1BORy9KUEVHL2V0Yy4gV2l0aCBzaW1w\nbGUgY29tbWFuZHMgbGlrZSBgcHJvY2Vzc2VkX2RhdGFzZXQgPSBkYXRhc2V0\nLm1hcChwcm9jZXNzX2V4YW1wbGUpYCwgZWZmaWNpZW50bHkgcHJlcGFyZSB0\naGUgZGF0YXNldCBmb3IgaW5zcGVjdGlvbiBhbmQgTUwgbW9kZWwgZXZhbHVh\ndGlvbiBhbmQgdHJhaW5pbmcuCgpb8J+OkyAqKkRvY3VtZW50YXRpb24qKl0o\naHR0cHM6Ly9odWdnaW5nZmFjZS5jby9kb2NzL2RhdGFzZXRzLykgW/Cflbkg\nKipDb2xhYiB0dXRvcmlhbCoqXShodHRwczovL2NvbGFiLnJlc2VhcmNoLmdv\nb2dsZS5jb20vZ2l0aHViL2h1Z2dpbmdmYWNlL2RhdGFzZXRzL2Jsb2IvbWFp\nbi9ub3RlYm9va3MvT3ZlcnZpZXcuaXB5bmIpCgpb8J+UjiAqKkZpbmQgYSBk\nYXRhc2V0IGluIHRoZSBIdWIqKl0oaHR0cHM6Ly9odWdnaW5nZmFjZS5jby9k\nYXRhc2V0cykgW/CfjJ8gKipBZGQgYSBuZXcgZGF0YXNldCB0byB0aGUgSHVi\nKipdKGh0dHBzOi8vZ2l0aHViLmNvbS9odWdnaW5nZmFjZS9kYXRhc2V0cy9i\nbG9iL21haW4vQUREX05FV19EQVRBU0VULm1kKQoKPGgzIGFsaWduPSJjZW50\nZXIiPgogICAgPGEgaHJlZj0iaHR0cHM6Ly9oZi5jby9jb3Vyc2UiPjxpbWcg\nc3JjPSJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vaHVnZ2lu\nZ2ZhY2UvZGF0YXNldHMvbWFpbi9kb2NzL3NvdXJjZS9pbWdzL2NvdXJzZV9i\nYW5uZXIucG5nIj48L2E+CjwvaDM+Cgrwn6SXIERhdGFzZXRzIGlzIGRlc2ln\nbmVkIHRvIGxldCB0aGUgY29tbXVuaXR5IGVhc2lseSBhZGQgYW5kIHNoYXJl\nIG5ldyBkYXRhc2V0cy4KCvCfpJcgRGF0YXNldHMgaGFzIG1hbnkgYWRkaXRp\nb25hbCBpbnRlcmVzdGluZyBmZWF0dXJlczoKCi0gVGhyaXZlIG9uIGxhcmdl\nIGRhdGFzZXRzOiDwn6SXIERhdGFzZXRzIG5hdHVyYWxseSBmcmVlcyB0aGUg\ndXNlciBmcm9tIFJBTSBtZW1vcnkgbGltaXRhdGlvbiwgYWxsIGRhdGFzZXRz\nIGFyZSBtZW1vcnktbWFwcGVkIHVzaW5nIGFuIGVmZmljaWVudCB6ZXJvLXNl\ncmlhbGl6YXRpb24gY29zdCBiYWNrZW5kIChBcGFjaGUgQXJyb3cpLgotIFNt\nYXJ0IGNhY2hpbmc6IG5ldmVyIHdhaXQgZm9yIHlvdXIgZGF0YSB0byBwcm9j\nZXNzIHNldmVyYWwgdGltZXMuCi0gTGlnaHR3ZWlnaHQgYW5kIGZhc3Qgd2l0\naCBhIHRyYW5zcGFyZW50IGFuZCBweXRob25pYyBBUEkgKG11bHRpLXByb2Nl\nc3NpbmcvY2FjaGluZy9tZW1vcnktbWFwcGluZykuCi0gQnVpbHQtaW4gaW50\nZXJvcGVyYWJpbGl0eSB3aXRoIE51bVB5LCBwYW5kYXMsIFB5VG9yY2gsIFRl\nbnNvcmZsb3cgMiBhbmQgSkFYLgoK8J+klyBEYXRhc2V0cyBvcmlnaW5hdGVk\nIGZyb20gYSBmb3JrIG9mIHRoZSBhd2Vzb21lIFtUZW5zb3JGbG93IERhdGFz\nZXRzXShodHRwczovL2dpdGh1Yi5jb20vdGVuc29yZmxvdy9kYXRhc2V0cykg\nYW5kIHRoZSBIdWdnaW5nRmFjZSB0ZWFtIHdhbnQgdG8gZGVlcGx5IHRoYW5r\nIHRoZSBUZW5zb3JGbG93IERhdGFzZXRzIHRlYW0gZm9yIGJ1aWxkaW5nIHRo\naXMgYW1hemluZyBsaWJyYXJ5LiBNb3JlIGRldGFpbHMgb24gdGhlIGRpZmZl\ncmVuY2VzIGJldHdlZW4g8J+klyBEYXRhc2V0cyBhbmQgYHRmZHNgIGNhbiBi\nZSBmb3VuZCBpbiB0aGUgc2VjdGlvbiBbTWFpbiBkaWZmZXJlbmNlcyBiZXR3\nZWVuIPCfpJcgRGF0YXNldHMgYW5kIGB0ZmRzYF0oI21haW4tZGlmZmVyZW5j\nZXMtYmV0d2Vlbi0tZGF0YXNldHMtYW5kLXRmZHMpLgoKIyBJbnN0YWxsYXRp\nb24KCiMjIFdpdGggcGlwCgrwn6SXIERhdGFzZXRzIGNhbiBiZSBpbnN0YWxs\nZWQgZnJvbSBQeVBpIGFuZCBoYXMgdG8gYmUgaW5zdGFsbGVkIGluIGEgdmly\ndHVhbCBlbnZpcm9ubWVudCAodmVudiBvciBjb25kYSBmb3IgaW5zdGFuY2Up\nCgpgYGBiYXNoCnBpcCBpbnN0YWxsIGRhdGFzZXRzCmBgYAoKIyMgV2l0aCBj\nb25kYQoK8J+klyBEYXRhc2V0cyBjYW4gYmUgaW5zdGFsbGVkIHVzaW5nIGNv\nbmRhIGFzIGZvbGxvd3M6CgpgYGBiYXNoCmNvbmRhIGluc3RhbGwgLWMgaHVn\nZ2luZ2ZhY2UgLWMgY29uZGEtZm9yZ2UgZGF0YXNldHMKYGBgCgpGb2xsb3cg\ndGhlIGluc3RhbGxhdGlvbiBwYWdlcyBvZiBUZW5zb3JGbG93IGFuZCBQeVRv\ncmNoIHRvIHNlZSBob3cgdG8gaW5zdGFsbCB0aGVtIHdpdGggY29uZGEuCgpG\nb3IgbW9yZSBkZXRhaWxzIG9uIGluc3RhbGxhdGlvbiwgY2hlY2sgdGhlIGlu\nc3RhbGxhdGlvbiBwYWdlIGluIHRoZSBkb2N1bWVudGF0aW9uOiBodHRwczov\nL2h1Z2dpbmdmYWNlLmNvL2RvY3MvZGF0YXNldHMvaW5zdGFsbGF0aW9uCgoj\nIyBJbnN0YWxsYXRpb24gdG8gdXNlIHdpdGggUHlUb3JjaC9UZW5zb3JGbG93\nL3BhbmRhcwoKSWYgeW91IHBsYW4gdG8gdXNlIPCfpJcgRGF0YXNldHMgd2l0\naCBQeVRvcmNoICgxLjArKSwgVGVuc29yRmxvdyAoMi4yKykgb3IgcGFuZGFz\nLCB5b3Ugc2hvdWxkIGFsc28gaW5zdGFsbCBQeVRvcmNoLCBUZW5zb3JGbG93\nIG9yIHBhbmRhcy4KCkZvciBtb3JlIGRldGFpbHMgb24gdXNpbmcgdGhlIGxp\nYnJhcnkgd2l0aCBOdW1QeSwgcGFuZGFzLCBQeVRvcmNoIG9yIFRlbnNvckZs\nb3csIGNoZWNrIHRoZSBxdWljayBzdGFydCBwYWdlIGluIHRoZSBkb2N1bWVu\ndGF0aW9uOiBodHRwczovL2h1Z2dpbmdmYWNlLmNvL2RvY3MvZGF0YXNldHMv\ncXVpY2tzdGFydAoKIyBVc2FnZQoK8J+klyBEYXRhc2V0cyBpcyBtYWRlIHRv\nIGJlIHZlcnkgc2ltcGxlIHRvIHVzZS4gVGhlIG1haW4gbWV0aG9kcyBhcmU6\nCgotIGBkYXRhc2V0cy5saXN0X2RhdGFzZXRzKClgIHRvIGxpc3QgdGhlIGF2\nYWlsYWJsZSBkYXRhc2V0cwotIGBkYXRhc2V0cy5sb2FkX2RhdGFzZXQoZGF0\nYXNldF9uYW1lLCAqKmt3YXJncylgIHRvIGluc3RhbnRpYXRlIGEgZGF0YXNl\ndAoKVGhpcyBsaWJyYXJ5IGNhbiBiZSB1c2VkIGZvciB0ZXh0L2ltYWdlL2F1\nZGlvL2V0Yy4gZGF0YXNldHMuIEhlcmUgaXMgYW4gZXhhbXBsZSB0byBsb2Fk\nIGEgdGV4dCBkYXRhc2V0OgoKSGVyZSBpcyBhIHF1aWNrIGV4YW1wbGU6Cgpg\nYGBweXRob24KZnJvbSBkYXRhc2V0cyBpbXBvcnQgbGlzdF9kYXRhc2V0cywg\nbG9hZF9kYXRhc2V0CgojIFByaW50IGFsbCB0aGUgYXZhaWxhYmxlIGRhdGFz\nZXRzCnByaW50KGxpc3RfZGF0YXNldHMoKSkKCiMgTG9hZCBhIGRhdGFzZXQg\nYW5kIHByaW50IHRoZSBmaXJzdCBleGFtcGxlIGluIHRoZSB0cmFpbmluZyBz\nZXQKc3F1YWRfZGF0YXNldCA9IGxvYWRfZGF0YXNldCgnc3F1YWQnKQpwcmlu\ndChzcXVhZF9kYXRhc2V0Wyd0cmFpbiddWzBdKQoKIyBQcm9jZXNzIHRoZSBk\nYXRhc2V0IC0gYWRkIGEgY29sdW1uIHdpdGggdGhlIGxlbmd0aCBvZiB0aGUg\nY29udGV4dCB0ZXh0cwpkYXRhc2V0X3dpdGhfbGVuZ3RoID0gc3F1YWRfZGF0\nYXNldC5tYXAobGFtYmRhIHg6IHsibGVuZ3RoIjogbGVuKHhbImNvbnRleHQi\nXSl9KQoKIyBQcm9jZXNzIHRoZSBkYXRhc2V0IC0gdG9rZW5pemUgdGhlIGNv\nbnRleHQgdGV4dHMgKHVzaW5nIGEgdG9rZW5pemVyIGZyb20gdGhlIPCfpJcg\nVHJhbnNmb3JtZXJzIGxpYnJhcnkpCmZyb20gdHJhbnNmb3JtZXJzIGltcG9y\ndCBBdXRvVG9rZW5pemVyCnRva2VuaXplciA9IEF1dG9Ub2tlbml6ZXIuZnJv\nbV9wcmV0cmFpbmVkKCdiZXJ0LWJhc2UtY2FzZWQnKQoKdG9rZW5pemVkX2Rh\ndGFzZXQgPSBzcXVhZF9kYXRhc2V0Lm1hcChsYW1iZGEgeDogdG9rZW5pemVy\nKHhbJ2NvbnRleHQnXSksIGJhdGNoZWQ9VHJ1ZSkKYGBgCgpGb3IgbW9yZSBk\nZXRhaWxzIG9uIHVzaW5nIHRoZSBsaWJyYXJ5LCBjaGVjayB0aGUgcXVpY2sg\nc3RhcnQgcGFnZSBpbiB0aGUgZG9jdW1lbnRhdGlvbjogaHR0cHM6Ly9odWdn\naW5nZmFjZS5jby9kb2NzL2RhdGFzZXRzL3F1aWNrc3RhcnQuaHRtbCBhbmQg\ndGhlIHNwZWNpZmljIHBhZ2VzIG9uOgoKLSBMb2FkaW5nIGEgZGF0YXNldCBo\ndHRwczovL2h1Z2dpbmdmYWNlLmNvL2RvY3MvZGF0YXNldHMvbG9hZGluZwot\nIFdoYXQncyBpbiBhIERhdGFzZXQ6IGh0dHBzOi8vaHVnZ2luZ2ZhY2UuY28v\nZG9jcy9kYXRhc2V0cy9hY2Nlc3MKLSBQcm9jZXNzaW5nIGRhdGEgd2l0aCDw\nn6SXIERhdGFzZXRzOiBodHRwczovL2h1Z2dpbmdmYWNlLmNvL2RvY3MvZGF0\nYXNldHMvcHJvY2VzcwotIFByb2Nlc3NpbmcgYXVkaW8gZGF0YTogaHR0cHM6\nLy9odWdnaW5nZmFjZS5jby9kb2NzL2RhdGFzZXRzL2F1ZGlvX3Byb2Nlc3MK\nLSBQcm9jZXNzaW5nIGltYWdlIGRhdGE6IGh0dHBzOi8vaHVnZ2luZ2ZhY2Uu\nY28vZG9jcy9kYXRhc2V0cy9pbWFnZV9wcm9jZXNzCi0gV3JpdGluZyB5b3Vy\nIG93biBkYXRhc2V0IGxvYWRpbmcgc2NyaXB0OiBodHRwczovL2h1Z2dpbmdm\nYWNlLmNvL2RvY3MvZGF0YXNldHMvZGF0YXNldF9zY3JpcHQKLSBldGMuCgpB\nbm90aGVyIGludHJvZHVjdGlvbiB0byDwn6SXIERhdGFzZXRzIGlzIHRoZSB0\ndXRvcmlhbCBvbiBHb29nbGUgQ29sYWIgaGVyZToKWyFbT3BlbiBJbiBDb2xh\nYl0oaHR0cHM6Ly9jb2xhYi5yZXNlYXJjaC5nb29nbGUuY29tL2Fzc2V0cy9j\nb2xhYi1iYWRnZS5zdmcpXShodHRwczovL2NvbGFiLnJlc2VhcmNoLmdvb2ds\nZS5jb20vZ2l0aHViL2h1Z2dpbmdmYWNlL2RhdGFzZXRzL2Jsb2IvbWFpbi9u\nb3RlYm9va3MvT3ZlcnZpZXcuaXB5bmIpCgojIEFkZCBhIG5ldyBkYXRhc2V0\nIHRvIHRoZSBIdWIKCldlIGhhdmUgYSB2ZXJ5IGRldGFpbGVkIHN0ZXAtYnkt\nc3RlcCBndWlkZSB0byBhZGQgYSBuZXcgZGF0YXNldCB0byB0aGUgIVtudW1i\nZXIgb2YgZGF0YXNldHNdKGh0dHBzOi8vaW1nLnNoaWVsZHMuaW8vZW5kcG9p\nbnQ/dXJsPWh0dHBzOi8vaHVnZ2luZ2ZhY2UuY28vYXBpL3NoaWVsZHMvZGF0\nYXNldHMmY29sb3I9YnJpZ2h0Z3JlZW4pIGRhdGFzZXRzIGFscmVhZHkgcHJv\ndmlkZWQgb24gdGhlIFtIdWdnaW5nRmFjZSBEYXRhc2V0cyBIdWJdKGh0dHBz\nOi8vaHVnZ2luZ2ZhY2UuY28vZGF0YXNldHMpLgoKWW91IHdpbGwgZmluZCBb\ndGhlIHN0ZXAtYnktc3RlcCBndWlkZSBoZXJlXShodHRwczovL2h1Z2dpbmdm\nYWNlLmNvL2RvY3MvZGF0YXNldHMvc2hhcmUuaHRtbCkgdG8gYWRkIGEgZGF0\nYXNldCBvbiB0aGUgSHViLgoKSG93ZXZlciBpZiB5b3UgcHJlZmVyIHRvIGFk\nZCB5b3VyIGRhdGFzZXQgaW4gdGhpcyByZXBvc2l0b3J5LCB5b3UgY2FuIGZp\nbmQgdGhlIGd1aWRlIFtoZXJlXShodHRwczovL2dpdGh1Yi5jb20vaHVnZ2lu\nZ2ZhY2UvZGF0YXNldHMvYmxvYi9tYWluL0FERF9ORVdfREFUQVNFVC5tZCku\nCgojIE1haW4gZGlmZmVyZW5jZXMgYmV0d2VlbiDwn6SXIERhdGFzZXRzIGFu\nZCBgdGZkc2AKCklmIHlvdSBhcmUgZmFtaWxpYXIgd2l0aCB0aGUgZ3JlYXQg\nVGVuc29yRmxvdyBEYXRhc2V0cywgaGVyZSBhcmUgdGhlIG1haW4gZGlmZmVy\nZW5jZXMgYmV0d2VlbiDwn6SXIERhdGFzZXRzIGFuZCBgdGZkc2A6CgotIHRo\nZSBzY3JpcHRzIGluIPCfpJcgRGF0YXNldHMgYXJlIG5vdCBwcm92aWRlZCB3\naXRoaW4gdGhlIGxpYnJhcnkgYnV0IGFyZSBxdWVyaWVkLCBkb3dubG9hZGVk\nL2NhY2hlZCBhbmQgZHluYW1pY2FsbHkgbG9hZGVkIHVwb24gcmVxdWVzdAot\nIPCfpJcgRGF0YXNldHMgYWxzbyBwcm92aWRlcyBldmFsdWF0aW9uIG1ldHJp\nY3MgaW4gYSBzaW1pbGFyIGZhc2hpb24gdG8gdGhlIGRhdGFzZXRzLCBpLmUu\nIGFzIGR5bmFtaWNhbGx5IGluc3RhbGxlZCBzY3JpcHRzIHdpdGggYSB1bmlm\naWVkIEFQSS4gVGhpcyBnaXZlcyBhY2Nlc3MgdG8gdGhlIHBhaXIgb2YgYSBi\nZW5jaG1hcmsgZGF0YXNldCBhbmQgYSBiZW5jaG1hcmsgbWV0cmljIGZvciBp\nbnN0YW5jZSBmb3IgYmVuY2htYXJrcyBsaWtlIFtTUXVBRF0oaHR0cHM6Ly9y\nYWpwdXJrYXIuZ2l0aHViLmlvL1NRdUFELWV4cGxvcmVyLykgb3IgW0dMVUVd\nKGh0dHBzOi8vZ2x1ZWJlbmNobWFyay5jb20vKS4KLSB0aGUgYmFja2VuZCBz\nZXJpYWxpemF0aW9uIG9mIPCfpJcgRGF0YXNldHMgaXMgYmFzZWQgb24gW0Fw\nYWNoZSBBcnJvd10oaHR0cHM6Ly9hcnJvdy5hcGFjaGUub3JnLykgaW5zdGVh\nZCBvZiBURiBSZWNvcmRzIGFuZCBsZXZlcmFnZSBweXRob24gZGF0YWNsYXNz\nZXMgZm9yIGluZm8gYW5kIGZlYXR1cmVzIHdpdGggc29tZSBkaXZlcmdpbmcg\nZmVhdHVyZXMgKHdlIG1vc3RseSBkb24ndCBkbyBlbmNvZGluZyBhbmQgc3Rv\ncmUgdGhlIHJhdyBkYXRhIGFzIG11Y2ggYXMgcG9zc2libGUgaW4gdGhlIGJh\nY2tlbmQgc2VyaWFsaXphdGlvbiBjYWNoZSkuCi0gdGhlIHVzZXItZmFjaW5n\nIGRhdGFzZXQgb2JqZWN0IG9mIPCfpJcgRGF0YXNldHMgaXMgbm90IGEgYHRm\nLmRhdGEuRGF0YXNldGAgYnV0IGEgYnVpbHQtaW4gZnJhbWV3b3JrLWFnbm9z\ndGljIGRhdGFzZXQgY2xhc3Mgd2l0aCBtZXRob2RzIGluc3BpcmVkIGJ5IHdo\nYXQgd2UgbGlrZSBpbiBgdGYuZGF0YWAgKGxpa2UgYSBgbWFwKClgIG1ldGhv\nZCkuIEl0IGJhc2ljYWxseSB3cmFwcyBhIG1lbW9yeS1tYXBwZWQgQXJyb3cg\ndGFibGUgY2FjaGUuCgojIERpc2NsYWltZXJzCgpTaW1pbGFyIHRvIFRlbnNv\nckZsb3cgRGF0YXNldHMsIPCfpJcgRGF0YXNldHMgaXMgYSB1dGlsaXR5IGxp\nYnJhcnkgdGhhdCBkb3dubG9hZHMgYW5kIHByZXBhcmVzIHB1YmxpYyBkYXRh\nc2V0cy4gV2UgZG8gbm90IGhvc3Qgb3IgZGlzdHJpYnV0ZSBtb3N0IG9mIHRo\nZXNlIGRhdGFzZXRzLCB2b3VjaCBmb3IgdGhlaXIgcXVhbGl0eSBvciBmYWly\nbmVzcywgb3IgY2xhaW0gdGhhdCB5b3UgaGF2ZSBsaWNlbnNlIHRvIHVzZSB0\naGVtLiBJdCBpcyB5b3VyIHJlc3BvbnNpYmlsaXR5IHRvIGRldGVybWluZSB3\naGV0aGVyIHlvdSBoYXZlIHBlcm1pc3Npb24gdG8gdXNlIHRoZSBkYXRhc2V0\nIHVuZGVyIHRoZSBkYXRhc2V0J3MgbGljZW5zZS4KCklmIHlvdSdyZSBhIGRh\ndGFzZXQgb3duZXIgYW5kIHdpc2ggdG8gdXBkYXRlIGFueSBwYXJ0IG9mIGl0\nIChkZXNjcmlwdGlvbiwgY2l0YXRpb24sIGV0Yy4pLCBvciBkbyBub3Qgd2Fu\ndCB5b3VyIGRhdGFzZXQgdG8gYmUgaW5jbHVkZWQgaW4gdGhpcyBsaWJyYXJ5\nLCBwbGVhc2UgZ2V0IGluIHRvdWNoIHRocm91Z2ggYSBbR2l0SHViIGlzc3Vl\nXShodHRwczovL2dpdGh1Yi5jb20vaHVnZ2luZ2ZhY2UvZGF0YXNldHMvaXNz\ndWVzL25ldykuIFRoYW5rcyBmb3IgeW91ciBjb250cmlidXRpb24gdG8gdGhl\nIE1MIGNvbW11bml0eSEKCiMjIEJpYlRlWAoKSWYgeW91IHdhbnQgdG8gY2l0\nZSBvdXIg8J+klyBEYXRhc2V0cyBsaWJyYXJ5LCB5b3UgY2FuIHVzZSBvdXIg\nW3BhcGVyXShodHRwczovL2FyeGl2Lm9yZy9hYnMvMjEwOS4wMjg0Nik6Cgpg\nYGBiaWJ0ZXgKQGlucHJvY2VlZGluZ3N7bGhvZXN0LWV0YWwtMjAyMS1kYXRh\nc2V0cywKICAgIHRpdGxlID0gIkRhdGFzZXRzOiBBIENvbW11bml0eSBMaWJy\nYXJ5IGZvciBOYXR1cmFsIExhbmd1YWdlIFByb2Nlc3NpbmciLAogICAgYXV0\naG9yID0gIkxob2VzdCwgUXVlbnRpbiAgYW5kCiAgICAgIFZpbGxhbm92YSBk\nZWwgTW9yYWwsIEFsYmVydCAgYW5kCiAgICAgIEplcm5pdGUsIFlhY2luZSAg\nYW5kCiAgICAgIFRoYWt1ciwgQWJoaXNoZWsgIGFuZAogICAgICB2b24gUGxh\ndGVuLCBQYXRyaWNrICBhbmQKICAgICAgUGF0aWwsIFN1cmFqICBhbmQKICAg\nICAgQ2hhdW1vbmQsIEp1bGllbiAgYW5kCiAgICAgIERyYW1lLCBNYXJpYW1h\nICBhbmQKICAgICAgUGx1LCBKdWxpZW4gIGFuZAogICAgICBUdW5zdGFsbCwg\nTGV3aXMgIGFuZAogICAgICBEYXZpc29uLCBKb2UgIGFuZAogICAgICB7XHZ7\nU319YXtcdntzfX1rbywgTWFyaW8gIGFuZAogICAgICBDaGhhYmxhbmksIEd1\nbmphbiAgYW5kCiAgICAgIE1hbGlrLCBCaGF2aXR2eWEgIGFuZAogICAgICBC\ncmFuZGVpcywgU2ltb24gIGFuZAogICAgICBMZSBTY2FvLCBUZXZlbiAgYW5k\nCiAgICAgIFNhbmgsIFZpY3RvciAgYW5kCiAgICAgIFh1LCBDYW53ZW4gIGFu\nZAogICAgICBQYXRyeSwgTmljb2xhcyAgYW5kCiAgICAgIE1jTWlsbGFuLU1h\nam9yLCBBbmdlbGluYSAgYW5kCiAgICAgIFNjaG1pZCwgUGhpbGlwcCAgYW5k\nCiAgICAgIEd1Z2dlciwgU3lsdmFpbiAgYW5kCiAgICAgIERlbGFuZ3VlLCBD\nbHtcJ2V9bWVudCAgYW5kCiAgICAgIE1hdHVzc2l7XGBlfXJlLCBUaHtcJ2V9\nbyAgYW5kCiAgICAgIERlYnV0LCBMeXNhbmRyZSAgYW5kCiAgICAgIEJla21h\nbiwgU3RhcyAgYW5kCiAgICAgIENpc3RhYywgUGllcnJpYyAgYW5kCiAgICAg\nIEdvZWhyaW5nZXIsIFRoaWJhdWx0ICBhbmQKICAgICAgTXVzdGFyLCBWaWN0\nb3IgIGFuZAogICAgICBMYWd1bmFzLCBGcmFue1xje2N9fW9pcyAgYW5kCiAg\nICAgIFJ1c2gsIEFsZXhhbmRlciAgYW5kCiAgICAgIFdvbGYsIFRob21hcyIs\nCiAgICBib29rdGl0bGUgPSAiUHJvY2VlZGluZ3Mgb2YgdGhlIDIwMjEgQ29u\nZmVyZW5jZSBvbiBFbXBpcmljYWwgTWV0aG9kcyBpbiBOYXR1cmFsIExhbmd1\nYWdlIFByb2Nlc3Npbmc6IFN5c3RlbSBEZW1vbnN0cmF0aW9ucyIsCiAgICBt\nb250aCA9IG5vdiwKICAgIHllYXIgPSAiMjAyMSIsCiAgICBhZGRyZXNzID0g\nIk9ubGluZSBhbmQgUHVudGEgQ2FuYSwgRG9taW5pY2FuIFJlcHVibGljIiwK\nICAgIHB1Ymxpc2hlciA9ICJBc3NvY2lhdGlvbiBmb3IgQ29tcHV0YXRpb25h\nbCBMaW5ndWlzdGljcyIsCiAgICB1cmwgPSAiaHR0cHM6Ly9hY2xhbnRob2xv\nZ3kub3JnLzIwMjEuZW1ubHAtZGVtby4yMSIsCiAgICBwYWdlcyA9ICIxNzUt\nLTE4NCIsCiAgICBhYnN0cmFjdCA9ICJUaGUgc2NhbGUsIHZhcmlldHksIGFu\nZCBxdWFudGl0eSBvZiBwdWJsaWNseS1hdmFpbGFibGUgTkxQIGRhdGFzZXRz\nIGhhcyBncm93biByYXBpZGx5IGFzIHJlc2VhcmNoZXJzIHByb3Bvc2UgbmV3\nIHRhc2tzLCBsYXJnZXIgbW9kZWxzLCBhbmQgbm92ZWwgYmVuY2htYXJrcy4g\nRGF0YXNldHMgaXMgYSBjb21tdW5pdHkgbGlicmFyeSBmb3IgY29udGVtcG9y\nYXJ5IE5MUCBkZXNpZ25lZCB0byBzdXBwb3J0IHRoaXMgZWNvc3lzdGVtLiBE\nYXRhc2V0cyBhaW1zIHRvIHN0YW5kYXJkaXplIGVuZC11c2VyIGludGVyZmFj\nZXMsIHZlcnNpb25pbmcsIGFuZCBkb2N1bWVudGF0aW9uLCB3aGlsZSBwcm92\naWRpbmcgYSBsaWdodHdlaWdodCBmcm9udC1lbmQgdGhhdCBiZWhhdmVzIHNp\nbWlsYXJseSBmb3Igc21hbGwgZGF0YXNldHMgYXMgZm9yIGludGVybmV0LXNj\nYWxlIGNvcnBvcmEuIFRoZSBkZXNpZ24gb2YgdGhlIGxpYnJhcnkgaW5jb3Jw\nb3JhdGVzIGEgZGlzdHJpYnV0ZWQsIGNvbW11bml0eS1kcml2ZW4gYXBwcm9h\nY2ggdG8gYWRkaW5nIGRhdGFzZXRzIGFuZCBkb2N1bWVudGluZyB1c2FnZS4g\nQWZ0ZXIgYSB5ZWFyIG9mIGRldmVsb3BtZW50LCB0aGUgbGlicmFyeSBub3cg\naW5jbHVkZXMgbW9yZSB0aGFuIDY1MCB1bmlxdWUgZGF0YXNldHMsIGhhcyBt\nb3JlIHRoYW4gMjUwIGNvbnRyaWJ1dG9ycywgYW5kIGhhcyBoZWxwZWQgc3Vw\ncG9ydCBhIHZhcmlldHkgb2Ygbm92ZWwgY3Jvc3MtZGF0YXNldCByZXNlYXJj\naCBwcm9qZWN0cyBhbmQgc2hhcmVkIHRhc2tzLiBUaGUgbGlicmFyeSBpcyBh\ndmFpbGFibGUgYXQgaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2Rh\ndGFzZXRzLiIsCiAgICBlcHJpbnQ9ezIxMDkuMDI4NDZ9LAogICAgYXJjaGl2\nZVByZWZpeD17YXJYaXZ9LAogICAgcHJpbWFyeUNsYXNzPXtjcy5DTH0sCn0K\nYGBgCgpJZiB5b3UgbmVlZCB0byBjaXRlIGEgc3BlY2lmaWMgdmVyc2lvbiBv\nZiBvdXIg8J+klyBEYXRhc2V0cyBsaWJyYXJ5IGZvciByZXByb2R1Y2liaWxp\ndHksIHlvdSBjYW4gdXNlIHRoZSBjb3JyZXNwb25kaW5nIHZlcnNpb24gWmVu\nb2RvIERPSSBmcm9tIHRoaXMgW2xpc3RdKGh0dHBzOi8vemVub2RvLm9yZy9z\nZWFyY2g/cT1jb25jZXB0cmVjaWQ6JTIyNDgxNzc2OCUyMiZzb3J0PS12ZXJz\naW9uJmFsbF92ZXJzaW9ucz1UcnVlKS4K\n", + "encoding": "base64", + "_links": { + "self": "https://api.github.com/repos/huggingface/datasets/contents/README.md?ref=main", + "git": "https://api.github.com/repos/huggingface/datasets/git/blobs/348c7bc54646230ea64bf3e22629f06f4c6f4b71", + "html": "https://github.com/huggingface/datasets/blob/main/README.md" + } +} diff --git a/GithubTrending/Mocks/MockedResponses/RepoResponse_ashawkey_stable-dreamfusion.json b/GithubTrending/Mocks/MockedResponses/RepoResponse_ashawkey_stable-dreamfusion.json new file mode 100644 index 0000000..a7f3bf1 --- /dev/null +++ b/GithubTrending/Mocks/MockedResponses/RepoResponse_ashawkey_stable-dreamfusion.json @@ -0,0 +1,114 @@ +{ + "id": 546476703, + "node_id": "R_kgDOIJKSnw", + "name": "stable-dreamfusion", + "full_name": "ashawkey/stable-dreamfusion", + "private": false, + "owner": { + "login": "ashawkey", + "id": 25863658, + "node_id": "MDQ6VXNlcjI1ODYzNjU4", + "avatar_url": "https://avatars.githubusercontent.com/u/25863658?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ashawkey", + "html_url": "https://github.com/ashawkey", + "followers_url": "https://api.github.com/users/ashawkey/followers", + "following_url": "https://api.github.com/users/ashawkey/following{/other_user}", + "gists_url": "https://api.github.com/users/ashawkey/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ashawkey/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ashawkey/subscriptions", + "organizations_url": "https://api.github.com/users/ashawkey/orgs", + "repos_url": "https://api.github.com/users/ashawkey/repos", + "events_url": "https://api.github.com/users/ashawkey/events{/privacy}", + "received_events_url": "https://api.github.com/users/ashawkey/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/ashawkey/stable-dreamfusion", + "description": "A pytorch implementation of text-to-3D dreamfusion, powered by stable diffusion.", + "fork": false, + "url": "https://api.github.com/repos/ashawkey/stable-dreamfusion", + "forks_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/forks", + "keys_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/teams", + "hooks_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/hooks", + "issue_events_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/issues/events{/number}", + "events_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/events", + "assignees_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/assignees{/user}", + "branches_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/branches{/branch}", + "tags_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/tags", + "blobs_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/statuses/{sha}", + "languages_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/languages", + "stargazers_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/stargazers", + "contributors_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/contributors", + "subscribers_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/subscribers", + "subscription_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/subscription", + "commits_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/contents/{+path}", + "compare_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/merges", + "archive_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/downloads", + "issues_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/issues{/number}", + "pulls_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/pulls{/number}", + "milestones_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/milestones{/number}", + "notifications_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/labels{/name}", + "releases_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/releases{/id}", + "deployments_url": "https://api.github.com/repos/ashawkey/stable-dreamfusion/deployments", + "created_at": "2022-10-06T06:18:39Z", + "updated_at": "2022-10-10T16:55:05Z", + "pushed_at": "2022-10-10T10:41:19Z", + "git_url": "git://github.com/ashawkey/stable-dreamfusion.git", + "ssh_url": "git@github.com:ashawkey/stable-dreamfusion.git", + "clone_url": "https://github.com/ashawkey/stable-dreamfusion.git", + "svn_url": "https://github.com/ashawkey/stable-dreamfusion", + "homepage": "", + "size": 109, + "stargazers_count": 1858, + "watchers_count": 1858, + "language": "Python", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 104, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 11, + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0", + "node_id": "MDc6TGljZW5zZTI=" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + "dreamfusion", + "gui", + "nerf", + "stable-diffusion", + "text-to-3d" + ], + "visibility": "public", + "forks": 104, + "open_issues": 11, + "watchers": 1858, + "default_branch": "main", + "temp_clone_token": null, + "network_count": 104, + "subscribers_count": 39 +} diff --git a/GithubTrending/Mocks/MockedResponses/RepoResponse_huggingface_datasets.json b/GithubTrending/Mocks/MockedResponses/RepoResponse_huggingface_datasets.json new file mode 100644 index 0000000..a117696 --- /dev/null +++ b/GithubTrending/Mocks/MockedResponses/RepoResponse_huggingface_datasets.json @@ -0,0 +1,143 @@ +{ + "id": 250213286, + "node_id": "MDEwOlJlcG9zaXRvcnkyNTAyMTMyODY=", + "name": "datasets", + "full_name": "huggingface/datasets", + "private": false, + "owner": { + "login": "huggingface", + "id": 25720743, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjI1NzIwNzQz", + "avatar_url": "https://avatars.githubusercontent.com/u/25720743?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/huggingface", + "html_url": "https://github.com/huggingface", + "followers_url": "https://api.github.com/users/huggingface/followers", + "following_url": "https://api.github.com/users/huggingface/following{/other_user}", + "gists_url": "https://api.github.com/users/huggingface/gists{/gist_id}", + "starred_url": "https://api.github.com/users/huggingface/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/huggingface/subscriptions", + "organizations_url": "https://api.github.com/users/huggingface/orgs", + "repos_url": "https://api.github.com/users/huggingface/repos", + "events_url": "https://api.github.com/users/huggingface/events{/privacy}", + "received_events_url": "https://api.github.com/users/huggingface/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/huggingface/datasets", + "description": "🤗 The largest hub of ready-to-use datasets for ML models with fast, easy-to-use and efficient data manipulation tools", + "fork": false, + "url": "https://api.github.com/repos/huggingface/datasets", + "forks_url": "https://api.github.com/repos/huggingface/datasets/forks", + "keys_url": "https://api.github.com/repos/huggingface/datasets/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/huggingface/datasets/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/huggingface/datasets/teams", + "hooks_url": "https://api.github.com/repos/huggingface/datasets/hooks", + "issue_events_url": "https://api.github.com/repos/huggingface/datasets/issues/events{/number}", + "events_url": "https://api.github.com/repos/huggingface/datasets/events", + "assignees_url": "https://api.github.com/repos/huggingface/datasets/assignees{/user}", + "branches_url": "https://api.github.com/repos/huggingface/datasets/branches{/branch}", + "tags_url": "https://api.github.com/repos/huggingface/datasets/tags", + "blobs_url": "https://api.github.com/repos/huggingface/datasets/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/huggingface/datasets/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/huggingface/datasets/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/huggingface/datasets/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/huggingface/datasets/statuses/{sha}", + "languages_url": "https://api.github.com/repos/huggingface/datasets/languages", + "stargazers_url": "https://api.github.com/repos/huggingface/datasets/stargazers", + "contributors_url": "https://api.github.com/repos/huggingface/datasets/contributors", + "subscribers_url": "https://api.github.com/repos/huggingface/datasets/subscribers", + "subscription_url": "https://api.github.com/repos/huggingface/datasets/subscription", + "commits_url": "https://api.github.com/repos/huggingface/datasets/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/huggingface/datasets/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/huggingface/datasets/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/huggingface/datasets/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/huggingface/datasets/contents/{+path}", + "compare_url": "https://api.github.com/repos/huggingface/datasets/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/huggingface/datasets/merges", + "archive_url": "https://api.github.com/repos/huggingface/datasets/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/huggingface/datasets/downloads", + "issues_url": "https://api.github.com/repos/huggingface/datasets/issues{/number}", + "pulls_url": "https://api.github.com/repos/huggingface/datasets/pulls{/number}", + "milestones_url": "https://api.github.com/repos/huggingface/datasets/milestones{/number}", + "notifications_url": "https://api.github.com/repos/huggingface/datasets/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/huggingface/datasets/labels{/name}", + "releases_url": "https://api.github.com/repos/huggingface/datasets/releases{/id}", + "deployments_url": "https://api.github.com/repos/huggingface/datasets/deployments", + "created_at": "2020-03-26T09:23:22Z", + "updated_at": "2022-10-10T20:08:55Z", + "pushed_at": "2022-10-10T17:55:16Z", + "git_url": "git://github.com/huggingface/datasets.git", + "ssh_url": "git@github.com:huggingface/datasets.git", + "clone_url": "https://github.com/huggingface/datasets.git", + "svn_url": "https://github.com/huggingface/datasets", + "homepage": "https://huggingface.co/docs/datasets", + "size": 92407, + "stargazers_count": 14530, + "watchers_count": 14530, + "language": "Python", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 1871, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 448, + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0", + "node_id": "MDc6TGljZW5zZTI=" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [ + "computer-vision", + "datasets", + "deep-learning", + "evaluation", + "hacktoberfest", + "machine-learning", + "metrics", + "natural-language-processing", + "nlp", + "numpy", + "pandas", + "pytorch", + "speech", + "tensorflow" + ], + "visibility": "public", + "forks": 1871, + "open_issues": 448, + "watchers": 14530, + "default_branch": "main", + "temp_clone_token": null, + "organization": { + "login": "huggingface", + "id": 25720743, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjI1NzIwNzQz", + "avatar_url": "https://avatars.githubusercontent.com/u/25720743?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/huggingface", + "html_url": "https://github.com/huggingface", + "followers_url": "https://api.github.com/users/huggingface/followers", + "following_url": "https://api.github.com/users/huggingface/following{/other_user}", + "gists_url": "https://api.github.com/users/huggingface/gists{/gist_id}", + "starred_url": "https://api.github.com/users/huggingface/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/huggingface/subscriptions", + "organizations_url": "https://api.github.com/users/huggingface/orgs", + "repos_url": "https://api.github.com/users/huggingface/repos", + "events_url": "https://api.github.com/users/huggingface/events{/privacy}", + "received_events_url": "https://api.github.com/users/huggingface/received_events", + "type": "Organization", + "site_admin": false + }, + "network_count": 1871, + "subscribers_count": 250 +} diff --git a/GithubTrending/Mocks/MockedResponses/ResponseAvatarImageSample.png b/GithubTrending/Mocks/MockedResponses/ResponseAvatarImageSample.png new file mode 100644 index 0000000..b89b723 Binary files /dev/null and b/GithubTrending/Mocks/MockedResponses/ResponseAvatarImageSample.png differ diff --git a/GithubTrending/Mocks/MockedResponses/ResponseReadMeSample.md b/GithubTrending/Mocks/MockedResponses/ResponseReadMeSample.md new file mode 100644 index 0000000..348c7bc --- /dev/null +++ b/GithubTrending/Mocks/MockedResponses/ResponseReadMeSample.md @@ -0,0 +1,200 @@ +

+
+ +
+

+

+ + Build + + + GitHub + + + Documentation + + + GitHub release + + + Number of datasets + + + Contributor Covenant + + DOI +

+ +🤗 Datasets is a lightweight library providing **two** main features: + +- **one-line dataloaders for many public datasets**: one-liners to download and pre-process any of the ![number of datasets](https://img.shields.io/endpoint?url=https://huggingface.co/api/shields/datasets&color=brightgreen) major public datasets (text datasets in 467 languages and dialects, image datasets, audio datasets, etc.) provided on the [HuggingFace Datasets Hub](https://huggingface.co/datasets). With a simple command like `squad_dataset = load_dataset("squad")`, get any of these datasets ready to use in a dataloader for training/evaluating a ML model (Numpy/Pandas/PyTorch/TensorFlow/JAX), +- **efficient data pre-processing**: simple, fast and reproducible data pre-processing for the above public datasets as well as your own local datasets in CSV/JSON/text/PNG/JPEG/etc. With simple commands like `processed_dataset = dataset.map(process_example)`, efficiently prepare the dataset for inspection and ML model evaluation and training. + +[🎓 **Documentation**](https://huggingface.co/docs/datasets/) [🕹 **Colab tutorial**](https://colab.research.google.com/github/huggingface/datasets/blob/main/notebooks/Overview.ipynb) + +[🔎 **Find a dataset in the Hub**](https://huggingface.co/datasets) [🌟 **Add a new dataset to the Hub**](https://github.com/huggingface/datasets/blob/main/ADD_NEW_DATASET.md) + +

+ +

+ +🤗 Datasets is designed to let the community easily add and share new datasets. + +🤗 Datasets has many additional interesting features: + +- Thrive on large datasets: 🤗 Datasets naturally frees the user from RAM memory limitation, all datasets are memory-mapped using an efficient zero-serialization cost backend (Apache Arrow). +- Smart caching: never wait for your data to process several times. +- Lightweight and fast with a transparent and pythonic API (multi-processing/caching/memory-mapping). +- Built-in interoperability with NumPy, pandas, PyTorch, Tensorflow 2 and JAX. + +🤗 Datasets originated from a fork of the awesome [TensorFlow Datasets](https://github.com/tensorflow/datasets) and the HuggingFace team want to deeply thank the TensorFlow Datasets team for building this amazing library. More details on the differences between 🤗 Datasets and `tfds` can be found in the section [Main differences between 🤗 Datasets and `tfds`](#main-differences-between--datasets-and-tfds). + +# Installation + +## With pip + +🤗 Datasets can be installed from PyPi and has to be installed in a virtual environment (venv or conda for instance) + +```bash +pip install datasets +``` + +## With conda + +🤗 Datasets can be installed using conda as follows: + +```bash +conda install -c huggingface -c conda-forge datasets +``` + +Follow the installation pages of TensorFlow and PyTorch to see how to install them with conda. + +For more details on installation, check the installation page in the documentation: https://huggingface.co/docs/datasets/installation + +## Installation to use with PyTorch/TensorFlow/pandas + +If you plan to use 🤗 Datasets with PyTorch (1.0+), TensorFlow (2.2+) or pandas, you should also install PyTorch, TensorFlow or pandas. + +For more details on using the library with NumPy, pandas, PyTorch or TensorFlow, check the quick start page in the documentation: https://huggingface.co/docs/datasets/quickstart + +# Usage + +🤗 Datasets is made to be very simple to use. The main methods are: + +- `datasets.list_datasets()` to list the available datasets +- `datasets.load_dataset(dataset_name, **kwargs)` to instantiate a dataset + +This library can be used for text/image/audio/etc. datasets. Here is an example to load a text dataset: + +Here is a quick example: + +```python +from datasets import list_datasets, load_dataset + +# Print all the available datasets +print(list_datasets()) + +# Load a dataset and print the first example in the training set +squad_dataset = load_dataset('squad') +print(squad_dataset['train'][0]) + +# Process the dataset - add a column with the length of the context texts +dataset_with_length = squad_dataset.map(lambda x: {"length": len(x["context"])}) + +# Process the dataset - tokenize the context texts (using a tokenizer from the 🤗 Transformers library) +from transformers import AutoTokenizer +tokenizer = AutoTokenizer.from_pretrained('bert-base-cased') + +tokenized_dataset = squad_dataset.map(lambda x: tokenizer(x['context']), batched=True) +``` + +For more details on using the library, check the quick start page in the documentation: https://huggingface.co/docs/datasets/quickstart.html and the specific pages on: + +- Loading a dataset https://huggingface.co/docs/datasets/loading +- What's in a Dataset: https://huggingface.co/docs/datasets/access +- Processing data with 🤗 Datasets: https://huggingface.co/docs/datasets/process +- Processing audio data: https://huggingface.co/docs/datasets/audio_process +- Processing image data: https://huggingface.co/docs/datasets/image_process +- Writing your own dataset loading script: https://huggingface.co/docs/datasets/dataset_script +- etc. + +Another introduction to 🤗 Datasets is the tutorial on Google Colab here: +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/datasets/blob/main/notebooks/Overview.ipynb) + +# Add a new dataset to the Hub + +We have a very detailed step-by-step guide to add a new dataset to the ![number of datasets](https://img.shields.io/endpoint?url=https://huggingface.co/api/shields/datasets&color=brightgreen) datasets already provided on the [HuggingFace Datasets Hub](https://huggingface.co/datasets). + +You will find [the step-by-step guide here](https://huggingface.co/docs/datasets/share.html) to add a dataset on the Hub. + +However if you prefer to add your dataset in this repository, you can find the guide [here](https://github.com/huggingface/datasets/blob/main/ADD_NEW_DATASET.md). + +# Main differences between 🤗 Datasets and `tfds` + +If you are familiar with the great TensorFlow Datasets, here are the main differences between 🤗 Datasets and `tfds`: + +- the scripts in 🤗 Datasets are not provided within the library but are queried, downloaded/cached and dynamically loaded upon request +- 🤗 Datasets also provides evaluation metrics in a similar fashion to the datasets, i.e. as dynamically installed scripts with a unified API. This gives access to the pair of a benchmark dataset and a benchmark metric for instance for benchmarks like [SQuAD](https://rajpurkar.github.io/SQuAD-explorer/) or [GLUE](https://gluebenchmark.com/). +- the backend serialization of 🤗 Datasets is based on [Apache Arrow](https://arrow.apache.org/) instead of TF Records and leverage python dataclasses for info and features with some diverging features (we mostly don't do encoding and store the raw data as much as possible in the backend serialization cache). +- the user-facing dataset object of 🤗 Datasets is not a `tf.data.Dataset` but a built-in framework-agnostic dataset class with methods inspired by what we like in `tf.data` (like a `map()` method). It basically wraps a memory-mapped Arrow table cache. + +# Disclaimers + +Similar to TensorFlow Datasets, 🤗 Datasets is a utility library that downloads and prepares public datasets. We do not host or distribute most of these datasets, vouch for their quality or fairness, or claim that you have license to use them. It is your responsibility to determine whether you have permission to use the dataset under the dataset's license. + +If you're a dataset owner and wish to update any part of it (description, citation, etc.), or do not want your dataset to be included in this library, please get in touch through a [GitHub issue](https://github.com/huggingface/datasets/issues/new). Thanks for your contribution to the ML community! + +## BibTeX + +If you want to cite our 🤗 Datasets library, you can use our [paper](https://arxiv.org/abs/2109.02846): + +```bibtex +@inproceedings{lhoest-etal-2021-datasets, + title = "Datasets: A Community Library for Natural Language Processing", + author = "Lhoest, Quentin and + Villanova del Moral, Albert and + Jernite, Yacine and + Thakur, Abhishek and + von Platen, Patrick and + Patil, Suraj and + Chaumond, Julien and + Drame, Mariama and + Plu, Julien and + Tunstall, Lewis and + Davison, Joe and + {\v{S}}a{\v{s}}ko, Mario and + Chhablani, Gunjan and + Malik, Bhavitvya and + Brandeis, Simon and + Le Scao, Teven and + Sanh, Victor and + Xu, Canwen and + Patry, Nicolas and + McMillan-Major, Angelina and + Schmid, Philipp and + Gugger, Sylvain and + Delangue, Cl{\'e}ment and + Matussi{\`e}re, Th{\'e}o and + Debut, Lysandre and + Bekman, Stas and + Cistac, Pierric and + Goehringer, Thibault and + Mustar, Victor and + Lagunas, Fran{\c{c}}ois and + Rush, Alexander and + Wolf, Thomas", + booktitle = "Proceedings of the 2021 Conference on Empirical Methods in Natural Language Processing: System Demonstrations", + month = nov, + year = "2021", + address = "Online and Punta Cana, Dominican Republic", + publisher = "Association for Computational Linguistics", + url = "https://aclanthology.org/2021.emnlp-demo.21", + pages = "175--184", + abstract = "The scale, variety, and quantity of publicly-available NLP datasets has grown rapidly as researchers propose new tasks, larger models, and novel benchmarks. Datasets is a community library for contemporary NLP designed to support this ecosystem. Datasets aims to standardize end-user interfaces, versioning, and documentation, while providing a lightweight front-end that behaves similarly for small datasets as for internet-scale corpora. The design of the library incorporates a distributed, community-driven approach to adding datasets and documenting usage. After a year of development, the library now includes more than 650 unique datasets, has more than 250 contributors, and has helped support a variety of novel cross-dataset research projects and shared tasks. The library is available at https://github.com/huggingface/datasets.", + eprint={2109.02846}, + archivePrefix={arXiv}, + primaryClass={cs.CL}, +} +``` + +If you need to cite a specific version of our 🤗 Datasets library for reproducibility, you can use the corresponding version Zenodo DOI from this [list](https://zenodo.org/search?q=conceptrecid:%224817768%22&sort=-version&all_versions=True). diff --git a/GithubTrending/Mocks/UITesting/UITestingHelper.swift b/GithubTrending/Mocks/UITesting/UITestingHelper.swift new file mode 100644 index 0000000..d89aaa9 --- /dev/null +++ b/GithubTrending/Mocks/UITesting/UITestingHelper.swift @@ -0,0 +1,22 @@ +// +// UITestingHelper.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 11.. +// + +#if DEBUG +import Foundation + +struct UITestingHelper { + static var isUITesting: Bool { + ProcessInfo.processInfo.arguments.contains("-ui-testing") + } + static var isTrendingRepoListViewNetworkingSuccessful: Bool { + ProcessInfo.processInfo.environment["-trendingList-networking-success"] == "1" + } + static var isRepoDetailedViewNetworkingSuccessful: Bool { + ProcessInfo.processInfo.environment["-repoDetailedView-networking-success"] == "1" + } +} +#endif diff --git a/GithubTrending/Models/OwnerModel.swift b/GithubTrending/Models/OwnerModel.swift new file mode 100644 index 0000000..5b6f49c --- /dev/null +++ b/GithubTrending/Models/OwnerModel.swift @@ -0,0 +1,22 @@ +// +// OwnerModel.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 06.. +// + +import Foundation + +struct OwnerModel: Identifiable, Codable { + let id: Int + let name: String + let githubURL: URL + let avatarURL: URL + + enum CodingKeys: String, CodingKey { + case id + case name = "login" + case githubURL = "html_url" + case avatarURL = "avatar_url" + } +} diff --git a/GithubTrending/Models/ReadMeModel.swift b/GithubTrending/Models/ReadMeModel.swift new file mode 100644 index 0000000..e90df19 --- /dev/null +++ b/GithubTrending/Models/ReadMeModel.swift @@ -0,0 +1,16 @@ +// +// ReadMeModel.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// + +import Foundation + +struct ReadMeModel: Codable { + let downloadUrl: URL? + + enum CodingKeys: String, CodingKey { + case downloadUrl = "download_url" + } +} diff --git a/GithubTrending/Models/RepoModel.swift b/GithubTrending/Models/RepoModel.swift new file mode 100644 index 0000000..dc1e14b --- /dev/null +++ b/GithubTrending/Models/RepoModel.swift @@ -0,0 +1,26 @@ +// +// RepoModel.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 06.. +// + +import Foundation + +struct RepoModel: Identifiable, Codable { + let id: Int + let name: String + let owner: OwnerModel + let description: String? + let stargazersCount: Int + let subscribersCount: Int + let language: String? + + enum CodingKeys: String, CodingKey { + case id, name, owner, description, language + case stargazersCount = "stargazers_count" + case subscribersCount = "subscribers_count" + } +} + + diff --git a/GithubTrending/Models/ScrapedRepoModel.swift b/GithubTrending/Models/ScrapedRepoModel.swift new file mode 100644 index 0000000..715ee9f --- /dev/null +++ b/GithubTrending/Models/ScrapedRepoModel.swift @@ -0,0 +1,13 @@ +// +// ScrapedRepoModel.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 06.. +// + +import Foundation + +struct ScrapedRepoModel { + let ownerName: String + let repoName: String +} diff --git a/GithubTrending/Preview Content/Preview Assets.xcassets/Contents.json b/GithubTrending/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/GithubTrending/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GithubTrending/Utilities/HapticManager.swift b/GithubTrending/Utilities/HapticManager.swift new file mode 100644 index 0000000..ccfbe90 --- /dev/null +++ b/GithubTrending/Utilities/HapticManager.swift @@ -0,0 +1,17 @@ +// +// HapticManager.swift +// GithubTrending +// +// Created by Bence Borsos on 2022. 10. 07.. +// + +import Foundation +import SwiftUI + +class HapticManager { + static private let generator = UINotificationFeedbackGenerator() + + static func notification(type: UINotificationFeedbackGenerator.FeedbackType) { + generator.notificationOccurred(type) + } +} diff --git a/GithubTrending/Utilities/Localization/en.lproj/Localizable.strings b/GithubTrending/Utilities/Localization/en.lproj/Localizable.strings new file mode 100644 index 0000000..1f74519 --- /dev/null +++ b/GithubTrending/Utilities/Localization/en.lproj/Localizable.strings @@ -0,0 +1,22 @@ +/* + Localizable.strings + GithubTrending + + Created by Bence Borsos on 2022. 10. 10.. + +*/ + +"launch_github_link" = "Go to Github"; +"launch_welcome" = "Technical Demo App"; +"launch_info" = "Github Trending"; +"launch_desc" = "SwiftUI - MVVM - Combine \n Unit tests for ViewModels \n UI test for the 3 main View \n Test coverage is 84.5%"; +"launch_enter_button" = "Enter the app"; +"launch_pp" = "Privacy policy"; +"launch_terms" = "Terms of use"; +"launch_and" = "and"; +"launch_github_url" = "https://www.github.com"; +"launch_github_pp_url" = "https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement"; +"launch_github_terms_url" = "https://docs.github.com/en/site-policy/github-terms/github-terms-of-service"; +"trending_repo_list_nav_title" = "Trending Repos"; +"repo_detail_nav_title" = "Repo Detail"; + diff --git a/GithubTrending/Utilities/Localization/hu.lproj/Localizable.strings b/GithubTrending/Utilities/Localization/hu.lproj/Localizable.strings new file mode 100644 index 0000000..be260ff --- /dev/null +++ b/GithubTrending/Utilities/Localization/hu.lproj/Localizable.strings @@ -0,0 +1,22 @@ +/* + Localizable.strings + GithubTrending + + Created by Bence Borsos on 2022. 10. 10.. + +*/ + +"launch_github_link" = "Go to Github"; +"launch_welcome" = "Technical Demo App"; +"launch_info" = "Github Trending"; +"launch_desc" = "SwiftUI - MVVM - Combine - Unit tests for ViewModels - UI test for the 3 main View - Test coverage is 84.5%"; +"launch_enter_button" = "Enter the app"; +"launch_pp" = "Privacy policy"; +"launch_terms" = "Terms of use"; +"launch_and" = "and"; +"launch_github_url" = "https://www.github.com"; +"launch_github_pp_url" = "https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement"; +"launch_github_terms_url" = "https://docs.github.com/en/site-policy/github-terms/github-terms-of-service"; +"trending_repo_list_nav_title" = "Trending Repos"; +"repo_detail_nav_title" = "Repo Detail"; + diff --git a/GithubTrendingTests/ViewModelTests/RepoAvatarImageViewModelTests.swift b/GithubTrendingTests/ViewModelTests/RepoAvatarImageViewModelTests.swift new file mode 100644 index 0000000..de7ba0d --- /dev/null +++ b/GithubTrendingTests/ViewModelTests/RepoAvatarImageViewModelTests.swift @@ -0,0 +1,52 @@ +// +// RepoAvatarImageViewModelTests.swift +// GithubTrendingTests +// +// Created by Bence Borsos on 2022. 10. 11.. +// + +import XCTest +@testable import GithubTrending +import Combine + +// Naming Structure: test_UnitOfWork_StateUnderTest_ExpectedBehavior +// Naming Structure: test_[struct or class]_[variable or function]_[expected result] +// Testing Structure: Given, When, Then + +final class RepoAvatarImageViewModelTests: XCTestCase { + var sut: RepoAvatarImageViewModel! // System Under Test + var mockImageFetchingService: MockImageFetchingService! + var cancellables: Set! + + override func setUp() { + super.setUp() + mockImageFetchingService = MockImageFetchingService() + let url = URL(string: "mockURL")! + sut = RepoAvatarImageViewModel(url: url, imageFetchingService: mockImageFetchingService) + cancellables = [] + } + + override func tearDown() { + mockImageFetchingService = nil + sut = nil + cancellables = nil + super.tearDown() + } + + func test_RepoAvatarImageViewModel_fetchImage_PopulatesImageData() async { + // given + let expectation = XCTestExpectation(description: "fetchImage() populates imageData") + let rawImageData = mockImageFetchingService.loadAvatarImageSample() + // when + await sut.fetchImage() + // then + sut.$imageData + .sink { value in + XCTAssertTrue(value != nil) + XCTAssertEqual(value, rawImageData) + expectation.fulfill() + } + .store(in: &cancellables) + wait(for: [expectation], timeout: 1) + } +} diff --git a/GithubTrendingTests/ViewModelTests/RepoDetailViewModelTests.swift b/GithubTrendingTests/ViewModelTests/RepoDetailViewModelTests.swift new file mode 100644 index 0000000..e3f013b --- /dev/null +++ b/GithubTrendingTests/ViewModelTests/RepoDetailViewModelTests.swift @@ -0,0 +1,76 @@ +// +// RepoDetailViewModelTests.swift +// GithubTrendingTests +// +// Created by Bence Borsos on 2022. 10. 10.. +// + +import XCTest +@testable import GithubTrending +import Combine + +// Naming Structure: test_UnitOfWork_StateUnderTest_ExpectedBehavior +// Naming Structure: test_[struct or class]_[variable or function]_[expected result] +// Testing Structure: Given, When, Then + +final class RepoDetailViewModelTests: XCTestCase { + var sut: RepoDetailViewModel! // System Under Test + var mockNetworkingManager: MockNetworkingManager! + var mockReadMeFetchingService: MockReadMeFetchingService! + var cancellables: Set! + var repo: RepoModel! + + override func setUp() { + super.setUp() + mockNetworkingManager = MockNetworkingManager(networkEnviroment: .dev) + repo = RepoModel(id: 0, name: "datasets", owner: OwnerModel(id: 0, name: "huggingface", githubURL: URL(string: "https://github.com/huggingface")!, avatarURL: URL(string: "https://avatars.githubusercontent.com/u/25720743?v=4")!), description: "🤗 The largest hub of ready-to-use datasets for ML models with fast, easy-to-use and efficient data manipulation tools", stargazersCount: 79, subscribersCount: 60, language: "Python") + mockReadMeFetchingService = MockReadMeFetchingService() + sut = RepoDetailViewModel(repo: repo, networkingManager: mockNetworkingManager, readMeFetchingService: mockReadMeFetchingService) + cancellables = [] + } + + override func tearDown() { + mockNetworkingManager = nil + mockReadMeFetchingService = nil + repo = nil + sut = nil + cancellables = nil + super.tearDown() + } + + func test_TrendingRepoListViewModel_markDownURL_PopulatedOnAppear() { + // given + let expectation = XCTestExpectation(description: "Fetched markDownUR on ViewModelAppear") + let awaitedMarkDownURLFromMockResponse: URL = URL(string: "https://raw.githubusercontent.com/huggingface/datasets/main/README.md")! + // when + sut.onViewModelAppear() + // then + sut.$markDownURL + .dropFirst() + .sink { value in + XCTAssertTrue(value != nil) + XCTAssertEqual(value!, awaitedMarkDownURLFromMockResponse) + expectation.fulfill() + } + .store(in: &cancellables) + wait(for: [expectation], timeout: 1) + } + + func test_TrendingRepoListViewModel_readMeMarkDown_PopulatedOnAppear() { + // given + let expectation = XCTestExpectation(description: "Fetched readMeMarkDown on ViewModelAppear") + let rawReadMeData = mockReadMeFetchingService.loadReadMeSample() + // when + sut.onViewModelAppear() + // then + sut.$readMeMarkDown + .dropFirst() + .sink { value in + XCTAssertTrue(value != nil) + XCTAssertEqual(value!, String(data: rawReadMeData!, encoding: .utf8)) + expectation.fulfill() + } + .store(in: &cancellables) + wait(for: [expectation], timeout: 1) + } +} diff --git a/GithubTrendingTests/ViewModelTests/TrendingRepoListViewModelTests.swift b/GithubTrendingTests/ViewModelTests/TrendingRepoListViewModelTests.swift new file mode 100644 index 0000000..81393a9 --- /dev/null +++ b/GithubTrendingTests/ViewModelTests/TrendingRepoListViewModelTests.swift @@ -0,0 +1,92 @@ +// +// TrendingRepoListViewModelTests.swift +// GithubTrendingTests +// +// Created by Bence Borsos on 2022. 10. 10.. +// + +import XCTest +@testable import GithubTrending +import Combine + +// Naming Structure: test_UnitOfWork_StateUnderTest_ExpectedBehavior +// Naming Structure: test_[struct or class]_[variable or function]_[expected result] +// Testing Structure: Given, When, Then + +final class TrendingRepoListViewModelTests: XCTestCase { + var sut: TrendingRepoListViewModel! // System Under Test + var mockTrendingScraperService: MockTrendingScraperService! + var mockNetworkingManager: MockNetworkingManager! + var cancellables: Set! + + override func setUp() { + super.setUp() + mockTrendingScraperService = MockTrendingScraperService() + mockNetworkingManager = MockNetworkingManager(networkEnviroment: .dev) + sut = TrendingRepoListViewModel(scraperService: mockTrendingScraperService, networkingManager: mockNetworkingManager) + cancellables = [] + } + + override func tearDown() { + mockTrendingScraperService = nil + mockNetworkingManager = nil + sut = nil + cancellables = nil + super.tearDown() + } + + func test_TrendingRepoListViewModel_Repos_PopulatedOnAppear() { + // given + let expectation = XCTestExpectation(description: "Fetched repos on ViewModelAppear") + let firstItem = ScrapedRepoModel(ownerName: "ashawkey", repoName: "stable-dreamfusion") + let secondItem = ScrapedRepoModel(ownerName: "huggingface", repoName: "datasets") + mockTrendingScraperService.scrapedRepoModelsResults = [firstItem, secondItem] + // when + sut.onViewModelAppear() + // then + sut.$repos + .dropFirst() + .sink { value in + XCTAssertEqual(value.count, 2) + expectation.fulfill() + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1) + XCTAssertEqual(sut.repos[0].owner.name, firstItem.ownerName) + XCTAssertEqual(sut.repos[1].owner.name, secondItem.ownerName) + } + + func test_TrendingRepoListViewModel_reload_NewReposPopulated() { + // given + let mainExpectation = XCTestExpectation(description: "Fetched from reload function, replace old elements") + let preExpectation1 = XCTestExpectation(description: "OnAppear populates oldItem to repos") + let oldItem = ScrapedRepoModel(ownerName: "ashawkey", repoName: "stable-dreamfusion") + mockTrendingScraperService.scrapedRepoModelsResults = [oldItem] + sut.onViewModelAppear() + sut.$repos + .dropFirst() + .sink { value in + XCTAssertEqual(value.count, 1) + XCTAssertEqual((value[0] as RepoModel).owner.name, oldItem.ownerName) + preExpectation1.fulfill() + } + .store(in: &cancellables) + wait(for: [preExpectation1], timeout: 1) + cancellables.removeAll() + // when + let newItem = ScrapedRepoModel(ownerName: "huggingface", repoName: "datasets") + mockTrendingScraperService.scrapedRepoModelsResults = [newItem] + sut.reload() + // then + sut.$repos + .dropFirst() + .sink { value in + XCTAssertNotEqual(value.count, 2) + XCTAssertEqual((value.first! as RepoModel).owner.name, newItem.ownerName) + mainExpectation.fulfill() + } + .store(in: &cancellables) + wait(for: [mainExpectation], timeout: 1) + } +} diff --git a/GithubTrendingUITests/RepoDetailView/RepoDetailViewFailureUITests.swift b/GithubTrendingUITests/RepoDetailView/RepoDetailViewFailureUITests.swift new file mode 100644 index 0000000..767371f --- /dev/null +++ b/GithubTrendingUITests/RepoDetailView/RepoDetailViewFailureUITests.swift @@ -0,0 +1,61 @@ +// +// RepoDetailViewFailureUITests.swift +// GithubTrendingUITests +// +// Created by Bence Borsos on 2022. 10. 12.. +// + +import XCTest + +// Naming Structure: test_UnitOfWork_StateUnderTest_ExpectedBehavior +// Naming Structure: test_[struct]_[ui component]_[expected result] +// Testing Structure: Given, When, Then + +final class RepoDetailViewFailureUITests: XCTestCase { + private var app: XCUIApplication! + private var trendingRepoList: XCUIElement! + + override func setUp() { + app = XCUIApplication() + app.launchArguments = ["-ui-testing"] + app.launchEnvironment = [ + "-trendingList-networking-success" : "1", + "-repoDetailedView-networking-success" : "0" + ] + app.launch() + let enterButton = app.buttons["enterButton"] + XCTAssertTrue(enterButton.waitForExistence(timeout: 2), "enterButton should be visible") + enterButton.tap() + if #available(iOS 16.0, *) { + trendingRepoList = app.collectionViews["trendingRepoList"] + } else { + trendingRepoList = app.tables["trendingRepoList"] + } + XCTAssertTrue(trendingRepoList.waitForExistence(timeout: 1), "trendingRepoList should be visible") + let predicate = NSPredicate(format: "identifier CONTAINS 'item_'") + let repoItems = trendingRepoList.cells.containing(predicate) + XCTAssertGreaterThan(repoItems.count, 2, "There should be multiple items on the screen") + repoItems.firstMatch.tap() + continueAfterFailure = false + } + + override func tearDown() { + app = nil + trendingRepoList = nil + } + + func test_TrendingRepoListView_list_failedToLoad() { + // Given + XCTAssert(app.staticTexts["huggingface"].exists) + XCTAssert(app.staticTexts["datasets"].exists) + XCTAssert(app.staticTexts["🤗 The largest hub of ready-to-use datasets for ML models with fast, easy-to-use and efficient data manipulation tools"].exists) + XCTAssert(app.staticTexts["14530"].exists) + XCTAssert(app.staticTexts["250"].exists) + // When + app.swipeUp() + // Then + XCTAssert(app.staticTexts["huggingface"].isHittable) + let readMeItemQuery = app.scrollViews.otherElements.containing(.any, identifier: "readMe") + XCTAssert(!readMeItemQuery.firstMatch.isHittable) + } +} diff --git a/GithubTrendingUITests/RepoDetailView/RepoDetailViewUITests.swift b/GithubTrendingUITests/RepoDetailView/RepoDetailViewUITests.swift new file mode 100644 index 0000000..d6a3586 --- /dev/null +++ b/GithubTrendingUITests/RepoDetailView/RepoDetailViewUITests.swift @@ -0,0 +1,72 @@ +// +// RepoDetailViewUITests.swift +// GithubTrendingUITests +// +// Created by Bence Borsos on 2022. 10. 12.. +// + +import XCTest + +// Naming Structure: test_UnitOfWork_StateUnderTest_ExpectedBehavior +// Naming Structure: test_[struct]_[ui component]_[expected result] +// Testing Structure: Given, When, Then + +final class RepoDetailViewUITests: XCTestCase { + private var app: XCUIApplication! + private var trendingRepoList: XCUIElement! + + override func setUp() { + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments = ["-ui-testing"] + app.launchEnvironment = [ + "-trendingList-networking-success" : "1", + "-repoDetailedView-networking-success" : "1" + ] + app.launch() + let enterButton = app.buttons["enterButton"] + XCTAssertTrue(enterButton.waitForExistence(timeout: 2), "enterButton should be visible") + enterButton.tap() + if #available(iOS 16.0, *) { + trendingRepoList = app.collectionViews["trendingRepoList"] + } else { + trendingRepoList = app.tables["trendingRepoList"] + } + XCTAssertTrue(trendingRepoList.waitForExistence(timeout: 1), "trendingRepoList should be visible") + let predicate = NSPredicate(format: "identifier CONTAINS 'item_'") + let repoItems = trendingRepoList.cells.containing(predicate) + XCTAssertGreaterThan(repoItems.count, 2, "There should be multiple items on the screen") + repoItems.firstMatch.tap() + } + + override func tearDown() { + app = nil + trendingRepoList = nil + } + + func test_TrendingRepoListView_repoRow_shouldExits() { + let repoRowItemQuery = app.scrollViews.otherElements.containing(.any, identifier: "repoRow") + XCTAssertTrue(repoRowItemQuery.firstMatch.waitForExistence(timeout: 2), "repoRow should be visible") + XCTAssert(app.staticTexts["huggingface"].exists) + XCTAssert(app.staticTexts["datasets"].exists) + XCTAssert(app.staticTexts["🤗 The largest hub of ready-to-use datasets for ML models with fast, easy-to-use and efficient data manipulation tools"].exists) + XCTAssert(app.staticTexts["14530"].exists) + XCTAssert(app.staticTexts["250"].exists) + } + + func test_TrendingRepoListView_readMe_shouldExits() { + let readMeItemQuery = app.scrollViews.otherElements.containing(.any, identifier: "readMe") + XCTAssertTrue(readMeItemQuery.firstMatch.waitForExistence(timeout: 2), "readMe should be visible") + } + + func test_TrendingRepoListView_scrollView_shouldBeScrollabe() { + // Given + let readMeItemQuery = app.scrollViews.otherElements.containing(.any, identifier: "readMe") + XCTAssertTrue(readMeItemQuery.firstMatch.waitForExistence(timeout: 2), "readMe should be visible") + // When + app.swipeUp() + // Then + XCTAssertTrue(readMeItemQuery.firstMatch.waitForExistence(timeout: 2), "waiting for scroll") + XCTAssert(!app.staticTexts["huggingface"].isHittable) + } +} diff --git a/GithubTrendingUITests/SplashAnimationViewUITests.swift b/GithubTrendingUITests/SplashAnimationViewUITests.swift new file mode 100644 index 0000000..805dd87 --- /dev/null +++ b/GithubTrendingUITests/SplashAnimationViewUITests.swift @@ -0,0 +1,45 @@ +// +// SplashAnimationViewUITests.swift +// GithubTrendingBorsosbeUITests +// +// Created by Bence Borsos on 2022. 10. 11.. +// + +import XCTest + +// Naming Structure: test_UnitOfWork_StateUnderTest_ExpectedBehavior +// Naming Structure: test_[struct]_[ui component]_[expected result] +// Testing Structure: Given, When, Then + +final class SplashAnimationViewUITests: XCTestCase { + private var app: XCUIApplication! + private var trendingRepoList: XCUIElement! + + override func setUp() { + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments = ["-ui-testing"] + app.launchEnvironment = ["-trendingList-networking-success" : "1"] + app.launch() + if #available(iOS 16.0, *) { + trendingRepoList = app.collectionViews["trendingRepoList"] + } else { + trendingRepoList = app.tables["trendingRepoList"] + } + } + + override func tearDown() { + app = nil + trendingRepoList = nil + } + + func test_SplashAnimationView_enterButton_shouldNavigate() { + // Given + let enterButton = app.buttons["enterButton"] + XCTAssertTrue(enterButton.waitForExistence(timeout: 2), "enterButton should be visible") + // When + enterButton.tap() + // Then + XCTAssertTrue(trendingRepoList.waitForExistence(timeout: 2), "trendingRepoList should be visible") + } +} diff --git a/GithubTrendingUITests/TrendingRepoListView/TrendingRepoListViewFailureUITests.swift b/GithubTrendingUITests/TrendingRepoListView/TrendingRepoListViewFailureUITests.swift new file mode 100644 index 0000000..e3bc13a --- /dev/null +++ b/GithubTrendingUITests/TrendingRepoListView/TrendingRepoListViewFailureUITests.swift @@ -0,0 +1,51 @@ +// +// TrendingRepoListViewFailureUITests.swift +// GithubTrendingUITests +// +// Created by Bence Borsos on 2022. 10. 12.. +// + +import XCTest + +// Naming Structure: test_UnitOfWork_StateUnderTest_ExpectedBehavior +// Naming Structure: test_[struct]_[ui component]_[expected result] +// Testing Structure: Given, When, Then + +final class TrendingRepoListViewFailureUITests: XCTestCase { + private var app: XCUIApplication! + private var trendingRepoList: XCUIElement! + + override func setUp() { + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments = ["-ui-testing"] + app.launchEnvironment = ["-trendingList-networking-success" : "0"] + app.launch() + if #available(iOS 16.0, *) { + trendingRepoList = app.collectionViews["trendingRepoList"] + } else { + trendingRepoList = app.tables["trendingRepoList"] + } + let enterButton = app.buttons["enterButton"] + XCTAssertTrue(enterButton.waitForExistence(timeout: 2), "enterButton should be visible") + enterButton.tap() + } + + override func tearDown() { + app = nil + trendingRepoList = nil + } + + func test_TrendingRepoListView_list_failedToLoad() { + // Given + let refreshButton = app.buttons["refreshButton"] + XCTAssertTrue(refreshButton.exists, "refreshButton should be visible") + XCTAssertFalse(trendingRepoList.exists, "trendingRepoList should be visible") + // When + let loadingIndicator = app.activityIndicators["loadingIndicator"] + XCTAssertTrue(loadingIndicator.exists, "loadingIndicator should be visible") + // Then + XCTAssertFalse(loadingIndicator.waitForExistence(timeout: 1), "loadingIndicator should not be visible") + XCTAssert(app.staticTexts["[⚠️] Unknown error occured"].exists) + } +} diff --git a/GithubTrendingUITests/TrendingRepoListView/TrendingRepoListViewUITests.swift b/GithubTrendingUITests/TrendingRepoListView/TrendingRepoListViewUITests.swift new file mode 100644 index 0000000..1d5ac17 --- /dev/null +++ b/GithubTrendingUITests/TrendingRepoListView/TrendingRepoListViewUITests.swift @@ -0,0 +1,99 @@ +// +// TrendingRepoListViewUITests.swift +// GithubTrendingUITests +// +// Created by Bence Borsos on 2022. 10. 11.. +// + +import XCTest + +// Naming Structure: test_UnitOfWork_StateUnderTest_ExpectedBehavior +// Naming Structure: test_[struct]_[ui component]_[expected result] +// Testing Structure: Given, When, Then + +final class TrendingRepoListViewUITests: XCTestCase { + private var app: XCUIApplication! + private var trendingRepoList: XCUIElement! + + override func setUp() { + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments = ["-ui-testing"] + app.launchEnvironment = ["-trendingList-networking-success" : "1"] + app.launch() + if #available(iOS 16.0, *) { + trendingRepoList = app.collectionViews["trendingRepoList"] + } else { + trendingRepoList = app.tables["trendingRepoList"] + } + let enterButton = app.buttons["enterButton"] + XCTAssertTrue(enterButton.waitForExistence(timeout: 2), "enterButton should be visible") + enterButton.tap() + } + + override func tearDown() { + app = nil + trendingRepoList = nil + } + + func test_TrendingRepoListView_list_shouldShowRepos() { + // Given + XCTAssertTrue(trendingRepoList.waitForExistence(timeout: 1), "trendingRepoList should be visible") + // When + let predicate = NSPredicate(format: "identifier CONTAINS 'item_'") + let repoItems = trendingRepoList.cells.containing(predicate) + XCTAssertGreaterThan(repoItems.count, 2, "There should be multiple items on the screen") + //Then + XCTAssert(app.staticTexts["huggingface"].exists) + XCTAssert(app.staticTexts["datasets"].exists) + XCTAssert(app.staticTexts["🤗 The largest hub of ready-to-use datasets for ML models with fast, easy-to-use and efficient data manipulation tools"].exists) + XCTAssert(app.staticTexts["14530"].exists) + XCTAssert(app.staticTexts["250"].exists) + } + + func test_TrendingRepoListView_refreshButton_shoulBeTappable() { + // Given + XCTAssertTrue(trendingRepoList.waitForExistence(timeout: 1), "trendingRepoList should be visible") + // When + let predicate = NSPredicate(format: "identifier CONTAINS 'item_'") + let repoItems = trendingRepoList.cells.containing(predicate) + XCTAssertGreaterThan(repoItems.count, 2, "There should be multiple items on the screen") + let refreshButton = app.buttons["refreshButton"] + refreshButton.tap() + //Then + let loadingIndicator = app.activityIndicators["loadingIndicator"] + XCTAssertTrue(loadingIndicator.waitForExistence(timeout: 0.2), "loadingIndicator should be visible") + } + + func test_TrendingRepoListView_repoRow_shoulBeTappable() { + // Given + XCTAssertTrue(trendingRepoList.waitForExistence(timeout: 1), "trendingRepoList should be visible") + // When + let predicate = NSPredicate(format: "identifier CONTAINS 'item_'") + let repoItems = trendingRepoList.cells.containing(predicate) + XCTAssertGreaterThan(repoItems.count, 2, "There should be multiple items on the screen") + repoItems.firstMatch.tap() + //Then + let repoRowItemQuery = app.scrollViews.otherElements.containing(.any, identifier: "repoRow") + let repoRow = repoRowItemQuery.firstMatch + XCTAssertTrue(repoRow.waitForExistence(timeout: 0.2), "repoRowView should be visible") + } + + func test_TrendingRepoListView_list_shoulBeScrollabe() { + // Given + XCTAssertTrue(trendingRepoList.waitForExistence(timeout: 1), "trendingRepoList should be visible") + // When + let predicate = NSPredicate(format: "identifier CONTAINS 'item_'") + let repoItems = trendingRepoList.cells.containing(predicate) + XCTAssertGreaterThan(repoItems.count, 0, "There should be multiple items on the screen") + app.swipeUp() + //Then + XCTAssertTrue(trendingRepoList.firstMatch.waitForExistence(timeout: 2), "waiting for scroll") + XCTAssert(app.staticTexts["ashawkey"].exists) + XCTAssert(app.staticTexts["stable-dreamfusion"].exists) + XCTAssert(app.staticTexts["A pytorch implementation of text-to-3D dreamfusion, powered by stable diffusion."].exists) + XCTAssert(app.staticTexts["1858"].exists) + XCTAssert(app.staticTexts["39"].exists) + XCTAssert(app.staticTexts["Python"].exists) + } +}