From 01357f6dfb9aec3ee1ac6f3c3763723af6b27f01 Mon Sep 17 00:00:00 2001 From: borsosbe Date: Sat, 29 Oct 2022 17:23:30 +0200 Subject: [PATCH] Release 1.0 --- .gitignore | 2 +- GithubTrending.xcodeproj/project.pbxproj | 1168 +++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 32 + .../xcschemes/GithubTrending.xcscheme | 111 ++ .../AppIcon.appiconset/Contents.json | 14 + .../AppIcon.appiconset/Fill 52-2.png | Bin 0 -> 39708 bytes GithubTrending/Assets.xcassets/Contents.json | 6 + .../Assets.xcassets/Images/Contents.json | 6 + .../Images/GithubLogo.imageset/Contents.json | 23 + .../Images/GithubLogo.imageset/Fill 52.png | Bin 0 -> 4265 bytes .../Images/GithubLogo.imageset/Fill 52@2x.png | Bin 0 -> 9457 bytes .../Images/GithubLogo.imageset/Fill 52@3x.png | Bin 0 -> 15484 bytes .../AccentColor.colorset/Contents.json | 38 + .../BackgroundColor.colorset/Contents.json | 38 + .../Assets.xcassets/ThemeColors/Contents.json | 6 + .../Contents.json | 38 + .../SecondaryColor.colorset/Contents.json | 38 + .../TextColor.colorset/Contents.json | 38 + .../Contents.json | 38 + GithubTrending/Core/BaseViewModel.swift | 27 + .../ButtonGroups/RoundedButtonView.swift | 29 + .../ButtonGroups/UnderlinedButtonView.swift | 23 + .../Core/Components/LoadingIndicator.swift | 25 + .../ViewModels/RepoAvatarImageViewModel.swift | 36 + .../Views/RepoAvatarImageView.swift | 49 + .../Core/Components/RepoRowView.swift | 64 + .../Components/TextViews/BodyTextView.swift | 30 + .../TextViews/LargeTitleTextView.swift | 29 + GithubTrending/Core/Components/WebView.swift | 21 + .../Launch/Views/Launch Screen.storyboard | 46 + .../Core/Launch/Views/LaunchView.swift | 127 ++ .../Launch/Views/SplashAnimationView.swift | 40 + .../Core/Launch/Views/SplashScreenView.swift | 105 ++ .../ViewModels/RepoDetailViewModel.swift | 59 + .../RepoDetail/Views/RepoDetailView.swift | 104 ++ .../TrendingRepoListViewModel.swift | 83 ++ .../Views/TrendingRepoListView.swift | 110 ++ .../List/ListBackgroundModifier.swift | 21 + .../List/ListUIRefreshControlModifier.swift | 21 + .../ViewModifiers/NavigationBarModifier.swift | 41 + GithubTrending/GithubTrendingApp.swift | 41 + GithubTrending/Helpers/Extensions/Color.swift | 22 + .../Helpers/Extensions/PreviewProvider.swift | 22 + .../Helpers/Extensions/String.swift | 39 + GithubTrending/Helpers/Extensions/View.swift | 22 + .../Helpers/LocalizedAlertError.swift | 23 + .../Helpers/Network/EndpointItem.swift | 53 + .../Helpers/Network/NetworkEnvironment.swift | 13 + .../Helpers/Network/NetworkingError.swift | 20 + .../Helpers/Network/NetworkingManager.swift | 49 + .../Network/NetworkingManagerProtocol.swift | 16 + .../Services/ImageFetchingService.swift | 29 + .../FileFetchingServiceProtocol.swift | 13 + .../Protocols/ScraperServiceProtocol.swift | 13 + .../Services/ReadMeFetchingService.swift | 30 + .../Services/TrendingScraperService.swift | 45 + .../Mocks/MockNetworkingManager.swift | 39 + .../MockImageFetchingService.swift | 20 + .../MockReadMeFetchingService.swift | 29 + .../MockTrendingScraperService.swift | 37 + GithubTrending/Mocks/Mockable.swift | 71 + .../MockedResponses/RepoReadMeResponse.json | 18 + ...oResponse_ashawkey_stable-dreamfusion.json | 114 ++ .../RepoResponse_huggingface_datasets.json | 143 ++ .../ResponseAvatarImageSample.png | Bin 0 -> 11028 bytes .../MockedResponses/ResponseReadMeSample.md | 200 +++ .../Mocks/UITesting/UITestingHelper.swift | 22 + GithubTrending/Models/OwnerModel.swift | 22 + GithubTrending/Models/ReadMeModel.swift | 16 + GithubTrending/Models/RepoModel.swift | 26 + GithubTrending/Models/ScrapedRepoModel.swift | 13 + .../Preview Assets.xcassets/Contents.json | 6 + GithubTrending/Utilities/HapticManager.swift | 17 + .../Localization/en.lproj/Localizable.strings | 22 + .../Localization/hu.lproj/Localizable.strings | 22 + .../RepoAvatarImageViewModelTests.swift | 52 + .../RepoDetailViewModelTests.swift | 76 ++ .../TrendingRepoListViewModelTests.swift | 92 ++ .../RepoDetailViewFailureUITests.swift | 61 + .../RepoDetailViewUITests.swift | 72 + .../SplashAnimationViewUITests.swift | 45 + .../TrendingRepoListViewFailureUITests.swift | 51 + .../TrendingRepoListViewUITests.swift | 99 ++ 85 files changed, 4535 insertions(+), 1 deletion(-) create mode 100644 GithubTrending.xcodeproj/project.pbxproj create mode 100644 GithubTrending.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 GithubTrending.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 GithubTrending.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 GithubTrending.xcodeproj/xcshareddata/xcschemes/GithubTrending.xcscheme create mode 100644 GithubTrending/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 GithubTrending/Assets.xcassets/AppIcon.appiconset/Fill 52-2.png create mode 100644 GithubTrending/Assets.xcassets/Contents.json create mode 100644 GithubTrending/Assets.xcassets/Images/Contents.json create mode 100644 GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Contents.json create mode 100644 GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Fill 52.png create mode 100644 GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Fill 52@2x.png create mode 100644 GithubTrending/Assets.xcassets/Images/GithubLogo.imageset/Fill 52@3x.png create mode 100644 GithubTrending/Assets.xcassets/ThemeColors/AccentColor.colorset/Contents.json create mode 100644 GithubTrending/Assets.xcassets/ThemeColors/BackgroundColor.colorset/Contents.json create mode 100644 GithubTrending/Assets.xcassets/ThemeColors/Contents.json create mode 100644 GithubTrending/Assets.xcassets/ThemeColors/LightBackgroundColor.colorset/Contents.json create mode 100644 GithubTrending/Assets.xcassets/ThemeColors/SecondaryColor.colorset/Contents.json create mode 100644 GithubTrending/Assets.xcassets/ThemeColors/TextColor.colorset/Contents.json create mode 100644 GithubTrending/Assets.xcassets/ThemeColors/UnderlineTextButtonColor.colorset/Contents.json create mode 100644 GithubTrending/Core/BaseViewModel.swift create mode 100644 GithubTrending/Core/Components/ButtonGroups/RoundedButtonView.swift create mode 100644 GithubTrending/Core/Components/ButtonGroups/UnderlinedButtonView.swift create mode 100644 GithubTrending/Core/Components/LoadingIndicator.swift create mode 100644 GithubTrending/Core/Components/RepoAvatarImage/ViewModels/RepoAvatarImageViewModel.swift create mode 100644 GithubTrending/Core/Components/RepoAvatarImage/Views/RepoAvatarImageView.swift create mode 100644 GithubTrending/Core/Components/RepoRowView.swift create mode 100644 GithubTrending/Core/Components/TextViews/BodyTextView.swift create mode 100644 GithubTrending/Core/Components/TextViews/LargeTitleTextView.swift create mode 100644 GithubTrending/Core/Components/WebView.swift create mode 100644 GithubTrending/Core/Launch/Views/Launch Screen.storyboard create mode 100644 GithubTrending/Core/Launch/Views/LaunchView.swift create mode 100644 GithubTrending/Core/Launch/Views/SplashAnimationView.swift create mode 100644 GithubTrending/Core/Launch/Views/SplashScreenView.swift create mode 100644 GithubTrending/Core/Trending/RepoDetail/ViewModels/RepoDetailViewModel.swift create mode 100644 GithubTrending/Core/Trending/RepoDetail/Views/RepoDetailView.swift create mode 100644 GithubTrending/Core/Trending/TrendingRepoList/ViewModels/TrendingRepoListViewModel.swift create mode 100644 GithubTrending/Core/Trending/TrendingRepoList/Views/TrendingRepoListView.swift create mode 100644 GithubTrending/Core/ViewModifiers/List/ListBackgroundModifier.swift create mode 100644 GithubTrending/Core/ViewModifiers/List/ListUIRefreshControlModifier.swift create mode 100644 GithubTrending/Core/ViewModifiers/NavigationBarModifier.swift create mode 100644 GithubTrending/GithubTrendingApp.swift create mode 100644 GithubTrending/Helpers/Extensions/Color.swift create mode 100644 GithubTrending/Helpers/Extensions/PreviewProvider.swift create mode 100644 GithubTrending/Helpers/Extensions/String.swift create mode 100644 GithubTrending/Helpers/Extensions/View.swift create mode 100644 GithubTrending/Helpers/LocalizedAlertError.swift create mode 100644 GithubTrending/Helpers/Network/EndpointItem.swift create mode 100644 GithubTrending/Helpers/Network/NetworkEnvironment.swift create mode 100644 GithubTrending/Helpers/Network/NetworkingError.swift create mode 100644 GithubTrending/Helpers/Network/NetworkingManager.swift create mode 100644 GithubTrending/Helpers/Network/NetworkingManagerProtocol.swift create mode 100644 GithubTrending/Helpers/Services/ImageFetchingService.swift create mode 100644 GithubTrending/Helpers/Services/Protocols/FileFetchingServiceProtocol.swift create mode 100644 GithubTrending/Helpers/Services/Protocols/ScraperServiceProtocol.swift create mode 100644 GithubTrending/Helpers/Services/ReadMeFetchingService.swift create mode 100644 GithubTrending/Helpers/Services/TrendingScraperService.swift create mode 100644 GithubTrending/Mocks/MockNetworkingManager.swift create mode 100644 GithubTrending/Mocks/MockServices/MockImageFetchingService.swift create mode 100644 GithubTrending/Mocks/MockServices/MockReadMeFetchingService.swift create mode 100644 GithubTrending/Mocks/MockServices/MockTrendingScraperService.swift create mode 100644 GithubTrending/Mocks/Mockable.swift create mode 100644 GithubTrending/Mocks/MockedResponses/RepoReadMeResponse.json create mode 100644 GithubTrending/Mocks/MockedResponses/RepoResponse_ashawkey_stable-dreamfusion.json create mode 100644 GithubTrending/Mocks/MockedResponses/RepoResponse_huggingface_datasets.json create mode 100644 GithubTrending/Mocks/MockedResponses/ResponseAvatarImageSample.png create mode 100644 GithubTrending/Mocks/MockedResponses/ResponseReadMeSample.md create mode 100644 GithubTrending/Mocks/UITesting/UITestingHelper.swift create mode 100644 GithubTrending/Models/OwnerModel.swift create mode 100644 GithubTrending/Models/ReadMeModel.swift create mode 100644 GithubTrending/Models/RepoModel.swift create mode 100644 GithubTrending/Models/ScrapedRepoModel.swift create mode 100644 GithubTrending/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 GithubTrending/Utilities/HapticManager.swift create mode 100644 GithubTrending/Utilities/Localization/en.lproj/Localizable.strings create mode 100644 GithubTrending/Utilities/Localization/hu.lproj/Localizable.strings create mode 100644 GithubTrendingTests/ViewModelTests/RepoAvatarImageViewModelTests.swift create mode 100644 GithubTrendingTests/ViewModelTests/RepoDetailViewModelTests.swift create mode 100644 GithubTrendingTests/ViewModelTests/TrendingRepoListViewModelTests.swift create mode 100644 GithubTrendingUITests/RepoDetailView/RepoDetailViewFailureUITests.swift create mode 100644 GithubTrendingUITests/RepoDetailView/RepoDetailViewUITests.swift create mode 100644 GithubTrendingUITests/SplashAnimationViewUITests.swift create mode 100644 GithubTrendingUITests/TrendingRepoListView/TrendingRepoListViewFailureUITests.swift create mode 100644 GithubTrendingUITests/TrendingRepoListView/TrendingRepoListViewUITests.swift 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 0000000000000000000000000000000000000000..35d26af1c2b547b7dcf59a4fb1950a52d0820a3c GIT binary patch literal 39708 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 LNkvXXu0mjfw@Ey% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..59a70ab2c130cc95406ca05ec53cbe46b98e4dcd GIT binary patch literal 9457 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 -> 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 0000000000000000000000000000000000000000..b89b7233dfda746bbf6025aa8666723c8ade8287 GIT binary patch literal 11028 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 urD{P=Um0jSLR!4Sx`96XHs} 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 Az5oCK literal 0 HcmV?d00001 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) + } +}