diff --git a/._.DS_Store b/._.DS_Store new file mode 100755 index 0000000000..5cc801b9aa Binary files /dev/null and b/._.DS_Store differ diff --git a/._FakeNFT.xcodeproj b/._FakeNFT.xcodeproj new file mode 100755 index 0000000000..697d9edeb4 Binary files /dev/null and b/._FakeNFT.xcodeproj differ diff --git a/._Helpers b/._Helpers new file mode 100755 index 0000000000..aea9f8e386 Binary files /dev/null and b/._Helpers differ diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index e27d31575f..330d1674f3 --- a/.gitignore +++ b/.gitignore @@ -88,5 +88,3 @@ fastlane/test_output # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ - -.DS_Store diff --git a/API.html b/API.html old mode 100644 new mode 100755 diff --git a/FakeNFT.xcodeproj/project.pbxproj b/FakeNFT.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index 074c62ba3d..9bb463f854 --- a/FakeNFT.xcodeproj/project.pbxproj +++ b/FakeNFT.xcodeproj/project.pbxproj @@ -20,19 +20,13 @@ 0CFCB7422A78013E0009A829 /* Nft.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFCB7412A78013E0009A829 /* Nft.swift */; }; 0CFCB7442A7802440009A829 /* NftDetailInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFCB7432A7802440009A829 /* NftDetailInput.swift */; }; 0CFCB7462A78064B0009A829 /* NftDetailCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFCB7452A78064B0009A829 /* NftDetailCellModel.swift */; }; - 0CFCB7492A7808900009A829 /* TestCatalogController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFCB7482A7808900009A829 /* TestCatalogController.swift */; }; 0CFCB74B2A780EA80009A829 /* UIView+Constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFCB74A2A780EA80009A829 /* UIView+Constraints.swift */; }; 0CFCB74E2A7817DC0009A829 /* NftStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFCB74D2A7817DC0009A829 /* NftStorage.swift */; }; - 3F478ECF29DB474E00F6D39E /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F478ECE29DB474E00F6D39E /* Colors.swift */; }; 3F478ED129DB476500F6D39E /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F478ED029DB476500F6D39E /* Fonts.swift */; }; 3F603CCD29DB4A53000C43D7 /* ProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = 3F603CCC29DB4A53000C43D7 /* ProgressHUD */; }; 3F603CD029DB4A74000C43D7 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 3F603CCF29DB4A74000C43D7 /* Kingfisher */; }; 3F68069729CBBAF100B4F915 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F68069629CBBAF100B4F915 /* AppDelegate.swift */; }; - 3F68069929CBBAF100B4F915 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F68069829CBBAF100B4F915 /* SceneDelegate.swift */; }; 3F68069B29CBBAF100B4F915 /* ProductDetailsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F68069A29CBBAF100B4F915 /* ProductDetailsTableViewController.swift */; }; - 3F68069E29CBBAF100B4F915 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3F68069C29CBBAF100B4F915 /* Main.storyboard */; }; - 3F6806A029CBBAF200B4F915 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3F68069F29CBBAF200B4F915 /* Assets.xcassets */; }; - 3F6806A329CBBAF200B4F915 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3F6806A129CBBAF200B4F915 /* LaunchScreen.storyboard */; }; 3F6806AE29CBBAF200B4F915 /* ExampleUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F6806AD29CBBAF200B4F915 /* ExampleUnitTests.swift */; }; 3F6806B829CBBAF200B4F915 /* FakeNFTUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F6806B729CBBAF200B4F915 /* FakeNFTUITests.swift */; }; 3F6806D129CBBE6B00B4F915 /* NetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F6806D029CBBE6B00B4F915 /* NetworkClient.swift */; }; @@ -42,9 +36,113 @@ 3FC8C38B29D242E90081F015 /* ProductDetailsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC8C38A29D242E90081F015 /* ProductDetailsTableViewCell.swift */; }; 3FC8C39129D2453B0081F015 /* ExampleRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC8C39029D2453B0081F015 /* ExampleRequest.swift */; }; 3FC8C39329D246BA0081F015 /* DateFormatters+Presets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC8C39229D246BA0081F015 /* DateFormatters+Presets.swift */; }; + 440CC8F62B06A07500E0689B /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440CC8F52B06A07500E0689B /* Strings.swift */; }; + 440DD6102AF6550A00B27450 /* CatalogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440DD60F2AF6550A00B27450 /* CatalogViewController.swift */; }; + 440DD6122AF6554C00B27450 /* CatalogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440DD6112AF6554C00B27450 /* CatalogView.swift */; }; + 440DD6142AF6558B00B27450 /* CatalogTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440DD6132AF6558B00B27450 /* CatalogTableViewCell.swift */; }; + 440DD6192AF655FD00B27450 /* CatalogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440DD6182AF655FD00B27450 /* CatalogViewModel.swift */; }; + 440DD61B2AF66AE500B27450 /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440DD61A2AF66AE500B27450 /* AlertPresenter.swift */; }; + 440DD61D2AF66B1A00B27450 /* AlertModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440DD61C2AF66B1A00B27450 /* AlertModel.swift */; }; + 442C48542AFFC8BF00EDA05E /* CatalogCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 442C48532AFFC8BF00EDA05E /* CatalogCollectionCell.swift */; }; + 4433E90E2B0BE43300867245 /* ProfileLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4433E90D2B0BE43300867245 /* ProfileLike.swift */; }; + 4433E9122B0BF55200867245 /* LikesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4433E9112B0BF55200867245 /* LikesStorage.swift */; }; + 444962CE2B0FE71900A28E2C /* CatalogViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 444962CD2B0FE71900A28E2C /* CatalogViewDelegate.swift */; }; + 444962D02B0FEA0F00A28E2C /* CatalogCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 444962CF2B0FEA0F00A28E2C /* CatalogCollectionViewDelegate.swift */; }; + 444962D22B0FEA2A00A28E2C /* CatalogCollectionCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 444962D12B0FEA2A00A28E2C /* CatalogCollectionCellDelegate.swift */; }; + 447057802B055BB9000F4C6C /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4470577F2B055BB9000F4C6C /* Author.swift */; }; + 447057822B055DEA000F4C6C /* AuthorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447057812B055DEA000F4C6C /* AuthorRequest.swift */; }; + 4474E0A02B07E38100E236C5 /* ProfileRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4474E09F2B07E38100E236C5 /* ProfileRequest.swift */; }; + 4474E0A22B07E50900E236C5 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4474E0A12B07E50900E236C5 /* Profile.swift */; }; + 4474E0A52B07E7FB00E236C5 /* GeometricParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4474E0A42B07E7FB00E236C5 /* GeometricParams.swift */; }; + 447D7D652B0E851D00D2D1E3 /* WebViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447D7D642B0E851D00D2D1E3 /* WebViewViewModel.swift */; }; + 447D7D672B0E85B700D2D1E3 /* WebViewViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447D7D662B0E85B700D2D1E3 /* WebViewViewModelProtocol.swift */; }; + 447D7D692B0E8FB200D2D1E3 /* CatalogCollectionViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447D7D682B0E8FB200D2D1E3 /* CatalogCollectionViewModelProtocol.swift */; }; + 447D7D6B2B0E953000D2D1E3 /* CatalogViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447D7D6A2B0E953000D2D1E3 /* CatalogViewModelProtocol.swift */; }; + 44AE69022B03FF0900C39DAD /* NftResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44AE69012B03FF0900C39DAD /* NftResult.swift */; }; + 44AE69042B03FFAD00C39DAD /* NftModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44AE69032B03FFAD00C39DAD /* NftModel.swift */; }; + 44AE69062B0414AC00C39DAD /* UIView+Flash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44AE69052B0414AC00C39DAD /* UIView+Flash.swift */; }; + 44B59CAC2B02B50B00306CE1 /* CatalogCollectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44B59CAB2B02B50B00306CE1 /* CatalogCollectionService.swift */; }; + 44C8AA802AFB82DC00635019 /* CatalogAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C8AA7F2AFB82DC00635019 /* CatalogAssembly.swift */; }; + 44C8AA822AFB958B00635019 /* UIBlockingProgressHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C8AA812AFB958B00635019 /* UIBlockingProgressHUD.swift */; }; + 44F299172B0D2A5B0052FB7F /* PurchaseCart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44F299162B0D2A5B0052FB7F /* PurchaseCart.swift */; }; + 44F299192B0D2AD10052FB7F /* PurchaseCartStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44F299182B0D2AD10052FB7F /* PurchaseCartStorage.swift */; }; + 44F2991B2B0D2C1A0052FB7F /* OrdersRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44F2991A2B0D2C1A0052FB7F /* OrdersRequest.swift */; }; + 44F2991D2B0D369F0052FB7F /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44F2991C2B0D369F0052FB7F /* WebViewController.swift */; }; + 44F2991F2B0D38E60052FB7F /* UserRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44F2991E2B0D38E60052FB7F /* UserRequest.swift */; }; + 44FA956B2AFA4E810085DBA7 /* CatalogResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44FA956A2AFA4E810085DBA7 /* CatalogResult.swift */; }; + 44FA956D2AFA4EE20085DBA7 /* Catalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44FA956C2AFA4EE20085DBA7 /* Catalog.swift */; }; + 44FA95702AFA4FFA0085DBA7 /* CatalogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44FA956F2AFA4FFA0085DBA7 /* CatalogService.swift */; }; + 44FA95722AFA57F70085DBA7 /* CatalogRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44FA95712AFA57F70085DBA7 /* CatalogRequest.swift */; }; + 44FA95742AFAA8C30085DBA7 /* CatalogFilterStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44FA95732AFAA8C30085DBA7 /* CatalogFilterStorage.swift */; }; + 44FA95772AFAAFFA0085DBA7 /* CatalogCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44FA95762AFAAFFA0085DBA7 /* CatalogCollectionViewController.swift */; }; + 44FA95792AFAB0380085DBA7 /* CatalogCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44FA95782AFAB0380085DBA7 /* CatalogCollectionView.swift */; }; + 44FA957B2AFAB06C0085DBA7 /* CatalogCollectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44FA957A2AFAB06C0085DBA7 /* CatalogCollectionViewModel.swift */; }; + 44FA957D2AFAB43B0085DBA7 /* GradientCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44FA957C2AFAB43B0085DBA7 /* GradientCell.swift */; }; + 7136458C2B3D6304002C9071 /* StatisticsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7136458B2B3D6304002C9071 /* StatisticsViewController.swift */; }; + 7136458D2B3D6378002C9071 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 71DF65E12B3CBD8A0033E09F /* Assets.xcassets */; }; + 7136458E2B3D6381002C9071 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 440CC8F92B06A0CE00E0689B /* Localizable.strings */; }; + 7136458F2B3D6386002C9071 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71DF65E32B3CBE9F0033E09F /* LaunchScreen.storyboard */; }; + 717C51D82B3BFFD0007AC951 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 717C51D72B3BFFD0007AC951 /* UIColor+Extensions.swift */; }; + 71CE2D352B3CCD0A008D16C8 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D342B3CCD0A008D16C8 /* UserProfile.swift */; }; + 71CE2D372B3CCD3D008D16C8 /* Observ.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D362B3CCD3D008D16C8 /* Observ.swift */; }; + 71CE2D3B2B3CCE22008D16C8 /* UIView + Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D3A2B3CCE22008D16C8 /* UIView + Extensions.swift */; }; + 71CE2D3D2B3CD1D5008D16C8 /* NFTServiceProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D3C2B3CD1D5008D16C8 /* NFTServiceProfile.swift */; }; + 71CE2D3F2B3CD61F008D16C8 /* NFTNetworkRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D3E2B3CD61F008D16C8 /* NFTNetworkRequests.swift */; }; + 71CE2D412B3CD8D2008D16C8 /* LoadingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D2D2B3CCC54008D16C8 /* LoadingState.swift */; }; + 71CE2D422B3CD8F4008D16C8 /* GeometricProfileParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D2C2B3CCC54008D16C8 /* GeometricProfileParams.swift */; }; + 71CE2D432B3CD91E008D16C8 /* ImageValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D302B3CCC54008D16C8 /* ImageValidator.swift */; }; + 71CE2D442B3CD937008D16C8 /* AlertService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D2E2B3CCC54008D16C8 /* AlertService.swift */; }; + 71CE2D452B3CDA0E008D16C8 /* NetworkServiceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D312B3CCC54008D16C8 /* NetworkServiceHelper.swift */; }; + 71CE2D462B3CDB50008D16C8 /* SortOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D2F2B3CCC54008D16C8 /* SortOptions.swift */; }; + 71CE2D482B3CDB8D008D16C8 /* ProfileNetworkRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D472B3CDB8D008D16C8 /* ProfileNetworkRequest.swift */; }; + 71CE2D4A2B3CDBC1008D16C8 /* ProfileUpdateDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D492B3CDBC1008D16C8 /* ProfileUpdateDTO.swift */; }; + 71CE2D4C2B3CDC30008D16C8 /* ProfileRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D4B2B3CDC30008D16C8 /* ProfileRouter.swift */; }; + 71CE2D4E2B3CDC79008D16C8 /* ProfileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D4D2B3CDC79008D16C8 /* ProfileService.swift */; }; + 71CE2D502B3CDCB4008D16C8 /* ViewControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE2D322B3CCC54008D16C8 /* ViewControllerFactory.swift */; }; + 71D1BDCF2B3CC9E200778C67 /* EditingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDBD2B3CC9E200778C67 /* EditingViewController.swift */; }; + 71D1BDD02B3CC9E200778C67 /* EditingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDBE2B3CC9E200778C67 /* EditingViewModel.swift */; }; + 71D1BDD12B3CC9E200778C67 /* FavoritesNFTCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDC02B3CC9E200778C67 /* FavoritesNFTCellViewModel.swift */; }; + 71D1BDD22B3CC9E200778C67 /* FavoritesNFTViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDC12B3CC9E200778C67 /* FavoritesNFTViewController.swift */; }; + 71D1BDD32B3CC9E200778C67 /* FavoritesNFTCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDC22B3CC9E200778C67 /* FavoritesNFTCell.swift */; }; + 71D1BDD42B3CC9E200778C67 /* FavoritesNFTViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDC32B3CC9E200778C67 /* FavoritesNFTViewModel.swift */; }; + 71D1BDD52B3CC9E200778C67 /* UserNFTViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDC62B3CC9E200778C67 /* UserNFTViewController.swift */; }; + 71D1BDD62B3CC9E200778C67 /* UserNFTViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDC72B3CC9E200778C67 /* UserNFTViewModel.swift */; }; + 71D1BDD72B3CC9E200778C67 /* NFTCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDC82B3CC9E200778C67 /* NFTCell.swift */; }; + 71D1BDD82B3CC9E200778C67 /* ProfileCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDCB2B3CC9E200778C67 /* ProfileCell.swift */; }; + 71D1BDD92B3CC9E200778C67 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDCC2B3CC9E200778C67 /* ProfileViewController.swift */; }; + 71D1BDDA2B3CC9E200778C67 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDCD2B3CC9E200778C67 /* ProfileViewModel.swift */; }; + 71D1BDDB2B3CC9E200778C67 /* WebViewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDCE2B3CC9E200778C67 /* WebViewViewController.swift */; }; + 71D1BDDD2B3CCA4D00778C67 /* NFTProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDDC2B3CCA4D00778C67 /* NFTProfile.swift */; }; + 71D1BDDF2B3CCA8E00778C67 /* NumberFormatter+Presets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D1BDDE2B3CCA8E00778C67 /* NumberFormatter+Presets.swift */; }; + 8441AC3F2AFBDA6400A34F4C /* CartViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8441AC3E2AFBDA6400A34F4C /* CartViewModel.swift */; }; + 8441AC412AFBDB8700A34F4C /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8441AC402AFBDB8700A34F4C /* Observable.swift */; }; + 8452F1752B4C3C41002E636D /* SF-Pro-Text-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8452F1742B4C3C23002E636D /* SF-Pro-Text-Regular.otf */; }; + 8452F1762B4C3C44002E636D /* SF-Pro-Text-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8452F1722B4C3C23002E636D /* SF-Pro-Text-Medium.otf */; }; + 8452F1772B4C3C46002E636D /* SF-Pro-Text-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8452F1732B4C3C23002E636D /* SF-Pro-Text-Bold.otf */; }; + 84555B022B0649BB00EDDB49 /* CurrencyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84555B012B0649BB00EDDB49 /* CurrencyViewController.swift */; }; + 84BA4D812B07A44500284C04 /* CurrencyCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BA4D802B07A44500284C04 /* CurrencyCollectionViewCell.swift */; }; + 84BA4D832B07AB3C00284C04 /* Currency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BA4D822B07AB3C00284C04 /* Currency.swift */; }; + 84CAEA532AF805D800EC9877 /* CartViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAEA522AF805D800EC9877 /* CartViewController.swift */; }; + 84CAEA552AF809D100EC9877 /* CartTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAEA542AF809D100EC9877 /* CartTableViewCell.swift */; }; + 84D04F452B1607D6002EF721 /* OnboardingPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D04F442B1607D6002EF721 /* OnboardingPageViewController.swift */; }; + 84D04F472B160800002EF721 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D04F462B160800002EF721 /* OnboardingViewController.swift */; }; + 84D04F492B16083A002EF721 /* CustomPageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D04F482B16083A002EF721 /* CustomPageControl.swift */; }; + 84D314342B035930002700ED /* CartFilterStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D314332B035930002700ED /* CartFilterStorage.swift */; }; + 84D9C5EE2AFCE2010063CED7 /* CartByldRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D9C5ED2AFCE2010063CED7 /* CartByldRequest.swift */; }; + 84D9C5F22AFD0B870063CED7 /* CartStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D9C5F12AFD0B870063CED7 /* CartStorage.swift */; }; + 84D9C5F52AFD0C370063CED7 /* Cart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D9C5F42AFD0C370063CED7 /* Cart.swift */; }; + 84D9C5F72AFD0FEF0063CED7 /* CartService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D9C5F62AFD0FEF0063CED7 /* CartService.swift */; }; + 84E4982A2B125CD900A44124 /* ModuleFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E498292B125CD900A44124 /* ModuleFactory.swift */; }; + 84ED1F912B0219340018820A /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED1F902B0219340018820A /* NumberFormatter.swift */; }; + 84F44B462B0F664000F667A3 /* PaymentSuccessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F44B452B0F664000F667A3 /* PaymentSuccessViewController.swift */; }; + 84F44B482B0F905E00F667A3 /* PaymentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F44B472B0F905E00F667A3 /* PaymentRequest.swift */; }; + 84F44B4A2B0F90D700F667A3 /* Payment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F44B492B0F90D700F667A3 /* Payment.swift */; }; + 84FC56C42B087AB60008515D /* CurrencyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FC56C32B087AB60008515D /* CurrencyViewModel.swift */; }; + 84FC56C62B087E7C0008515D /* CurrencyByldRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FC56C52B087E7C0008515D /* CurrencyByldRequest.swift */; }; + 84FC56C82B087EDD0008515D /* CurrencyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FC56C72B087EDD0008515D /* CurrencyService.swift */; }; + 84FC56CA2B087F990008515D /* CurrencyStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FC56C92B087F990008515D /* CurrencyStorage.swift */; }; E19CD5AB2A98B56600CA39A5 /* NftImageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19CD5AA2A98B56600CA39A5 /* NftImageCollectionViewCell.swift */; }; E1A1B9DA2AA01CE400C3AFBC /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A1B9D92AA01CE400C3AFBC /* TabBarController.swift */; }; - E1CD40DC2A96BECC00BE7FE8 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E1CD40DB2A96BECC00BE7FE8 /* Localizable.strings */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -78,19 +176,12 @@ 0CFCB7412A78013E0009A829 /* Nft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nft.swift; sourceTree = ""; }; 0CFCB7432A7802440009A829 /* NftDetailInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftDetailInput.swift; sourceTree = ""; }; 0CFCB7452A78064B0009A829 /* NftDetailCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftDetailCellModel.swift; sourceTree = ""; }; - 0CFCB7482A7808900009A829 /* TestCatalogController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCatalogController.swift; sourceTree = ""; }; 0CFCB74A2A780EA80009A829 /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = ""; }; 0CFCB74D2A7817DC0009A829 /* NftStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftStorage.swift; sourceTree = ""; }; - 3F478ECE29DB474E00F6D39E /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; 3F478ED029DB476500F6D39E /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = ""; }; 3F68069329CBBAF100B4F915 /* FakeNFT.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FakeNFT.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3F68069629CBBAF100B4F915 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 3F68069829CBBAF100B4F915 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 3F68069A29CBBAF100B4F915 /* ProductDetailsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailsTableViewController.swift; sourceTree = ""; }; - 3F68069D29CBBAF100B4F915 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 3F68069F29CBBAF200B4F915 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 3F6806A229CBBAF200B4F915 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 3F6806A429CBBAF200B4F915 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3F6806A929CBBAF200B4F915 /* FakeNFTTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FakeNFTTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3F6806AD29CBBAF200B4F915 /* ExampleUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUnitTests.swift; sourceTree = ""; }; 3F6806B329CBBAF200B4F915 /* FakeNFTUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FakeNFTUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -102,11 +193,119 @@ 3FC8C38A29D242E90081F015 /* ProductDetailsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailsTableViewCell.swift; sourceTree = ""; }; 3FC8C39029D2453B0081F015 /* ExampleRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleRequest.swift; sourceTree = ""; }; 3FC8C39229D246BA0081F015 /* DateFormatters+Presets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatters+Presets.swift"; sourceTree = ""; }; + 440CC8F52B06A07500E0689B /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; + 440CC8F82B06A0CE00E0689B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 440CC8FA2B06A0FB00E0689B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 440DD60F2AF6550A00B27450 /* CatalogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogViewController.swift; sourceTree = ""; }; + 440DD6112AF6554C00B27450 /* CatalogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogView.swift; sourceTree = ""; }; + 440DD6132AF6558B00B27450 /* CatalogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogTableViewCell.swift; sourceTree = ""; }; + 440DD6182AF655FD00B27450 /* CatalogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogViewModel.swift; sourceTree = ""; }; + 440DD61A2AF66AE500B27450 /* AlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresenter.swift; sourceTree = ""; }; + 440DD61C2AF66B1A00B27450 /* AlertModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertModel.swift; sourceTree = ""; }; + 442C48532AFFC8BF00EDA05E /* CatalogCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogCollectionCell.swift; sourceTree = ""; }; + 4433E90D2B0BE43300867245 /* ProfileLike.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileLike.swift; sourceTree = ""; }; + 4433E9112B0BF55200867245 /* LikesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikesStorage.swift; sourceTree = ""; }; + 444962CD2B0FE71900A28E2C /* CatalogViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogViewDelegate.swift; sourceTree = ""; }; + 444962CF2B0FEA0F00A28E2C /* CatalogCollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogCollectionViewDelegate.swift; sourceTree = ""; }; + 444962D12B0FEA2A00A28E2C /* CatalogCollectionCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogCollectionCellDelegate.swift; sourceTree = ""; }; + 4470577F2B055BB9000F4C6C /* Author.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Author.swift; sourceTree = ""; }; + 447057812B055DEA000F4C6C /* AuthorRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorRequest.swift; sourceTree = ""; }; + 4474E09F2B07E38100E236C5 /* ProfileRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRequest.swift; sourceTree = ""; }; + 4474E0A12B07E50900E236C5 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; + 4474E0A42B07E7FB00E236C5 /* GeometricParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometricParams.swift; sourceTree = ""; }; + 447D7D642B0E851D00D2D1E3 /* WebViewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewViewModel.swift; sourceTree = ""; }; + 447D7D662B0E85B700D2D1E3 /* WebViewViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewViewModelProtocol.swift; sourceTree = ""; }; + 447D7D682B0E8FB200D2D1E3 /* CatalogCollectionViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogCollectionViewModelProtocol.swift; sourceTree = ""; }; + 447D7D6A2B0E953000D2D1E3 /* CatalogViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogViewModelProtocol.swift; sourceTree = ""; }; + 44A2B6E82AFE612200D3A63D /* CatalogDecomposition.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CatalogDecomposition.md; sourceTree = ""; }; + 44AE69012B03FF0900C39DAD /* NftResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftResult.swift; sourceTree = ""; }; + 44AE69032B03FFAD00C39DAD /* NftModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftModel.swift; sourceTree = ""; }; + 44AE69052B0414AC00C39DAD /* UIView+Flash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Flash.swift"; sourceTree = ""; }; + 44B59CAB2B02B50B00306CE1 /* CatalogCollectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogCollectionService.swift; sourceTree = ""; }; + 44C8AA7F2AFB82DC00635019 /* CatalogAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogAssembly.swift; sourceTree = ""; }; + 44C8AA812AFB958B00635019 /* UIBlockingProgressHUD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBlockingProgressHUD.swift; sourceTree = ""; }; + 44F299162B0D2A5B0052FB7F /* PurchaseCart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseCart.swift; sourceTree = ""; }; + 44F299182B0D2AD10052FB7F /* PurchaseCartStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseCartStorage.swift; sourceTree = ""; }; + 44F2991A2B0D2C1A0052FB7F /* OrdersRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrdersRequest.swift; sourceTree = ""; }; + 44F2991C2B0D369F0052FB7F /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; + 44F2991E2B0D38E60052FB7F /* UserRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRequest.swift; sourceTree = ""; }; + 44FA956A2AFA4E810085DBA7 /* CatalogResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogResult.swift; sourceTree = ""; }; + 44FA956C2AFA4EE20085DBA7 /* Catalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Catalog.swift; sourceTree = ""; }; + 44FA956F2AFA4FFA0085DBA7 /* CatalogService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogService.swift; sourceTree = ""; }; + 44FA95712AFA57F70085DBA7 /* CatalogRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogRequest.swift; sourceTree = ""; }; + 44FA95732AFAA8C30085DBA7 /* CatalogFilterStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogFilterStorage.swift; sourceTree = ""; }; + 44FA95762AFAAFFA0085DBA7 /* CatalogCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogCollectionViewController.swift; sourceTree = ""; }; + 44FA95782AFAB0380085DBA7 /* CatalogCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogCollectionView.swift; sourceTree = ""; }; + 44FA957A2AFAB06C0085DBA7 /* CatalogCollectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogCollectionViewModel.swift; sourceTree = ""; }; + 44FA957C2AFAB43B0085DBA7 /* GradientCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientCell.swift; sourceTree = ""; }; + 7136458B2B3D6304002C9071 /* StatisticsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatisticsViewController.swift; sourceTree = ""; }; + 717C51D72B3BFFD0007AC951 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; + 71CE2D2C2B3CCC54008D16C8 /* GeometricProfileParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometricProfileParams.swift; sourceTree = ""; }; + 71CE2D2D2B3CCC54008D16C8 /* LoadingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingState.swift; sourceTree = ""; }; + 71CE2D2E2B3CCC54008D16C8 /* AlertService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertService.swift; sourceTree = ""; }; + 71CE2D2F2B3CCC54008D16C8 /* SortOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortOptions.swift; sourceTree = ""; }; + 71CE2D302B3CCC54008D16C8 /* ImageValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageValidator.swift; sourceTree = ""; }; + 71CE2D312B3CCC54008D16C8 /* NetworkServiceHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkServiceHelper.swift; sourceTree = ""; }; + 71CE2D322B3CCC54008D16C8 /* ViewControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerFactory.swift; sourceTree = ""; }; + 71CE2D332B3CCC70008D16C8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 71CE2D342B3CCD0A008D16C8 /* UserProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; + 71CE2D362B3CCD3D008D16C8 /* Observ.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observ.swift; sourceTree = ""; }; + 71CE2D3A2B3CCE22008D16C8 /* UIView + Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView + Extensions.swift"; sourceTree = ""; }; + 71CE2D3C2B3CD1D5008D16C8 /* NFTServiceProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFTServiceProfile.swift; sourceTree = ""; }; + 71CE2D3E2B3CD61F008D16C8 /* NFTNetworkRequests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NFTNetworkRequests.swift; sourceTree = ""; }; + 71CE2D472B3CDB8D008D16C8 /* ProfileNetworkRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileNetworkRequest.swift; sourceTree = ""; }; + 71CE2D492B3CDBC1008D16C8 /* ProfileUpdateDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileUpdateDTO.swift; sourceTree = ""; }; + 71CE2D4B2B3CDC30008D16C8 /* ProfileRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileRouter.swift; sourceTree = ""; }; + 71CE2D4D2B3CDC79008D16C8 /* ProfileService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileService.swift; sourceTree = ""; }; + 71CE2D542B3CE0DF008D16C8 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 71D1BDBD2B3CC9E200778C67 /* EditingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditingViewController.swift; sourceTree = ""; }; + 71D1BDBE2B3CC9E200778C67 /* EditingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditingViewModel.swift; sourceTree = ""; }; + 71D1BDC02B3CC9E200778C67 /* FavoritesNFTCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoritesNFTCellViewModel.swift; sourceTree = ""; }; + 71D1BDC12B3CC9E200778C67 /* FavoritesNFTViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoritesNFTViewController.swift; sourceTree = ""; }; + 71D1BDC22B3CC9E200778C67 /* FavoritesNFTCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoritesNFTCell.swift; sourceTree = ""; }; + 71D1BDC32B3CC9E200778C67 /* FavoritesNFTViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoritesNFTViewModel.swift; sourceTree = ""; }; + 71D1BDC62B3CC9E200778C67 /* UserNFTViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNFTViewController.swift; sourceTree = ""; }; + 71D1BDC72B3CC9E200778C67 /* UserNFTViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNFTViewModel.swift; sourceTree = ""; }; + 71D1BDC82B3CC9E200778C67 /* NFTCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NFTCell.swift; sourceTree = ""; }; + 71D1BDCB2B3CC9E200778C67 /* ProfileCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileCell.swift; sourceTree = ""; }; + 71D1BDCC2B3CC9E200778C67 /* ProfileViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; + 71D1BDCD2B3CC9E200778C67 /* ProfileViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; + 71D1BDCE2B3CC9E200778C67 /* WebViewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewViewController.swift; sourceTree = ""; }; + 71D1BDDC2B3CCA4D00778C67 /* NFTProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NFTProfile.swift; sourceTree = ""; }; + 71D1BDDE2B3CCA8E00778C67 /* NumberFormatter+Presets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+Presets.swift"; sourceTree = ""; }; + 71D1BDE22B3CCB3800778C67 /* Profile Vadim.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = "Profile Vadim.md"; sourceTree = ""; }; + 71DF65E12B3CBD8A0033E09F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 71DF65E32B3CBE9F0033E09F /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 8441AC3E2AFBDA6400A34F4C /* CartViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartViewModel.swift; sourceTree = ""; }; + 8441AC402AFBDB8700A34F4C /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; + 8452F1722B4C3C23002E636D /* SF-Pro-Text-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SF-Pro-Text-Medium.otf"; path = "../FontsResources/SF-Pro-Text-Medium.otf"; sourceTree = ""; }; + 8452F1732B4C3C23002E636D /* SF-Pro-Text-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SF-Pro-Text-Bold.otf"; path = "../FontsResources/SF-Pro-Text-Bold.otf"; sourceTree = ""; }; + 8452F1742B4C3C23002E636D /* SF-Pro-Text-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SF-Pro-Text-Regular.otf"; path = "../FontsResources/SF-Pro-Text-Regular.otf"; sourceTree = ""; }; + 84555B012B0649BB00EDDB49 /* CurrencyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyViewController.swift; sourceTree = ""; }; + 84BA4D802B07A44500284C04 /* CurrencyCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyCollectionViewCell.swift; sourceTree = ""; }; + 84BA4D822B07AB3C00284C04 /* Currency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Currency.swift; sourceTree = ""; }; + 84CAEA522AF805D800EC9877 /* CartViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartViewController.swift; sourceTree = ""; }; + 84CAEA542AF809D100EC9877 /* CartTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartTableViewCell.swift; sourceTree = ""; }; + 84D04F442B1607D6002EF721 /* OnboardingPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPageViewController.swift; sourceTree = ""; }; + 84D04F462B160800002EF721 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; + 84D04F482B16083A002EF721 /* CustomPageControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPageControl.swift; sourceTree = ""; }; + 84D314332B035930002700ED /* CartFilterStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartFilterStorage.swift; sourceTree = ""; }; + 84D5231F2AFE658300A1C791 /* CartDecomposition.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CartDecomposition.md; sourceTree = ""; }; + 84D9C5ED2AFCE2010063CED7 /* CartByldRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartByldRequest.swift; sourceTree = ""; }; + 84D9C5F12AFD0B870063CED7 /* CartStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartStorage.swift; sourceTree = ""; }; + 84D9C5F42AFD0C370063CED7 /* Cart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cart.swift; sourceTree = ""; }; + 84D9C5F62AFD0FEF0063CED7 /* CartService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartService.swift; sourceTree = ""; }; + 84E498292B125CD900A44124 /* ModuleFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleFactory.swift; sourceTree = ""; }; + 84ED1F902B0219340018820A /* NumberFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatter.swift; sourceTree = ""; }; + 84F44B452B0F664000F667A3 /* PaymentSuccessViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSuccessViewController.swift; sourceTree = ""; }; + 84F44B472B0F905E00F667A3 /* PaymentRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentRequest.swift; sourceTree = ""; }; + 84F44B492B0F90D700F667A3 /* Payment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Payment.swift; sourceTree = ""; }; + 84FC56C32B087AB60008515D /* CurrencyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyViewModel.swift; sourceTree = ""; }; + 84FC56C52B087E7C0008515D /* CurrencyByldRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyByldRequest.swift; sourceTree = ""; }; + 84FC56C72B087EDD0008515D /* CurrencyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyService.swift; sourceTree = ""; }; + 84FC56C92B087F990008515D /* CurrencyStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyStorage.swift; sourceTree = ""; }; E19CD5AA2A98B56600CA39A5 /* NftImageCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftImageCollectionViewCell.swift; sourceTree = ""; }; E1A1B9D92AA01CE400C3AFBC /* TabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; }; - E1CD40D82A96BE7D00BE7FE8 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Main.strings; sourceTree = ""; }; - E1CD40D92A96BE7D00BE7FE8 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = ""; }; - E1CD40DB2A96BECC00BE7FE8 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -151,6 +350,7 @@ 0CF2C2D92A783C1600FDC837 /* Common */ = { isa = PBXGroup; children = ( + 447D7D632B0E83D400D2D1E3 /* WebView */, 0CF2C2DF2A784C5600FDC837 /* Views */, 0CF2C2DE2A784C4C00FDC837 /* Protocols */, ); @@ -177,7 +377,8 @@ 0CFCB7472A7808870009A829 /* Catalog */ = { isa = PBXGroup; children = ( - 0CFCB7482A7808900009A829 /* TestCatalogController.swift */, + 440DD6162AF655C600B27450 /* CatalogView */, + 44FA95752AFAAFE40085DBA7 /* CatalogCollectionView */, ); path = Catalog; sourceTree = ""; @@ -186,6 +387,10 @@ isa = PBXGroup; children = ( 0CFCB74D2A7817DC0009A829 /* NftStorage.swift */, + 84D9C5F12AFD0B870063CED7 /* CartStorage.swift */, + 84FC56C92B087F990008515D /* CurrencyStorage.swift */, + 4433E9112B0BF55200867245 /* LikesStorage.swift */, + 44F299182B0D2AD10052FB7F /* PurchaseCartStorage.swift */, ); path = MemoryStorage; sourceTree = ""; @@ -193,8 +398,11 @@ 3F478ECD29DB473000F6D39E /* DesignSystem */ = { isa = PBXGroup; children = ( - 3F478ECE29DB474E00F6D39E /* Colors.swift */, + 8452F1712B4C3B24002E636D /* FountResources */, 3F478ED029DB476500F6D39E /* Fonts.swift */, + 44AE69052B0414AC00C39DAD /* UIView+Flash.swift */, + 71CE2D3A2B3CCE22008D16C8 /* UIView + Extensions.swift */, + 717C51D72B3BFFD0007AC951 /* UIColor+Extensions.swift */, ); path = DesignSystem; sourceTree = ""; @@ -222,15 +430,19 @@ 3F68069529CBBAF100B4F915 /* FakeNFT */ = { isa = PBXGroup; children = ( + 71DF65E12B3CBD8A0033E09F /* Assets.xcassets */, + 71CE2D332B3CCC70008D16C8 /* Info.plist */, + 71CE2D2B2B3CCC54008D16C8 /* Helpers */, + 84D523212AFE660D00A1C791 /* Docs */, 3F68069629CBBAF100B4F915 /* AppDelegate.swift */, - 3F68069829CBBAF100B4F915 /* SceneDelegate.swift */, + 440CC8F42B06A06500E0689B /* Generated */, E1CD40DA2A96BE9B00BE7FE8 /* Resources */, 3F478ECD29DB473000F6D39E /* DesignSystem */, 3F6806CE29CBBD1B00B4F915 /* Foundation */, 3F6806C929CBBCAF00B4F915 /* Models */, 3F6806C729CBBC5B00B4F915 /* Scenes */, 3F6806C629CBBC4E00B4F915 /* Services */, - 3F6806A429CBBAF200B4F915 /* Info.plist */, + 71CE2D542B3CE0DF008D16C8 /* Info.plist */, ); path = FakeNFT; sourceTree = ""; @@ -254,9 +466,17 @@ 3F6806C629CBBC4E00B4F915 /* Services */ = { isa = PBXGroup; children = ( + 84D04F422B15F5B0002EF721 /* CommonService */, + 84D314322B03590A002700ED /* CartService */, + 44FA956E2AFA4FEC0085DBA7 /* CatalogServices */, 3FC8C38F29D245250081F015 /* Requests */, 0C79EE6B2A76DE2E00EE90EA /* ServicesAssemly.swift */, 0CFCB73F2A78002A0009A829 /* NftService.swift */, + 71CE2D3C2B3CD1D5008D16C8 /* NFTServiceProfile.swift */, + 84D9C5F62AFD0FEF0063CED7 /* CartService.swift */, + 84FC56C72B087EDD0008515D /* CurrencyService.swift */, + 71CE2D4D2B3CDC79008D16C8 /* ProfileService.swift */, + 71CE2D4B2B3CDC30008D16C8 /* ProfileRouter.swift */, ); path = Services; sourceTree = ""; @@ -264,11 +484,15 @@ 3F6806C729CBBC5B00B4F915 /* Scenes */ = { isa = PBXGroup; children = ( + 713645882B3D6279002C9071 /* Statistic */, + 71D1BDBB2B3CC9E200778C67 /* Profile */, + 84D04F432B160727002EF721 /* Onboarding */, E1A1B9D82AA01C9700C3AFBC /* TabBarController */, 0CF2C2D92A783C1600FDC837 /* Common */, 0CFCB7472A7808870009A829 /* Catalog */, 0C79EE642A76DDCF00EE90EA /* NftDetails */, 3F6806C829CBBC8100B4F915 /* ProductDetails */, + 84CAEA512AF804E000EC9877 /* Cart */, ); path = "Scenes "; sourceTree = ""; @@ -285,6 +509,7 @@ 3F6806C929CBBCAF00B4F915 /* Models */ = { isa = PBXGroup; children = ( + 44FA95692AFA4E1E0085DBA7 /* CatalogModels */, 3F6806D829CC979D00B4F915 /* Network */, ); path = Models; @@ -297,7 +522,11 @@ 3F6806CF29CBBDB100B4F915 /* NetworkClient */, 3F6806D629CBC50A00B4F915 /* CellsReusingUtils.swift */, 3FC8C39229D246BA0081F015 /* DateFormatters+Presets.swift */, + 84ED1F902B0219340018820A /* NumberFormatter.swift */, 0CFCB74A2A780EA80009A829 /* UIView+Constraints.swift */, + 71CE2D362B3CCD3D008D16C8 /* Observ.swift */, + 8441AC402AFBDB8700A34F4C /* Observable.swift */, + 71D1BDDE2B3CCA8E00778C67 /* NumberFormatter+Presets.swift */, ); path = Foundation; sourceTree = ""; @@ -316,6 +545,14 @@ isa = PBXGroup; children = ( 0CFCB7412A78013E0009A829 /* Nft.swift */, + 71CE2D472B3CDB8D008D16C8 /* ProfileNetworkRequest.swift */, + 71CE2D492B3CDBC1008D16C8 /* ProfileUpdateDTO.swift */, + 71D1BDDC2B3CCA4D00778C67 /* NFTProfile.swift */, + 71CE2D3E2B3CD61F008D16C8 /* NFTNetworkRequests.swift */, + 71CE2D342B3CCD0A008D16C8 /* UserProfile.swift */, + 84D9C5F42AFD0C370063CED7 /* Cart.swift */, + 84BA4D822B07AB3C00284C04 /* Currency.swift */, + 84F44B492B0F90D700F667A3 /* Payment.swift */, ); path = Network; sourceTree = ""; @@ -323,13 +560,344 @@ 3FC8C38F29D245250081F015 /* Requests */ = { isa = PBXGroup; children = ( + 447057812B055DEA000F4C6C /* AuthorRequest.swift */, + 44FA95712AFA57F70085DBA7 /* CatalogRequest.swift */, + 4474E09F2B07E38100E236C5 /* ProfileRequest.swift */, + 44F2991A2B0D2C1A0052FB7F /* OrdersRequest.swift */, + 44F2991E2B0D38E60052FB7F /* UserRequest.swift */, 3FC8C39029D2453B0081F015 /* ExampleRequest.swift */, - 0C79EE602A76DCD600EE90EA /* NftByIdRequest.swift */, 0C79EE622A76DD1900EE90EA /* RequestConstants.swift */, + 0C79EE602A76DCD600EE90EA /* NftByIdRequest.swift */, + 84D9C5ED2AFCE2010063CED7 /* CartByldRequest.swift */, + 84FC56C52B087E7C0008515D /* CurrencyByldRequest.swift */, + 84F44B472B0F905E00F667A3 /* PaymentRequest.swift */, ); path = Requests; sourceTree = ""; }; + 440CC8F42B06A06500E0689B /* Generated */ = { + isa = PBXGroup; + children = ( + 440CC8F52B06A07500E0689B /* Strings.swift */, + ); + path = Generated; + sourceTree = ""; + }; + 440DD6152AF655BA00B27450 /* Cells */ = { + isa = PBXGroup; + children = ( + 440DD6132AF6558B00B27450 /* CatalogTableViewCell.swift */, + 44FA957C2AFAB43B0085DBA7 /* GradientCell.swift */, + ); + path = Cells; + sourceTree = ""; + }; + 440DD6162AF655C600B27450 /* CatalogView */ = { + isa = PBXGroup; + children = ( + 444962C92B0FE48200A28E2C /* View */, + 440DD6152AF655BA00B27450 /* Cells */, + 444962C82B0FE46B00A28E2C /* ViewModel */, + ); + path = CatalogView; + sourceTree = ""; + }; + 444962C82B0FE46B00A28E2C /* ViewModel */ = { + isa = PBXGroup; + children = ( + 440DD6182AF655FD00B27450 /* CatalogViewModel.swift */, + 447D7D6A2B0E953000D2D1E3 /* CatalogViewModelProtocol.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 444962C92B0FE48200A28E2C /* View */ = { + isa = PBXGroup; + children = ( + 440DD6112AF6554C00B27450 /* CatalogView.swift */, + 440DD60F2AF6550A00B27450 /* CatalogViewController.swift */, + 444962CD2B0FE71900A28E2C /* CatalogViewDelegate.swift */, + ); + path = View; + sourceTree = ""; + }; + 444962CA2B0FE4B300A28E2C /* View */ = { + isa = PBXGroup; + children = ( + 44FA95782AFAB0380085DBA7 /* CatalogCollectionView.swift */, + 44FA95762AFAAFFA0085DBA7 /* CatalogCollectionViewController.swift */, + 444962CF2B0FEA0F00A28E2C /* CatalogCollectionViewDelegate.swift */, + 444962D12B0FEA2A00A28E2C /* CatalogCollectionCellDelegate.swift */, + ); + path = View; + sourceTree = ""; + }; + 444962CB2B0FE4C100A28E2C /* Cells */ = { + isa = PBXGroup; + children = ( + 442C48532AFFC8BF00EDA05E /* CatalogCollectionCell.swift */, + ); + path = Cells; + sourceTree = ""; + }; + 444962CC2B0FE4CB00A28E2C /* ViewModel */ = { + isa = PBXGroup; + children = ( + 447D7D682B0E8FB200D2D1E3 /* CatalogCollectionViewModelProtocol.swift */, + 44FA957A2AFAB06C0085DBA7 /* CatalogCollectionViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 447D7D632B0E83D400D2D1E3 /* WebView */ = { + isa = PBXGroup; + children = ( + 44F2991C2B0D369F0052FB7F /* WebViewController.swift */, + 447D7D642B0E851D00D2D1E3 /* WebViewViewModel.swift */, + 447D7D662B0E85B700D2D1E3 /* WebViewViewModelProtocol.swift */, + ); + path = WebView; + sourceTree = ""; + }; + 44FA95692AFA4E1E0085DBA7 /* CatalogModels */ = { + isa = PBXGroup; + children = ( + 4474E0A42B07E7FB00E236C5 /* GeometricParams.swift */, + 440DD61C2AF66B1A00B27450 /* AlertModel.swift */, + 44FA956C2AFA4EE20085DBA7 /* Catalog.swift */, + 44FA956A2AFA4E810085DBA7 /* CatalogResult.swift */, + 44AE69012B03FF0900C39DAD /* NftResult.swift */, + 44AE69032B03FFAD00C39DAD /* NftModel.swift */, + 4470577F2B055BB9000F4C6C /* Author.swift */, + 4474E0A12B07E50900E236C5 /* Profile.swift */, + 4433E90D2B0BE43300867245 /* ProfileLike.swift */, + 44F299162B0D2A5B0052FB7F /* PurchaseCart.swift */, + ); + path = CatalogModels; + sourceTree = ""; + }; + 44FA956E2AFA4FEC0085DBA7 /* CatalogServices */ = { + isa = PBXGroup; + children = ( + 44FA956F2AFA4FFA0085DBA7 /* CatalogService.swift */, + 44B59CAB2B02B50B00306CE1 /* CatalogCollectionService.swift */, + 44FA95732AFAA8C30085DBA7 /* CatalogFilterStorage.swift */, + 44C8AA7F2AFB82DC00635019 /* CatalogAssembly.swift */, + 44C8AA812AFB958B00635019 /* UIBlockingProgressHUD.swift */, + ); + path = CatalogServices; + sourceTree = ""; + }; + 44FA95752AFAAFE40085DBA7 /* CatalogCollectionView */ = { + isa = PBXGroup; + children = ( + 444962CA2B0FE4B300A28E2C /* View */, + 444962CB2B0FE4C100A28E2C /* Cells */, + 444962CC2B0FE4CB00A28E2C /* ViewModel */, + ); + path = CatalogCollectionView; + sourceTree = ""; + }; + 713645882B3D6279002C9071 /* Statistic */ = { + isa = PBXGroup; + children = ( + 7136458B2B3D6304002C9071 /* StatisticsViewController.swift */, + ); + path = Statistic; + sourceTree = ""; + }; + 71CE2D2B2B3CCC54008D16C8 /* Helpers */ = { + isa = PBXGroup; + children = ( + 71CE2D2C2B3CCC54008D16C8 /* GeometricProfileParams.swift */, + 71CE2D2D2B3CCC54008D16C8 /* LoadingState.swift */, + 71CE2D2E2B3CCC54008D16C8 /* AlertService.swift */, + 71CE2D2F2B3CCC54008D16C8 /* SortOptions.swift */, + 71CE2D302B3CCC54008D16C8 /* ImageValidator.swift */, + 71CE2D312B3CCC54008D16C8 /* NetworkServiceHelper.swift */, + 71CE2D322B3CCC54008D16C8 /* ViewControllerFactory.swift */, + ); + name = Helpers; + path = ../Helpers; + sourceTree = ""; + }; + 71D1BDBB2B3CC9E200778C67 /* Profile */ = { + isa = PBXGroup; + children = ( + 71D1BDBC2B3CC9E200778C67 /* EditScreen */, + 71D1BDBF2B3CC9E200778C67 /* FavoritesNFTScreen */, + 71D1BDC42B3CC9E200778C67 /* UserNFTScreen */, + 71D1BDC92B3CC9E200778C67 /* ProfileScreen */, + 71D1BDCE2B3CC9E200778C67 /* WebViewViewController.swift */, + ); + path = Profile; + sourceTree = ""; + }; + 71D1BDBC2B3CC9E200778C67 /* EditScreen */ = { + isa = PBXGroup; + children = ( + 71D1BDBD2B3CC9E200778C67 /* EditingViewController.swift */, + 71D1BDBE2B3CC9E200778C67 /* EditingViewModel.swift */, + ); + path = EditScreen; + sourceTree = ""; + }; + 71D1BDBF2B3CC9E200778C67 /* FavoritesNFTScreen */ = { + isa = PBXGroup; + children = ( + 71D1BDC02B3CC9E200778C67 /* FavoritesNFTCellViewModel.swift */, + 71D1BDC12B3CC9E200778C67 /* FavoritesNFTViewController.swift */, + 71D1BDC22B3CC9E200778C67 /* FavoritesNFTCell.swift */, + 71D1BDC32B3CC9E200778C67 /* FavoritesNFTViewModel.swift */, + ); + path = FavoritesNFTScreen; + sourceTree = ""; + }; + 71D1BDC42B3CC9E200778C67 /* UserNFTScreen */ = { + isa = PBXGroup; + children = ( + 71D1BDC52B3CC9E200778C67 /* Views */, + ); + path = UserNFTScreen; + sourceTree = ""; + }; + 71D1BDC52B3CC9E200778C67 /* Views */ = { + isa = PBXGroup; + children = ( + 71D1BDC62B3CC9E200778C67 /* UserNFTViewController.swift */, + 71D1BDC72B3CC9E200778C67 /* UserNFTViewModel.swift */, + 71D1BDC82B3CC9E200778C67 /* NFTCell.swift */, + ); + path = Views; + sourceTree = ""; + }; + 71D1BDC92B3CC9E200778C67 /* ProfileScreen */ = { + isa = PBXGroup; + children = ( + 71D1BDCA2B3CC9E200778C67 /* Views */, + 71D1BDCD2B3CC9E200778C67 /* ProfileViewModel.swift */, + ); + path = ProfileScreen; + sourceTree = ""; + }; + 71D1BDCA2B3CC9E200778C67 /* Views */ = { + isa = PBXGroup; + children = ( + 71D1BDCB2B3CC9E200778C67 /* ProfileCell.swift */, + 71D1BDCC2B3CC9E200778C67 /* ProfileViewController.swift */, + ); + path = Views; + sourceTree = ""; + }; + 8452F1712B4C3B24002E636D /* FountResources */ = { + isa = PBXGroup; + children = ( + 8452F1732B4C3C23002E636D /* SF-Pro-Text-Bold.otf */, + 8452F1722B4C3C23002E636D /* SF-Pro-Text-Medium.otf */, + 8452F1742B4C3C23002E636D /* SF-Pro-Text-Regular.otf */, + ); + path = FountResources; + sourceTree = ""; + }; + 84BA4D7C2B07A3C000284C04 /* Cart */ = { + isa = PBXGroup; + children = ( + 84D5231D2AFE429A00A1C791 /* View */, + 84D5231E2AFE42A400A1C791 /* ViewModel */, + ); + path = Cart; + sourceTree = ""; + }; + 84BA4D7D2B07A3C900284C04 /* Currency */ = { + isa = PBXGroup; + children = ( + 84BA4D7E2B07A3FF00284C04 /* View */, + 84BA4D7F2B07A40500284C04 /* ViewModel */, + ); + path = Currency; + sourceTree = ""; + }; + 84BA4D7E2B07A3FF00284C04 /* View */ = { + isa = PBXGroup; + children = ( + 84555B012B0649BB00EDDB49 /* CurrencyViewController.swift */, + 84BA4D802B07A44500284C04 /* CurrencyCollectionViewCell.swift */, + 84F44B452B0F664000F667A3 /* PaymentSuccessViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 84BA4D7F2B07A40500284C04 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 84FC56C32B087AB60008515D /* CurrencyViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 84CAEA512AF804E000EC9877 /* Cart */ = { + isa = PBXGroup; + children = ( + 84BA4D7C2B07A3C000284C04 /* Cart */, + 84BA4D7D2B07A3C900284C04 /* Currency */, + ); + path = Cart; + sourceTree = ""; + }; + 84D04F422B15F5B0002EF721 /* CommonService */ = { + isa = PBXGroup; + children = ( + 440DD61A2AF66AE500B27450 /* AlertPresenter.swift */, + ); + path = CommonService; + sourceTree = ""; + }; + 84D04F432B160727002EF721 /* Onboarding */ = { + isa = PBXGroup; + children = ( + 84D04F442B1607D6002EF721 /* OnboardingPageViewController.swift */, + 84D04F462B160800002EF721 /* OnboardingViewController.swift */, + 84D04F482B16083A002EF721 /* CustomPageControl.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; + 84D314322B03590A002700ED /* CartService */ = { + isa = PBXGroup; + children = ( + 84D314332B035930002700ED /* CartFilterStorage.swift */, + 84E498292B125CD900A44124 /* ModuleFactory.swift */, + ); + path = CartService; + sourceTree = ""; + }; + 84D5231D2AFE429A00A1C791 /* View */ = { + isa = PBXGroup; + children = ( + 84CAEA522AF805D800EC9877 /* CartViewController.swift */, + 84CAEA542AF809D100EC9877 /* CartTableViewCell.swift */, + ); + path = View; + sourceTree = ""; + }; + 84D5231E2AFE42A400A1C791 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 8441AC3E2AFBDA6400A34F4C /* CartViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 84D523212AFE660D00A1C791 /* Docs */ = { + isa = PBXGroup; + children = ( + 44A2B6E82AFE612200D3A63D /* CatalogDecomposition.md */, + 84D5231F2AFE658300A1C791 /* CartDecomposition.md */, + 71D1BDE22B3CCB3800778C67 /* Profile Vadim.md */, + ); + path = Docs; + sourceTree = ""; + }; E19CD5A92A98B55900CA39A5 /* Cell */ = { isa = PBXGroup; children = ( @@ -350,10 +918,8 @@ E1CD40DA2A96BE9B00BE7FE8 /* Resources */ = { isa = PBXGroup; children = ( - 3F68069C29CBBAF100B4F915 /* Main.storyboard */, - 3F68069F29CBBAF200B4F915 /* Assets.xcassets */, - 3F6806A129CBBAF200B4F915 /* LaunchScreen.storyboard */, - E1CD40DB2A96BECC00BE7FE8 /* Localizable.strings */, + 71DF65E32B3CBE9F0033E09F /* LaunchScreen.storyboard */, + 440CC8F92B06A0CE00E0689B /* Localizable.strings */, ); path = Resources; sourceTree = ""; @@ -427,7 +993,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1500; TargetAttributes = { 3F68069229CBBAF100B4F915 = { CreatedOnToolsVersion = 14.2; @@ -472,10 +1038,12 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3F6806A329CBBAF200B4F915 /* LaunchScreen.storyboard in Resources */, - E1CD40DC2A96BECC00BE7FE8 /* Localizable.strings in Resources */, - 3F6806A029CBBAF200B4F915 /* Assets.xcassets in Resources */, - 3F68069E29CBBAF100B4F915 /* Main.storyboard in Resources */, + 7136458F2B3D6386002C9071 /* LaunchScreen.storyboard in Resources */, + 7136458D2B3D6378002C9071 /* Assets.xcassets in Resources */, + 8452F1772B4C3C46002E636D /* SF-Pro-Text-Bold.otf in Resources */, + 8452F1752B4C3C41002E636D /* SF-Pro-Text-Regular.otf in Resources */, + 8452F1762B4C3C44002E636D /* SF-Pro-Text-Medium.otf in Resources */, + 7136458E2B3D6381002C9071 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -498,7 +1066,8 @@ /* Begin PBXShellScriptBuildPhase section */ 3FC8C39529D24CDB0081F015 /* Swiftlint */ = { isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; + alwaysOutOfDate = 1; + buildActionMask = 12; files = ( ); inputFileListPaths = ( @@ -521,35 +1090,131 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 84F44B4A2B0F90D700F667A3 /* Payment.swift in Sources */, 3FC8C38B29D242E90081F015 /* ProductDetailsTableViewCell.swift in Sources */, - 3F478ECF29DB474E00F6D39E /* Colors.swift in Sources */, 0C79EE662A76DDFF00EE90EA /* NftDetailViewController.swift in Sources */, + 440DD61D2AF66B1A00B27450 /* AlertModel.swift in Sources */, E1A1B9DA2AA01CE400C3AFBC /* TabBarController.swift in Sources */, 3F478ED129DB476500F6D39E /* Fonts.swift in Sources */, + 444962D02B0FEA0F00A28E2C /* CatalogCollectionViewDelegate.swift in Sources */, + 440DD6142AF6558B00B27450 /* CatalogTableViewCell.swift in Sources */, + 71CE2D372B3CCD3D008D16C8 /* Observ.swift in Sources */, + 4474E0A52B07E7FB00E236C5 /* GeometricParams.swift in Sources */, + 447D7D692B0E8FB200D2D1E3 /* CatalogCollectionViewModelProtocol.swift in Sources */, + 84D04F492B16083A002EF721 /* CustomPageControl.swift in Sources */, + 44FA95722AFA57F70085DBA7 /* CatalogRequest.swift in Sources */, + 447D7D652B0E851D00D2D1E3 /* WebViewViewModel.swift in Sources */, 0C79EE6A2A76DE1000EE90EA /* NftDetailAssembly.swift in Sources */, 3FC8C39329D246BA0081F015 /* DateFormatters+Presets.swift in Sources */, + 71CE2D3D2B3CD1D5008D16C8 /* NFTServiceProfile.swift in Sources */, + 44AE69042B03FFAD00C39DAD /* NftModel.swift in Sources */, + 71D1BDD62B3CC9E200778C67 /* UserNFTViewModel.swift in Sources */, + 71CE2D4C2B3CDC30008D16C8 /* ProfileRouter.swift in Sources */, + 447D7D672B0E85B700D2D1E3 /* WebViewViewModelProtocol.swift in Sources */, + 71D1BDD42B3CC9E200778C67 /* FavoritesNFTViewModel.swift in Sources */, + 440DD6122AF6554C00B27450 /* CatalogView.swift in Sources */, + 440CC8F62B06A07500E0689B /* Strings.swift in Sources */, + 4433E9122B0BF55200867245 /* LikesStorage.swift in Sources */, + 84D04F452B1607D6002EF721 /* OnboardingPageViewController.swift in Sources */, + 44C8AA802AFB82DC00635019 /* CatalogAssembly.swift in Sources */, + 44FA957B2AFAB06C0085DBA7 /* CatalogCollectionViewModel.swift in Sources */, 0CFCB7422A78013E0009A829 /* Nft.swift in Sources */, + 4474E0A22B07E50900E236C5 /* Profile.swift in Sources */, + 71D1BDCF2B3CC9E200778C67 /* EditingViewController.swift in Sources */, 0CF2C2DD2A783CE600FDC837 /* ErrorView.swift in Sources */, + 84BA4D812B07A44500284C04 /* CurrencyCollectionViewCell.swift in Sources */, + 84D9C5EE2AFCE2010063CED7 /* CartByldRequest.swift in Sources */, 0C79EE682A76DE0900EE90EA /* NftDetailPresenter.swift in Sources */, + 71D1BDD92B3CC9E200778C67 /* ProfileViewController.swift in Sources */, + 440DD6192AF655FD00B27450 /* CatalogViewModel.swift in Sources */, + 44F2991F2B0D38E60052FB7F /* UserRequest.swift in Sources */, + 84555B022B0649BB00EDDB49 /* CurrencyViewController.swift in Sources */, 3F68069B29CBBAF100B4F915 /* ProductDetailsTableViewController.swift in Sources */, + 447D7D6B2B0E953000D2D1E3 /* CatalogViewModelProtocol.swift in Sources */, + 44B59CAC2B02B50B00306CE1 /* CatalogCollectionService.swift in Sources */, + 71D1BDD32B3CC9E200778C67 /* FavoritesNFTCell.swift in Sources */, + 4433E90E2B0BE43300867245 /* ProfileLike.swift in Sources */, 0CFCB7402A78002A0009A829 /* NftService.swift in Sources */, + 44FA95792AFAB0380085DBA7 /* CatalogCollectionView.swift in Sources */, 3F6806D529CBBEC700B4F915 /* NetworkTask.swift in Sources */, + 71D1BDD02B3CC9E200778C67 /* EditingViewModel.swift in Sources */, + 71CE2D432B3CD91E008D16C8 /* ImageValidator.swift in Sources */, + 44FA956D2AFA4EE20085DBA7 /* Catalog.swift in Sources */, + 44F299192B0D2AD10052FB7F /* PurchaseCartStorage.swift in Sources */, + 71CE2D502B3CDCB4008D16C8 /* ViewControllerFactory.swift in Sources */, + 84D04F472B160800002EF721 /* OnboardingViewController.swift in Sources */, + 71D1BDD52B3CC9E200778C67 /* UserNFTViewController.swift in Sources */, + 44FA95702AFA4FFA0085DBA7 /* CatalogService.swift in Sources */, + 71CE2D462B3CDB50008D16C8 /* SortOptions.swift in Sources */, + 444962CE2B0FE71900A28E2C /* CatalogViewDelegate.swift in Sources */, + 44FA957D2AFAB43B0085DBA7 /* GradientCell.swift in Sources */, + 444962D22B0FEA2A00A28E2C /* CatalogCollectionCellDelegate.swift in Sources */, + 84FC56CA2B087F990008515D /* CurrencyStorage.swift in Sources */, 0CF2C2DB2A783C1B00FDC837 /* LoadingView.swift in Sources */, 3F68069729CBBAF100B4F915 /* AppDelegate.swift in Sources */, - 3F68069929CBBAF100B4F915 /* SceneDelegate.swift in Sources */, + 71D1BDDF2B3CCA8E00778C67 /* NumberFormatter+Presets.swift in Sources */, + 440DD6102AF6550A00B27450 /* CatalogViewController.swift in Sources */, + 447057802B055BB9000F4C6C /* Author.swift in Sources */, + 71CE2D352B3CCD0A008D16C8 /* UserProfile.swift in Sources */, 0C79EE6C2A76DE2E00EE90EA /* ServicesAssemly.swift in Sources */, 0CFCB74E2A7817DC0009A829 /* NftStorage.swift in Sources */, + 440DD61B2AF66AE500B27450 /* AlertPresenter.swift in Sources */, + 7136458C2B3D6304002C9071 /* StatisticsViewController.swift in Sources */, + 84D9C5F72AFD0FEF0063CED7 /* CartService.swift in Sources */, + 84ED1F912B0219340018820A /* NumberFormatter.swift in Sources */, + 84E4982A2B125CD900A44124 /* ModuleFactory.swift in Sources */, 3FC8C39129D2453B0081F015 /* ExampleRequest.swift in Sources */, 0CFCB7462A78064B0009A829 /* NftDetailCellModel.swift in Sources */, 0CFCB74B2A780EA80009A829 /* UIView+Constraints.swift in Sources */, + 44C8AA822AFB958B00635019 /* UIBlockingProgressHUD.swift in Sources */, + 44FA956B2AFA4E810085DBA7 /* CatalogResult.swift in Sources */, + 71CE2D422B3CD8F4008D16C8 /* GeometricProfileParams.swift in Sources */, + 71D1BDDB2B3CC9E200778C67 /* WebViewViewController.swift in Sources */, + 71D1BDD82B3CC9E200778C67 /* ProfileCell.swift in Sources */, + 71CE2D4A2B3CDBC1008D16C8 /* ProfileUpdateDTO.swift in Sources */, + 44F299172B0D2A5B0052FB7F /* PurchaseCart.swift in Sources */, + 71CE2D4E2B3CDC79008D16C8 /* ProfileService.swift in Sources */, + 8441AC3F2AFBDA6400A34F4C /* CartViewModel.swift in Sources */, + 71D1BDD12B3CC9E200778C67 /* FavoritesNFTCellViewModel.swift in Sources */, + 71D1BDD72B3CC9E200778C67 /* NFTCell.swift in Sources */, + 84D9C5F22AFD0B870063CED7 /* CartStorage.swift in Sources */, + 84D9C5F52AFD0C370063CED7 /* Cart.swift in Sources */, 3F6806D729CBC50A00B4F915 /* CellsReusingUtils.swift in Sources */, + 44F2991B2B0D2C1A0052FB7F /* OrdersRequest.swift in Sources */, + 44FA95742AFAA8C30085DBA7 /* CatalogFilterStorage.swift in Sources */, + 84F44B482B0F905E00F667A3 /* PaymentRequest.swift in Sources */, 0C79EE612A76DCD600EE90EA /* NftByIdRequest.swift in Sources */, + 442C48542AFFC8BF00EDA05E /* CatalogCollectionCell.swift in Sources */, + 44AE69022B03FF0900C39DAD /* NftResult.swift in Sources */, + 84F44B462B0F664000F667A3 /* PaymentSuccessViewController.swift in Sources */, + 84FC56C42B087AB60008515D /* CurrencyViewModel.swift in Sources */, + 71CE2D442B3CD937008D16C8 /* AlertService.swift in Sources */, E19CD5AB2A98B56600CA39A5 /* NftImageCollectionViewCell.swift in Sources */, + 71CE2D3F2B3CD61F008D16C8 /* NFTNetworkRequests.swift in Sources */, + 44AE69062B0414AC00C39DAD /* UIView+Flash.swift in Sources */, 0CF2C2E12A784C7000FDC837 /* LinePageControl.swift in Sources */, + 71CE2D412B3CD8D2008D16C8 /* LoadingState.swift in Sources */, + 71CE2D482B3CDB8D008D16C8 /* ProfileNetworkRequest.swift in Sources */, + 4474E0A02B07E38100E236C5 /* ProfileRequest.swift in Sources */, + 44F2991D2B0D369F0052FB7F /* WebViewController.swift in Sources */, + 44FA95772AFAAFFA0085DBA7 /* CatalogCollectionViewController.swift in Sources */, + 84FC56C62B087E7C0008515D /* CurrencyByldRequest.swift in Sources */, 3F6806D329CBBE9600B4F915 /* NetworkRequest.swift in Sources */, - 0CFCB7492A7808900009A829 /* TestCatalogController.swift in Sources */, + 84BA4D832B07AB3C00284C04 /* Currency.swift in Sources */, + 8441AC412AFBDB8700A34F4C /* Observable.swift in Sources */, + 717C51D82B3BFFD0007AC951 /* UIColor+Extensions.swift in Sources */, + 84CAEA552AF809D100EC9877 /* CartTableViewCell.swift in Sources */, + 71D1BDDD2B3CCA4D00778C67 /* NFTProfile.swift in Sources */, + 71CE2D452B3CDA0E008D16C8 /* NetworkServiceHelper.swift in Sources */, + 71D1BDDA2B3CC9E200778C67 /* ProfileViewModel.swift in Sources */, + 84CAEA532AF805D800EC9877 /* CartViewController.swift in Sources */, 3F6806D129CBBE6B00B4F915 /* NetworkClient.swift in Sources */, + 84FC56C82B087EDD0008515D /* CurrencyService.swift in Sources */, 0CFCB7442A7802440009A829 /* NftDetailInput.swift in Sources */, + 447057822B055DEA000F4C6C /* AuthorRequest.swift in Sources */, + 71D1BDD22B3CC9E200778C67 /* FavoritesNFTViewController.swift in Sources */, + 84D314342B035930002700ED /* CartFilterStorage.swift in Sources */, + 71CE2D3B2B3CCE22008D16C8 /* UIView + Extensions.swift in Sources */, 0C79EE632A76DD1900EE90EA /* RequestConstants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -586,22 +1251,13 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 3F68069C29CBBAF100B4F915 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 3F68069D29CBBAF100B4F915 /* Base */, - E1CD40D82A96BE7D00BE7FE8 /* ru */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 3F6806A129CBBAF200B4F915 /* LaunchScreen.storyboard */ = { + 440CC8F92B06A0CE00E0689B /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( - 3F6806A229CBBAF200B4F915 /* Base */, - E1CD40D92A96BE7D00BE7FE8 /* ru */, + 440CC8F82B06A0CE00E0689B /* ru */, + 440CC8FA2B06A0FB00E0689B /* en */, ); - name = LaunchScreen.storyboard; + name = Localizable.strings; sourceTree = ""; }; /* End PBXVariantGroup section */ @@ -644,6 +1300,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -705,6 +1362,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -730,11 +1388,11 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = NU9QMJ4324; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = FakeNFT/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -743,7 +1401,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.practicum.FakeNFT; + PRODUCT_BUNDLE_IDENTIFIER = "com.practicum.FakeNFT-20901"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -758,11 +1416,11 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = NU9QMJ4324; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = FakeNFT/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -771,7 +1429,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.practicum.FakeNFT; + PRODUCT_BUNDLE_IDENTIFIER = "com.practicum.FakeNFT-20901"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; diff --git a/FakeNFT.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/FakeNFT.xcodeproj/project.xcworkspace/contents.xcworkspacedata old mode 100644 new mode 100755 diff --git a/FakeNFT.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/FakeNFT.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist old mode 100644 new mode 100755 diff --git a/FakeNFT.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/FakeNFT.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100755 index 0000000000..0c67376eba --- /dev/null +++ b/FakeNFT.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/FakeNFT.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FakeNFT.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved old mode 100644 new mode 100755 diff --git a/FakeNFT.xcodeproj/xcshareddata/xcschemes/FakeNFT.xcscheme b/FakeNFT.xcodeproj/xcshareddata/xcschemes/FakeNFT.xcscheme old mode 100644 new mode 100755 index d682c47de1..0a913eab26 --- a/FakeNFT.xcodeproj/xcshareddata/xcschemes/FakeNFT.xcscheme +++ b/FakeNFT.xcodeproj/xcshareddata/xcschemes/FakeNFT.xcscheme @@ -1,6 +1,6 @@ Bool { +// ProgressHUD.animationType = .systemActivityIndicator +// ProgressHUD.colorHUD = UIColor.nftBlack +// ProgressHUD.colorAnimation = UIColor.nftLightgrey +// return true +// } +// +// // MARK: UISceneSession Lifecycle +// +// func application( +// _: UIApplication, +// configurationForConnecting connectingSceneSession: UISceneSession, +// options _: UIScene.ConnectionOptions +// ) -> UISceneConfiguration { +// return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) +// } +// } + +@UIApplicationMain final class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + func application( - _: UIApplication, - didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + window = UIWindow() + let shownOnboardingEarlier = UserDefaults.standard.bool(forKey: "shownOnboardingEarlier") + if shownOnboardingEarlier { + window?.rootViewController = TabBarController() + } else { + window?.rootViewController = OnboardingPageViewController(transitionStyle: .scroll, + navigationOrientation: .horizontal) + } + window?.makeKeyAndVisible() return true } - - // MARK: UISceneSession Lifecycle - - func application( - _: UIApplication, - configurationForConnecting connectingSceneSession: UISceneSession, - options _: UIScene.ConnectionOptions - ) -> UISceneConfiguration { - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } } + +// if let windowScene = scene as? UIWindowScene { +// let window = UIWindow(windowScene: windowScene) +// let shownOnboardingEarlier = UserDefaults.standard.bool(forKey: "shownOnboardingEarlier") +// if shownOnboardingEarlier { +// window.rootViewController = TabBarController() +// } else { +// window.rootViewController = OnboardingPageViewController(transitionStyle: .scroll, +// navigationOrientation: .horizontal) +// } +// window.makeKeyAndVisible() +// self.window = window +// } diff --git a/FakeNFT/Assets.xcassets/AccentColor.colorset/Contents.json b/FakeNFT/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100755 index 0000000000..a7bb3ef40d --- /dev/null +++ b/FakeNFT/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.518", + "red" : "0.039" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/Colors/Background Universal.colorset/Contents.json b/FakeNFT/Assets.xcassets/Colors/Background Universal.colorset/Contents.json new file mode 100755 index 0000000000..0a9634fb44 --- /dev/null +++ b/FakeNFT/Assets.xcassets/Colors/Background Universal.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.500", + "blue" : "0.133", + "green" : "0.106", + "red" : "0.102" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/Colors/Black Universal.colorset/Contents.json b/FakeNFT/Assets.xcassets/Colors/Black Universal.colorset/Contents.json new file mode 100755 index 0000000000..b298f7191d --- /dev/null +++ b/FakeNFT/Assets.xcassets/Colors/Black Universal.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.133", + "green" : "0.106", + "red" : "0.102" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/Colors/Black.colorset/Contents.json b/FakeNFT/Assets.xcassets/Colors/Black.colorset/Contents.json new file mode 100755 index 0000000000..64b1965e4e --- /dev/null +++ b/FakeNFT/Assets.xcassets/Colors/Black.colorset/Contents.json @@ -0,0 +1,59 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.133", + "green" : "0.106", + "red" : "0.102" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.133", + "green" : "0.106", + "red" : "0.102" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "localizable" : true + } +} diff --git a/FakeNFT/Assets.xcassets/Colors/Blue Universal.colorset/Contents.json b/FakeNFT/Assets.xcassets/Colors/Blue Universal.colorset/Contents.json new file mode 100755 index 0000000000..a7bb3ef40d --- /dev/null +++ b/FakeNFT/Assets.xcassets/Colors/Blue Universal.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.518", + "red" : "0.039" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Resources/Assets.xcassets/Contents.json b/FakeNFT/Assets.xcassets/Colors/Contents.json old mode 100644 new mode 100755 similarity index 100% rename from FakeNFT/Resources/Assets.xcassets/Contents.json rename to FakeNFT/Assets.xcassets/Colors/Contents.json diff --git a/FakeNFT/Assets.xcassets/Colors/Gray Universal.colorset/Contents.json b/FakeNFT/Assets.xcassets/Colors/Gray Universal.colorset/Contents.json new file mode 100755 index 0000000000..d030805e11 --- /dev/null +++ b/FakeNFT/Assets.xcassets/Colors/Gray Universal.colorset/Contents.json @@ -0,0 +1,23 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.361", + "green" : "0.361", + "red" : "0.384" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "localizable" : true + } +} diff --git a/FakeNFT/Assets.xcassets/Colors/Green Universal.colorset/Contents.json b/FakeNFT/Assets.xcassets/Colors/Green Universal.colorset/Contents.json new file mode 100755 index 0000000000..ff90426208 --- /dev/null +++ b/FakeNFT/Assets.xcassets/Colors/Green Universal.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.000", + "green" : "0.624", + "red" : "0.110" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/Colors/Light grey.colorset/Contents.json b/FakeNFT/Assets.xcassets/Colors/Light grey.colorset/Contents.json new file mode 100755 index 0000000000..63d5b987bc --- /dev/null +++ b/FakeNFT/Assets.xcassets/Colors/Light grey.colorset/Contents.json @@ -0,0 +1,59 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.973", + "green" : "0.969", + "red" : "0.969" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.973", + "green" : "0.969", + "red" : "0.969" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.180", + "green" : "0.173", + "red" : "0.173" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "localizable" : true + } +} diff --git a/FakeNFT/Assets.xcassets/Colors/Red Universal.colorset/Contents.json b/FakeNFT/Assets.xcassets/Colors/Red Universal.colorset/Contents.json new file mode 100755 index 0000000000..0fb6f93909 --- /dev/null +++ b/FakeNFT/Assets.xcassets/Colors/Red Universal.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.424", + "green" : "0.420", + "red" : "0.961" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/Colors/White Universal.colorset/Contents.json b/FakeNFT/Assets.xcassets/Colors/White Universal.colorset/Contents.json new file mode 100755 index 0000000000..97650a1a6e --- /dev/null +++ b/FakeNFT/Assets.xcassets/Colors/White Universal.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/Colors/White.colorset/Contents.json b/FakeNFT/Assets.xcassets/Colors/White.colorset/Contents.json new file mode 100755 index 0000000000..4a98b1f5bd --- /dev/null +++ b/FakeNFT/Assets.xcassets/Colors/White.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.133", + "green" : "0.106", + "red" : "0.102" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/Colors/Yellow Universal.colorset/Contents.json b/FakeNFT/Assets.xcassets/Colors/Yellow Universal.colorset/Contents.json new file mode 100755 index 0000000000..9ae6e2d31a --- /dev/null +++ b/FakeNFT/Assets.xcassets/Colors/Yellow Universal.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.051", + "green" : "0.937", + "red" : "0.996" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/Contents.json b/FakeNFT/Assets.xcassets/Contents.json new file mode 100755 index 0000000000..73c00596a7 --- /dev/null +++ b/FakeNFT/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/FakeNFT/Assets.xcassets/images/AppIcon.appiconset/Contents.json old mode 100644 new mode 100755 similarity index 83% rename from FakeNFT/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json rename to FakeNFT/Assets.xcassets/images/AppIcon.appiconset/Contents.json index 13613e3ee1..eb481e3a6f --- a/FakeNFT/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/FakeNFT/Assets.xcassets/images/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "image 1.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/FakeNFT/Assets.xcassets/images/AppIcon.appiconset/image 1.png b/FakeNFT/Assets.xcassets/images/AppIcon.appiconset/image 1.png new file mode 100755 index 0000000000..cd35b362f1 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/AppIcon.appiconset/image 1.png differ diff --git a/FakeNFT/Assets.xcassets/images/Contents.json b/FakeNFT/Assets.xcassets/images/Contents.json new file mode 100755 index 0000000000..73c00596a7 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/add_plus_button.imageset/Active@1x.pdf b/FakeNFT/Assets.xcassets/images/add_plus_button.imageset/Active@1x.pdf new file mode 100755 index 0000000000..b7d03a09fb Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/add_plus_button.imageset/Active@1x.pdf differ diff --git a/FakeNFT/Assets.xcassets/images/add_plus_button.imageset/Active@2x.pdf b/FakeNFT/Assets.xcassets/images/add_plus_button.imageset/Active@2x.pdf new file mode 100755 index 0000000000..b7d03a09fb Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/add_plus_button.imageset/Active@2x.pdf differ diff --git a/FakeNFT/Assets.xcassets/images/add_plus_button.imageset/Active@3x.pdf b/FakeNFT/Assets.xcassets/images/add_plus_button.imageset/Active@3x.pdf new file mode 100755 index 0000000000..b7d03a09fb Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/add_plus_button.imageset/Active@3x.pdf differ diff --git a/FakeNFT/Assets.xcassets/images/add_plus_button.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/add_plus_button.imageset/Contents.json new file mode 100755 index 0000000000..0d0bc0efd2 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/add_plus_button.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Active@1x.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Active@2x.pdf", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Active@3x.pdf", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/backward.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/backward.imageset/Contents.json new file mode 100755 index 0000000000..8880dff63e --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/backward.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Light.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Light@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Light@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/backward.imageset/Light.png b/FakeNFT/Assets.xcassets/images/backward.imageset/Light.png new file mode 100755 index 0000000000..73bd422847 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/backward.imageset/Light.png differ diff --git a/FakeNFT/Assets.xcassets/images/backward.imageset/Light@2x.png b/FakeNFT/Assets.xcassets/images/backward.imageset/Light@2x.png new file mode 100755 index 0000000000..8a9db02f35 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/backward.imageset/Light@2x.png differ diff --git a/FakeNFT/Assets.xcassets/images/backward.imageset/Light@3x.png b/FakeNFT/Assets.xcassets/images/backward.imageset/Light@3x.png new file mode 100755 index 0000000000..d4e6f1a9ea Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/backward.imageset/Light@3x.png differ diff --git a/FakeNFT/Assets.xcassets/images/basket_add.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/basket_add.imageset/Contents.json new file mode 100755 index 0000000000..82c9abe174 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/basket_add.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Property 1=Add.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Property 1=Add@2x.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Property 1=Add@3x.svg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/basket_add.imageset/Property 1=Add.svg b/FakeNFT/Assets.xcassets/images/basket_add.imageset/Property 1=Add.svg new file mode 100755 index 0000000000..cf69dcc80b --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/basket_add.imageset/Property 1=Add.svg @@ -0,0 +1,4 @@ + + + + diff --git a/FakeNFT/Assets.xcassets/images/basket_add.imageset/Property 1=Add@2x.svg b/FakeNFT/Assets.xcassets/images/basket_add.imageset/Property 1=Add@2x.svg new file mode 100755 index 0000000000..cf69dcc80b --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/basket_add.imageset/Property 1=Add@2x.svg @@ -0,0 +1,4 @@ + + + + diff --git a/FakeNFT/Assets.xcassets/images/basket_add.imageset/Property 1=Add@3x.svg b/FakeNFT/Assets.xcassets/images/basket_add.imageset/Property 1=Add@3x.svg new file mode 100755 index 0000000000..cf69dcc80b --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/basket_add.imageset/Property 1=Add@3x.svg @@ -0,0 +1,4 @@ + + + + diff --git a/FakeNFT/Assets.xcassets/images/basket_delete.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/basket_delete.imageset/Contents.json new file mode 100755 index 0000000000..e4164d4251 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/basket_delete.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Property 1=Delete.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Property 1=Delete@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Property 1=Delete@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/basket_delete.imageset/Property 1=Delete.png b/FakeNFT/Assets.xcassets/images/basket_delete.imageset/Property 1=Delete.png new file mode 100755 index 0000000000..14b6381aca Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/basket_delete.imageset/Property 1=Delete.png differ diff --git a/FakeNFT/Assets.xcassets/images/basket_delete.imageset/Property 1=Delete@2x.png b/FakeNFT/Assets.xcassets/images/basket_delete.imageset/Property 1=Delete@2x.png new file mode 100755 index 0000000000..701ccaedfd Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/basket_delete.imageset/Property 1=Delete@2x.png differ diff --git a/FakeNFT/Assets.xcassets/images/basket_delete.imageset/Property 1=Delete@3x.png b/FakeNFT/Assets.xcassets/images/basket_delete.imageset/Property 1=Delete@3x.png new file mode 100755 index 0000000000..f34a767620 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/basket_delete.imageset/Property 1=Delete@3x.png differ diff --git a/FakeNFT/Assets.xcassets/images/cell_stub.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/cell_stub.imageset/Contents.json new file mode 100755 index 0000000000..24cc34682a --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/cell_stub.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Frame 9430.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/cell_stub.imageset/Frame 9430.pdf b/FakeNFT/Assets.xcassets/images/cell_stub.imageset/Frame 9430.pdf new file mode 100755 index 0000000000..c6f5a43465 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/cell_stub.imageset/Frame 9430.pdf differ diff --git a/FakeNFT/Assets.xcassets/images/close.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/close.imageset/Contents.json new file mode 100755 index 0000000000..337868b2b7 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/close.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "plus.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "plus@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "plus@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/close.imageset/plus.png b/FakeNFT/Assets.xcassets/images/close.imageset/plus.png new file mode 100644 index 0000000000..f1c1a0eb5c Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/close.imageset/plus.png differ diff --git a/FakeNFT/Assets.xcassets/images/close.imageset/plus@2x.png b/FakeNFT/Assets.xcassets/images/close.imageset/plus@2x.png new file mode 100644 index 0000000000..fd8936a456 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/close.imageset/plus@2x.png differ diff --git a/FakeNFT/Assets.xcassets/images/close.imageset/plus@3x.png b/FakeNFT/Assets.xcassets/images/close.imageset/plus@3x.png new file mode 100644 index 0000000000..b283bf311e Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/close.imageset/plus@3x.png differ diff --git a/FakeNFT/Assets.xcassets/images/closeWhite.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/closeWhite.imageset/Contents.json new file mode 100755 index 0000000000..8880dff63e --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/closeWhite.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Light.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Light@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Light@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/closeWhite.imageset/Light.png b/FakeNFT/Assets.xcassets/images/closeWhite.imageset/Light.png new file mode 100755 index 0000000000..9b18fcb92a Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/closeWhite.imageset/Light.png differ diff --git a/FakeNFT/Assets.xcassets/images/closeWhite.imageset/Light@2x.png b/FakeNFT/Assets.xcassets/images/closeWhite.imageset/Light@2x.png new file mode 100755 index 0000000000..b19f572236 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/closeWhite.imageset/Light@2x.png differ diff --git a/FakeNFT/Assets.xcassets/images/closeWhite.imageset/Light@3x.png b/FakeNFT/Assets.xcassets/images/closeWhite.imageset/Light@3x.png new file mode 100755 index 0000000000..800a60580b Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/closeWhite.imageset/Light@3x.png differ diff --git a/FakeNFT/Assets.xcassets/images/done_button.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/done_button.imageset/Contents.json new file mode 100755 index 0000000000..fb3a9be90f --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/done_button.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Done@1x.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Done@2x.pdf", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Done@3x.pdf", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/done_button.imageset/Done@1x.pdf b/FakeNFT/Assets.xcassets/images/done_button.imageset/Done@1x.pdf new file mode 100755 index 0000000000..11c67a6b0f Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/done_button.imageset/Done@1x.pdf differ diff --git a/FakeNFT/Assets.xcassets/images/done_button.imageset/Done@2x.pdf b/FakeNFT/Assets.xcassets/images/done_button.imageset/Done@2x.pdf new file mode 100755 index 0000000000..11c67a6b0f Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/done_button.imageset/Done@2x.pdf differ diff --git a/FakeNFT/Assets.xcassets/images/done_button.imageset/Done@3x.pdf b/FakeNFT/Assets.xcassets/images/done_button.imageset/Done@3x.pdf new file mode 100755 index 0000000000..11c67a6b0f Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/done_button.imageset/Done@3x.pdf differ diff --git a/FakeNFT/Resources/Assets.xcassets/close.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/edit.imageset/Contents.json old mode 100644 new mode 100755 similarity index 74% rename from FakeNFT/Resources/Assets.xcassets/close.imageset/Contents.json rename to FakeNFT/Assets.xcassets/images/edit.imageset/Contents.json index 214455b598..7510eac2c8 --- a/FakeNFT/Resources/Assets.xcassets/close.imageset/Contents.json +++ b/FakeNFT/Assets.xcassets/images/edit.imageset/Contents.json @@ -1,16 +1,17 @@ { "images" : [ { + "filename" : "Dark.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "Dark-1.png", + "filename" : "Dark@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "Dark.png", + "filename" : "Dark@3x.png", "idiom" : "universal", "scale" : "3x" } @@ -18,8 +19,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" } } diff --git a/FakeNFT/Assets.xcassets/images/edit.imageset/Dark.png b/FakeNFT/Assets.xcassets/images/edit.imageset/Dark.png new file mode 100644 index 0000000000..faebffd5a2 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/edit.imageset/Dark.png differ diff --git a/FakeNFT/Assets.xcassets/images/edit.imageset/Dark@2x.png b/FakeNFT/Assets.xcassets/images/edit.imageset/Dark@2x.png new file mode 100644 index 0000000000..6c8a6e9b2d Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/edit.imageset/Dark@2x.png differ diff --git a/FakeNFT/Assets.xcassets/images/edit.imageset/Dark@3x.png b/FakeNFT/Assets.xcassets/images/edit.imageset/Dark@3x.png new file mode 100644 index 0000000000..ec004f1131 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/edit.imageset/Dark@3x.png differ diff --git a/FakeNFT/Assets.xcassets/images/forward.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/forward.imageset/Contents.json new file mode 100644 index 0000000000..9695024a47 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/forward.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "chevron.forward.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "chevron.forward@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "chevron.forward@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/forward.imageset/chevron.forward.png b/FakeNFT/Assets.xcassets/images/forward.imageset/chevron.forward.png new file mode 100644 index 0000000000..98c6fe9e9e Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/forward.imageset/chevron.forward.png differ diff --git a/FakeNFT/Assets.xcassets/images/forward.imageset/chevron.forward@2x.png b/FakeNFT/Assets.xcassets/images/forward.imageset/chevron.forward@2x.png new file mode 100644 index 0000000000..f388eed8e5 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/forward.imageset/chevron.forward@2x.png differ diff --git a/FakeNFT/Assets.xcassets/images/forward.imageset/chevron.forward@3x.png b/FakeNFT/Assets.xcassets/images/forward.imageset/chevron.forward@3x.png new file mode 100644 index 0000000000..45cb037ec8 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/forward.imageset/chevron.forward@3x.png differ diff --git a/FakeNFT/Assets.xcassets/images/like_active.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/like_active.imageset/Contents.json new file mode 100755 index 0000000000..1f5455650e --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/like_active.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "filledHeartButtonImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "filledHeartButtonImage2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "filledHeartButtonImage3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/like_active.imageset/filledHeartButtonImage.png b/FakeNFT/Assets.xcassets/images/like_active.imageset/filledHeartButtonImage.png new file mode 100644 index 0000000000..8a5363b834 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/like_active.imageset/filledHeartButtonImage.png differ diff --git a/FakeNFT/Assets.xcassets/images/like_active.imageset/filledHeartButtonImage2x.png b/FakeNFT/Assets.xcassets/images/like_active.imageset/filledHeartButtonImage2x.png new file mode 100644 index 0000000000..5ad62a0b54 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/like_active.imageset/filledHeartButtonImage2x.png differ diff --git a/FakeNFT/Assets.xcassets/images/like_active.imageset/filledHeartButtonImage3x.png b/FakeNFT/Assets.xcassets/images/like_active.imageset/filledHeartButtonImage3x.png new file mode 100644 index 0000000000..ab8bb65bc1 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/like_active.imageset/filledHeartButtonImage3x.png differ diff --git a/FakeNFT/Assets.xcassets/images/like_inactive.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/like_inactive.imageset/Contents.json new file mode 100755 index 0000000000..ed53908ded --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/like_inactive.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "No Active@1x.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/like_inactive.imageset/No Active@1x.pdf b/FakeNFT/Assets.xcassets/images/like_inactive.imageset/No Active@1x.pdf new file mode 100755 index 0000000000..a9708ae5cf Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/like_inactive.imageset/No Active@1x.pdf differ diff --git a/FakeNFT/Assets.xcassets/images/onboarding1.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/onboarding1.imageset/Contents.json new file mode 100755 index 0000000000..fbcea5d9a4 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/onboarding1.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "image 15.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "image 15@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "image 15@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/onboarding1.imageset/image 15.png b/FakeNFT/Assets.xcassets/images/onboarding1.imageset/image 15.png new file mode 100755 index 0000000000..cb12f82e26 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/onboarding1.imageset/image 15.png differ diff --git a/FakeNFT/Assets.xcassets/images/onboarding1.imageset/image 15@2x.png b/FakeNFT/Assets.xcassets/images/onboarding1.imageset/image 15@2x.png new file mode 100755 index 0000000000..e359d4d078 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/onboarding1.imageset/image 15@2x.png differ diff --git a/FakeNFT/Assets.xcassets/images/onboarding1.imageset/image 15@3x.png b/FakeNFT/Assets.xcassets/images/onboarding1.imageset/image 15@3x.png new file mode 100755 index 0000000000..2eaccb3a04 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/onboarding1.imageset/image 15@3x.png differ diff --git a/FakeNFT/Assets.xcassets/images/onboarding2.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/onboarding2.imageset/Contents.json new file mode 100755 index 0000000000..6afaf6af29 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/onboarding2.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "image 14.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "image 14@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "image 14@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/onboarding2.imageset/image 14.png b/FakeNFT/Assets.xcassets/images/onboarding2.imageset/image 14.png new file mode 100755 index 0000000000..f0d32a88f7 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/onboarding2.imageset/image 14.png differ diff --git a/FakeNFT/Assets.xcassets/images/onboarding2.imageset/image 14@2x.png b/FakeNFT/Assets.xcassets/images/onboarding2.imageset/image 14@2x.png new file mode 100755 index 0000000000..cab19a16b4 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/onboarding2.imageset/image 14@2x.png differ diff --git a/FakeNFT/Assets.xcassets/images/onboarding2.imageset/image 14@3x.png b/FakeNFT/Assets.xcassets/images/onboarding2.imageset/image 14@3x.png new file mode 100755 index 0000000000..63d7a4bbc7 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/onboarding2.imageset/image 14@3x.png differ diff --git a/FakeNFT/Assets.xcassets/images/onboarding3.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/onboarding3.imageset/Contents.json new file mode 100755 index 0000000000..0d3ff20f48 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/onboarding3.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "image 16.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "image 16@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "image 16@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/onboarding3.imageset/image 16.png b/FakeNFT/Assets.xcassets/images/onboarding3.imageset/image 16.png new file mode 100755 index 0000000000..c9e52b0477 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/onboarding3.imageset/image 16.png differ diff --git a/FakeNFT/Assets.xcassets/images/onboarding3.imageset/image 16@2x.png b/FakeNFT/Assets.xcassets/images/onboarding3.imageset/image 16@2x.png new file mode 100755 index 0000000000..661e305589 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/onboarding3.imageset/image 16@2x.png differ diff --git a/FakeNFT/Assets.xcassets/images/onboarding3.imageset/image 16@3x.png b/FakeNFT/Assets.xcassets/images/onboarding3.imageset/image 16@3x.png new file mode 100755 index 0000000000..90a0415f0e Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/onboarding3.imageset/image 16@3x.png differ diff --git a/FakeNFT/Assets.xcassets/images/payment_success.imageset/56_digital_art_x4@1x.png b/FakeNFT/Assets.xcassets/images/payment_success.imageset/56_digital_art_x4@1x.png new file mode 100755 index 0000000000..c969cd7d7f Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/payment_success.imageset/56_digital_art_x4@1x.png differ diff --git a/FakeNFT/Assets.xcassets/images/payment_success.imageset/56_digital_art_x4@2x.png b/FakeNFT/Assets.xcassets/images/payment_success.imageset/56_digital_art_x4@2x.png new file mode 100755 index 0000000000..dc630f1774 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/payment_success.imageset/56_digital_art_x4@2x.png differ diff --git a/FakeNFT/Assets.xcassets/images/payment_success.imageset/56_digital_art_x4@3x.png b/FakeNFT/Assets.xcassets/images/payment_success.imageset/56_digital_art_x4@3x.png new file mode 100755 index 0000000000..9e0815b03c Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/payment_success.imageset/56_digital_art_x4@3x.png differ diff --git a/FakeNFT/Assets.xcassets/images/payment_success.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/payment_success.imageset/Contents.json new file mode 100755 index 0000000000..094900634f --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/payment_success.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "56_digital_art_x4@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "56_digital_art_x4@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "56_digital_art_x4@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/profile_stub.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/profile_stub.imageset/Contents.json new file mode 100755 index 0000000000..cc50864684 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/profile_stub.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Property 1=Stub.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/profile_stub.imageset/Property 1=Stub.pdf b/FakeNFT/Assets.xcassets/images/profile_stub.imageset/Property 1=Stub.pdf new file mode 100755 index 0000000000..4a71fd4cbf Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/profile_stub.imageset/Property 1=Stub.pdf differ diff --git a/FakeNFT/Assets.xcassets/images/sort_button.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/sort_button.imageset/Contents.json new file mode 100755 index 0000000000..8880dff63e --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/sort_button.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Light.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Light@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Light@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/sort_button.imageset/Light.png b/FakeNFT/Assets.xcassets/images/sort_button.imageset/Light.png new file mode 100755 index 0000000000..f666c1ed8f Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/sort_button.imageset/Light.png differ diff --git a/FakeNFT/Assets.xcassets/images/sort_button.imageset/Light@2x.png b/FakeNFT/Assets.xcassets/images/sort_button.imageset/Light@2x.png new file mode 100755 index 0000000000..152b20fdf4 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/sort_button.imageset/Light@2x.png differ diff --git a/FakeNFT/Assets.xcassets/images/sort_button.imageset/Light@3x.png b/FakeNFT/Assets.xcassets/images/sort_button.imageset/Light@3x.png new file mode 100755 index 0000000000..094900bba6 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/sort_button.imageset/Light@3x.png differ diff --git a/FakeNFT/Assets.xcassets/images/star_active.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/star_active.imageset/Contents.json new file mode 100755 index 0000000000..a6a7d44cfb --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/star_active.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Property 1=12x12, Property 2=Star, Property 3=Active.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/star_active.imageset/Property 1=12x12, Property 2=Star, Property 3=Active.pdf b/FakeNFT/Assets.xcassets/images/star_active.imageset/Property 1=12x12, Property 2=Star, Property 3=Active.pdf new file mode 100755 index 0000000000..196a2b8f7b Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/star_active.imageset/Property 1=12x12, Property 2=Star, Property 3=Active.pdf differ diff --git a/FakeNFT/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/FakeNFT/Assets.xcassets/images/star_inactive.imageset/Contents.json old mode 100644 new mode 100755 similarity index 67% rename from FakeNFT/Resources/Assets.xcassets/AccentColor.colorset/Contents.json rename to FakeNFT/Assets.xcassets/images/star_inactive.imageset/Contents.json index eb87897008..77c14d0052 --- a/FakeNFT/Resources/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/FakeNFT/Assets.xcassets/images/star_inactive.imageset/Contents.json @@ -1,6 +1,7 @@ { - "colors" : [ + "images" : [ { + "filename" : "No Active.pdf", "idiom" : "universal" } ], diff --git a/FakeNFT/Assets.xcassets/images/star_inactive.imageset/No Active.pdf b/FakeNFT/Assets.xcassets/images/star_inactive.imageset/No Active.pdf new file mode 100755 index 0000000000..1d8e0a19f4 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/star_inactive.imageset/No Active.pdf differ diff --git a/FakeNFT/Assets.xcassets/images/stars.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/stars.imageset/Contents.json new file mode 100644 index 0000000000..2e166755ec --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/stars.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Stars.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Stars@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Stars@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/stars.imageset/Stars.png b/FakeNFT/Assets.xcassets/images/stars.imageset/Stars.png new file mode 100644 index 0000000000..09f34c02a1 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/stars.imageset/Stars.png differ diff --git a/FakeNFT/Assets.xcassets/images/stars.imageset/Stars@2x.png b/FakeNFT/Assets.xcassets/images/stars.imageset/Stars@2x.png new file mode 100644 index 0000000000..ca86fd4796 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/stars.imageset/Stars@2x.png differ diff --git a/FakeNFT/Assets.xcassets/images/stars.imageset/Stars@3x.png b/FakeNFT/Assets.xcassets/images/stars.imageset/Stars@3x.png new file mode 100644 index 0000000000..cbbf3cb1b5 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/stars.imageset/Stars@3x.png differ diff --git a/FakeNFT/Assets.xcassets/images/tabbar_basket.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/tabbar_basket.imageset/Contents.json new file mode 100755 index 0000000000..77c14d0052 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/tabbar_basket.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "No Active.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/tabbar_basket.imageset/No Active.pdf b/FakeNFT/Assets.xcassets/images/tabbar_basket.imageset/No Active.pdf new file mode 100755 index 0000000000..6cb73e606c Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/tabbar_basket.imageset/No Active.pdf differ diff --git a/FakeNFT/Assets.xcassets/images/tabbar_catalogue.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/tabbar_catalogue.imageset/Contents.json new file mode 100755 index 0000000000..77c14d0052 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/tabbar_catalogue.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "No Active.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/tabbar_catalogue.imageset/No Active.pdf b/FakeNFT/Assets.xcassets/images/tabbar_catalogue.imageset/No Active.pdf new file mode 100755 index 0000000000..09b1d3a546 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/tabbar_catalogue.imageset/No Active.pdf differ diff --git a/FakeNFT/Assets.xcassets/images/tabbar_profile.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/tabbar_profile.imageset/Contents.json new file mode 100755 index 0000000000..77c14d0052 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/tabbar_profile.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "No Active.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/tabbar_profile.imageset/No Active.pdf b/FakeNFT/Assets.xcassets/images/tabbar_profile.imageset/No Active.pdf new file mode 100755 index 0000000000..5b73833c71 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/tabbar_profile.imageset/No Active.pdf differ diff --git a/FakeNFT/Assets.xcassets/images/tabbar_statistics.imageset/Contents.json b/FakeNFT/Assets.xcassets/images/tabbar_statistics.imageset/Contents.json new file mode 100755 index 0000000000..77c14d0052 --- /dev/null +++ b/FakeNFT/Assets.xcassets/images/tabbar_statistics.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "No Active.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Assets.xcassets/images/tabbar_statistics.imageset/No Active.pdf b/FakeNFT/Assets.xcassets/images/tabbar_statistics.imageset/No Active.pdf new file mode 100755 index 0000000000..7265d5b880 Binary files /dev/null and b/FakeNFT/Assets.xcassets/images/tabbar_statistics.imageset/No Active.pdf differ diff --git a/FakeNFT/DesignSystem/Colors.swift b/FakeNFT/DesignSystem/Colors.swift old mode 100644 new mode 100755 index 8f95ac024d..685dff4b0d --- a/FakeNFT/DesignSystem/Colors.swift +++ b/FakeNFT/DesignSystem/Colors.swift @@ -17,12 +17,7 @@ extension UIColor { default: (alpha, red, green, blue) = (255, 0, 0, 0) } - self.init( - red: CGFloat(red) / 255, - green: CGFloat(green) / 255, - blue: CGFloat(blue) / 255, - alpha: CGFloat(alpha) / 255 - ) + self.init(red: CGFloat(red) / 255, green: CGFloat(green) / 255, blue: CGFloat(blue) / 255, alpha: CGFloat(alpha) / 255) } // Ниже приведены примеры цветов, настоящие цвета надо взять из фигмы @@ -41,27 +36,4 @@ extension UIColor { static let textSecondary = UIColor.gray static let textOnPrimary = UIColor.white static let textOnSecondary = UIColor.black - - private static let yaBlackLight = UIColor(hexString: "1A1B22") - private static let yaBlackDark = UIColor.white - private static let yaLightGrayLight = UIColor(hexString: "#F7F7F8") - private static let yaLightGrayDark = UIColor(hexString: "#2C2C2E") - - static let segmentActive = UIColor { traits in - return traits.userInterfaceStyle == .dark - ? .yaBlackDark - : .yaBlackLight - } - - static let segmentInactive = UIColor { traits in - return traits.userInterfaceStyle == .dark - ? .yaLightGrayDark - : .yaLightGrayLight - } - - static let closeButton = UIColor { traits in - return traits.userInterfaceStyle == .dark - ? .yaBlackDark - : .yaBlackLight - } } diff --git a/FakeNFT/DesignSystem/Fonts.swift b/FakeNFT/DesignSystem/Fonts.swift old mode 100644 new mode 100755 index d8e1f6657d..a882fae74e --- a/FakeNFT/DesignSystem/Fonts.swift +++ b/FakeNFT/DesignSystem/Fonts.swift @@ -1,19 +1,23 @@ import UIKit extension UIFont { - // Ниже приведены примеры шрифтов, настоящие шрифты надо взять из фигмы + + static let nftFontRegularName = "SFProText-Regular" + static let nftFontMediumName = "SFProText-Medium" + static let nftFontBoldName = "SFProText-Bold" // Headline Fonts - static var headline1 = UIFont.systemFont(ofSize: 34, weight: .bold) - static var headline2 = UIFont.systemFont(ofSize: 28, weight: .bold) + static let headline1 = UIFont(name: nftFontBoldName, size: 34) + static let headline2 = UIFont(name: nftFontBoldName, size: 22) static var headline3 = UIFont.systemFont(ofSize: 22, weight: .bold) static var headline4 = UIFont.systemFont(ofSize: 20, weight: .bold) // Body Fonts - static var bodyRegular = UIFont.systemFont(ofSize: 17, weight: .regular) - static var bodyBold = UIFont.systemFont(ofSize: 17, weight: .bold) + static let bodyRegular = UIFont(name: nftFontRegularName, size: 17) + static let bodyBold = UIFont(name: nftFontBoldName, size: 17) // Caption Fonts - static var caption1 = UIFont.systemFont(ofSize: 15, weight: .regular) - static var caption2 = UIFont.systemFont(ofSize: 13, weight: .regular) + static let caption1 = UIFont(name: nftFontRegularName, size: 15) + static let caption2 = UIFont(name: nftFontRegularName, size: 13) + static let caption3 = UIFont(name: nftFontMediumName, size: 10) } diff --git a/FakeNFT/DesignSystem/FontsResources/SF-Pro-Text-Bold.otf b/FakeNFT/DesignSystem/FontsResources/SF-Pro-Text-Bold.otf new file mode 100755 index 0000000000..559e1df396 Binary files /dev/null and b/FakeNFT/DesignSystem/FontsResources/SF-Pro-Text-Bold.otf differ diff --git a/FakeNFT/DesignSystem/FontsResources/SF-Pro-Text-Medium.otf b/FakeNFT/DesignSystem/FontsResources/SF-Pro-Text-Medium.otf new file mode 100755 index 0000000000..1689eddb6d Binary files /dev/null and b/FakeNFT/DesignSystem/FontsResources/SF-Pro-Text-Medium.otf differ diff --git a/FakeNFT/DesignSystem/FontsResources/SF-Pro-Text-Regular.otf b/FakeNFT/DesignSystem/FontsResources/SF-Pro-Text-Regular.otf new file mode 100755 index 0000000000..2f719c361b Binary files /dev/null and b/FakeNFT/DesignSystem/FontsResources/SF-Pro-Text-Regular.otf differ diff --git a/FakeNFT/DesignSystem/UIColor+Extensions.swift b/FakeNFT/DesignSystem/UIColor+Extensions.swift new file mode 100755 index 0000000000..1436d59a1e --- /dev/null +++ b/FakeNFT/DesignSystem/UIColor+Extensions.swift @@ -0,0 +1,15 @@ +import UIKit + +extension UIColor { + static var nftBackgroundUniversal: UIColor { UIColor(named: "Background Universal") ?? clear } + static var nftBlackUniversal: UIColor { UIColor(named: "Black Universal") ?? clear } + static var nftBlack: UIColor { UIColor(named: "Black") ?? clear } + static var nftBlueUniversal: UIColor { UIColor(named: "Blue Universal") ?? clear } + static var nftGrayUniversal: UIColor { UIColor(named: "Gray Universal") ?? clear } + static var nftGreenUniversal: UIColor { UIColor(named: "Green Universal") ?? clear } + static var nftLightgrey: UIColor { UIColor(named: "Light grey") ?? clear } + static var nftRedUniversal: UIColor { UIColor(named: "Red Universal") ?? clear } + static var nftWhiteUniversal: UIColor { UIColor(named: "White Universal") ?? clear } + static var nftWhite: UIColor { UIColor(named: "White") ?? clear } + static var nftYellowUniversal: UIColor { UIColor(named: "Yellow Universal") ?? clear } +} diff --git a/FakeNFT/DesignSystem/UIView + Extensions.swift b/FakeNFT/DesignSystem/UIView + Extensions.swift new file mode 100755 index 0000000000..20e491514f --- /dev/null +++ b/FakeNFT/DesignSystem/UIView + Extensions.swift @@ -0,0 +1,49 @@ +import UIKit + +extension UIView { + func addViewWithNoTAMIC(_ views: UIView) { + self.addSubview(views) + views.translatesAutoresizingMaskIntoConstraints = false + } +} + +extension UIView { + func addTapGestureToHideKeyboard() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + addGestureRecognizer(tapGesture) + } + + private var topSuperview: UIView? { + var view = superview + while view?.superview != nil { + view = view!.superview + } + return view + } + + @objc + private func dismissKeyboard() { + topSuperview?.endEditing(true) + } +} + +extension UIViewController { + + func setupCustomBackButton() { + let backButtonImage = UIImage(named: "backward")?.withRenderingMode(.alwaysTemplate) + + navigationItem.leftBarButtonItem = UIBarButtonItem( + image: backButtonImage, + style: .plain, + target: self, + action: #selector(self.customBackAction) + ) + + navigationItem.leftBarButtonItem?.tintColor = .black + } + + @objc + func customBackAction() { + self.navigationController?.popViewController(animated: true) + } +} diff --git a/FakeNFT/DesignSystem/UIView+Flash.swift b/FakeNFT/DesignSystem/UIView+Flash.swift new file mode 100755 index 0000000000..0ca05c3ad9 --- /dev/null +++ b/FakeNFT/DesignSystem/UIView+Flash.swift @@ -0,0 +1,21 @@ +// +// UIView+Flash.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 14.11.2023. +// + +import UIKit + +extension UIView { + func addFlashLayer() { + let flash = CABasicAnimation(keyPath: "opacity") + flash.duration = 0.5 + flash.fromValue = 0.5 + flash.toValue = 0.1 + flash.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) + flash.autoreverses = true + flash.repeatCount = .infinity + layer.add(flash, forKey: nil) + } +} diff --git a/FakeNFT/DesignSystem/UIViewController + Extensions.swift b/FakeNFT/DesignSystem/UIViewController + Extensions.swift new file mode 100755 index 0000000000..61d28b8c81 --- /dev/null +++ b/FakeNFT/DesignSystem/UIViewController + Extensions.swift @@ -0,0 +1,18 @@ +import UIKit + +extension UIViewController { + + func setupCustomBackButton() { + navigationItem.leftBarButtonItem = UIBarButtonItem( + image: UIImage(named: "backward"), + style: .plain, + target: self, + action: #selector(self.customBackAction) + ) + } + + @objc + func customBackAction() { + self.navigationController?.popViewController(animated: true) + } +} diff --git a/FakeNFT/Docs/CartDecomposition.md b/FakeNFT/Docs/CartDecomposition.md new file mode 100755 index 0000000000..0390909b22 --- /dev/null +++ b/FakeNFT/Docs/CartDecomposition.md @@ -0,0 +1,36 @@ +07_ios_teamwork3 + +Декомпозиция эпика «Корзина» +Верстка: кодом +Архитектура: MVVM + +Модуль 1: +1. Верстка экрана корзины, содержащей NFT (план 7 час, факт 7 час): + * создание TableView с ячейкой, содержащей изображение, название, рейтинг, цену, кнопку удаления; + * кнопка сортировки; + * StackView с кнопкой «К оплате» +2. Верстка экрана для состояния «Корзина пуста» (план 2 час, факт 1 час) +3. Загрузка данных из сети (план 6 час, факт 8 час) +Итого время (план): 15 час. +Итого время (факт): 16 час. + +Модуль 2: +1. Верстка экрана фильтров (план 2 час, факт 1 час) +2. Реализация логики фильтрации (план 3 час, факт 3 час) +3. Реализация экрана удаления NFT из корзины (картинка, вопрос, кнопки удаления и возврата) (план 3 час, факт 5 час) +4. Реализация сетевого запроса на удаление из корзины NFT (план 6 час, факт 4 час) +Итого время (план): 14 час. +Итого время (факт): 13 час. + +Модуль 3: +1. Верстка экрана выбора валюты (план 5 час, факт 5 час): + * создание CollectionView с ячейкой, содержащей изображение, полное и сокращенное имя; - кнопка оплаты; + * лэйбл о пользовательском соглашении; + * кнопка возврата на предыдущий экран; +2. Загрузка данных из сети (план 2 час, факт 2 час) +3. Реализация перехода на WebView для показа пользовательского соглашения (план 2 час, факт 2 час) +4. Реализация запроса на оплату (план 7 час, факт 6): + * оплата прошла успешно; + * оплата не прошла. +Итого время (план): 16 час. +Итого время (факт): 15 час. diff --git a/FakeNFT/Docs/CatalogDecomposition.md b/FakeNFT/Docs/CatalogDecomposition.md new file mode 100755 index 0000000000..52c73eecd6 --- /dev/null +++ b/FakeNFT/Docs/CatalogDecomposition.md @@ -0,0 +1,45 @@ +Колесников Евгений +
Архитектура проекта: MVVM +
Верстка UI: в коде +
Task-tracker: https://github.com/users/Kolesnikov-Eugene/projects/2/views/1?layout=board +
Эпик: Каталог + +# Decomposition (Catalog) + +## Экран Catalogue + +### 1 часть эпика (расчет: 10 часов, факт: 9 часов) +### Экран каталога +- Верстка UI (расчет: 4 часа, факт: 2 часа); +- Получение и обработка данных с сервера (расчет: 3 часа, факт: 5 часов); +- Обработка взаимодействий пользователя с экраном: + - сортировка (расчет: 2 часа, факт: 1 час); + - тап по ячейке с коллекцией (переход на экран выбранной коллекции NFT) (расчет: 1 час, факт: 1 час); + +#### Issues: +- [CatalogUI](https://github.com/Kolesnikov-Eugene/iOS-FakeNFT-StarterProject-Public/issues/1) +- [Catalog data processing](https://github.com/Kolesnikov-Eugene/iOS-FakeNFT-StarterProject-Public/issues/2) +- [Catalog user interaction](https://github.com/Kolesnikov-Eugene/iOS-FakeNFT-StarterProject-Public/issues/4) + +### 2 часть эпика (расчет: 14 часов, факт: 13 часов) +### Экран коллекции NFT (UI и получение данных с сервера) +- Верстка UI (4 часа, факт: 5 часов); +- Получение и обработка данных с сервера (3 часа, факт: 8 часов); + +#### Issues: +- [CatalogCollection UI](https://github.com/Kolesnikov-Eugene/iOS-FakeNFT-StarterProject-Public/issues/6) +- [CatalogCollection data processing](https://github.com/Kolesnikov-Eugene/iOS-FakeNFT-StarterProject-Public/issues/7) + +### 3 часть эпика (заключительная) (расчет: 11 часов, факт: 12 часов) +- Взаимодействие пользователя с экраном коллекции NFT: + - Переход на страницу профиля владельца коллекции (расчет: 2 часа, факт: 3 часа); + - Обработка лайка (расчет: 2 часа, факт: 2 час); + - Обработка добавления NFT в корзину (расчет: 2 часа, факт: 1 час); + - Финальный рефакторинг (расчет: 5 часов, факт: 6 часов); + +#### Issues: +- [CatalogCollection user interaction](https://github.com/Kolesnikov-Eugene/iOS-FakeNFT-StarterProject-Public/issues/8) +- [Author page](https://github.com/Kolesnikov-Eugene/iOS-FakeNFT-StarterProject-Public/issues/9) + +### Итого: расчет: 35 часов, факт: 34 часа + diff --git a/FakeNFT/Docs/Profile Vadim.md b/FakeNFT/Docs/Profile Vadim.md new file mode 100755 index 0000000000..c8e5c909d7 --- /dev/null +++ b/FakeNFT/Docs/Profile Vadim.md @@ -0,0 +1,51 @@ +Нуретдинов Вадим +Архитектура MVVM +
Ссылка на доску:https://github.com/users/Kolesnikov-Eugene/projects/2/views/1 д +## Эпик 1/3 + +### Экран профиля +Экран показывает информацию о пользователе. + +##### UI - верстка: +- Кнопка редактирования профиля - **30 минут** +- Фото пользователя - **30 минут** +- Имя пользователя - **30 минут** +- Описание пользователя - **30 минут** +- Таблица (UITableView) с ячейками Мои NFT (ведет на экран NFT пользователя), Избранные NFT (ведет на экран с избранными NFT), о разработчике - ссылка на сайт пользователя (открывает в вебвью сайт пользователя) - **3 часа** +- Экран редактирования, всплывающий, на котором может отредактировать имя пользователя, описание, сайт и ссылку на изображение - **3 часа** + +Планируемое время на блок: **10 часов** + +##### Логика работы экрана: +- Нажав на кнопку редактирования, пользователь видит всплывающий экран, на котором может отредактировать имя пользователя, описание, сайт и ссылку на изображение. Загружать само изображение через приложение не нужно, обновляется только ссылка на изображение - **3 часа** +- Переход с таблицы на экран NFT пользователя (логика перехода, без UI конечной страницы) - **1 час** +- Переход с таблицы на экран с избранными NFT (логика перехода, без UI конечной страницы) - **1 час** +- Переход с таблицы на сайт (открывает в вебвью сайт пользователя) - **1 час** +- Логика перехода на экран редактирования профиля (логика перехода, без UI конечной страницы) - **30 минут** +- Логика заполнения и сохранения данных на экране редактирования. - **3 часа** + +Планируемое время на блок: **10,5 часов** +## Эпик 2/3 + +### Module 2: Экран мои NFT + +- Реализация NavigationBar (est: 30 min; fact: 30 min) +- Создание UITableView (est: 60 min; fact: 60 min) +- Верстка кастомной ячейки (est: 50 min; fact: 40 min) +- Реализация функции сортировки (est: 90 min; fact: 90 min) +- Создание модели (est: 20 min; fact: 20 min) +- Создание сетевого слоя (est: 60 min; fact: 70 min) + +`Total:` est: 310 min; fact: 310 min +## Эпик 3/3 + +### Module 3: Экран избранные NFT + +- Реализация NavigationBar (est: 30 min; fact: 30 min) +- Создание UICollectionView (est: 60 min; fact: 60 min) +- Верстка кастомной ячейки (est: 90 min; fact: 90 min) +- Создание модели (est: 40 min; fact: 40 min) +- Взаимодействие со страницей каталога (est: 30 min; fact: 60 min) +- Создание сетевого слоя (est: 60 min; fact: 60 min) + +`Total:` est: 310 min; fact: 340 min diff --git a/FakeNFT/Foundation/CellsReusingUtils.swift b/FakeNFT/Foundation/CellsReusingUtils.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Foundation/DateFormatters+Presets.swift b/FakeNFT/Foundation/DateFormatters+Presets.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Foundation/MemoryStorage/CartStorage.swift b/FakeNFT/Foundation/MemoryStorage/CartStorage.swift new file mode 100755 index 0000000000..dc773ba9a0 --- /dev/null +++ b/FakeNFT/Foundation/MemoryStorage/CartStorage.swift @@ -0,0 +1,24 @@ +import Foundation + +protocol CartStorage: AnyObject { + func saveCart(_ cart: CartModel) + func getCart(with id: String) -> CartModel? +} + +final class CartStorageImpl: CartStorage { + private var storage: [String: CartModel] = [:] + + private let syncQueue = DispatchQueue(label: "sync-cart-queue") + + func saveCart(_ cart: CartModel) { + syncQueue.async { [weak self] in + self?.storage[cart.id] = cart + } + } + + func getCart(with id: String) -> CartModel? { + syncQueue.sync { + storage[id] + } + } +} diff --git a/FakeNFT/Foundation/MemoryStorage/CurrencyStorage.swift b/FakeNFT/Foundation/MemoryStorage/CurrencyStorage.swift new file mode 100755 index 0000000000..87a257a7f5 --- /dev/null +++ b/FakeNFT/Foundation/MemoryStorage/CurrencyStorage.swift @@ -0,0 +1,24 @@ +import Foundation + +protocol CurrencyStorage: AnyObject { + func saveCurrency(_ currency: CurrencyModel) + func getCurrency(with id: String) -> CurrencyModel? +} + +final class CurrencyStorageImpl: CurrencyStorage { + private var storage: [String: CurrencyModel] = [:] + + private let syncQueue = DispatchQueue(label: "sync-cart-queue") + + func saveCurrency(_ currency: CurrencyModel) { + syncQueue.async { [weak self] in + self?.storage[currency.id] = currency + } + } + + func getCurrency(with id: String) -> CurrencyModel? { + syncQueue.sync { + storage[id] + } + } +} diff --git a/FakeNFT/Foundation/MemoryStorage/LikesStorage.swift b/FakeNFT/Foundation/MemoryStorage/LikesStorage.swift new file mode 100755 index 0000000000..f203d4feb2 --- /dev/null +++ b/FakeNFT/Foundation/MemoryStorage/LikesStorage.swift @@ -0,0 +1,15 @@ +// +// LikesStorage.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 20.11.2023. +// + +import Foundation + +final class LikesStorage { + static let shared = LikesStorage() + var likes: [String] = [] + + private init() {} +} diff --git a/FakeNFT/Foundation/MemoryStorage/NftStorage.swift b/FakeNFT/Foundation/MemoryStorage/NftStorage.swift old mode 100644 new mode 100755 index 0873a1122d..edc7877bad --- a/FakeNFT/Foundation/MemoryStorage/NftStorage.swift +++ b/FakeNFT/Foundation/MemoryStorage/NftStorage.swift @@ -11,6 +11,10 @@ final class NftStorageImpl: NftStorage { private let syncQueue = DispatchQueue(label: "sync-nft-queue") + static let shared = NftStorageImpl() + + init() {} + func saveNft(_ nft: Nft) { syncQueue.async { [weak self] in self?.storage[nft.id] = nft @@ -19,7 +23,7 @@ final class NftStorageImpl: NftStorage { func getNft(with id: String) -> Nft? { syncQueue.sync { - storage[id] + return storage[id] } } } diff --git a/FakeNFT/Foundation/MemoryStorage/PurchaseCartStorage.swift b/FakeNFT/Foundation/MemoryStorage/PurchaseCartStorage.swift new file mode 100755 index 0000000000..1e60abe6e5 --- /dev/null +++ b/FakeNFT/Foundation/MemoryStorage/PurchaseCartStorage.swift @@ -0,0 +1,15 @@ +// +// PurchaseCartStorage.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 21.11.2023. +// + +import Foundation + +final class PurchaseCartStorage { + static let shared = PurchaseCartStorage() + var nfts: [String] = [] + + private init() {} +} diff --git a/FakeNFT/Foundation/NetworkClient/NetworkClient.swift b/FakeNFT/Foundation/NetworkClient/NetworkClient.swift old mode 100644 new mode 100755 index 940c442f12..64286e955e --- a/FakeNFT/Foundation/NetworkClient/NetworkClient.swift +++ b/FakeNFT/Foundation/NetworkClient/NetworkClient.swift @@ -5,37 +5,21 @@ enum NetworkClientError: Error { case urlRequestError(Error) case urlSessionError case parsingError + case taskCreationFailed + } protocol NetworkClient { @discardableResult func send(request: NetworkRequest, - completionQueue: DispatchQueue, onResponse: @escaping (Result) -> Void) -> NetworkTask? @discardableResult func send(request: NetworkRequest, type: T.Type, - completionQueue: DispatchQueue, onResponse: @escaping (Result) -> Void) -> NetworkTask? } -extension NetworkClient { - - @discardableResult - func send(request: NetworkRequest, - onResponse: @escaping (Result) -> Void) -> NetworkTask? { - send(request: request, completionQueue: .main, onResponse: onResponse) - } - - @discardableResult - func send(request: NetworkRequest, - type: T.Type, - onResponse: @escaping (Result) -> Void) -> NetworkTask? { - send(request: request, type: type, completionQueue: .main, onResponse: onResponse) - } -} - struct DefaultNetworkClient: NetworkClient { private let session: URLSession private let decoder: JSONDecoder @@ -50,16 +34,7 @@ struct DefaultNetworkClient: NetworkClient { } @discardableResult - func send( - request: NetworkRequest, - completionQueue: DispatchQueue, - onResponse: @escaping (Result) -> Void - ) -> NetworkTask? { - let onResponse: (Result) -> Void = { result in - completionQueue.async { - onResponse(result) - } - } + func send(request: NetworkRequest, onResponse: @escaping (Result) -> Void) -> NetworkTask? { guard let urlRequest = create(request: request) else { return nil } let task = session.dataTask(with: urlRequest) { data, response, error in @@ -91,13 +66,8 @@ struct DefaultNetworkClient: NetworkClient { } @discardableResult - func send( - request: NetworkRequest, - type: T.Type, - completionQueue: DispatchQueue, - onResponse: @escaping (Result) -> Void - ) -> NetworkTask? { - return send(request: request, completionQueue: completionQueue) { result in + func send(request: NetworkRequest, type: T.Type, onResponse: @escaping (Result) -> Void) -> NetworkTask? { + return send(request: request) { result in switch result { case let .success(data): self.parse(data: data, type: type, onResponse: onResponse) diff --git a/FakeNFT/Foundation/NetworkClient/NetworkRequest.swift b/FakeNFT/Foundation/NetworkClient/NetworkRequest.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Foundation/NetworkClient/NetworkTask.swift b/FakeNFT/Foundation/NetworkClient/NetworkTask.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Foundation/NetworkClient/Request.swift b/FakeNFT/Foundation/NetworkClient/Request.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Foundation/NumberFormatter+Presets.swift b/FakeNFT/Foundation/NumberFormatter+Presets.swift new file mode 100755 index 0000000000..78c95929dc --- /dev/null +++ b/FakeNFT/Foundation/NumberFormatter+Presets.swift @@ -0,0 +1,11 @@ +import Foundation + +extension NumberFormatter { + static let defaultPriceFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 2 + formatter.decimalSeparator = "," + return formatter + }() +} diff --git a/FakeNFT/Foundation/NumberFormatter.swift b/FakeNFT/Foundation/NumberFormatter.swift new file mode 100755 index 0000000000..be14e22ee3 --- /dev/null +++ b/FakeNFT/Foundation/NumberFormatter.swift @@ -0,0 +1,11 @@ +import Foundation + +extension NumberFormatter { + static var priceFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.locale = .current + formatter.numberStyle = .decimal + formatter.currencyDecimalSeparator = "," + return formatter + }() +} diff --git a/FakeNFT/Foundation/Observ.swift b/FakeNFT/Foundation/Observ.swift new file mode 100755 index 0000000000..d7f1279d6c --- /dev/null +++ b/FakeNFT/Foundation/Observ.swift @@ -0,0 +1,27 @@ +import Foundation + +@propertyWrapper +final class Observ { + private var observers: [(Value) -> Void] = [] + + var wrappedValue: Value { + didSet { + for observer in observers { + observer(wrappedValue) + } + } + } + + var projectedValue: Observ { + return self + } + + init(wrappedValue: Value) { + self.wrappedValue = wrappedValue + } + + func observe(_ observer: @escaping (Value) -> Void) { + observers.append(observer) + observer(wrappedValue) + } +} diff --git a/FakeNFT/Foundation/Observable.swift b/FakeNFT/Foundation/Observable.swift new file mode 100755 index 0000000000..a04434e989 --- /dev/null +++ b/FakeNFT/Foundation/Observable.swift @@ -0,0 +1,24 @@ +import Foundation + +@propertyWrapper +final class Observable { + private var onChange: ((Value) -> Void)? + + var wrappedValue: Value { + didSet { + onChange?(wrappedValue) + } + } + + var projectedValue: Observable { + return self + } + + init(wrappedValue: Value) { + self.wrappedValue = wrappedValue + } + + func bind(action: @escaping (Value) -> Void) { + self.onChange = action + } +} diff --git a/FakeNFT/Foundation/UIView+Constraints.swift b/FakeNFT/Foundation/UIView+Constraints.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Generated/Strings.swift b/FakeNFT/Generated/Strings.swift new file mode 100755 index 0000000000..74c610d614 --- /dev/null +++ b/FakeNFT/Generated/Strings.swift @@ -0,0 +1,162 @@ +// swiftlint:disable all +// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen + +import Foundation + +// swiftlint:disable superfluous_disable_command file_length implicit_return prefer_self_in_static_references + +// MARK: - Strings + +// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces +internal enum L10n { + internal enum Catalog { + /// Открыть Nft + internal static let openNft = L10n.tr("Localizable", "Catalog.openNft", fallback: "Открыть Nft") + } + internal enum CatalogCollection { + /// Автор коллекции: + internal static let authorLabel = L10n.tr("Localizable", "CatalogCollection.authorLabel", fallback: "Автор коллекции:") + } + internal enum CatalogFilterStorage { + /// catalogFilter + internal static let key = L10n.tr("Localizable", "CatalogFilterStorage.key", fallback: "catalogFilter") + } + internal enum Error { + /// Произошла ошибка сети + internal static let network = L10n.tr("Localizable", "Error.network", fallback: "Произошла ошибка сети") + /// Повторить + internal static let `repeat` = L10n.tr("Localizable", "Error.repeat", fallback: "Повторить") + /// Ошибка + internal static let title = L10n.tr("Localizable", "Error.title", fallback: "Ошибка") + /// Произошла неизвестная ошибка + internal static let unknown = L10n.tr("Localizable", "Error.unknown", fallback: "Произошла неизвестная ошибка") + } + internal enum FilterAlert { + /// Отменить + internal static let cancelButtonTitle = L10n.tr("Localizable", "FilterAlert.cancelButtonTitle", fallback: "Отменить") + /// По названию + internal static let nameSortTitle = L10n.tr("Localizable", "FilterAlert.nameSortTitle", fallback: "По названию") + /// По количеству NFT + internal static let quantitySortTitle = L10n.tr("Localizable", "FilterAlert.quantitySortTitle", fallback: "По количеству NFT") + /// Сортировка + internal static let title = L10n.tr("Localizable", "FilterAlert.title", fallback: "Сортировка") + } + internal enum NetworkErrorAlert { + /// Попробуйте еще раз + internal static let message = L10n.tr("Localizable", "NetworkErrorAlert.message", fallback: "Попробуйте еще раз") + /// OK + internal static let okButton = L10n.tr("Localizable", "NetworkErrorAlert.okButton", fallback: "OK") + /// Упс( + internal static let title = L10n.tr("Localizable", "NetworkErrorAlert.title", fallback: "Упс(") + } + internal enum NftErrorAlert { + /// Попробуйте еще раз + internal static let message = L10n.tr("Localizable", "NftErrorAlert.message", fallback: "Попробуйте еще раз") + /// OK + internal static let okButton = L10n.tr("Localizable", "NftErrorAlert.okButton", fallback: "OK") + /// Что-то пошло не так( + internal static let title = L10n.tr("Localizable", "NftErrorAlert.title", fallback: "Что-то пошло не так(") + } + internal enum Onboarding { + /// Что внутри? + internal static let buttonText = L10n.tr("Localizable", "Onboarding.buttonText", fallback: "Что внутри?") + /// Присоединяйтесь и откройте новый мир уникальных NFT для коллекционеров + internal static let infoFirst = L10n.tr("Localizable", "Onboarding.infoFirst", fallback: "Присоединяйтесь и откройте новый мир уникальных NFT для коллекционеров") + /// Пополняйте свою коллекцию эксклюзивными картинками, созданными нейросетью! + internal static let infoSecond = L10n.tr("Localizable", "Onboarding.infoSecond", fallback: "Пополняйте свою коллекцию эксклюзивными картинками, созданными нейросетью!") + /// Смотрите статистику других и покажите всем, что у вас самая ценная коллекция + internal static let infoThird = L10n.tr("Localizable", "Onboarding.infoThird", fallback: "Смотрите статистику других и покажите всем, что у вас самая ценная коллекция") + /// Исследуйте + internal static let titleFirst = L10n.tr("Localizable", "Onboarding.titleFirst", fallback: "Исследуйте") + /// Коллекционируйте + internal static let titleSecond = L10n.tr("Localizable", "Onboarding.titleSecond", fallback: "Коллекционируйте") + /// Состязайтесь + internal static let titleThird = L10n.tr("Localizable", "Onboarding.titleThird", fallback: "Состязайтесь") + } + internal enum Payment { + /// Вернуться в каталог + internal static let backToCatalogueText = L10n.tr("Localizable", "Payment.backToCatalogueText", fallback: "Вернуться в каталог") + /// Отмена + internal static let cancelText = L10n.tr("Localizable", "Payment.cancelText", fallback: "Отмена") + /// Не удалось произвести оплату + internal static let errorText = L10n.tr("Localizable", "Payment.errorText", fallback: "Не удалось произвести оплату") + /// ОК + internal static let okText = L10n.tr("Localizable", "Payment.okText", fallback: "ОК") + /// Повторить + internal static let retryText = L10n.tr("Localizable", "Payment.retryText", fallback: "Повторить") + /// Успех! Оплата прошла, поздравляем с покупкой! + internal static let successText = L10n.tr("Localizable", "Payment.successText", fallback: "Успех! Оплата прошла, поздравляем с покупкой!") + } + internal enum Tab { + /// Каталог + internal static let catalog = L10n.tr("Localizable", "Tab.catalog", fallback: "Каталог") + } + internal enum Tabbar { + /// Корзина + internal static let cartTitle = L10n.tr("Localizable", "Tabbar.cartTitle", fallback: "Корзина") + /// Каталог + internal static let catalogTitle = L10n.tr("Localizable", "Tabbar.catalogTitle", fallback: "Каталог") + } + internal enum Cart { + /// Вернуться + internal static let backButtonText = L10n.tr("Localizable", "cart.backButtonText", fallback: "Вернуться") + /// К оплате + internal static let buttonText = L10n.tr("Localizable", "cart.ButtonText", fallback: "К оплате") + /// Закрыть + internal static let closeButtonText = L10n.tr("Localizable", "cart.closeButtonText", fallback: "Закрыть") + /// Удалить + internal static let deleteButtonText = L10n.tr("Localizable", "cart.deleteButtonText", fallback: "Удалить") + /// Вы уверены, что хотите удалить объект из корзины? + internal static let deleteConfirmText = L10n.tr("Localizable", "cart.deleteConfirmText", fallback: "Вы уверены, что хотите удалить объект из корзины?") + /// Корзина пуста + internal static let emptyCart = L10n.tr("Localizable", "cart.emptyCart", fallback: "Корзина пуста") + /// Ошибка загрузки, попробуйте еще раз + internal static let loadDataErrorText = L10n.tr("Localizable", "cart.loadDataErrorText", fallback: "Ошибка загрузки, попробуйте еще раз") + /// Цена + internal static let priceLabel = L10n.tr("Localizable", "cart.priceLabel", fallback: "Цена") + } + internal enum CartFilterAlert { + /// По названию + internal static let name = L10n.tr("Localizable", "cartFilterAlert.name", fallback: "По названию") + /// По цене + internal static let price = L10n.tr("Localizable", "cartFilterAlert.price", fallback: "По цене") + /// По рейтингу + internal static let rating = L10n.tr("Localizable", "cartFilterAlert.rating", fallback: "По рейтингу") + /// Сортировка + internal static let title = L10n.tr("Localizable", "cartFilterAlert.title", fallback: "Сортировка") + } + internal enum Currency { + /// Пользовательского соглашения + internal static let cartUserAgreementLinkText = L10n.tr("Localizable", "currency.cartUserAgreementLinkText", fallback: "Пользовательского соглашения") + /// Совершая покупку, вы соглашаетесь с условиями + internal static let cartUserAgreementText = L10n.tr("Localizable", "currency.cartUserAgreementText", fallback: "Совершая покупку, вы соглашаетесь с условиями") + /// Оплатить + internal static let paymentConfirmButtonText = L10n.tr("Localizable", "currency.paymentConfirmButtonText", fallback: "Оплатить") + /// Выберите способ оплаты + internal static let paymentTypeText = L10n.tr("Localizable", "currency.paymentTypeText", fallback: "Выберите способ оплаты") + } +} +// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces + +// MARK: - Implementation Details + +extension L10n { + private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String { + let format = BundleToken.bundle.localizedString(forKey: key, value: value, table: table) + return String(format: format, locale: Locale.current, arguments: args) + } +} + +// swiftlint:disable convenience_type +private final class BundleToken { + static let bundle: Bundle = { + #if SWIFT_PACKAGE + return Bundle.module + #else + return Bundle(for: BundleToken.self) + #endif + }() +} +// swiftlint:enable convenience_type diff --git a/FakeNFT/Helpers/AlertService.swift b/FakeNFT/Helpers/AlertService.swift new file mode 100755 index 0000000000..923bb4427b --- /dev/null +++ b/FakeNFT/Helpers/AlertService.swift @@ -0,0 +1,54 @@ +import UIKit + +struct AlertActionModel { + let title: String + let style: UIAlertAction.Style + let handler: ((String?) -> Void)? +} + +struct AlertModel { + let title: String? + let message: String? + let style: UIAlertController.Style + let actions: [AlertActionModel] + let textFieldPlaceholder: String? +} + +protocol AlertServiceProtocol { + func showAlert(model: AlertModel) +} + +class AlertService: AlertServiceProtocol { + private weak var viewController: UIViewController? + + init(viewController: UIViewController) { + self.viewController = viewController + } + + func showAlert(model: AlertModel) { + let alertController = UIAlertController( + title: model.title, + message: model.message, + preferredStyle: model.style + ) + + if let placeholder = model.textFieldPlaceholder { + alertController.addTextField { textField in + textField.placeholder = placeholder + } + } + + model.actions.forEach { actionModel in + let action = UIAlertAction(title: actionModel.title, style: actionModel.style) { _ in + if let textField = alertController.textFields?.first { + actionModel.handler?(textField.text) + } else { + actionModel.handler?(nil) + } + } + alertController.addAction(action) + } + + viewController?.present(alertController, animated: true, completion: nil) + } +} diff --git a/FakeNFT/Helpers/GeometricParams.swift b/FakeNFT/Helpers/GeometricParams.swift new file mode 100755 index 0000000000..1e610093b9 --- /dev/null +++ b/FakeNFT/Helpers/GeometricParams.swift @@ -0,0 +1,25 @@ +import Foundation + +struct GeometricParams { + let cellPerRowCount: CGFloat + let cellSpacing: CGFloat + let cellLeftInset: CGFloat + let cellRightInset: CGFloat + let cellHeight: CGFloat + let paddingWight: CGFloat + + init( + cellPerRowCount: CGFloat, + cellSpacing: CGFloat, + cellLeftInset: CGFloat, + cellRightInset: CGFloat, + cellHeight: CGFloat + ) { + self.cellPerRowCount = cellPerRowCount + self.cellSpacing = cellSpacing + self.cellLeftInset = cellLeftInset + self.cellRightInset = cellRightInset + self.cellHeight = cellHeight + self.paddingWight = (cellPerRowCount - 1) * cellSpacing + cellLeftInset + cellRightInset + } +} diff --git a/FakeNFT/Helpers/ImageValidator.swift b/FakeNFT/Helpers/ImageValidator.swift new file mode 100755 index 0000000000..d28e9019bf --- /dev/null +++ b/FakeNFT/Helpers/ImageValidator.swift @@ -0,0 +1,19 @@ +import Foundation +import Kingfisher + +protocol ImageValidatorProtocol { + func isValidImageURL(_ url: URL, completion: @escaping (Bool) -> Void) +} + +final class ImageValidator: ImageValidatorProtocol { + func isValidImageURL(_ url: URL, completion: @escaping (Bool) -> Void) { + KingfisherManager.shared.retrieveImage(with: url) { result in + switch result { + case .success: + completion(true) + case .failure: + completion(false) + } + } + } +} diff --git a/FakeNFT/Helpers/LoadingState.swift b/FakeNFT/Helpers/LoadingState.swift new file mode 100755 index 0000000000..c6a569396e --- /dev/null +++ b/FakeNFT/Helpers/LoadingState.swift @@ -0,0 +1,8 @@ +import Foundation + +enum LoadingState { + case idle + case loading + case loaded(hasData: Bool) + case error(Error) +} diff --git a/FakeNFT/Helpers/NetworkServiceHelper.swift b/FakeNFT/Helpers/NetworkServiceHelper.swift new file mode 100755 index 0000000000..9510ea827c --- /dev/null +++ b/FakeNFT/Helpers/NetworkServiceHelper.swift @@ -0,0 +1,57 @@ +import Foundation + +final class NetworkServiceHelper { + private let networkClient: NetworkClient + private var currentTasks: [NetworkTask] = [] + + init(networkClient: NetworkClient) { + self.networkClient = networkClient + } + + func fetchData(request: NetworkRequest, type: T.Type, completion: @escaping (Result) -> Void) { + fetchData(request: request, type: T.self, retryCount: 0, delayInterval: 3.0, completion: completion) + } + + func stopAllTasks() { + currentTasks.forEach { $0.cancel() } + currentTasks.removeAll() + } + + private func fetchData( + request: NetworkRequest, + type: T.Type, + retryCount: Int, + delayInterval: Double, + completion: @escaping (Result) -> Void + ) { + let task = networkClient.send(request: request, type: T.self) { [weak self] result in + guard let self = self else { return } + + switch result { + case .success(let data): + completion(.success(data)) + case .failure(let error): + if retryCount < 3 { + if case NetworkClientError.httpStatusCode(429) = error { + let newRetryCount = retryCount + 1 + let newDelayInterval = delayInterval * 2 + DispatchQueue.global().asyncAfter(deadline: .now() + delayInterval) { + self.fetchData(request: request, type: T.self, retryCount: newRetryCount, delayInterval: newDelayInterval, completion: completion) + } + } else { + completion(.failure(error)) + } + } else { + completion(.failure(error)) + } + } + } + + guard let unwrappedTask = task else { + completion(.failure(NetworkClientError.taskCreationFailed)) + return + } + + self.currentTasks.append(unwrappedTask) + } +} diff --git a/FakeNFT/Helpers/SortOptions.swift b/FakeNFT/Helpers/SortOptions.swift new file mode 100755 index 0000000000..6a6a4efdb6 --- /dev/null +++ b/FakeNFT/Helpers/SortOptions.swift @@ -0,0 +1,18 @@ +import Foundation + +enum SortOption { + case price + case rating + case title + + var description: String { + switch self { + case .price: + return NSLocalizedString("SortOptions.price", comment: "") + case .rating: + return NSLocalizedString("SortOptions.rate", comment: "") + case .title: + return NSLocalizedString("SortOptions.name", comment: "") + } + } +} diff --git a/FakeNFT/Helpers/ViewControllerFactory.swift b/FakeNFT/Helpers/ViewControllerFactory.swift new file mode 100755 index 0000000000..0197f6a21e --- /dev/null +++ b/FakeNFT/Helpers/ViewControllerFactory.swift @@ -0,0 +1,23 @@ +import UIKit + +final class ViewControllerFactory { + func makeWebView(url: URL) -> WebViewViewController { + let controller = WebViewViewController(url: url) + return controller + } + + func makeUserNFTViewController(nftList: [String]) -> UserNFTViewController { + return UserNFTViewController(nftList: nftList, + viewModel: UserNFTViewModel(nftService: NFTService.shared)) + } + + func makeFavoritesNFTViewController(nftList: [String]) -> FavoritesNFTViewController { + return FavoritesNFTViewController(nftList: nftList, + viewModel: FavoritesNFTViewModel(nftService: NFTService.shared, + profileService: ProfileService.shared)) + } + + func makeEditingViewController() -> EditingViewController { + return EditingViewController(viewModel: EditingViewModel(profileService: ProfileService.shared)) + } +} diff --git a/FakeNFT/Info.plist b/FakeNFT/Info.plist old mode 100644 new mode 100755 index dd3c9afdae..8850447b79 --- a/FakeNFT/Info.plist +++ b/FakeNFT/Info.plist @@ -2,24 +2,16 @@ + UIAppFonts + + SF-Pro-Text-Medium.otf + SF-Pro-Text-Regular.otf + SF-Pro-Text-Bold.otf + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - diff --git a/FakeNFT/Models/CatalogModels/AlertModel.swift b/FakeNFT/Models/CatalogModels/AlertModel.swift new file mode 100755 index 0000000000..b1e5a644f4 --- /dev/null +++ b/FakeNFT/Models/CatalogModels/AlertModel.swift @@ -0,0 +1,17 @@ +// +// AlertModel.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 04.11.2023. +// + +import Foundation + +struct AlertModel { + let message: String + let nameSortText: String + let quantitySortText: String + let cancelButtonText: String + let sortNameCompletion: () -> Void + let sortQuantityCompletion: () -> Void +} diff --git a/FakeNFT/Models/CatalogModels/Author.swift b/FakeNFT/Models/CatalogModels/Author.swift new file mode 100755 index 0000000000..671d03d2fd --- /dev/null +++ b/FakeNFT/Models/CatalogModels/Author.swift @@ -0,0 +1,13 @@ +// +// Author.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 15.11.2023. +// + +import Foundation + +struct Author: Decodable { + let name: String + let website: URL +} diff --git a/FakeNFT/Models/CatalogModels/Catalog.swift b/FakeNFT/Models/CatalogModels/Catalog.swift new file mode 100755 index 0000000000..8ad03dddd1 --- /dev/null +++ b/FakeNFT/Models/CatalogModels/Catalog.swift @@ -0,0 +1,17 @@ +// +// Catalog.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 07.11.2023. +// + +import Foundation + +struct Catalog { + let name: String + let coverURL: URL? + let nfts: [String] + let desription: String + let authorID: String + let id: String +} diff --git a/FakeNFT/Models/CatalogModels/CatalogResult.swift b/FakeNFT/Models/CatalogModels/CatalogResult.swift new file mode 100755 index 0000000000..ff62dc3a3d --- /dev/null +++ b/FakeNFT/Models/CatalogModels/CatalogResult.swift @@ -0,0 +1,18 @@ +// +// CatalogResult.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 07.11.2023. +// + +import Foundation + +struct CatalogResult: Codable { + let createdAt: String + let name: String + let cover: String + let nfts: [String] + let description: String + let author: String + let id: String +} diff --git a/FakeNFT/Models/CatalogModels/GeometricParams.swift b/FakeNFT/Models/CatalogModels/GeometricParams.swift new file mode 100755 index 0000000000..e80931e22c --- /dev/null +++ b/FakeNFT/Models/CatalogModels/GeometricParams.swift @@ -0,0 +1,24 @@ +// +// GeometricParams.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 17.11.2023. +// + +import Foundation + +struct GeometricParams { + let cellCount: Int + let leftInset: CGFloat + let rightInset: CGFloat + let cellSpacing: CGFloat + let paddingWidth: CGFloat + + init(cellCount: Int, leftInset: CGFloat, rightInset: CGFloat, cellSpacing: CGFloat) { + self.cellCount = cellCount + self.leftInset = leftInset + self.rightInset = rightInset + self.cellSpacing = cellSpacing + self.paddingWidth = leftInset + rightInset + CGFloat(cellCount - 1) * cellSpacing + } +} diff --git a/FakeNFT/Models/CatalogModels/NftModel.swift b/FakeNFT/Models/CatalogModels/NftModel.swift new file mode 100755 index 0000000000..26665c734d --- /dev/null +++ b/FakeNFT/Models/CatalogModels/NftModel.swift @@ -0,0 +1,16 @@ +// +// NftModel.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 14.11.2023. +// + +import Foundation + +struct NftModel { + let id: String + let images: [URL] + let rating: Int + let name: String + let price: Float +} diff --git a/FakeNFT/Models/CatalogModels/NftResult.swift b/FakeNFT/Models/CatalogModels/NftResult.swift new file mode 100755 index 0000000000..38a42affa4 --- /dev/null +++ b/FakeNFT/Models/CatalogModels/NftResult.swift @@ -0,0 +1,16 @@ +// +// NftModel.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 14.11.2023. +// + +import Foundation + +struct NftResult: Decodable { + let id: String + let images: [URL] + let rating: Int + let name: String + let price: Float +} diff --git a/FakeNFT/Models/CatalogModels/Profile.swift b/FakeNFT/Models/CatalogModels/Profile.swift new file mode 100755 index 0000000000..379db19e18 --- /dev/null +++ b/FakeNFT/Models/CatalogModels/Profile.swift @@ -0,0 +1,14 @@ +// +// Profile.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 17.11.2023. +// + +import Foundation + +struct Profile: Decodable { + let nfts: [String] + let likes: [String] + let id: String +} diff --git a/FakeNFT/Models/CatalogModels/ProfileLike.swift b/FakeNFT/Models/CatalogModels/ProfileLike.swift new file mode 100755 index 0000000000..2246832034 --- /dev/null +++ b/FakeNFT/Models/CatalogModels/ProfileLike.swift @@ -0,0 +1,12 @@ +// +// ProfileLike.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 20.11.2023. +// + +import Foundation + +struct ProfileLike: Codable { + let likes: [String] +} diff --git a/FakeNFT/Models/CatalogModels/PurchaseCart.swift b/FakeNFT/Models/CatalogModels/PurchaseCart.swift new file mode 100755 index 0000000000..62a13a962b --- /dev/null +++ b/FakeNFT/Models/CatalogModels/PurchaseCart.swift @@ -0,0 +1,12 @@ +// +// PurchaseCart.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 21.11.2023. +// + +import Foundation + +struct PurchaseCart: Codable { + let nfts: [String] +} diff --git a/FakeNFT/Models/Network/Author.swift b/FakeNFT/Models/Network/Author.swift new file mode 100755 index 0000000000..0cea05070f --- /dev/null +++ b/FakeNFT/Models/Network/Author.swift @@ -0,0 +1,11 @@ +import Foundation + +struct Author: Codable { + let name: String + let avatar: String + let description: String + let website: URL + let nfts: [String] + let rating: String + let id: String +} diff --git a/FakeNFT/Models/Network/Cart.swift b/FakeNFT/Models/Network/Cart.swift new file mode 100755 index 0000000000..500638be89 --- /dev/null +++ b/FakeNFT/Models/Network/Cart.swift @@ -0,0 +1,6 @@ +import Foundation + +struct CartModel: Codable { + let id: String + let nfts: [String] +} diff --git a/FakeNFT/Models/Network/Currency.swift b/FakeNFT/Models/Network/Currency.swift new file mode 100755 index 0000000000..4de21f3100 --- /dev/null +++ b/FakeNFT/Models/Network/Currency.swift @@ -0,0 +1,8 @@ +import Foundation + +struct CurrencyModel: Codable { + let id: String + let title: String + let name: String + let image: String +} diff --git a/FakeNFT/Models/Network/ExampleNetworkModel.swift b/FakeNFT/Models/Network/ExampleNetworkModel.swift new file mode 100755 index 0000000000..84149019db --- /dev/null +++ b/FakeNFT/Models/Network/ExampleNetworkModel.swift @@ -0,0 +1,3 @@ +import Foundation + +struct ExampleNetworkModel: Codable {} diff --git a/FakeNFT/Models/Network/NFTNetworkRequests.swift b/FakeNFT/Models/Network/NFTNetworkRequests.swift new file mode 100755 index 0000000000..65513a56a3 --- /dev/null +++ b/FakeNFT/Models/Network/NFTNetworkRequests.swift @@ -0,0 +1,29 @@ +import Foundation + +struct FetchNFTNetworkRequest: NetworkRequest { + let nftID: String + + var endpoint: URL? { + URL(string: "https://651ff0d9906e276284c3c20a.mockapi.io/api/v1/nft/\(nftID)") + } + + var httpMethod: HttpMethod { + return .get + } + + init(nftID: String) { + self.nftID = nftID + } +} + +struct FetchAuthorNetworkRequest: NetworkRequest { + let authorID: String + + var endpoint: URL? { + URL(string: "https://651ff0d9906e276284c3c20a.mockapi.io/api/v1/users/\(authorID)") + } + + var httpMethod: HttpMethod { + return .get + } +} diff --git a/FakeNFT/Models/Network/NFTProfile.swift b/FakeNFT/Models/Network/NFTProfile.swift new file mode 100755 index 0000000000..280b57edd1 --- /dev/null +++ b/FakeNFT/Models/Network/NFTProfile.swift @@ -0,0 +1,12 @@ +import Foundation + +struct NFTProfile: Codable { + let createdAt: String + let name: String + let images: [String] + let rating: Int + let description: String + let price: Float + let author: String + let id: String +} diff --git a/FakeNFT/Models/Network/Nft.swift b/FakeNFT/Models/Network/Nft.swift old mode 100644 new mode 100755 index 59e761a821..ad62e68915 --- a/FakeNFT/Models/Network/Nft.swift +++ b/FakeNFT/Models/Network/Nft.swift @@ -1,6 +1,9 @@ import Foundation -struct Nft: Decodable { +struct Nft: Codable { let id: String let images: [URL] + let name: String + let rating: Int + let price: Float } diff --git a/FakeNFT/Models/Network/Payment.swift b/FakeNFT/Models/Network/Payment.swift new file mode 100755 index 0000000000..1638e92b87 --- /dev/null +++ b/FakeNFT/Models/Network/Payment.swift @@ -0,0 +1,7 @@ +import Foundation + +struct PaymentModel: Codable { + let success: Bool + let id: String + let orderId: String +} diff --git a/FakeNFT/Models/Network/ProfileNetworkRequest.swift b/FakeNFT/Models/Network/ProfileNetworkRequest.swift new file mode 100755 index 0000000000..5b9649b54e --- /dev/null +++ b/FakeNFT/Models/Network/ProfileNetworkRequest.swift @@ -0,0 +1,31 @@ +import Foundation + +struct FetchProfileNetworkRequest: NetworkRequest { + var endpoint: URL? { + URL(string: "https://651ff0d9906e276284c3c20a.mockapi.io/api/v1/profile/1") + } + + var httpMethod: HttpMethod { + return .get + } +} + +struct UpdateProfileNetworkRequest: NetworkRequest { + var endpoint: URL? { + URL(string: "https://651ff0d9906e276284c3c20a.mockapi.io/api/v1/profile/1") + } + + var httpMethod: HttpMethod { + return .put + } + + var dto: Encodable? { + return profileDTO + } + + let profileDTO: ProfileUpdateDTO + + init(userProfile: UserProfile) { + profileDTO = ProfileUpdateDTO(from: userProfile) + } +} diff --git a/FakeNFT/Models/Network/ProfileUpdateDTO.swift b/FakeNFT/Models/Network/ProfileUpdateDTO.swift new file mode 100755 index 0000000000..abf6d8fda9 --- /dev/null +++ b/FakeNFT/Models/Network/ProfileUpdateDTO.swift @@ -0,0 +1,15 @@ +import Foundation + +struct ProfileUpdateDTO: Encodable { + let name: String + let description: String + let website: String + let likes: [String] + + init(from userProfile: UserProfile) { + self.name = userProfile.name + self.description = userProfile.description + self.website = userProfile.website + self.likes = userProfile.likes + } +} diff --git a/FakeNFT/Models/Network/UserProfile.swift b/FakeNFT/Models/Network/UserProfile.swift new file mode 100755 index 0000000000..30e541e2ec --- /dev/null +++ b/FakeNFT/Models/Network/UserProfile.swift @@ -0,0 +1,11 @@ +import Foundation + +struct UserProfile: Codable { + let name: String + let avatar: String + let description: String + let website: String + let nfts: [String] + let likes: [String] + let id: String +} diff --git a/FakeNFT/Resources/Assets.xcassets/close.imageset/Dark-1.png b/FakeNFT/Resources/Assets.xcassets/close.imageset/Dark-1.png deleted file mode 100644 index cc7d831560..0000000000 Binary files a/FakeNFT/Resources/Assets.xcassets/close.imageset/Dark-1.png and /dev/null differ diff --git a/FakeNFT/Resources/Assets.xcassets/close.imageset/Dark.png b/FakeNFT/Resources/Assets.xcassets/close.imageset/Dark.png deleted file mode 100644 index c7d617f1c9..0000000000 Binary files a/FakeNFT/Resources/Assets.xcassets/close.imageset/Dark.png and /dev/null differ diff --git a/FakeNFT/Resources/Base.lproj/LaunchScreen.storyboard b/FakeNFT/Resources/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e9329f3..0000000000 --- a/FakeNFT/Resources/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/FakeNFT/Resources/Base.lproj/Main.storyboard b/FakeNFT/Resources/Base.lproj/Main.storyboard deleted file mode 100644 index 264407c712..0000000000 --- a/FakeNFT/Resources/Base.lproj/Main.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/FakeNFT/Resources/LaunchScreen.storyboard b/FakeNFT/Resources/LaunchScreen.storyboard new file mode 100755 index 0000000000..b56dc4a8f3 --- /dev/null +++ b/FakeNFT/Resources/LaunchScreen.storyboard @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FakeNFT/Resources/Localizable.strings b/FakeNFT/Resources/Localizable.strings deleted file mode 100644 index c498776710..0000000000 --- a/FakeNFT/Resources/Localizable.strings +++ /dev/null @@ -1,6 +0,0 @@ -"Tab.catalog" = "Каталог"; -"Catalog.openNft" = "Открыть Nft"; -"Error.network" = "Произошла ошибка сети"; -"Error.unknown" = "Произошла неизвестная ошибка"; -"Error.repeat" = "Повторить"; -"Error.title" = "Ошибка"; diff --git a/FakeNFT/Resources/en.lproj/Localizable.strings b/FakeNFT/Resources/en.lproj/Localizable.strings new file mode 100755 index 0000000000..6b37a1b537 --- /dev/null +++ b/FakeNFT/Resources/en.lproj/Localizable.strings @@ -0,0 +1,122 @@ +"Tab.catalog" = "Catalog"; +"Catalog.openNft" = "Open Nft"; +"Error.network" = "A network error has occurred"; +"Error.unknown" = "An unknown error has occurred"; +"Error.repeat" = "Repeat"; +"Error.title" = "Error"; + +//Tabbar titles +"Tabbar.catalogTitle" = "Catalog"; +"Tabbar.cartTitle" = "Cart"; + +//Catalog filter alert +"FilterAlert.title" = "Sorting"; +"FilterAlert.nameSortTitle" = "By name"; +"FilterAlert.quantitySortTitle" = "By number of NFTs"; +"FilterAlert.cancelButtonTitle" = "Cancel"; + +//CatalogCollection network error alert +"NetworkErrorAlert.title" = "Oops("; +"NetworkErrorAlert.message" = "Try again"; +"NetworkErrorAlert.okButton" = "OK"; + +//NftInteractionError alert +"NftErrorAlert.title" = "Something went wrong("; +"NftErrorAlert.message" = "Try again"; +"NftErrorAlert.okButton" = "OK"; + +//Catalog collection screen +"CatalogCollection.authorLabel" = "Author of the collection:"; + +//CatalogFilter storage +"CatalogFilterStorage.key" = "catalogFilter"; + +//Cart storage +"cart.ButtonText" = "To pay"; +"cart.emptyCart" = "Cart is empty"; +"cart.priceLabel" = "Price"; +"cart.closeButtonText" = "Close"; +"cart.deleteConfirmText" = "Are you sure you want to remove the item from the cart?"; +"cart.deleteButtonText" = "Remove"; +"cart.backButtonText" = "Return"; +"cart.loadDataErrorText" = "Loading error, try again"; + +//Cart filter alert +"cartFilterAlert.title" = "Sorting"; +"cartFilterAlert.price" = "By price"; +"cartFilterAlert.rating" = "By rating"; +"cartFilterAlert.name" = "By name"; + +//Currency storage +"currency.paymentTypeText" = "Choose a payment method"; +"currency.paymentConfirmButtonText" = "Pay"; +"currency.cartUserAgreementText" = "By making a purchase, you agree to the terms and conditions of the"; +"currency.cartUserAgreementLinkText" = "User Agreement"; + +//Payment storage +"Payment.successText" = "Success! Payment has been processed, congratulations on your purchase!"; +"Payment.backToCatalogueText" = "Back to catalog"; +"Payment.errorText" = "Failed to make payment"; +"Payment.cancelText" = "Cancel"; +"Payment.retryText" = "Repeat"; +"Payment.okText" = "OK"; + +//Onboarding storage +"Onboarding.titleFirst" = "Explore"; +"Onboarding.titleSecond" = "Collect"; +"Onboarding.titleThird" = "Compete"; +"Onboarding.infoFirst" = "Join and discover a new world of unique NFTs for collectors"; +"Onboarding.infoSecond" = "Enhance your collection with exclusive pictures created by neural network!"; +"Onboarding.infoThird" = "See others' stats and show everyone that you have the most valuable collection"; +"Onboarding.buttonText" = "What's inside?"; +"TabBarController.Profile" = "Profile"; +"TabBarController.Catalog" = "Catalog"; +"TabBarController.Basket" = "Basket"; +"TabBarController.Statistics" = "Statistics"; + +// MARK: - Profile + +// MainScreen +"ProfileViewController.myNFT" = "My NFT"; +"ProfileViewController.favouritesNFT" = "Favorites NFT"; +"ProfileViewController.aboutDeveloper" = "About developer"; + +// EditScreen +"EditingViewController.changePhoto" = "Change photo"; +"EditingViewController.name" = "Name"; +"EditingViewController.description" = "Description"; +"EditingViewController.site" = "Website"; + +//ProgressHUD +"ProgressHUD.loading" = "Loading ..."; + +//NFTCell +"NFTCell.from" = "from"; +"NFTCell.price" = "Price"; + +//UserNFTViewController +"UserNFTViewController.nonft" = "You don't have an NFT yet"; + +//AlertAction +"AlertAction.close" = "Close"; +"AlertAction.sort" = "Sorting"; +"AlertAction.ok" = "OK"; +"AlertAction.error" = "Error"; +"AlertAction.incorrURL" = "Incorrect URL entered"; +"AlertAction.cancel" = "Cancel"; +"AlertAction.enterURL" = "Enter URL"; +"AlertAction.imageURL" = "Image URL"; +"AlertAction.UpdateError" = "Profile update error"; + +//SortOptions +"SortOptions.price" = "By price"; +"SortOptions.rate" = "By rating"; +"SortOptions.name" = "By name"; + +//FavoritesNFTViewController +"FavoritesNFTViewController.nonft" = "You don't have any favorite NFTs yet"; + + + + + diff --git a/FakeNFT/Resources/ru.lproj/LaunchScreen.strings b/FakeNFT/Resources/ru.lproj/LaunchScreen.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/FakeNFT/Resources/ru.lproj/LaunchScreen.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/FakeNFT/Resources/ru.lproj/Localizable.strings b/FakeNFT/Resources/ru.lproj/Localizable.strings new file mode 100755 index 0000000000..6f0e4c786b --- /dev/null +++ b/FakeNFT/Resources/ru.lproj/Localizable.strings @@ -0,0 +1,119 @@ +"Tab.catalog" = "Каталог"; +"Catalog.openNft" = "Открыть Nft"; +"Error.network" = "Произошла ошибка сети"; +"Error.unknown" = "Произошла неизвестная ошибка"; +"Error.repeat" = "Повторить"; +"Error.title" = "Ошибка"; + +//Tabbar titles +"Tabbar.catalogTitle" = "Каталог"; +"Tabbar.cartTitle" = "Корзина"; + +//Catalog filter alert +"FilterAlert.title" = "Сортировка"; +"FilterAlert.nameSortTitle" = "По названию"; +"FilterAlert.quantitySortTitle" = "По количеству NFT"; +"FilterAlert.cancelButtonTitle" = "Отменить"; + +//CatalogCollection network error alert +"NetworkErrorAlert.title" = "Упс("; +"NetworkErrorAlert.message" = "Попробуйте еще раз"; +"NetworkErrorAlert.okButton" = "OK"; + +//NftInteractionError alert +"NftErrorAlert.title" = "Что-то пошло не так("; +"NftErrorAlert.message" = "Попробуйте еще раз"; +"NftErrorAlert.okButton" = "OK"; + +//Catalog collection screen +"CatalogCollection.authorLabel" = "Автор коллекции:"; + +//CatalogFilter storage +"CatalogFilterStorage.key" = "catalogFilter"; + +//Cart storage +"cart.ButtonText" = "К оплате"; +"cart.emptyCart" = "Корзина пуста"; +"cart.priceLabel" = "Цена"; +"cart.closeButtonText" = "Закрыть"; +"cart.deleteConfirmText" = "Вы уверены, что хотите удалить объект из корзины?"; +"cart.deleteButtonText" = "Удалить"; +"cart.backButtonText" = "Вернуться"; +"cart.loadDataErrorText" = "Ошибка загрузки, попробуйте еще раз"; + +//Cart filter alert +"cartFilterAlert.title" = "Сортировка"; +"cartFilterAlert.price" = "По цене"; +"cartFilterAlert.rating" = "По рейтингу"; +"cartFilterAlert.name" = "По названию"; + +//Currency storage +"currency.paymentTypeText" = "Выберите способ оплаты"; +"currency.paymentConfirmButtonText" = "Оплатить"; +"currency.cartUserAgreementText" = "Совершая покупку, вы соглашаетесь с условиями"; +"currency.cartUserAgreementLinkText" = "Пользовательского соглашения"; + +//Payment storage +"Payment.successText" = "Успех! Оплата прошла, поздравляем с покупкой!"; +"Payment.backToCatalogueText" = "Вернуться в каталог"; +"Payment.errorText" = "Не удалось произвести оплату"; +"Payment.cancelText" = "Отмена"; +"Payment.retryText" = "Повторить"; +"Payment.okText" = "ОК"; + +//Onboarding storage +"Onboarding.titleFirst" = "Исследуйте"; +"Onboarding.titleSecond" = "Коллекционируйте"; +"Onboarding.titleThird" = "Состязайтесь"; +"Onboarding.infoFirst" = "Присоединяйтесь и откройте новый мир уникальных NFT для коллекционеров"; +"Onboarding.infoSecond" = "Пополняйте свою коллекцию эксклюзивными картинками, созданными нейросетью!"; +"Onboarding.infoThird" = "Смотрите статистику других и покажите всем, что у вас самая ценная коллекция"; +"Onboarding.buttonText" = "Что внутри?"; +"TabBarController.Profile" = "Профиль"; +"TabBarController.Catalog" = "Каталог"; +"TabBarController.Basket" = "Корзина"; +"TabBarController.Statistics" = "Статистика"; + +// MARK: - Profile + +// MainScreen +"ProfileViewController.myNFT" = "Мои NFT"; +"ProfileViewController.favouritesNFT" = "Избранные NFT"; +"ProfileViewController.aboutDeveloper" = "О разработчике"; + +// EditScreen +"EditingViewController.changePhoto" = "Сменить фото"; +"EditingViewController.name" = "Имя"; +"EditingViewController.description" = "Описание"; +"EditingViewController.site" = "Cайт"; + +//ProgressHUD +"ProgressHUD.loading" = "Загрузка ..."; + +//NFTCell +"NFTCell.from" = "от"; +"NFTCell.price" = "Цена"; + +//UserNFTViewController +"UserNFTViewController.nonft" = "У вас еще нет NFT"; + +//AlertAction +"AlertAction.close" = "Закрыть"; +"AlertAction.sort" = "Сортировка"; +"AlertAction.ok" = "Подтвердить"; +"AlertAction.error" = "Ошибка"; +"AlertAction.incorrURL" = "Введен не корректный URL адрес"; +"AlertAction.cancel" = "Отмена"; +"AlertAction.enterURL" = "Введите URL"; +"AlertAction.imageURL" = "URL изображения"; +"AlertAction.UpdateError" = "Ошибка обновления профиля"; + +//SortOptions +"SortOptions.price" = "По цене"; +"SortOptions.rate" = "По рейтингу"; +"SortOptions.name" = "По названию"; + +//FavoritesNFTViewController +"FavoritesNFTViewController.nonft" = "У вас еще нет избранных NFT"; + + diff --git a/FakeNFT/Resources/ru.lproj/Main.strings b/FakeNFT/Resources/ru.lproj/Main.strings deleted file mode 100644 index 6d61671411..0000000000 --- a/FakeNFT/Resources/ru.lproj/Main.strings +++ /dev/null @@ -1,30 +0,0 @@ - -/* Class = "UITabBarItem"; title = "Корзина"; ObjectID = "4N1-bS-JmX"; */ -"4N1-bS-JmX.title" = "Корзина"; - -/* Class = "UITabBarItem"; title = "Профиль"; ObjectID = "Bq6-vk-Wno"; */ -"Bq6-vk-Wno.title" = "Профиль"; - -/* Class = "UITabBarItem"; title = "Каталог"; ObjectID = "X74-Ak-ux8"; */ -"X74-Ak-ux8.title" = "Каталог"; - -/* Class = "UILabel"; text = "Статистика"; ObjectID = "YLD-QU-GF5"; */ -"YLD-QU-GF5.text" = "Статистика"; - -/* Class = "UILabel"; text = "Профиль"; ObjectID = "cJe-qj-ep5"; */ -"cJe-qj-ep5.text" = "Профиль"; - -/* Class = "UIButton"; configuration.title = "Show nft id = 22"; ObjectID = "co5-kx-oQ8"; */ -"co5-kx-oQ8.configuration.title" = "Show nft id = 22"; - -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "co5-kx-oQ8"; */ -"co5-kx-oQ8.normalTitle" = "Button"; - -/* Class = "UILabel"; text = "Корзина"; ObjectID = "hE3-IK-5YF"; */ -"hE3-IK-5YF.text" = "Корзина"; - -/* Class = "UILabel"; text = "Каталог"; ObjectID = "qWk-zT-AG8"; */ -"qWk-zT-AG8.text" = "Каталог"; - -/* Class = "UITabBarItem"; title = "Корзина"; ObjectID = "xgD-9Y-EGT"; */ -"xgD-9Y-EGT.title" = "Корзина"; diff --git a/FakeNFT/SceneDelegate.swift b/FakeNFT/SceneDelegate.swift deleted file mode 100644 index 5cc4b031b3..0000000000 --- a/FakeNFT/SceneDelegate.swift +++ /dev/null @@ -1,15 +0,0 @@ -import UIKit - -final class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? - - let servicesAssembly = ServicesAssembly( - networkClient: DefaultNetworkClient(), - nftStorage: NftStorageImpl() - ) - - func scene(_: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { - let tabBarController = window?.rootViewController as? TabBarController - tabBarController?.servicesAssembly = servicesAssembly - } -} diff --git a/FakeNFT/Scenes /Basket/BasketViewController.swift b/FakeNFT/Scenes /Basket/BasketViewController.swift new file mode 100755 index 0000000000..48adf3d7a7 --- /dev/null +++ b/FakeNFT/Scenes /Basket/BasketViewController.swift @@ -0,0 +1,8 @@ +import UIKit + +final class BasketViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .nftWhite + } +} diff --git a/FakeNFT/Scenes /Cart/Cart/View/CartTableViewCell.swift b/FakeNFT/Scenes /Cart/Cart/View/CartTableViewCell.swift new file mode 100755 index 0000000000..9d35cd7fa8 --- /dev/null +++ b/FakeNFT/Scenes /Cart/Cart/View/CartTableViewCell.swift @@ -0,0 +1,229 @@ +import UIKit +import Kingfisher + +protocol CartCellDelegate: AnyObject { + func didTapDeleteNft(at index: Int) +} + +final class CartTableViewCell: UITableViewCell { + static let reuseIdentifier = "cartNFTTableViewCell" + weak var delegate: CartCellDelegate? + var cellIndex: Int? + var onState: (() -> Void)? + + private lazy var imageViewNFT: UIImageView = { + let imageView = UIImageView() + imageView.layer.cornerRadius = 12 + imageView.layer.masksToBounds = true + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .bodyBold + label.textColor = UIColor.nftBlack + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var ratingStarStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.distribution = .fill + stackView.spacing = 3 + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var priceTitleLabel: UILabel = { + let label = UILabel() + label.font = .caption1 + label.textColor = UIColor.nftBlack + label.text = L10n.Cart.priceLabel + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var priceLabel: UILabel = { + let label = UILabel() + label.font = .bodyBold + label.textColor = UIColor.nftBlack + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var infoNFTView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private lazy var deleteButton: UIButton = { + let button = UIButton() + let image = UIImage(named: "basket_delete")?.withTintColor(UIColor.nftBlack) + button.setImage(image?.withRenderingMode(.alwaysOriginal), for: .normal) + button.addTarget(self, action: #selector(tapDeleteButton), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private lazy var gradient: GradientView = { + return GradientView(frame: self.bounds) + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + createSubviews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + imageViewNFT.kf.cancelDownloadTask() + } + + func configure(with model: Nft) { + contentView.backgroundColor = UIColor.nftWhite + startAnimation() + imageViewNFT.kf.setImage( + with: model.images.first, + placeholder: nil, + completionHandler: { [weak self] _ in + guard let self = self else { return } + self.stopAnimation() + self.onState?() + } + ) + self.titleLabel.text = model.name + self.priceLabel.text = getPrice(with: model.price) + getRating(from: model.rating) + } + + private func createSubviews() { + addImageViewNFT() + addInfoNFTView() + addTitleLabel() + addRatingStarStackView() + addPriceTitleLabel() + addPriceLabel() + addDeleteButton() + addGradientLayer() + gradient.translatesAutoresizingMaskIntoConstraints = false + gradient.isHidden = true + } + + private func addImageViewNFT() { + contentView.addSubview(imageViewNFT) + NSLayoutConstraint.activate([ + imageViewNFT.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + imageViewNFT.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + imageViewNFT.widthAnchor.constraint(equalToConstant: 108), + imageViewNFT.heightAnchor.constraint(equalToConstant: 108) + ]) + } + + private func addGradientLayer() { + contentView.addSubview(gradient) + NSLayoutConstraint.activate([ + gradient.topAnchor.constraint(equalTo: imageViewNFT.topAnchor), + gradient.leadingAnchor.constraint(equalTo: imageViewNFT.leadingAnchor), + gradient.trailingAnchor.constraint(equalTo: imageViewNFT.trailingAnchor), + gradient.bottomAnchor.constraint(equalTo: imageViewNFT.bottomAnchor) + ]) + } + + private func addInfoNFTView() { + contentView.addSubview(infoNFTView) + NSLayoutConstraint.activate([ + infoNFTView.leadingAnchor.constraint(equalTo: imageViewNFT.trailingAnchor, constant: 16), + infoNFTView.topAnchor.constraint(equalTo: imageViewNFT.topAnchor, constant: 8), + infoNFTView.bottomAnchor.constraint(equalTo: imageViewNFT.bottomAnchor, constant: -8) + ]) + } + + private func addTitleLabel() { + infoNFTView.addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.leadingAnchor.constraint(equalTo: infoNFTView.leadingAnchor), + titleLabel.topAnchor.constraint(equalTo: infoNFTView.topAnchor) + ]) + } + + private func addRatingStarStackView() { + infoNFTView.addSubview(ratingStarStackView) + NSLayoutConstraint.activate([ + ratingStarStackView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + ratingStarStackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5) + ]) + } + + private func addPriceTitleLabel() { + infoNFTView.addSubview(priceTitleLabel) + NSLayoutConstraint.activate([ + priceTitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + priceTitleLabel.topAnchor.constraint(equalTo: ratingStarStackView.bottomAnchor, constant: 10) + ]) + } + + private func addPriceLabel() { + infoNFTView.addSubview(priceLabel) + NSLayoutConstraint.activate([ + priceLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + priceLabel.topAnchor.constraint(equalTo: priceTitleLabel.bottomAnchor, constant: 5) + ]) + } + + private func addDeleteButton() { + contentView.addSubview(deleteButton) + NSLayoutConstraint.activate([ + deleteButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + deleteButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) + ]) + } + + private func getPrice(with price: Float) -> String { + let formatedPrice = NumberFormatter.priceFormatter.string(from: NSNumber(value: price)) ?? "\(price)" + let priceString = formatedPrice + " ETH" + return priceString + } + + private func getRating(from rating: Int) { + ratingStarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + (0.. UIImageView { + let imageView = UIImageView() + imageView.image = active ? UIImage(named: "star_active") + : UIImage(named: "star_inactive")?.withTintColor(UIColor.nftLightgrey) + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + } + + private func startAnimation() { + gradient.isHidden = false + gradient.startAnimating() + } + + private func stopAnimation() { + gradient.stopAnimating() + gradient.isHidden = true + } + + @objc private func tapDeleteButton() { + delegate?.didTapDeleteNft(at: cellIndex ?? 0) + } +} diff --git a/FakeNFT/Scenes /Cart/Cart/View/CartViewController.swift b/FakeNFT/Scenes /Cart/Cart/View/CartViewController.swift new file mode 100755 index 0000000000..193e4ed51b --- /dev/null +++ b/FakeNFT/Scenes /Cart/Cart/View/CartViewController.swift @@ -0,0 +1,391 @@ +import UIKit + +final class CartViewController: UIViewController, LoadingView { + private let viewModel: CartViewModel + private var deleteNftIndex: Int = 0 + + private lazy var filterButton: UIButton = { + let button = UIButton() + let image = UIImage(named: "sort_button")?.withTintColor(UIColor.nftBlack) + button.setImage(image, for: .normal) + button.addTarget(self, action: #selector(tapFilterButton), for: .touchUpInside) + return button + }() + + private lazy var cartTableView: UITableView = { + let tableView = UITableView() + tableView.backgroundColor = .clear + tableView.separatorStyle = .none + tableView.register(CartTableViewCell.self, forCellReuseIdentifier: CartTableViewCell.reuseIdentifier) + tableView.dataSource = self + tableView.delegate = self + tableView.translatesAutoresizingMaskIntoConstraints = false + return tableView + }() + + private lazy var countNFTLabel: UILabel = { + let label = UILabel() + label.text = "0 NFT" + label.textColor = UIColor.nftBlack + label.font = .caption2 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var totalPriceLabel: UILabel = { + let label = UILabel() + label.text = "0 ETH" + label.textColor = UIColor.nftGreenUniversal + label.font = .bodyBold + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var paymentInfoStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 3 + stackView.addArrangedSubview(countNFTLabel) + stackView.addArrangedSubview(totalPriceLabel) + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var paymentButton: UIButton = { + let button = UIButton() + button.layer.cornerRadius = 16 + button.setTitle(L10n.Cart.buttonText, for: .normal) + button.setTitleColor(UIColor.nftWhite, for: .normal) + button.titleLabel?.font = .bodyBold + button.backgroundColor = UIColor.nftBlack + button.addTarget(self, action: #selector(tapPayButton), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private lazy var paymentContainView: UIView = { + let view = UIView() + view.backgroundColor = UIColor.nftLightgrey + view.layer.cornerRadius = 12 + view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private lazy var emptyCartLabel: UILabel = { + let label = UILabel() + label.text = L10n.Cart.emptyCart + label.textColor = UIColor.nftBlack + label.font = .bodyBold + label.textAlignment = .center + label.isHidden = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var nftImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.layer.cornerRadius = 12 + imageView.layer.masksToBounds = true + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private lazy var confirmDeleteLabel: UILabel = { + let label = UILabel() + label.text = L10n.Cart.deleteConfirmText + label.textColor = UIColor.nftBlack + label.font = .caption2 + label.textAlignment = .center + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var deleteNftButton: UIButton = { + let button = UIButton() + button.layer.cornerRadius = 16 + button.setTitle(L10n.Cart.deleteButtonText, for: .normal) + button.setTitleColor(UIColor.nftRedUniversal, for: .normal) + button.titleLabel?.font = .bodyRegular + button.backgroundColor = UIColor.nftBlack + button.addTarget(self, action: #selector(tapDeleteButton), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private lazy var cancelButton: UIButton = { + let button = UIButton() + button.layer.cornerRadius = 16 + button.setTitle(L10n.Cart.backButtonText, for: .normal) + button.setTitleColor(UIColor.nftWhite, for: .normal) + button.titleLabel?.font = .bodyRegular + button.backgroundColor = UIColor.nftBlack + button.addTarget(self, action: #selector(tapBackButton), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private lazy var buttonsStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.spacing = 10 + stackView.addArrangedSubview(deleteNftButton) + stackView.addArrangedSubview(cancelButton) + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var backgroundBlurView: UIVisualEffectView = { + let view = UIVisualEffectView() + view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.effect = UIBlurEffect(style: .regular) + view.frame = UIScreen.main.bounds + view.isHidden = true + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + init(viewModel: CartViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + observeViewModelChanges() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.nftWhite + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: filterButton) + createSubviews() + viewModel.loadData() + showLoading() + viewModel.onDataErrorResult = { [weak self] in + self?.showLoadDataError() + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if PurchaseCartStorage.shared.nfts.count != viewModel.nfts.count { + viewModel.loadData() + showLoading() + viewModel.onDataErrorResult = { [weak self] in + self?.showLoadDataError() + } + } + } + + private func observeViewModelChanges() { + viewModel.$nfts.bind { [weak self] nfts in + guard let self else { return } + self.updateUI(with: nfts) + } + } + + private func updateUI(with nfts: [Nft]) { + cartTableView.reloadData() + countNFTLabel.text = "\(viewModel.countNftInCart()) NFT" + totalPriceLabel.text = "\(viewModel.getTotalPrice()) ETH" + hideLoading() + checkPlaceholder() + } + + private func checkPlaceholder() { + if viewModel.nfts.isEmpty { + paymentContainView.isHidden = true + filterButton.isHidden = true + cartTableView.isHidden = true + emptyCartLabel.isHidden = false + } else { + paymentContainView.isHidden = false + filterButton.isHidden = false + cartTableView.isHidden = false + emptyCartLabel.isHidden = true + } + } + + private func showFiltersAlert() { + AlertPresenter.showCartFiltersAlert(on: self, viewModel: viewModel) + } + + private func showLoadDataError() { + AlertPresenter.showDataError(on: self) { [weak self] in + self?.showLoading() + self?.viewModel.loadData() + } + } + + @objc + private func tapFilterButton() { + showFiltersAlert() + } + + @objc + private func tapPayButton() { + let moduleFactory = ModuleFactory(servicesAssembly: viewModel.servicesAssembly) + let currencyModule = moduleFactory.makeCurrencyModule() + present(currencyModule, animated: true) + } + + @objc + private func tapDeleteButton() { + viewModel.deleteNftFromCart(at: deleteNftIndex) + hideBackroundBlurView() + } + + @objc + private func tapBackButton() { + hideBackroundBlurView() + } +} + +extension CartViewController { + private func createSubviews() { + addCartTableView() + addPaymentContainView() + addPaymentInfoStackView() + addPayButton() + addEmptyCartLabel() + } + + private func addCartTableView() { + view.addSubview(cartTableView) + NSLayoutConstraint.activate([ + cartTableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + cartTableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + cartTableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16) + ]) + } + + private func addPaymentContainView() { + view.addSubview(paymentContainView) + NSLayoutConstraint.activate([ + paymentContainView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + paymentContainView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + paymentContainView.topAnchor.constraint(equalTo: cartTableView.bottomAnchor), + paymentContainView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + paymentContainView.heightAnchor.constraint(equalToConstant: 76) + ]) + } + + private func addPaymentInfoStackView() { + paymentContainView.addSubview(paymentInfoStackView) + NSLayoutConstraint.activate([ + paymentInfoStackView.leadingAnchor.constraint(equalTo: paymentContainView.leadingAnchor, constant: 16), + paymentInfoStackView.centerYAnchor.constraint(equalTo: paymentContainView.centerYAnchor) + ]) + } + + private func addPayButton() { + paymentContainView.addSubview(paymentButton) + NSLayoutConstraint.activate([ + paymentButton.leadingAnchor.constraint(equalTo: paymentInfoStackView.trailingAnchor, constant: 24), + paymentButton.trailingAnchor.constraint(equalTo: paymentContainView.trailingAnchor, constant: -16), + paymentButton.centerYAnchor.constraint(equalTo: paymentContainView.centerYAnchor), + paymentButton.heightAnchor.constraint(equalToConstant: 44) + ]) + } + + private func addEmptyCartLabel() { + view.addSubview(emptyCartLabel) + NSLayoutConstraint.activate([ + emptyCartLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), + emptyCartLabel.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor) + ]) + } +} + +extension CartViewController: CartCellDelegate { + func didTapDeleteNft(at index: Int) { + backgroundBlurView.isHidden = false + nftImageView.kf.setImage(with: viewModel.getNft(at: index).images.first) + navigationController?.navigationBar.isHidden = true + tabBarController?.tabBar.isHidden = true + deleteNftIndex = index + createDeleteView() + } + + private func createDeleteView() { + addBackgroundBlurView() + addNftImageView() + addConfirmDeleteLabel() + addButtonsStackView() + } + + private func hideBackroundBlurView() { + backgroundBlurView.removeFromSuperview() + navigationController?.navigationBar.isHidden = false + tabBarController?.tabBar.isHidden = false + } + + private func addBackgroundBlurView() { + view.addSubview(backgroundBlurView) + NSLayoutConstraint.activate([ + backgroundBlurView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + backgroundBlurView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + backgroundBlurView.topAnchor.constraint(equalTo: view.topAnchor), + backgroundBlurView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + private func addNftImageView() { + backgroundBlurView.contentView.addSubview(nftImageView) + NSLayoutConstraint.activate([ + nftImageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 244), + nftImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + nftImageView.widthAnchor.constraint(equalToConstant: 108), + nftImageView.heightAnchor.constraint(equalToConstant: 108) + ]) + } + + private func addConfirmDeleteLabel() { + backgroundBlurView.contentView.addSubview(confirmDeleteLabel) + NSLayoutConstraint.activate([ + confirmDeleteLabel.topAnchor.constraint(equalTo: nftImageView.bottomAnchor, constant: 12), + confirmDeleteLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + confirmDeleteLabel.widthAnchor.constraint(equalToConstant: 180) + ]) + } + + private func addButtonsStackView() { + backgroundBlurView.contentView.addSubview(buttonsStackView) + NSLayoutConstraint.activate([ + buttonsStackView.topAnchor.constraint(equalTo: confirmDeleteLabel.bottomAnchor, constant: 20), + buttonsStackView.leadingAnchor.constraint(equalTo: backgroundBlurView.leadingAnchor, constant: 56), + buttonsStackView.trailingAnchor.constraint(equalTo: backgroundBlurView.trailingAnchor, constant: -56), + buttonsStackView.heightAnchor.constraint(equalToConstant: 44) + ]) + } +} + +extension CartViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 140 + } +} + +extension CartViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: CartTableViewCell.reuseIdentifier) + as? CartTableViewCell else { + return UITableViewCell() + } + let nft = viewModel.getNft(at: indexPath.row) + cell.configure(with: nft) + cell.delegate = self + cell.cellIndex = indexPath.row + return cell + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.countNftInCart() + } +} diff --git a/FakeNFT/Scenes /Cart/Cart/ViewModel/CartViewModel.swift b/FakeNFT/Scenes /Cart/Cart/ViewModel/CartViewModel.swift new file mode 100755 index 0000000000..d0530872d3 --- /dev/null +++ b/FakeNFT/Scenes /Cart/Cart/ViewModel/CartViewModel.swift @@ -0,0 +1,78 @@ +import Foundation + + class CartViewModel { + let servicesAssembly: ServicesAssembly + var isEmpty: Bool { + nfts.isEmpty + } + var onDataErrorResult: (() -> Void)? + + @Observable + private (set) var nfts: [Nft] = [] + + private lazy var cartFilterStorage = CartFilterStorage.shared + + init(servicesAssembly: ServicesAssembly) { + self.servicesAssembly = servicesAssembly + } + + func loadData() { + servicesAssembly.cartService.loadNFTs { [weak self] result in + guard let self = self else { return } + DispatchQueue.main.async { + switch result { + case .success(let nfts): + self.nfts = nfts + self.sort(by: self.cartFilterStorage.cartSortType) + case .failure: + self.onDataErrorResult?() + } + } + } + } + + func countNftInCart() -> Int { + nfts.count + } + + func getNft(at index: Int) -> Nft { + nfts[index] + } + + func getTotalPrice() -> String { + let total = totalPriceCount() + let formattedTotal = NumberFormatter.priceFormatter.string(from: NSNumber(value: total)) ?? "\(total)" + return formattedTotal + } + + func sort(by type: CartSortType) { + switch type { + case .price: + nfts.sort { $0.price < $1.price } + case .rating: + nfts.sort { $0.rating > $1.rating } + case .name: + nfts.sort { $0.name < $1.name } + } + cartFilterStorage.cartSortType = type + } + + func deleteNftFromCart(at index: Int) { + nfts.remove(at: index) + servicesAssembly.cartService.deleteNftFromCart(cartId: "1", nfts: nfts.map { $0.id }) { result in + DispatchQueue.main.async { + switch result { + case .success(let cart): + PurchaseCartStorage.shared.nfts = cart.nfts + self.loadData() + case .failure: + self.onDataErrorResult?() + } + } + } + } + + private func totalPriceCount() -> Float { + nfts.reduce(0) { $0 + $1.price } + } + } diff --git a/FakeNFT/Scenes /Cart/Currency/View/CurrencyCollectionViewCell.swift b/FakeNFT/Scenes /Cart/Currency/View/CurrencyCollectionViewCell.swift new file mode 100755 index 0000000000..2f17414f1f --- /dev/null +++ b/FakeNFT/Scenes /Cart/Currency/View/CurrencyCollectionViewCell.swift @@ -0,0 +1,101 @@ +import UIKit + +final class CurrencyCollectionViewCell: UICollectionViewCell { + static let reuseIdentifier = "currencyCollectionViewCell" + + override var isSelected: Bool { + didSet { + layer.cornerRadius = 12 + layer.borderWidth = isSelected ? 1 : 0 + layer.borderColor = isSelected ? UIColor.nftBlack.cgColor : nil + } + } + + private lazy var currencyImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.layer.cornerRadius = 6 + imageView.layer.masksToBounds = true + imageView.backgroundColor = .black + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private lazy var currencyFullNameLabel: UILabel = { + let label = UILabel() + label.font = .caption1 + label.textColor = UIColor.nftBlack + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var currencyShortNameLabel: UILabel = { + let label = UILabel() + label.font = .caption1 + label.textColor = UIColor.nftGreenUniversal + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var currencyNameStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.distribution = .equalSpacing + stackView.addArrangedSubview(currencyFullNameLabel) + stackView.addArrangedSubview(currencyShortNameLabel) + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var currencyStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = 7 + stackView.addArrangedSubview(currencyImageView) + stackView.addArrangedSubview(currencyNameStackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var currencyContainView: UIView = { + let view = UIView() + view.backgroundColor = UIColor.nftLightgrey + view.layer.cornerRadius = 12 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + createSubviews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(with model: CurrencyModel) { + self.currencyFullNameLabel.text = model.title + self.currencyShortNameLabel.text = model.name + self.currencyImageView.kf.setImage(with: URL(string: model.image)) + } + + private func createSubviews() { + backgroundColor = .clear + contentView.addSubview(currencyContainView) + currencyContainView.addSubview(currencyStackView) + NSLayoutConstraint.activate([ + currencyContainView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + currencyContainView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + currencyContainView.topAnchor.constraint(equalTo: contentView.topAnchor), + currencyContainView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + currencyContainView.heightAnchor.constraint(equalToConstant: 46), + currencyStackView.leadingAnchor.constraint(equalTo: currencyContainView.leadingAnchor, constant: 16), + currencyStackView.trailingAnchor.constraint(equalTo: currencyContainView.trailingAnchor, constant: -16), + currencyStackView.topAnchor.constraint(equalTo: currencyContainView.topAnchor, constant: 5), + currencyStackView.bottomAnchor.constraint(equalTo: currencyContainView.bottomAnchor, constant: -5), + currencyImageView.heightAnchor.constraint(equalToConstant: 36), + currencyImageView.widthAnchor.constraint(equalToConstant: 36) + ]) + } +} diff --git a/FakeNFT/Scenes /Cart/Currency/View/CurrencyViewController.swift b/FakeNFT/Scenes /Cart/Currency/View/CurrencyViewController.swift new file mode 100755 index 0000000000..6ae4f0dab5 --- /dev/null +++ b/FakeNFT/Scenes /Cart/Currency/View/CurrencyViewController.swift @@ -0,0 +1,262 @@ +import UIKit + +final class CurrencyScreenViewController: UIViewController, LoadingView { + private var viewModel: CurrencyViewModelProtocol + private var selectedCurrencyIndex: IndexPath? + + private lazy var backButton: UIButton = { + let button = UIButton() + let image = UIImage(named: "backward")?.withTintColor(UIColor.nftBlack) + button.setImage(image, for: .normal) + button.tintColor = UIColor.nftBlack + button.addTarget(self, action: #selector(tapBackButton), for: .touchUpInside) + return button + }() + + private lazy var currencyCollectionViewLayout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + layout.minimumLineSpacing = 7 + layout.minimumInteritemSpacing = 7 + return layout + }() + + private lazy var currencyCollectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: currencyCollectionViewLayout) + collectionView.backgroundColor = .clear + collectionView.allowsMultipleSelection = false + collectionView.showsVerticalScrollIndicator = false + collectionView.register(CurrencyCollectionViewCell.self, + forCellWithReuseIdentifier: CurrencyCollectionViewCell.reuseIdentifier) + collectionView.dataSource = self + collectionView.delegate = self + collectionView.translatesAutoresizingMaskIntoConstraints = false + return collectionView + }() + + private lazy var agreementLabel: UILabel = { + let label = UILabel() + label.font = .caption2 + label.textColor = UIColor.nftBlack + label.numberOfLines = 0 + label.text = L10n.Currency.cartUserAgreementText + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var agreementLinkButton: UIButton = { + let button = UIButton() + button.setTitle(L10n.Currency.cartUserAgreementLinkText, for: .normal) + button.titleLabel?.font = .caption2 + button.setTitleColor(UIColor.nftBlueUniversal, for: .normal) + button.addTarget(self, action: #selector(tapUserAgreementLink), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private lazy var userAgreementStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.alignment = .leading + stackView.distribution = .fill + stackView.addArrangedSubview(agreementLabel) + stackView.addArrangedSubview(agreementLinkButton) + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var paymentButton: UIButton = { + let button = UIButton() + button.layer.cornerRadius = 16 + button.setTitle(L10n.Currency.paymentConfirmButtonText, for: .normal) + button.setTitleColor(UIColor.nftWhite, for: .normal) + button.titleLabel?.font = .bodyBold + button.backgroundColor = UIColor.nftBlack + button.addTarget(self, action: #selector(tapPayButton), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private lazy var paymentStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 20 + stackView.addArrangedSubview(userAgreementStackView) + stackView.addArrangedSubview(paymentButton) + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var paymentContainView: UIView = { + let view = UIView() + view.backgroundColor = UIColor.nftLightgrey + view.layer.cornerRadius = 12 + view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + init(viewModel: CurrencyViewModelProtocol) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.nftWhite + createSubviews() + viewModel.onDataUpdate = { [weak self] in + DispatchQueue.main.async { + self?.hideLoading() + self?.currencyCollectionView.reloadData() + } + } + viewModel.onSuccessResult = { [weak self] in + DispatchQueue.main.async { + self?.showSuccessResult() + } + } + viewModel.onErrorResult = { [weak self] in + DispatchQueue.main.async { + self?.showErrorResult() + } + } + viewModel.onDataErrorResult = { [weak self] in + self?.showLoadDataError() + } + showLoading() + viewModel.loadData() + } + + private func showErrorResult() { + AlertPresenter.showPaymentError(on: self) { [weak self] in + self?.viewModel.getPaymentResult(with: self?.viewModel.selectedCurrencyID ?? "") + } + } + + private func showSuccessResult() { + let paymentViewController = PaymentSuccessViewController() + let navigationController = UINavigationController(rootViewController: paymentViewController) + navigationController.modalPresentationStyle = .fullScreen + navigationController.hidesBottomBarWhenPushed = true + present(navigationController, animated: true) + } + + private func showLoadDataError() { + AlertPresenter.showDataError(on: self) { [weak self] in + self?.viewModel.loadData() + } + } + + @objc + private func tapBackButton() { + dismiss(animated: true) + } + + @objc + private func tapUserAgreementLink() { + let viewModel = WebViewViewModel() + let url = URL(string: RequestConstants.cartUserAgreementLink) + let view = WebViewController(viewModel: viewModel ,url: url) + navigationController?.pushViewController(view, animated: true) + } + + @objc + private func tapPayButton() { + guard let selectedCurrencyID = viewModel.selectedCurrencyID else { + AlertPresenter.showError(on: self) + return + } + viewModel.getPaymentResult(with: selectedCurrencyID) + } +} + +extension CurrencyScreenViewController { + private func createSubviews() { + view.backgroundColor = .systemBackground + navigationItem.leftBarButtonItems = [UIBarButtonItem(customView: backButton)] + navigationItem.title = L10n.Currency.paymentTypeText + navigationController?.navigationBar.titleTextAttributes = [ + .font: UIFont.bodyBold, + .foregroundColor: UIColor.nftBlack + ] + addCurrencyCollectionView() + addPaymentContainView() + addAgreementStackView() + } + + private func addCurrencyCollectionView() { + view.addSubview(currencyCollectionView) + NSLayoutConstraint.activate([ + currencyCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + currencyCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + currencyCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20) + ]) + } + + private func addPaymentContainView() { + view.addSubview(paymentContainView) + NSLayoutConstraint.activate([ + paymentContainView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + paymentContainView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + paymentContainView.topAnchor.constraint(equalTo: currencyCollectionView.bottomAnchor), + paymentContainView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + private func addAgreementStackView() { + paymentContainView.addSubview(paymentStackView) + NSLayoutConstraint.activate([ + paymentStackView.leadingAnchor.constraint(equalTo: paymentContainView.leadingAnchor, constant: 16), + paymentStackView.trailingAnchor.constraint(equalTo: paymentContainView.trailingAnchor, constant: -16), + paymentStackView.topAnchor.constraint(equalTo: paymentContainView.topAnchor, constant: 16), + paymentStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), + paymentButton.heightAnchor.constraint(equalToConstant: 60) + ]) + } +} + +extension CurrencyScreenViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: CurrencyCollectionViewCell.reuseIdentifier, + for: indexPath) as? CurrencyCollectionViewCell else { return } + cell.isSelected = true + if let selectedCurrencyIndex, selectedCurrencyIndex != indexPath { + let cell = collectionView.cellForItem(at: selectedCurrencyIndex) as? CurrencyCollectionViewCell + cell?.isSelected = false + } + viewModel.selectCurrency(at: indexPath) + } +} + +extension CurrencyScreenViewController: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + viewModel.currencies.count + } + + func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: CurrencyCollectionViewCell.reuseIdentifier, + for: indexPath) as? CurrencyCollectionViewCell else { + return UICollectionViewCell() + } + let currency = viewModel.currencies[indexPath.row] + cell.configure(with: currency) + return cell + } +} + +extension CurrencyScreenViewController: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + let width = (collectionView.bounds.width - 7) / 2 + return CGSize(width: width, height: 46) + } +} diff --git a/FakeNFT/Scenes /Cart/Currency/View/PaymentSuccessViewController.swift b/FakeNFT/Scenes /Cart/Currency/View/PaymentSuccessViewController.swift new file mode 100755 index 0000000000..4f5c769c5b --- /dev/null +++ b/FakeNFT/Scenes /Cart/Currency/View/PaymentSuccessViewController.swift @@ -0,0 +1,74 @@ +import UIKit + +final class PaymentSuccessViewController: UIViewController { + + private lazy var imageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "payment_success") + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private lazy var paymentResultLabel: UILabel = { + let label = UILabel() + label.text = L10n.Payment.successText + label.textColor = UIColor.nftBlack + label.font = .headline3 + label.textAlignment = .center + label.numberOfLines = 3 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var paymentStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.distribution = .fill + stackView.alignment = .center + stackView.spacing = 20 + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(paymentResultLabel) + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var backToCatalogButton: UIButton = { + let button = UIButton() + button.layer.cornerRadius = 16 + button.setTitle(L10n.Payment.backToCatalogueText, for: .normal) + button.setTitleColor(UIColor.nftWhite, for: .normal) + button.titleLabel?.font = .bodyBold + button.backgroundColor = UIColor.nftBlack + button.addTarget(self, action: #selector(tapBackToCatalogButton), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.nftWhite + createSubviews() + } + + private func createSubviews() { + view.addSubview(paymentStackView) + view.addSubview(backToCatalogButton) + NSLayoutConstraint.activate([ + paymentStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 36), + paymentStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -36), + paymentStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + backToCatalogButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + backToCatalogButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + backToCatalogButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), + backToCatalogButton.heightAnchor.constraint(equalToConstant: 60) + ]) + } + + @objc + private func tapBackToCatalogButton() { + let tabBarController = TabBarController() + tabBarController.modalPresentationStyle = .fullScreen + tabBarController.selectedIndex = 0 + present(tabBarController, animated: true) + } +} diff --git a/FakeNFT/Scenes /Cart/Currency/ViewModel/CurrencyViewModel.swift b/FakeNFT/Scenes /Cart/Currency/ViewModel/CurrencyViewModel.swift new file mode 100755 index 0000000000..21bb0d64f1 --- /dev/null +++ b/FakeNFT/Scenes /Cart/Currency/ViewModel/CurrencyViewModel.swift @@ -0,0 +1,89 @@ +import Foundation + +protocol CurrencyViewModelProtocol { + var onDataUpdate: (() -> Void)? { get set } + var onErrorResult: (() -> Void)? { get set } + var onSuccessResult: (() -> Void)? { get set } + var onDataErrorResult: (() -> Void)? { get set } + + var currencies: [CurrencyModel] { get } + var selectedCurrencyID: String? { get } + + func loadData() + func getPaymentResult(with id: String) + func selectCurrency(at indexPath: IndexPath) +} + +final class CurrencyViewModel: CurrencyViewModelProtocol { + let servicesAssembly: ServicesAssembly + + var onDataUpdate: (() -> Void)? { + didSet { + onDataUpdate?() + } + } + + var onErrorResult: (() -> Void)? + var onSuccessResult: (() -> Void)? + var onDataErrorResult: (() -> Void)? + + private (set) var currencies: [CurrencyModel] = [] + private (set) var selectedCurrencyID: String? + + init(servicesAssembly: ServicesAssembly) { + self.servicesAssembly = servicesAssembly + } + + func loadData() { + servicesAssembly.currencyService.loadCurrencies { [weak self] result in + guard let self = self else { return } + DispatchQueue.main.async { + switch result { + case .success(let currencies): + self.currencies = currencies + self.onDataUpdate?() + case .failure: + self.onDataErrorResult?() + } + } + } + } + + func getPaymentResult(with id: String) { + servicesAssembly.currencyService.getPaymentResult(currencyId: id) { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let payment): + self.handlePaymentResult(payment) + case .failure: + self.onErrorResult?() + } + } + } + + func selectCurrency(at indexPath: IndexPath) { + selectedCurrencyID = currencies[indexPath.row].id + } + + private func clearCart() { + servicesAssembly.cartService.deleteNftFromCart(cartId: "1", nfts: []) { result in + DispatchQueue.main.async { + switch result { + case .success: + return + case .failure: + self.onDataErrorResult?() + } + } + } + } + + private func handlePaymentResult(_ payment: PaymentModel) { + guard payment.success else { + self.onErrorResult?() + return + } + self.onSuccessResult?() + self.clearCart() + } +} diff --git a/FakeNFT/Scenes /Catalog/CatalogCollectionView/Cells/CatalogCollectionCell.swift b/FakeNFT/Scenes /Catalog/CatalogCollectionView/Cells/CatalogCollectionCell.swift new file mode 100755 index 0000000000..cecefab672 --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogCollectionView/Cells/CatalogCollectionCell.swift @@ -0,0 +1,275 @@ +// +// CatalogCollectionViewCell.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 11.11.2023. +// + +import UIKit +import Kingfisher + +final class CatalogCollectionCell: UICollectionViewCell { + + // MARK: - Public properties + var nftIsLiked = false + var nftIsAddedToBasket = false + weak var delegate: CatalogCollectionCellDelegate? + + // MARK: - private properties + private let starsQuantity = 5 + private var selectedRate: Int = 0 + private let feedbackGenerator = UISelectionFeedbackGenerator() + private let nftImageView: UIImageView = { + let view = UIImageView() + + view.clipsToBounds = true + view.layer.cornerRadius = 12 + view.backgroundColor = .clear + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + private lazy var likeButton: UIButton = { + let button = UIButton() + + button.addTarget(self, action: #selector(didTapLikeButton), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + + return button + }() + private lazy var starsContainer: UIStackView = { + let stackView = UIStackView() + + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.translatesAutoresizingMaskIntoConstraints = false + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didSelectRate)) + stackView.addGestureRecognizer(tapGesture) + + return stackView + }() + private let nftCardNameLabel: UILabel = { + let label = UILabel() + + label.font = UIFont.systemFont(ofSize: 17, weight: .bold) + label.textColor = UIColor.nftBlack + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + private let nftPriceLabel: UILabel = { + let label = UILabel() + + label.font = UIFont.systemFont(ofSize: 10, weight: .medium) + label.textColor = UIColor.nftBlack + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + private lazy var addToBasketButton: UIButton = { + let button = UIButton() + + button.addTarget(self, action: #selector(basketButtonTapped), for: .touchUpInside) + button.setTitleColor(UIColor.nftBlack, for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + + return button + }() + private lazy var animationView: UIView = { + let view = UIView() + + view.isHidden = true + view.clipsToBounds = true + view.layer.cornerRadius = 12 + view.backgroundColor = .lightGray.withAlphaComponent(0.6) + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubviews() + applyConstraints() + + contentView.backgroundColor = .clear + } + + override func prepareForReuse() { + super.prepareForReuse() + nftIsLiked = false + nftIsAddedToBasket = false + nftImageView.kf.cancelDownloadTask() + starsContainer.arrangedSubviews.forEach { $0.removeFromSuperview() } + nftImageView.image = nil + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - public methods + func createAnimationView() { + startAnimation() + } + + func configureCell(_ nft: Nft) { + let url = nft.images[0] + + nftImageView.kf.setImage( + with: url, + placeholder: nil, + completionHandler: { [weak self] _ in + guard let self = self else { return } + self.stopAnimation() + }) + + switchLikeImage() + switchBasketImage() + + nftCardNameLabel.text = nft.name + nftPriceLabel.text = "\(String(nft.price)) ETH" + selectedRate = nft.rating + createStars() + } + + func changeLikeState() { + nftIsLiked = !nftIsLiked + switchLikeImage() + } + + func switchBasketState() { + nftIsAddedToBasket = !nftIsAddedToBasket + switchBasketImage() + } + + // MARK: - private methods + private func addSubviews() { + contentView.addSubview(nftImageView) + contentView.addSubview(likeButton) + contentView.addSubview(starsContainer) + contentView.addSubview(nftCardNameLabel) + contentView.addSubview(nftPriceLabel) + contentView.addSubview(addToBasketButton) + contentView.addSubview(animationView) + } + + private func applyConstraints() { + NSLayoutConstraint.activate([ + // nftImageViewConstraints + nftImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + nftImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + nftImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + nftImageView.heightAnchor.constraint(equalToConstant: 108), + nftImageView.widthAnchor.constraint(equalTo: contentView.widthAnchor), + + // likeButton constraints + likeButton.topAnchor.constraint(equalTo: nftImageView.topAnchor), + likeButton.trailingAnchor.constraint(equalTo: nftImageView.trailingAnchor), + likeButton.widthAnchor.constraint(equalToConstant: 42), + likeButton.heightAnchor.constraint(equalToConstant: 42), + + // animationView constraints + animationView.topAnchor.constraint(equalTo: contentView.topAnchor), + animationView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + animationView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + animationView.heightAnchor.constraint(equalToConstant: 108), + animationView.widthAnchor.constraint(equalTo: contentView.widthAnchor), + + // starsContainerConstraints + starsContainer.topAnchor.constraint(equalTo: nftImageView.bottomAnchor, constant: 8), + starsContainer.leadingAnchor.constraint(equalTo: nftImageView.leadingAnchor), + starsContainer.heightAnchor.constraint(equalToConstant: 12), + starsContainer.widthAnchor.constraint(equalToConstant: 68), + + // nftCardNameLabel constraints + nftCardNameLabel.topAnchor.constraint(equalTo: starsContainer.bottomAnchor, constant: 4), + nftCardNameLabel.leadingAnchor.constraint(equalTo: starsContainer.leadingAnchor), + nftCardNameLabel.heightAnchor.constraint(equalToConstant: 22), + + // nftPriceLabelConstraints + nftPriceLabel.topAnchor.constraint(equalTo: nftCardNameLabel.bottomAnchor, constant: 4), + nftPriceLabel.leadingAnchor.constraint(equalTo: nftCardNameLabel.leadingAnchor), + nftPriceLabel.heightAnchor.constraint(equalToConstant: 12), + + // addToBasketButton constraints + addToBasketButton.topAnchor.constraint(equalTo: nftCardNameLabel.topAnchor), + addToBasketButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + addToBasketButton.heightAnchor.constraint(equalToConstant: 40), + addToBasketButton.widthAnchor.constraint(equalToConstant: 40) + ]) + } + + private func createStars() { + for index in 1...starsQuantity { + let star = makeStarIcon() + star.tag = index + if index <= selectedRate { + star.isHighlighted = true + } + starsContainer.addArrangedSubview(star) + } + } + + private func makeStarIcon() -> UIImageView { + let imageView = UIImageView( + image: UIImage(named: "star_inactive")?.withTintColor(UIColor.nftLightgrey), + highlightedImage: UIImage(named: "star_active") + ) + imageView.contentMode = .scaleAspectFit + imageView.isUserInteractionEnabled = true + return imageView + } + + private func startAnimation() { + animationView.isHidden = false + animationView.addFlashLayer() + } + + private func stopAnimation() { + animationView.isHidden = true + } + + private func switchLikeImage() { + let image = nftIsLiked ? UIImage(named: "like_active") : UIImage(named: "like_inactive") + likeButton.setImage(image, for: .normal) + } + + private func switchBasketImage() { + let image = nftIsAddedToBasket ? + UIImage(named: "basket_delete")?.withTintColor(UIColor.nftBlack) + : UIImage(named: "basket_add")?.withTintColor(UIColor.nftBlack) + addToBasketButton.setImage(image?.withRenderingMode(.automatic), for: .normal) + } + + @objc + private func didTapLikeButton() { + delegate?.didChangeLike(self) + } + + @objc + private func basketButtonTapped() { + delegate?.switchNftBasketState(self) + } + + @objc + private func didSelectRate(gesture: UITapGestureRecognizer) { + let location = gesture.location(in: starsContainer) + let starWidth = starsContainer.bounds.width / CGFloat(starsQuantity) + let rate = Int(location.x / starWidth) + 1 + + if rate != self.selectedRate { + feedbackGenerator.selectionChanged() + self.selectedRate = rate + } + + starsContainer.arrangedSubviews.forEach { subview in + guard let starImageView = subview as? UIImageView else { + return + } + starImageView.isHighlighted = starImageView.tag <= rate + } + } +} diff --git a/FakeNFT/Scenes /Catalog/CatalogCollectionView/View/CatalogCollectionCellDelegate.swift b/FakeNFT/Scenes /Catalog/CatalogCollectionView/View/CatalogCollectionCellDelegate.swift new file mode 100755 index 0000000000..7d120051e6 --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogCollectionView/View/CatalogCollectionCellDelegate.swift @@ -0,0 +1,13 @@ +// +// CatalogCollectionCellDelegate.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 23.11.2023. +// + +import Foundation + +protocol CatalogCollectionCellDelegate: AnyObject { + func didChangeLike(_ cell: CatalogCollectionCell) + func switchNftBasketState(_ cell: CatalogCollectionCell) +} diff --git a/FakeNFT/Scenes /Catalog/CatalogCollectionView/View/CatalogCollectionView.swift b/FakeNFT/Scenes /Catalog/CatalogCollectionView/View/CatalogCollectionView.swift new file mode 100755 index 0000000000..879e1a0ea1 --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogCollectionView/View/CatalogCollectionView.swift @@ -0,0 +1,379 @@ +// +// CatalogView.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 07.11.2023. + +import UIKit +import Kingfisher +import Combine + +final class CatalogCollectionView: UIView { + // MARK: - public properties + weak var delegate: CatalogCollectionViewDelegate? + + // MARK: - Private properties + private let reuseIdentifier = "CatalogCollectionCell" + private let viewModel: CatalogCollectionViewModelProtocol + private lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + + scrollView.contentInsetAdjustmentBehavior = .never + scrollView.alwaysBounceVertical = true + scrollView.isScrollEnabled = true + scrollView.translatesAutoresizingMaskIntoConstraints = false + + return scrollView + }() + private lazy var contentView: UIView = { + let view = UIView() + + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + private lazy var collectionCoverImageView: UIImageView = { + let imageView = UIImageView() + + imageView.clipsToBounds = true + imageView.layer.cornerRadius = 12 + imageView.translatesAutoresizingMaskIntoConstraints = false + + return imageView + }() + private lazy var catalogNameLabel: UILabel = { + let label = UILabel() + + label.font = UIFont.systemFont(ofSize: 22, weight: .bold) + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + private lazy var authorNameLabel: UILabel = { + let label = UILabel() + + label.text = L10n.CatalogCollection.authorLabel + label.font = UIFont.systemFont(ofSize: 13, weight: .regular) + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + private lazy var authorPageLinkButton: UIButton = { + let button = UIButton() + + button.isHidden = true + button.titleLabel?.textAlignment = .center + button.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .regular) + button.setTitleColor(UIColor.nftBlueUniversal, for: .normal) + button.addTarget(self, action: #selector(authorButtonTapped), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + + return button + }() + private lazy var catalogDescriptionLabel: UILabel = { + let label = UILabel() + + label.font = UIFont.systemFont(ofSize: 13, weight: .regular) + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + private lazy var collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .vertical + + let collection = UICollectionView(frame: .zero, collectionViewLayout: layout) + collection.isScrollEnabled = false + collection.allowsMultipleSelection = false + collection.backgroundColor = UIColor.nftWhite + collection.translatesAutoresizingMaskIntoConstraints = false + collection.allowsSelection = false + + return collection + }() + private lazy var authorLinkAnimationView: UIView = { + let view = UIView() + + view.isHidden = false + view.clipsToBounds = true + view.layer.cornerRadius = 12 + view.backgroundColor = .lightGray.withAlphaComponent(0.4) + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + private lazy var numberOfCellsInRow: CGFloat = { + viewModel.calculateCollectionViewHeight(numberOfCellsInRow: 3) + }() + private var subscribes = [AnyCancellable]() + + init(frame: CGRect, viewModel: CatalogCollectionViewModelProtocol, delegate: CatalogCollectionViewDelegate) { + self.viewModel = viewModel + self.delegate = delegate + super.init(frame: frame) + backgroundColor = UIColor.nftWhite + setupUI() + setupCollectionCoverImageView() + bind() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + collectionCoverImageView.kf.cancelDownloadTask() + } + + // MARK: - public methods + func reloadData() { + viewModel.fetchData() + } + + // MARK: - private methods + private func bind() { + viewModel.nftsLoaderPublisher.receive(on: DispatchQueue.main) + .sink { [weak self] isCompleted in + guard let self = self else { return } + + if isCompleted { + self.collectionView.reloadData() + } + }.store(in: &subscribes) + + viewModel.authorPublisher.receive(on: DispatchQueue.main) + .sink { [weak self] author in + guard let self = self else { return } + + self.startAuthorPageLinkAnimation() + + if author != nil { + self.configureAuthor(author) + } + }.store(in: &subscribes) + + viewModel.networkErrorPublisher.receive(on: DispatchQueue.main) + .sink { [weak self] error in + guard let self = self else { return } + if error != nil { + self.delegate?.showErrorAlert() + } + }.store(in: &subscribes) + } + + private func configureAuthor(_ author: Author?) { + stopAuthorPageLinkAnimation() + authorPageLinkButton.setTitle(author?.name, for: .normal) + layoutSubviews() + } + + private func setupCollectionCoverImageView() { + collectionCoverImageView.kf.setImage(with: viewModel.catalogCollection.coverURL) + } + + private func setupUI() { + collectionView.register(CatalogCollectionCell.self, forCellWithReuseIdentifier: reuseIdentifier) + collectionView.dataSource = self + collectionView.delegate = self + + addSubviews() + applyConstraints() + + catalogNameLabel.text = viewModel.catalogCollection.name + catalogDescriptionLabel.text = viewModel.catalogCollection.desription + } + + private func addSubviews() { + addSubview(scrollView) + + scrollView.addSubview(contentView) + + contentView.addSubview(collectionCoverImageView) + contentView.addSubview(catalogNameLabel) + contentView.addSubview(authorNameLabel) + contentView.addSubview(authorLinkAnimationView) + contentView.addSubview(authorPageLinkButton) + contentView.addSubview(catalogDescriptionLabel) + contentView.addSubview(collectionView) + } + + private func applyConstraints() { + + NSLayoutConstraint.activate([ + + // scrollView constraints + scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: topAnchor), + scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor), + scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor), + scrollView.widthAnchor.constraint(equalTo: widthAnchor), + scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), + + // contentView constraints + contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), + contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 1), + + // collectionCoverImageView contraints + collectionCoverImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + collectionCoverImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + collectionCoverImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + collectionCoverImageView.heightAnchor.constraint(equalToConstant: 310), + + // catalogNameLabel constraints + catalogNameLabel.topAnchor.constraint(equalTo: collectionCoverImageView.bottomAnchor, constant: 16), + catalogNameLabel.leadingAnchor.constraint(equalTo: collectionCoverImageView.leadingAnchor, constant: 16), + + // authorNameLabel constraints + authorNameLabel.topAnchor.constraint(equalTo: catalogNameLabel.bottomAnchor, constant: 13), + authorNameLabel.leadingAnchor.constraint(equalTo: catalogNameLabel.leadingAnchor), + + // authorPageLinkButton constraints + authorPageLinkButton.topAnchor.constraint(equalTo: catalogNameLabel.bottomAnchor, constant: 6), + authorPageLinkButton.leadingAnchor.constraint(equalTo: authorNameLabel.trailingAnchor, constant: 4), + authorPageLinkButton.heightAnchor.constraint(equalToConstant: 28), + + // authorLinkAnimationView constraints + authorLinkAnimationView.topAnchor.constraint(equalTo: catalogNameLabel.bottomAnchor, constant: 8), + authorLinkAnimationView.leadingAnchor.constraint(equalTo: authorNameLabel.trailingAnchor, constant: 4), + authorLinkAnimationView.heightAnchor.constraint(equalToConstant: 24), + authorLinkAnimationView.widthAnchor.constraint(equalToConstant: 200), + + // catalogDescriptionLabel constraints + catalogDescriptionLabel.topAnchor.constraint(equalTo: authorNameLabel.bottomAnchor, constant: 5), + catalogDescriptionLabel.leadingAnchor.constraint(equalTo: authorNameLabel.leadingAnchor), + catalogDescriptionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + + // collectionView constraits + collectionView.topAnchor.constraint(equalTo: catalogDescriptionLabel.bottomAnchor, constant: 24), + collectionView.leadingAnchor.constraint(equalTo: catalogDescriptionLabel.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: catalogDescriptionLabel.trailingAnchor), + collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + collectionView.heightAnchor.constraint(equalToConstant: numberOfCellsInRow) + ]) + } + + @objc + private func authorButtonTapped() { + delegate?.presentAuthorPage(viewModel.author?.website) + } + + private func startAuthorPageLinkAnimation() { + authorLinkAnimationView.isHidden = false + authorLinkAnimationView.addFlashLayer() + } + + private func stopAuthorPageLinkAnimation() { + authorLinkAnimationView.isHidden = true + authorPageLinkButton.isHidden = false + } +} + +// MARK: - UICollectionViewDataSource +extension CatalogCollectionView: UICollectionViewDataSource { + func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + viewModel.catalogCollection.nfts.count + } + + func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: reuseIdentifier, + for: indexPath) as? CatalogCollectionCell else { + return UICollectionViewCell() + } + + if viewModel.nftsLoadingIsCompleted { + cell.delegate = self + + let model = viewModel.nfts[indexPath.row] + + cell.nftIsLiked = viewModel.nftIsLiked(model.id) + cell.nftIsAddedToBasket = viewModel.nftsIsAddedToCart(model.id) + cell.configureCell(model) + } else { + cell.createAnimationView() + } + return cell + } +} + +// MARK: - UICollectionViewDelegateFlowLayout +extension CatalogCollectionView: UICollectionViewDelegateFlowLayout { + func collectionView( + _ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + let geometricParams = GeometricParams(cellCount: 3, leftInset: 20, rightInset: 20, cellSpacing: 9) + + let availableWidth = frame.width - geometricParams.paddingWidth + let cellWidth = availableWidth / CGFloat(geometricParams.cellCount) + + return CGSize(width: cellWidth, height: 192) + } + + func collectionView( + _ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return 8 + } + + func collectionView( + _ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return 9 + } +} + +// MARK: - CatalogCollectionCellDelegate +extension CatalogCollectionView: CatalogCollectionCellDelegate { + func switchNftBasketState(_ cell: CatalogCollectionCell) { + delegate?.startAnimatingActivityIndicator() + guard let indexPath = collectionView.indexPath(for: cell) else { + return + } + let id = viewModel.nfts[indexPath.row].id + + viewModel.switchNftBasketState(with: id) { [weak self] result in + guard let self = self else { return } + switch result { + case .success: + self.delegate?.stopAnimatingActivityIndicator() + cell.switchBasketState() + case .failure: + self.delegate?.stopAnimatingActivityIndicator() + self.delegate?.showNftInteractionErrorAlert() + } + } + } + + func didChangeLike(_ cell: CatalogCollectionCell) { + delegate?.startAnimatingActivityIndicator() + + guard let indexPath = collectionView.indexPath(for: cell) else { + return + } + + let id = viewModel.nfts[indexPath.row].id + viewModel.changeLikeForNft(with: id) { [weak self] result in + guard let self = self else { return } + switch result { + case .success: + self.delegate?.stopAnimatingActivityIndicator() + cell.changeLikeState() + case .failure: + self.delegate?.stopAnimatingActivityIndicator() + self.delegate?.showNftInteractionErrorAlert() + } + } + } +} diff --git a/FakeNFT/Scenes /Catalog/CatalogCollectionView/View/CatalogCollectionViewController.swift b/FakeNFT/Scenes /Catalog/CatalogCollectionView/View/CatalogCollectionViewController.swift new file mode 100755 index 0000000000..e67cb3ecbb --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogCollectionView/View/CatalogCollectionViewController.swift @@ -0,0 +1,85 @@ +// +// CatalogCollectionViewController.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 07.11.2023. +// + +import UIKit + +final class CatalogCollectionViewController: UIViewController { + + // MARK: - Private properties + private let catalog: Catalog + private var catalogCollectionView: CatalogCollectionView! + private lazy var backButton: UIBarButtonItem = { + let button = UIBarButtonItem() + + button.image = UIImage(named: "backward")?.withRenderingMode(.alwaysTemplate) + button.style = .plain + button.target = self + button.action = #selector(backButtonTapped) + button.imageInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + button.tintColor = UIColor.nftBlack + + return button + }() + + init(catalog: Catalog) { + self.catalog = catalog + super.init(nibName: nil, bundle: nil) + + let viewModel = CatalogAssembly.buildCatalogCollectionViewModel(catalog: catalog) + + catalogCollectionView = CatalogCollectionView(frame: .zero, viewModel: viewModel, delegate: self) + self.view = catalogCollectionView + + configureNavBar() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureNavBar() { + navigationItem.hidesBackButton = true + navigationItem.leftBarButtonItem = backButton + } + + @objc + private func backButtonTapped() { + navigationController?.popViewController(animated: true) + } +} + +// MARK: - CatalogCollectionViewDelegate +extension CatalogCollectionViewController: CatalogCollectionViewDelegate { + func presentAuthorPage(_ url: URL?) { + let viewModel = WebViewViewModel() + let view = WebViewController(viewModel: viewModel ,url: url) + navigationController?.pushViewController(view, animated: true) + } + + func dismissView() { + navigationController?.popViewController(animated: true) + } + + func showErrorAlert() { + AlertPresenter.showError(in: self) { [weak self] in + guard let self = self else { return } + self.catalogCollectionView.reloadData() + } + } + + func startAnimatingActivityIndicator() { + UIBlockingProgressHUD.show() + } + + func stopAnimatingActivityIndicator() { + UIBlockingProgressHUD.dismiss() + } + + func showNftInteractionErrorAlert() { + AlertPresenter.showNftInteractionError(in: self) + } +} diff --git a/FakeNFT/Scenes /Catalog/CatalogCollectionView/View/CatalogCollectionViewDelegate.swift b/FakeNFT/Scenes /Catalog/CatalogCollectionView/View/CatalogCollectionViewDelegate.swift new file mode 100755 index 0000000000..858dab6a6e --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogCollectionView/View/CatalogCollectionViewDelegate.swift @@ -0,0 +1,17 @@ +// +// CatalogCollectionViewDelegate.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 23.11.2023. +// + +import Foundation + +protocol CatalogCollectionViewDelegate: AnyObject { + func dismissView() + func showErrorAlert() + func showNftInteractionErrorAlert() + func startAnimatingActivityIndicator() + func stopAnimatingActivityIndicator() + func presentAuthorPage(_ url: URL?) +} diff --git a/FakeNFT/Scenes /Catalog/CatalogCollectionView/ViewModel/CatalogCollectionViewModel.swift b/FakeNFT/Scenes /Catalog/CatalogCollectionView/ViewModel/CatalogCollectionViewModel.swift new file mode 100755 index 0000000000..cd6bfe5652 --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogCollectionView/ViewModel/CatalogCollectionViewModel.swift @@ -0,0 +1,156 @@ +// +// CatalogCollectionViewModel.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 07.11.2023. +// + +import Foundation +import Combine + +final class CatalogCollectionViewModel: CatalogCollectionViewModelProtocol { + + // MARK: - public properties + @Published var nftsLoadingIsCompleted: Bool = false + @Published var author: Author? + @Published var networkError: Error? + var nftsLoaderPublisher: Published.Publisher { $nftsLoadingIsCompleted } + var authorPublisher: Published.Publisher { $author } + var networkErrorPublisher: Published.Publisher { $networkError } + var nfts: [Nft] = [] + var catalogCollection: Catalog + var profileLikes: [String] = LikesStorage.shared.likes + + // MARK: - private properties + private let collectionService: CatalogCollectionService + private var likesLoadingIsCompleted = false + private var addedToCartNfts = PurchaseCartStorage.shared.nfts + + init(catalogCollection: Catalog, service: CatalogCollectionService) { + self.catalogCollection = catalogCollection + self.collectionService = service + fetchData() + } + + // MARK: - public methods + func fetchData() { + networkError = nil + if !nftsLoadingIsCompleted { + clearNfts() + fetchNfts() + } + if author == nil { + fetchAuthorProfile() + } + } + + func nftIsLiked(_ id: String) -> Bool { + profileLikes.contains(id) + } + + func nftsIsAddedToCart(_ id: String) -> Bool { + addedToCartNfts.contains(id) + } + + func calculateCollectionViewHeight(numberOfCellsInRow: Int) -> CGFloat { + let numberOfCells = catalogCollection.nfts.count + let height = 192 + let spacing = 9 + var result: Int = 0 + if numberOfCells % numberOfCellsInRow == 0 { + result = (numberOfCells / numberOfCellsInRow) * height + + ((numberOfCells / numberOfCellsInRow) * spacing) + } else { + result = (numberOfCells / numberOfCellsInRow + 1) * height + + ((numberOfCells / numberOfCellsInRow) * spacing) + } + return CGFloat(result) + } + + func changeLikeForNft(with id: String, completion: @escaping (Result) -> Void) { + if profileLikes.contains(id) { + profileLikes.removeAll { like in + like == id + } + } else { + profileLikes.append(id) + } + + let profile = ProfileLike(likes: profileLikes) + + collectionService.putProfileLikes(profile: profile) { result in + switch result { + case .success(let profile): + LikesStorage.shared.likes = profile.likes + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } + } + + func switchNftBasketState(with id: String, completion: @escaping (Result) -> Void) { + if addedToCartNfts.contains(id) { + addedToCartNfts.removeAll { nftId in + nftId == id + } + } else { + addedToCartNfts.append(id) + } + let cart = PurchaseCart(nfts: addedToCartNfts) + + collectionService.putNftsToCart(cart: cart) { result in + switch result { + case .success(let cart): + PurchaseCartStorage.shared.nfts = cart.nfts + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } + } + + // MARK: - private methods + private func fetchNfts() { + catalogCollection.nfts.forEach { id in + collectionService.loadNftForCollection(id: String(id)) { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let nft): + self.nfts.append(nft) + if self.nfts.count == self.catalogCollection.nfts.count { + self.sortNfts() + self.nftsLoadingIsCompleted = true + } + case .failure(let error): + self.clearNfts() + if self.networkError == nil && !self.nftsLoadingIsCompleted { + self.networkError = error + } + } + } + } + } + + private func fetchAuthorProfile() { + collectionService.fetchAuthorProfile(id: catalogCollection.authorID) { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let authorLink): + self.author = authorLink + case .failure(let error): + if self.networkError == nil && self.author == nil { + self.networkError = error + } + } + } + } + + private func sortNfts() { + nfts.sort { $0.name < $1.name } + } + + private func clearNfts() { + nfts.removeAll() + } +} diff --git a/FakeNFT/Scenes /Catalog/CatalogCollectionView/ViewModel/CatalogCollectionViewModelProtocol.swift b/FakeNFT/Scenes /Catalog/CatalogCollectionView/ViewModel/CatalogCollectionViewModelProtocol.swift new file mode 100755 index 0000000000..b683b35618 --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogCollectionView/ViewModel/CatalogCollectionViewModelProtocol.swift @@ -0,0 +1,26 @@ +// +// CatalogCollectionViewModelProtocol.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 22.11.2023. +// + +import Foundation + +protocol CatalogCollectionViewModelProtocol: AnyObject { + var nftsLoadingIsCompleted: Bool { get } + var nftsLoaderPublisher: Published.Publisher { get } + var nfts: [Nft] { get } + var catalogCollection: Catalog { get } + var author: Author? { get } + var authorPublisher: Published.Publisher { get } + var networkError: Error? { get } + var networkErrorPublisher: Published.Publisher { get } + var profileLikes: [String] { get set } + func calculateCollectionViewHeight(numberOfCellsInRow: Int) -> CGFloat + func fetchData() + func changeLikeForNft(with id: String, completion: @escaping (Result) -> Void) + func nftIsLiked(_ id: String) -> Bool + func nftsIsAddedToCart(_ id: String) -> Bool + func switchNftBasketState(with id: String, completion: @escaping (Result) -> Void) +} diff --git a/FakeNFT/Scenes /Catalog/CatalogView/Cells/CatalogTableViewCell.swift b/FakeNFT/Scenes /Catalog/CatalogView/Cells/CatalogTableViewCell.swift new file mode 100755 index 0000000000..c035cac73f --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogView/Cells/CatalogTableViewCell.swift @@ -0,0 +1,114 @@ +// +// CatalogTableViewCell.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 04.11.2023. +// + +import UIKit +import Kingfisher + +final class CatalogTableViewCell: UITableViewCell { + + // MARK: - Private properties + private let nftImageView: UIImageView = { + let imageView = UIImageView() + + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + imageView.layer.cornerRadius = 12 + imageView.translatesAutoresizingMaskIntoConstraints = false + + return imageView + }() + private let descriptionLabel: UILabel = { + let label = UILabel() + + label.textColor = UIColor.nftBlack // UIColor(red: 0.102, green: 0.106, blue: 0.133, alpha: 1) + label.font = UIFont.systemFont(ofSize: 17, weight: .bold) + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + private lazy var gradient: GradientView = { + return GradientView(frame: self.bounds) + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + nftImageView.kf.cancelDownloadTask() + } + + // MARK: - Public methods + func configureCell(model: Catalog) { + startAnimation() + + descriptionLabel.text = "\(model.name) (\(model.nfts.count))" + + nftImageView.kf.setImage( + with: model.coverURL, + placeholder: nil, + completionHandler: { [weak self] _ in + guard let self = self else { return } + self.stopAnimation() + self.isUserInteractionEnabled = true + } + ) + } + + // MARK: - Private methods + private func setupUI() { + contentView.backgroundColor = .clear + contentView.layer.cornerRadius = 12 + + addSubviews() + applyConstraints() + + gradient.translatesAutoresizingMaskIntoConstraints = false + gradient.isHidden = true + } + + private func addSubviews() { + contentView.addSubview(nftImageView) + contentView.addSubview(descriptionLabel) + contentView.addSubview(gradient) + } + + private func applyConstraints() { + NSLayoutConstraint.activate([ + nftImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), + nftImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + nftImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -6), + nftImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -27), + + gradient.topAnchor.constraint(equalTo: nftImageView.topAnchor), + gradient.leadingAnchor.constraint(equalTo: nftImageView.leadingAnchor), + gradient.trailingAnchor.constraint(equalTo: nftImageView.trailingAnchor), + gradient.bottomAnchor.constraint(equalTo: nftImageView.bottomAnchor), + + descriptionLabel.topAnchor.constraint(equalTo: nftImageView.bottomAnchor, constant: 4), + descriptionLabel.leadingAnchor.constraint(equalTo: nftImageView.leadingAnchor), + descriptionLabel.trailingAnchor.constraint(equalTo: nftImageView.trailingAnchor), + descriptionLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -1) + ]) + } + + private func startAnimation() { + gradient.isHidden = false + gradient.startAnimating() + } + + private func stopAnimation() { + gradient.stopAnimating() + gradient.isHidden = true + } +} diff --git a/FakeNFT/Scenes /Catalog/CatalogView/Cells/GradientCell.swift b/FakeNFT/Scenes /Catalog/CatalogView/Cells/GradientCell.swift new file mode 100755 index 0000000000..4352f38bae --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogView/Cells/GradientCell.swift @@ -0,0 +1,65 @@ +// +// GradientCell.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 07.11.2023. +// + +import UIKit + +final class GradientView: UIView { + + // MARK: - Private properties + private let gradientLayer = CAGradientLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + setupGradient() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + gradientLayer.frame = bounds + } + + // MARK: - Public methods + func startAnimating() { + let animation = CABasicAnimation(keyPath: "locations") + + animation.fromValue = [-0.1, 0, 0.1, 0.2, 0.3] + animation.toValue = [1, 1.1, 1.2, 1.3, 1.4] + animation.duration = 0.7 + animation.repeatCount = .infinity + + gradientLayer.add(animation, forKey: "locations") + } + + func stopAnimating() { + gradientLayer.removeAnimation(forKey: "locations") + } + + // MARK: - Private methods + private func setupGradient() { + gradientLayer.colors = [ + UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 0.2).cgColor, + UIColor(red: 0.8, green: 0.8, blue: 0.8, alpha: 0.2).cgColor, + UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 0.2).cgColor, + UIColor(red: 0.8, green: 0.8, blue: 0.8, alpha: 0.2).cgColor, + UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 0.2).cgColor + ] + + gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.3) + gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.7) + gradientLayer.locations = [0.4, 0.5, 0.6] + + gradientLayer.frame = bounds + gradientLayer.cornerRadius = 12 + gradientLayer.masksToBounds = true + + layer.addSublayer(gradientLayer) + } +} diff --git a/FakeNFT/Scenes /Catalog/CatalogView/View/CatalogView.swift b/FakeNFT/Scenes /Catalog/CatalogView/View/CatalogView.swift new file mode 100755 index 0000000000..a57795d73c --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogView/View/CatalogView.swift @@ -0,0 +1,141 @@ +// +// CatalogView.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 04.11.2023. +// + +import UIKit +import Combine + +final class CatalogView: UIView { + + // MARK: - Public properties + var viewModel: CatalogViewModelProtocol + weak var delegate: CatalogViewDelegate? + + // MARK: - Private properties + private let reuseIdentifier = "CatalogTableViewCell" + private let tableView: UITableView = { + let tableView = UITableView() + + tableView.verticalScrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + tableView.backgroundColor = .clear + tableView.separatorStyle = .none + tableView.allowsMultipleSelection = false + tableView.translatesAutoresizingMaskIntoConstraints = false + + return tableView + }() + private var subscribes = [AnyCancellable]() + + init(frame: CGRect, viewModel: CatalogViewModelProtocol, delegate: CatalogViewDelegate) { + self.viewModel = viewModel + self.delegate = delegate + super.init(frame: frame) + setupUI() + bind() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Public methods + func reloadData() { + viewModel.fetchCatalog() + } + + func updateStorages() { + viewModel.updateStorages() + } + + // MARK: - Private methods + private func setupUI() { + backgroundColor = .systemBackground + + tableView.dataSource = self + tableView.delegate = self + tableView.register(CatalogTableViewCell.self, forCellReuseIdentifier: reuseIdentifier) + + addSubviews() + applyConstraints() + } + + private func bind() { + viewModel.catalogPublisher.receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + + self.tableView.reloadData() + + }.store(in: &subscribes) + + viewModel.loadingDataPublisher.receive(on: DispatchQueue.main) + .sink { [weak self] isLoading in + guard let self = self else { return } + + if isLoading { + self.delegate?.startAnimatingActivityIndicator() + } else { + self.delegate?.stopAnimatingActivityIndicator() + } + }.store(in: &subscribes) + + viewModel.errorPublisher.receive(on: DispatchQueue.main) + .sink { [weak self] error in + guard let self = self else { return } + if error != nil { + self.delegate?.showErrorAlert() + } + }.store(in: &subscribes) + } + + private func addSubviews() { + addSubview(tableView) + } + + private func applyConstraints() { + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: topAnchor, constant: 40), + tableView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + tableView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10), + tableView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } +} + +// MARK: - UITableViewDataSource +extension CatalogView: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.catalog.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell( + withIdentifier: reuseIdentifier, + for: indexPath) as? CatalogTableViewCell else { + return UITableViewCell() + } + cell.selectionStyle = .none + + cell.isUserInteractionEnabled = false + + let model = viewModel.catalog[indexPath.row] + + cell.configureCell(model: model) + return cell + } +} + +// MARK: - UITableViewDelegate +extension CatalogView: UITableViewDelegate { + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 187 + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let model = viewModel.catalog[indexPath.row] + delegate?.selectedCategory(model) + } +} diff --git a/FakeNFT/Scenes /Catalog/CatalogView/View/CatalogViewController.swift b/FakeNFT/Scenes /Catalog/CatalogView/View/CatalogViewController.swift new file mode 100755 index 0000000000..576e1ad616 --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogView/View/CatalogViewController.swift @@ -0,0 +1,96 @@ +// +// CatalogViewController.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 04.11.2023. +// + +import UIKit + +final class CatalogViewController: UIViewController { + + // MARK: - Private properties + private var viewModel: CatalogViewModelProtocol! + private var catalogView: CatalogView! + private lazy var sortButton: UIBarButtonItem = { + let button = UIBarButtonItem() + + button.image = UIImage(named: "sort_button")?.withRenderingMode(.alwaysTemplate) + button.style = .plain + button.target = self + button.action = #selector(sortButtonTapped) + button.imageInsets = UIEdgeInsets(top: 2, left: 0, bottom: 0, right: 9) + button.tintColor = UIColor.nftBlack + + return button + }() + + init() { + super.init(nibName: nil, bundle: nil) + setupUI() + + viewModel = CatalogAssembly.buildCatalogViewModel() + + catalogView = CatalogView(frame: .zero, viewModel: viewModel, delegate: self) + self.view = catalogView + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + catalogView.updateStorages() + } + + // MARK: - Private mathods + private func setupUI() { + configureNavBar() + } + + private func configureNavBar() { + navigationItem.rightBarButtonItem = sortButton + } + + @objc + private func sortButtonTapped() { + let model = AlertModel( + message: L10n.FilterAlert.title, + nameSortText: L10n.FilterAlert.nameSortTitle, + quantitySortText: L10n.FilterAlert.quantitySortTitle, + cancelButtonText: L10n.FilterAlert.cancelButtonTitle) { [weak self] in + guard let self = self else { return } + self.viewModel.sortCatalogByName() + } sortQuantityCompletion: { [weak self] in + guard let self = self else { return } + self.viewModel.sortCatalogByQuantity() + } + + AlertPresenter.show(in: self, model: model) + } +} + +// MARK: - CatalogViewControllerDelegate +extension CatalogViewController: CatalogViewDelegate { + func showErrorAlert() { + AlertPresenter.showError(in: self) { [weak self] in + guard let self = self else { return } + self.catalogView.reloadData() + } + } + + func selectedCategory(_ model: Catalog) { + let viewController = CatalogCollectionViewController(catalog: model) + viewController.hidesBottomBarWhenPushed = true + navigationController?.pushViewController(viewController, animated: true) + } + + func startAnimatingActivityIndicator() { + UIBlockingProgressHUD.show() + } + + func stopAnimatingActivityIndicator() { + UIBlockingProgressHUD.dismiss() + } +} diff --git a/FakeNFT/Scenes /Catalog/CatalogView/View/CatalogViewDelegate.swift b/FakeNFT/Scenes /Catalog/CatalogView/View/CatalogViewDelegate.swift new file mode 100755 index 0000000000..341621feb2 --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogView/View/CatalogViewDelegate.swift @@ -0,0 +1,15 @@ +// +// CatalogViewDelegate.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 23.11.2023. +// + +import Foundation + +protocol CatalogViewDelegate: AnyObject { + func selectedCategory(_ model: Catalog) + func showErrorAlert() + func startAnimatingActivityIndicator() + func stopAnimatingActivityIndicator() +} diff --git a/FakeNFT/Scenes /Catalog/CatalogView/ViewModel/CatalogViewModel.swift b/FakeNFT/Scenes /Catalog/CatalogView/ViewModel/CatalogViewModel.swift new file mode 100755 index 0000000000..0c26a3c3f7 --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogView/ViewModel/CatalogViewModel.swift @@ -0,0 +1,107 @@ +// +// CatalogViewModel.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 04.11.2023. +// + +import Foundation +import Combine + +final class CatalogViewModel: CatalogViewModelProtocol { + + // MARK: - Public properties + @Published var isLoadingData: Bool = true + @Published var catalog: [Catalog] = [] + @Published var networkError: Error? + var catalogPublisher: Published>.Publisher { $catalog } + var loadingDataPublisher: Published.Publisher { $isLoadingData } + var errorPublisher: Published.Publisher { $networkError } + + // MARK: - Private properties + private var filter: CatalogFilter? + private var catalogService: CatalogServiceProtocol + private var subscribes = [AnyCancellable]() + + init(catalogService: CatalogServiceProtocol) { + self.catalogService = catalogService + + fetchCatalog() + fetchProfileLikes() + fetchAddedToBasketNfts() + + self.filter = CatalogFilter( + rawValue: CatalogFilterStorage.shared.filterDescriptor ?? CatalogFilter.filterQuantity.rawValue + ) + sortCatalog() + } + + // MARK: - Public methods + func sortCatalog() { + switch filter { + case .filterName: + sortCatalogByName() + case .filterQuantity: + sortCatalogByQuantity() + default: + sortCatalogByQuantity() + } + } + + func sortCatalogByName() { + catalog.sort { $0.name < $1.name } + CatalogFilterStorage.shared.filterDescriptor = CatalogFilter.filterName.rawValue + } + + func sortCatalogByQuantity() { + catalog.sort { $0.nfts.count > $1.nfts.count } + CatalogFilterStorage.shared.filterDescriptor = CatalogFilter.filterQuantity.rawValue + } + + func updateStorages() { + fetchProfileLikes() + fetchProfileLikes() + } + + // MARK: - Private mathods + func fetchCatalog() { + isLoadingData = true + catalogService.fetchCatalog { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let catalogRes): + self.isLoadingData = false + self.catalog = catalogRes + self.sortCatalog() + case .failure(let error): + self.isLoadingData = false + self.networkError = error + } + } + } + + // methods implemented for temporary use in catalog epic to handle likes and addToBasket interaction + // when the full project is merged - these two methods will be removed + // both methods should be implemented in Profile epic + private func fetchProfileLikes() { + catalogService.fetchProfileLikes { result in + switch result { + case .success(let profile): + LikesStorage.shared.likes = profile.likes + case .failure(let error): + print(error) + } + } + } + + private func fetchAddedToBasketNfts() { + catalogService.fetchAddedToBasketNfts { result in + switch result { + case .success(let order): + PurchaseCartStorage.shared.nfts = order.nfts + case .failure(let error): + print(error) + } + } + } +} diff --git a/FakeNFT/Scenes /Catalog/CatalogView/ViewModel/CatalogViewModelProtocol.swift b/FakeNFT/Scenes /Catalog/CatalogView/ViewModel/CatalogViewModelProtocol.swift new file mode 100755 index 0000000000..08998a62ae --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogView/ViewModel/CatalogViewModelProtocol.swift @@ -0,0 +1,22 @@ +// +// CatalogViewModelProtocol.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 22.11.2023. +// + +import Foundation + +protocol CatalogViewModelProtocol: AnyObject { + var catalog: [Catalog] { get set } + var catalogPublisher: Published>.Publisher { get } + var isLoadingData: Bool { get } + var loadingDataPublisher: Published.Publisher { get } + var networkError: Error? { get } + var errorPublisher: Published.Publisher { get } + func sortCatalogByName() + func sortCatalogByQuantity() + func sortCatalog() + func fetchCatalog() + func updateStorages() +} diff --git a/FakeNFT/Scenes /Catalog/CatalogViewController.swift b/FakeNFT/Scenes /Catalog/CatalogViewController.swift new file mode 100755 index 0000000000..483e315b6b --- /dev/null +++ b/FakeNFT/Scenes /Catalog/CatalogViewController.swift @@ -0,0 +1,8 @@ +import UIKit + +final class CatalogViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .nftWhite + } +} diff --git a/FakeNFT/Scenes /Catalog/TestCatalogController.swift b/FakeNFT/Scenes /Catalog/TestCatalogController.swift deleted file mode 100644 index d58a55667a..0000000000 --- a/FakeNFT/Scenes /Catalog/TestCatalogController.swift +++ /dev/null @@ -1,41 +0,0 @@ -import UIKit - -final class TestCatalogViewController: UIViewController { - - let servicesAssembly: ServicesAssembly - let testNftButton = UIButton() - - init(servicesAssembly: ServicesAssembly) { - self.servicesAssembly = servicesAssembly - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .systemBackground - - view.addSubview(testNftButton) - testNftButton.constraintCenters(to: view) - testNftButton.setTitle(Constants.openNftTitle, for: .normal) - testNftButton.addTarget(self, action: #selector(showNft), for: .touchUpInside) - testNftButton.setTitleColor(.systemBlue, for: .normal) - } - - @objc - func showNft() { - let assembly = NftDetailAssembly(servicesAssembler: servicesAssembly) - let nftInput = NftDetailInput(id: Constants.testNftId) - let nftViewController = assembly.build(with: nftInput) - present(nftViewController, animated: true) - } -} - -private enum Constants { - static let openNftTitle = NSLocalizedString("Catalog.openNft", comment: "") - static let testNftId = "22" -} diff --git a/FakeNFT/Scenes /Common/Protocols/ErrorView.swift b/FakeNFT/Scenes /Common/Protocols/ErrorView.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Scenes /Common/Protocols/LoadingView.swift b/FakeNFT/Scenes /Common/Protocols/LoadingView.swift old mode 100644 new mode 100755 index 4b5b24cc50..78988c32d1 --- a/FakeNFT/Scenes /Common/Protocols/LoadingView.swift +++ b/FakeNFT/Scenes /Common/Protocols/LoadingView.swift @@ -2,17 +2,22 @@ import ProgressHUD import UIKit protocol LoadingView { - var activityIndicator: UIActivityIndicatorView { get } func showLoading() func hideLoading() } extension LoadingView { + private static var window: UIWindow? { + return UIApplication.shared.windows.first + } + func showLoading() { - activityIndicator.startAnimating() + Self.window?.isUserInteractionEnabled = false + ProgressHUD.show() } func hideLoading() { - activityIndicator.stopAnimating() + Self.window?.isUserInteractionEnabled = true + ProgressHUD.dismiss() } } diff --git a/FakeNFT/Scenes /Common/Views/LinePageControl.swift b/FakeNFT/Scenes /Common/Views/LinePageControl.swift old mode 100644 new mode 100755 index 2a27b4c91a..bc49207c3c --- a/FakeNFT/Scenes /Common/Views/LinePageControl.swift +++ b/FakeNFT/Scenes /Common/Views/LinePageControl.swift @@ -61,7 +61,7 @@ final class LinePageControl: UIView { func selectedSegmentChanged() { for (index, subview) in stackView.arrangedSubviews.enumerated() { let isSelected = index == selectedItem - subview.backgroundColor = isSelected ? .segmentActive : .segmentInactive + subview.backgroundColor = isSelected ? UIColor.nftBlack : UIColor.nftLightgrey } } } diff --git a/FakeNFT/Scenes /Common/WebView/WebViewController.swift b/FakeNFT/Scenes /Common/WebView/WebViewController.swift new file mode 100755 index 0000000000..2d28648bd1 --- /dev/null +++ b/FakeNFT/Scenes /Common/WebView/WebViewController.swift @@ -0,0 +1,132 @@ +// +// ProfileWebView.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 21.11.2023. +// + +import UIKit +import WebKit +import Combine + +final class WebViewController: UIViewController { + + // MARK: - Private properties + private lazy var backButton: UIBarButtonItem = { + let button = UIBarButtonItem() + + button.image = UIImage(named: "backward")?.withRenderingMode(.alwaysTemplate) + button.style = .plain + button.target = self + button.action = #selector(backButtonTapped) + button.imageInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + button.tintColor = UIColor.nftBlack + + return button + }() + private lazy var webView: WKWebView = { + let view = WKWebView() + + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + private lazy var progressView: UIProgressView = { + let view = UIProgressView() + + view.progressTintColor = UIColor.nftBlack + view.trackTintColor = UIColor.nftLightgrey + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + private var viewModel: WebViewViewModelProtocol + private var subscribes = [AnyCancellable]() + + init(viewModel: WebViewViewModelProtocol ,url: URL?) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + + setupUI() + viewModel.viewDidLoad() + loadWebView(with: url) + + bind() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Private methods + private func setupUI() { + view.backgroundColor = UIColor.nftWhite + configureNavBar() + addSubviews() + applyConstraints() + } + + private func configureNavBar() { + navigationItem.hidesBackButton = true + navigationItem.leftBarButtonItem = backButton + } + + private func addSubviews() { + view.addSubview(webView) + view.addSubview(progressView) + } + + private func applyConstraints() { + NSLayoutConstraint.activate([ + webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + progressView.topAnchor.constraint(equalTo: webView.safeAreaLayoutGuide.topAnchor), + progressView.leadingAnchor.constraint(equalTo: webView.leadingAnchor), + progressView.trailingAnchor.constraint(equalTo: webView.trailingAnchor) + ]) + } + + private func bind() { + + viewModel.progressPublisher.receive(on: DispatchQueue.main) + .sink { [weak self] newValue in + guard let self = self else { return } + + self.setProgressValue(newValue) + self.viewModel.didUpdateProgressValue(self.webView.estimatedProgress) + + }.store(in: &subscribes) + + viewModel.progressStatePublisher.receive(on: DispatchQueue.main) + .sink { [weak self] shouldHide in + guard let self = self else { return } + + if shouldHide { + self.setProgressHidden(shouldHide) + } + }.store(in: &subscribes) + } + + private func setProgressValue (_ newValue: Float) { + progressView.progress = newValue + } + + private func setProgressHidden(_ isHidden: Bool) { + progressView.isHidden = isHidden + } + + private func loadWebView(with url: URL?) { + if let url = url { + let request = URLRequest(url: url) + webView.load(request) + } + } + + @objc + private func backButtonTapped() { + navigationController?.popViewController(animated: true) + } +} diff --git a/FakeNFT/Scenes /Common/WebView/WebViewViewModel.swift b/FakeNFT/Scenes /Common/WebView/WebViewViewModel.swift new file mode 100755 index 0000000000..1760d87979 --- /dev/null +++ b/FakeNFT/Scenes /Common/WebView/WebViewViewModel.swift @@ -0,0 +1,36 @@ +// +// AuthorWebViewViewModel.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 22.11.2023. +// + +import Foundation +import Combine + +final class WebViewViewModel: WebViewViewModelProtocol { + + // MARK: - Public properties + @Published var progressValue: Float = 0 + @Published var shouldHideProgress: Bool = false + var progressPublisher: Published.Publisher { $progressValue } + var progressStatePublisher: Published.Publisher { $shouldHideProgress } + + init() {} + + // MARK: - Public methods + func viewDidLoad() { + didUpdateProgressValue(0) + } + + func didUpdateProgressValue(_ newValue: Double) { + progressValue = Float(newValue) + + shouldHideProgress = shouldHideProgress(for: Float(newValue)) + } + + // MARK: - Private methods + private func shouldHideProgress(for value: Float) -> Bool { + abs(value - 1.0) <= 0.0001 + } +} diff --git a/FakeNFT/Scenes /Common/WebView/WebViewViewModelProtocol.swift b/FakeNFT/Scenes /Common/WebView/WebViewViewModelProtocol.swift new file mode 100755 index 0000000000..427c5a7878 --- /dev/null +++ b/FakeNFT/Scenes /Common/WebView/WebViewViewModelProtocol.swift @@ -0,0 +1,17 @@ +// +// AuthorWebViewViewModelProtocol.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 22.11.2023. +// + +import Foundation + +protocol WebViewViewModelProtocol { + var progressValue: Float { get set } + var progressPublisher: Published.Publisher { get } + var shouldHideProgress: Bool { get set } + var progressStatePublisher: Published.Publisher { get } + func viewDidLoad() + func didUpdateProgressValue(_ newValue: Double) +} diff --git a/FakeNFT/Scenes /NftDetails/Cell/NftDetailCellModel.swift b/FakeNFT/Scenes /NftDetails/Cell/NftDetailCellModel.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Scenes /NftDetails/Cell/NftImageCollectionViewCell.swift b/FakeNFT/Scenes /NftDetails/Cell/NftImageCollectionViewCell.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Scenes /NftDetails/NftByIdRequest.swift b/FakeNFT/Scenes /NftDetails/NftByIdRequest.swift new file mode 100755 index 0000000000..2ee96fbb2d --- /dev/null +++ b/FakeNFT/Scenes /NftDetails/NftByIdRequest.swift @@ -0,0 +1,10 @@ +import Foundation + +struct NFTRequest: NetworkRequest { + + let id: String + + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/nft/\(id)") + } +} diff --git a/FakeNFT/Scenes /NftDetails/NftDetailAssembly.swift b/FakeNFT/Scenes /NftDetails/NftDetailAssembly.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Scenes /NftDetails/NftDetailInput.swift b/FakeNFT/Scenes /NftDetails/NftDetailInput.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Scenes /NftDetails/NftDetailPresenter.swift b/FakeNFT/Scenes /NftDetails/NftDetailPresenter.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Scenes /NftDetails/NftDetailViewController.swift b/FakeNFT/Scenes /NftDetails/NftDetailViewController.swift old mode 100644 new mode 100755 index 6bb84a9b14..7dc3a0bc71 --- a/FakeNFT/Scenes /NftDetails/NftDetailViewController.swift +++ b/FakeNFT/Scenes /NftDetails/NftDetailViewController.swift @@ -26,7 +26,7 @@ final class NftDetailViewController: UIViewController { private lazy var closeButton: UIButton = { let button = UIButton() - button.tintColor = .closeButton + button.tintColor = UIColor.nftBlack button.setImage(UIImage(named: "close"), for: .normal) button.addTarget(self, action: #selector(close), for: .touchUpInside) return button @@ -53,7 +53,7 @@ final class NftDetailViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .white + view.backgroundColor = UIColor.nftWhite setupLayout() presenter.viewDidLoad() } diff --git a/FakeNFT/Scenes /Onboarding/CustomPageControl.swift b/FakeNFT/Scenes /Onboarding/CustomPageControl.swift new file mode 100755 index 0000000000..c36f6b742d --- /dev/null +++ b/FakeNFT/Scenes /Onboarding/CustomPageControl.swift @@ -0,0 +1,65 @@ +import UIKit + +final class CustomPageControl: UIView { + var selectSegment: Int = 0 { + didSet { + setupDynamicSegments() + } + } + + var countSegment: Int = 0 { + didSet { + createSegmentStackView() + } + } + + private let segmentStackView: UIStackView = { + let stackView = UIStackView() + stackView.spacing = 8 + stackView.distribution = .fillEqually + stackView.axis = .horizontal + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + init() { + super.init(frame: .zero) + addSubview(segmentStackView) + createSegmentStackView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupStaticSegments() { + segmentStackView.arrangedSubviews.forEach { segmentStackView.removeArrangedSubview($0) } + for _ in (0.. UIViewController? { + guard let viewControllerIndex = pages.firstIndex(of: viewController), viewControllerIndex > 0 else { + return nil + } + let previousIndex = viewControllerIndex - 1 + return pages[previousIndex] + } + + func pageViewController(_ pageViewController: UIPageViewController, + viewControllerAfter viewController: UIViewController) -> UIViewController? { + guard let viewControllerIndex = pages.firstIndex(of: viewController), + viewControllerIndex < pages.count - 1 else { + return nil + } + let nextIndex = viewControllerIndex + 1 + return pages[nextIndex] + } +} diff --git a/FakeNFT/Scenes /Onboarding/OnboardingViewController.swift b/FakeNFT/Scenes /Onboarding/OnboardingViewController.swift new file mode 100755 index 0000000000..b8aa0dd742 --- /dev/null +++ b/FakeNFT/Scenes /Onboarding/OnboardingViewController.swift @@ -0,0 +1,93 @@ +import UIKit + +final class OnboardingViewController: UIViewController { + private let pageImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + return imageView + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.textColor = UIColor.nftWhiteUniversal + label.font = UIFont.systemFont(ofSize: 32, weight: .bold) + label.textAlignment = .left + return label + }() + + private let textInfoLabel: UILabel = { + let label = UILabel() + label.textColor = UIColor.nftWhiteUniversal + label.font = .caption1 + label.textAlignment = .left + label.numberOfLines = 4 + label.lineBreakMode = .byWordWrapping + return label + }() + + private lazy var textStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 20 + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(textInfoLabel) + return stackView + }() + + init(pageImageView: UIImage, title: String, text: String) { + self.pageImageView.image = pageImageView + self.titleLabel.text = title + self.textInfoLabel.text = text + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + createView() + addGradient() + setLetterSpacing(for: titleLabel) + } + + private func createView() { + [pageImageView, textStackView].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + activateConstrants() + } + + private func activateConstrants() { + NSLayoutConstraint.activate([ + pageImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + pageImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + pageImageView.topAnchor.constraint(equalTo: view.topAnchor), + pageImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + textStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + textStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + textStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 230) + ]) + } + + private func addGradient() { + pageImageView.layoutIfNeeded() + let gradientLayer = CAGradientLayer() + gradientLayer.frame = pageImageView.bounds + gradientLayer.colors = [ + UIColor.clear.cgColor, + UIColor.nftBlackUniversal.withAlphaComponent(0.8).cgColor + ] + gradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0) + gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0) + pageImageView.layer.addSublayer(gradientLayer) + } + + private func setLetterSpacing(for label: UILabel) { + let attributedString = NSMutableAttributedString(string: label.text ?? "") + attributedString.addAttribute(.kern, value: 1.5, range: NSRange(location: 0, length: attributedString.length)) + label.attributedText = attributedString + } +} diff --git a/FakeNFT/Scenes /ProductDetails/ProductDetailsTableViewCell.swift b/FakeNFT/Scenes /ProductDetails/ProductDetailsTableViewCell.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Scenes /ProductDetails/ProductDetailsTableViewController.swift b/FakeNFT/Scenes /ProductDetails/ProductDetailsTableViewController.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Scenes /Profile/EditScreen/EditingViewController.swift b/FakeNFT/Scenes /Profile/EditScreen/EditingViewController.swift new file mode 100755 index 0000000000..ce4e949955 --- /dev/null +++ b/FakeNFT/Scenes /Profile/EditScreen/EditingViewController.swift @@ -0,0 +1,279 @@ +import UIKit +import Kingfisher +import ProgressHUD + +final class EditingViewController: UIViewController { + + // MARK: - UI properties + private lazy var nameLabel: UILabel = { + let label = UILabel() + label.font = .headline2 + return label + }() + + private lazy var nameTextView: UITextView = { + let textView = UITextView() + textView.isScrollEnabled = false + textView.font = .bodyRegular + textView.backgroundColor = .nftLightgrey + textView.layer.cornerRadius = 12 + textView.textContainerInset = UIEdgeInsets(top: 11, left: 10, bottom: 11, right: 10) + return textView + }() + + private lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.font = .headline2 + return label + }() + + private lazy var descriptionTextView: UITextView = { + let textView = UITextView() + textView.isScrollEnabled = false + textView.font = .bodyRegular + textView.backgroundColor = .nftLightgrey + textView.layer.cornerRadius = 12 + textView.textContainerInset = UIEdgeInsets(top: 11, left: 10, bottom: 11, right: 10) + return textView + }() + + private lazy var webSiteLabel: UILabel = { + let label = UILabel() + label.font = .headline2 + return label + }() + + private lazy var webSiteTextView: UITextView = { + let textView = UITextView() + textView.isScrollEnabled = false + textView.font = .bodyRegular + textView.backgroundColor = .nftLightgrey + textView.layer.cornerRadius = 12 + textView.textContainerInset = UIEdgeInsets(top: 11, left: 10, bottom: 11, right: 10) + return textView + }() + + private let userPhotoImageView: UIImageView = { + let imageView = UIImageView() + imageView.layer.cornerRadius = 35 + imageView.clipsToBounds = true + imageView.contentMode = .scaleAspectFill + return imageView + }() + + private let overlayView: UIView = { + let view = UIView() + view.backgroundColor = .nftBackgroundUniversal + view.layer.cornerRadius = 35 + return view + }() + + private lazy var exitButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(named: "close"), for: .normal) + button.tintColor = .nftBlack + button.addTarget(self, action: #selector(exitButtonTapped), for: .touchUpInside) + return button + }() + + private lazy var changePhotoButton: UIButton = { + let button = UIButton() + let title = NSLocalizedString("EditingViewController.changePhoto", comment: "") + button.setTitle(title, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 10, weight: .medium) + button.titleLabel?.numberOfLines = 2 + button.titleLabel?.textAlignment = .center + button.addTarget(self, action: #selector(changePhotoTapped), for: .touchUpInside) + return button + }() + + // MARK: - Properties + + private lazy var alertService: AlertServiceProtocol + = { + return AlertService(viewController: self) + }() + + private let viewModel: EditingViewModelProtocol + + // MARK: - Lifecycle + + init(viewModel: EditingViewModelProtocol) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + self.bind() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + viewModel.viewDidLoad() + + setupViews() + setupDelegates() + } + + // MARK: - Действия + + @objc + private func exitButtonTapped() { + viewModel.exitButtonTapped() + dismiss(animated: true) + } + + @objc + private func changePhotoTapped() { + // Определяем действие подтверждения для оповещения + let confirmAction = AlertActionModel(title: NSLocalizedString("AlertAction.ok", comment: ""), + style: .default) { [weak self] urlText in + guard let strongSelf = self else { return } + if let urlText = urlText, let url = URL(string: urlText) { + strongSelf.viewModel.photoURLdidChanged(with: url) + } else { + // Создаем модель оповещения об ошибке + let errorModel = AlertProfileModel(title: NSLocalizedString("AlertAction.error", comment: ""), + message: NSLocalizedString("AlertAction.incorrURL", comment: ""), + style: .alert, + actions: [AlertActionModel(title: "OK", + style: .cancel, + handler: nil)], + textFieldPlaceholder: nil) + // Используем 'strongSelf' для доступа к 'alertService' + strongSelf.alertService.showAlert(model: errorModel) + } + } + + // Определяем действие отмены для оповещения + let cancelAction = AlertActionModel(title: NSLocalizedString("AlertAction.cancel", comment: ""), + style: .cancel, + handler: nil) + + // Создаем основную модель оповещения + let alertModel = AlertProfileModel(title: NSLocalizedString("AlertAction.enterURL", comment: ""), + message: nil, + style: .alert, + actions: [confirmAction, cancelAction], + textFieldPlaceholder: NSLocalizedString("AlertAction.imageURL", comment: "")) + + // Показываем оповещение + alertService.showAlert(model: alertModel) + } + + // MARK: - Methods + + private func bind() { + viewModel.observeUserProfileChanges { [weak self] (profile: UserProfile?) in + guard + let self = self, + let profile = profile + else { return } + self.updateUIElements(with: profile) + } + } + + private func setupDelegates() { + [nameTextView, descriptionTextView, webSiteTextView].forEach { $0.delegate = self } + } + + private func updateUIElements(with profile: UserProfile) { + DispatchQueue.main.async { [weak self] in + self?.userPhotoImageView.kf.setImage(with: URL(string: profile.avatar)) + self?.nameLabel.text = NSLocalizedString("EditingViewController.name", comment: "") + self?.nameTextView.text = profile.name + self?.descriptionLabel.text = NSLocalizedString("EditingViewController.description", comment: "") + self?.descriptionTextView.text = profile.description + self?.webSiteLabel.text = NSLocalizedString("EditingViewController.site", comment: "") + self?.webSiteTextView.text = profile.website + } + ProgressHUD.dismiss() + } + + // MARK: - Layout methods + + private func setupViews() { + view.backgroundColor = .white + view.addTapGestureToHideKeyboard() + + [exitButton, userPhotoImageView, overlayView, + changePhotoButton, nameLabel, nameTextView, + descriptionLabel, descriptionTextView, webSiteLabel, + webSiteTextView].forEach { view.addViewWithNoTAMIC($0) } + + NSLayoutConstraint.activate([ + exitButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 30), + exitButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + exitButton.widthAnchor.constraint(equalToConstant: 42), + exitButton.heightAnchor.constraint(equalToConstant: 42), + + userPhotoImageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 90), + userPhotoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + userPhotoImageView.widthAnchor.constraint(equalToConstant: 70), + userPhotoImageView.heightAnchor.constraint(equalToConstant: 70), + + overlayView.topAnchor.constraint(equalTo: userPhotoImageView.topAnchor), + overlayView.bottomAnchor.constraint(equalTo: userPhotoImageView.bottomAnchor), + overlayView.leadingAnchor.constraint(equalTo: userPhotoImageView.leadingAnchor), + overlayView.trailingAnchor.constraint(equalTo: userPhotoImageView.trailingAnchor), + + changePhotoButton.topAnchor.constraint(equalTo: overlayView.topAnchor), + changePhotoButton.bottomAnchor.constraint(equalTo: overlayView.bottomAnchor), + changePhotoButton.leadingAnchor.constraint(equalTo: overlayView.leadingAnchor), + changePhotoButton.trailingAnchor.constraint(equalTo: overlayView.trailingAnchor), + + nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + nameLabel.topAnchor.constraint(equalTo: userPhotoImageView.bottomAnchor, constant: 24), + + nameTextView.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor), + nameTextView.trailingAnchor.constraint(equalTo: nameLabel.trailingAnchor), + nameTextView.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8), + + descriptionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + descriptionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + descriptionLabel.topAnchor.constraint(equalTo: nameTextView.bottomAnchor, constant: 24), + + descriptionTextView.leadingAnchor.constraint(equalTo: descriptionLabel.leadingAnchor), + descriptionTextView.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor), + descriptionTextView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 8), + + webSiteLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + webSiteLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + webSiteLabel.topAnchor.constraint(equalTo: descriptionTextView.bottomAnchor, constant: 24), + + webSiteTextView.leadingAnchor.constraint(equalTo: webSiteLabel.leadingAnchor), + webSiteTextView.trailingAnchor.constraint(equalTo: webSiteLabel.trailingAnchor), + webSiteTextView.topAnchor.constraint(equalTo: webSiteLabel.bottomAnchor, constant: 8) + + ]) + } +} + +// MARK: - UITextViewDelegate + +extension EditingViewController: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + if let text = textView.text { + switch textView { + case nameTextView: + viewModel.updateName(text) + case descriptionTextView: + viewModel.updateDescription(text) + case webSiteTextView: + viewModel.updateWebSite(text) + default: + break + } + } + } + + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if text == "\n" { + textView.resignFirstResponder() + return false + } + return true + } +} diff --git a/FakeNFT/Scenes /Profile/EditScreen/EditingViewModel.swift b/FakeNFT/Scenes /Profile/EditScreen/EditingViewModel.swift new file mode 100755 index 0000000000..38bf1bd612 --- /dev/null +++ b/FakeNFT/Scenes /Profile/EditScreen/EditingViewModel.swift @@ -0,0 +1,140 @@ +import Foundation +import ProgressHUD + +protocol EditingViewModelProtocol { + var userProfile: UserProfile? { get } + func observeUserProfileChanges(_ handler: @escaping (UserProfile?) -> Void) + + func viewDidLoad() + func exitButtonTapped() + + func updateName(_ name: String) + func updateDescription(_ description: String) + func updateWebSite(_ website: String) + + func photoURLdidChanged(with url: URL) +} + +final class EditingViewModel: EditingViewModelProtocol { + @Observ + private(set) var userProfile: UserProfile? + + private let profileService: ProfileService + private let imageValidator: ImageValidatorProtocol + + private var isChanged: Bool = false + + init(profileService: ProfileService, imageValidator: ImageValidatorProtocol = ImageValidator()) { + self.profileService = profileService + self.imageValidator = imageValidator + } + + func observeUserProfileChanges(_ handler: @escaping (UserProfile?) -> Void) { + $userProfile.observe(handler) + } + + func viewDidLoad() { + fetchUserProfile() + } + + func updateName(_ name: String) { + guard let currentProfile = userProfile else { return } + userProfile = UserProfile( + name: name, + avatar: currentProfile.avatar, + description: currentProfile.description, + website: currentProfile.website, + nfts: currentProfile.nfts, + likes: currentProfile.likes, + id: currentProfile.id + ) + isChanged = true + } + + func updateDescription(_ description: String) { + guard let currentProfile = userProfile else { return } + userProfile = UserProfile( + name: currentProfile.name, + avatar: currentProfile.avatar, + description: description, + website: currentProfile.website, + nfts: currentProfile.nfts, + likes: currentProfile.likes, + id: currentProfile.id + ) + isChanged = true + } + + func updateWebSite(_ website: String) { + guard let currentProfile = userProfile else { return } + userProfile = UserProfile( + name: currentProfile.name, + avatar: currentProfile.avatar, + description: currentProfile.description, + website: website, + nfts: currentProfile.nfts, + likes: currentProfile.likes, + id: currentProfile.id + ) + isChanged = true + } + + func exitButtonTapped() { + guard isChanged, let userProfile = userProfile else { return } + + let group = DispatchGroup() + let backgroundQueue = DispatchQueue(label: "com.appname.backgroundQueue", qos: .background) + + backgroundQueue.async(group: group) { + group.enter() + + self.profileService.updateProfile(with: userProfile) { [weak self] result in + defer { group.leave() } + + guard let self = self else { return } + + DispatchQueue.main.async { [weak self] in + switch result { + case .success(let updatedProfile): + self?.userProfile = updatedProfile + NotificationCenter.default.post(name: NSNotification.Name("profileUpdated"), object: nil) + case .failure(let error): + NotificationCenter.default.post(name: NSNotification.Name("profileUpdateErrorToastNotification"), object: NSLocalizedString("AlertAction.UpdateError", comment: "")) + print(error) + } + } + } + } + } + + func photoURLdidChanged(with url: URL) { + imageValidator.isValidImageURL(url) { [weak self] isValid in + guard let self = self else { return } + if isValid, + let currentProfile = self.userProfile { + self.userProfile = UserProfile( + name: currentProfile.name, + avatar: url.absoluteString, + description: currentProfile.description, + website: currentProfile.website, + nfts: currentProfile.nfts, + likes: currentProfile.likes, + id: currentProfile.id + ) + } + } + } + + private func fetchUserProfile() { + ProgressHUD.show(NSLocalizedString("ProgressHUD.loading", comment: "")) + profileService.fetchProfile { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let userProfile): + self.userProfile = userProfile + case .failure(let error): + print(error) + } + } + } +} diff --git a/FakeNFT/Scenes /Profile/FavoritesNFTScreen/FavoritesNFTCell.swift b/FakeNFT/Scenes /Profile/FavoritesNFTScreen/FavoritesNFTCell.swift new file mode 100755 index 0000000000..286a111e75 --- /dev/null +++ b/FakeNFT/Scenes /Profile/FavoritesNFTScreen/FavoritesNFTCell.swift @@ -0,0 +1,118 @@ +import UIKit +import Kingfisher + +protocol FavoritesNFTCellDelegateProtocol: AnyObject { + func didTapHeartButton(in cell: FavoritesNFTCell) +} + +final class FavoritesNFTCell: UICollectionViewCell, ReuseIdentifying { + + weak var delegate: FavoritesNFTCellDelegateProtocol? + + private let currentPriceLabel: UILabel = { + let label = UILabel() + label.font = .caption1 + return label + }() + + private var nftImageView: UIImageView = { + let imageView = UIImageView() + imageView.layer.cornerRadius = 12 + imageView.clipsToBounds = true + return imageView + }() + + private var likeImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "emptyHeartButtonImage") + return imageView + }() + + private lazy var starsImage: [UIImageView] = { + (1...5).map { _ in + let view = UIImageView() + view.image = UIImage() + return view + } + }() + + private lazy var starsView: UIStackView = { + let view = UIStackView(arrangedSubviews: starsImage) + view.axis = .horizontal + view.spacing = CGFloat(2) + view.distribution = .fillEqually + view.alignment = .fill + return view + }() + + private let nftDetailsStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 4 + return stackView + }() + + private var name: UILabel = { + let label = UILabel() + label.font = .bodyBold + return label + }() + + private lazy var likeButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(UIImage(named: "like_active"), for: .normal) + button.addTarget(self, action: #selector(likeButtonTapped), for: .touchUpInside) + return button + }() + + @objc + private func likeButtonTapped() { + delegate?.didTapHeartButton(in: self) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setStarsState(_ state: Int) { + starsImage.enumerated().forEach { position, star in + let color = position < state ? UIColor.nftYellowUniversal : UIColor.nftLightgrey + star.image = UIImage(named: "star_inactive")?.withTintColor(color, renderingMode: .alwaysOriginal) + } + } + + func configure(with viewModel: FavoritesNFTCellViewModel) { + self.nftImageView.kf.setImage(with: viewModel.imageUrl, + placeholder: UIImage(named: "nullImage")) + self.setStarsState(viewModel.formattedRating) + self.name.text = viewModel.title + self.currentPriceLabel.text = viewModel.formattedPrice + } + + private func setupViews() { + [name, starsView, currentPriceLabel].forEach { nftDetailsStackView.addArrangedSubview($0) } + [nftImageView, nftDetailsStackView, likeButton].forEach { contentView.addViewWithNoTAMIC($0) } + + NSLayoutConstraint.activate([ + likeButton.topAnchor.constraint(equalTo: nftImageView.topAnchor, constant: -6), + likeButton.trailingAnchor.constraint(equalTo: nftImageView.trailingAnchor, constant: 6), + likeButton.heightAnchor.constraint(equalToConstant: 44), + likeButton.widthAnchor.constraint(equalToConstant: 44), + + nftImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + nftImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + nftImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + nftImageView.heightAnchor.constraint(equalToConstant: 80), + nftImageView.widthAnchor.constraint(equalTo: nftImageView.heightAnchor), + + nftDetailsStackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + nftDetailsStackView.leadingAnchor.constraint(equalTo: nftImageView.trailingAnchor, constant: 12) + ]) + } +} diff --git a/FakeNFT/Scenes /Profile/FavoritesNFTScreen/FavoritesNFTCellViewModel.swift b/FakeNFT/Scenes /Profile/FavoritesNFTScreen/FavoritesNFTCellViewModel.swift new file mode 100755 index 0000000000..99f983a270 --- /dev/null +++ b/FakeNFT/Scenes /Profile/FavoritesNFTScreen/FavoritesNFTCellViewModel.swift @@ -0,0 +1,20 @@ +import UIKit + +struct FavoritesNFTCellViewModel { + let title: String + let imageUrl: URL? + let formattedRating: Int + let formattedPrice: String + + init(nft: NFTProfile) { + self.title = nft.name + self.imageUrl = URL(string: nft.images.first ?? "") + self.formattedRating = Int(nft.rating) + + if let formattedPrice = NumberFormatter.defaultPriceFormatter.string(from: NSNumber(value: nft.price)) { + self.formattedPrice = "\(formattedPrice) ETH" + } else { + self.formattedPrice = "N/A" + } + } +} diff --git a/FakeNFT/Scenes /Profile/FavoritesNFTScreen/FavoritesNFTViewController.swift b/FakeNFT/Scenes /Profile/FavoritesNFTScreen/FavoritesNFTViewController.swift new file mode 100755 index 0000000000..f75fde9ead --- /dev/null +++ b/FakeNFT/Scenes /Profile/FavoritesNFTScreen/FavoritesNFTViewController.swift @@ -0,0 +1,180 @@ +import UIKit +import ProgressHUD + +final class FavoritesNFTViewController: UIViewController { + + // MARK: - UI properties + + private let geometricParams: GeometricProfileParams = { + GeometricProfileParams(cellPerRowCount: 2, + cellSpacing: 7, + cellLeftInset: 16, + cellRightInset: 16, + cellHeight: 80) + }() + + private lazy var nftCollectionView: UICollectionView = { + let collection = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + collection.delegate = self + collection.dataSource = self + collection.register(FavoritesNFTCell.self) + return collection + }() + + private lazy var noNFTLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("FavoritesNFTViewController.nonft", comment: "") + label.font = .bodyBold + label.isHidden = true + return label + }() + + // MARK: - Properties + + private let viewModel: FavoritesNFTViewModelProtocol + private let nftList: [String] + + // MARK: - Lifecycle + + init(nftList: [String], viewModel: FavoritesNFTViewModelProtocol) { + self.nftList = nftList + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + self.bind() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + viewModel.viewDidLoad(nftList: self.nftList) + + configNavigationBar() + setupViews() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewModel.viewWillDisappear() + } + + // MARK: - Methods + + private func bind() { + viewModel.observeFavoritesNFT { [weak self] _ in + guard let self = self else { return } + self.nftCollectionView.reloadData() + } + + viewModel.observeState { [weak self] state in + guard let self = self else { return } + + switch state { + case .loading: + self.showProgressHUD() + self.setUIInteraction(false) + case .loaded(let hasData): + self.hideProgressHUD() + self.updateUI(isNoNFT: !hasData) + self.nftCollectionView.reloadData() + case .error: + print("Ошибка") + default: + break + + } + } + } + + private func setUIInteraction(_ enabled: Bool) { + DispatchQueue.main.async { [weak self] in + self?.nftCollectionView.isUserInteractionEnabled = enabled + self?.navigationItem.leftBarButtonItem?.isEnabled = enabled + self?.nftCollectionView.alpha = enabled ? 1.0 : 0.5 + } + } + + private func updateUI(isNoNFT: Bool) { + setUIInteraction(true) + self.noNFTLabel.isHidden = !isNoNFT + navigationItem.title = isNoNFT ? nil : NSLocalizedString("ProfileViewController.favouritesNFT", comment: "") + } + + private func configNavigationBar() { + setupCustomBackButton() + } + private func showProgressHUD() { + ProgressHUD.show(NSLocalizedString("ProgressHUD.loading", comment: "")) + } + + private func hideProgressHUD() { + ProgressHUD.dismiss() + } + // MARK: - Layout methods + + private func setupViews() { + view.backgroundColor = .white + + [nftCollectionView, noNFTLabel].forEach { view.addViewWithNoTAMIC($0) } + + NSLayoutConstraint.activate([ + nftCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + nftCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + nftCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + nftCollectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + + noNFTLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + noNFTLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ]) + } +} + +// MARK: - FavoritesNFTViewController + +extension FavoritesNFTViewController: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return viewModel.favoritesNFT.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell: FavoritesNFTCell = collectionView.dequeueReusableCell(indexPath: indexPath) + let nft = viewModel.favoritesNFT[indexPath.row] + let viewModel = FavoritesNFTCellViewModel(nft: nft) + cell.configure(with: viewModel) + cell.delegate = self + return cell + } +} + +// MARK: - UICollectionViewDelegateFlowLayout + +extension FavoritesNFTViewController: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let width = (collectionView.bounds.width - geometricParams.paddingWight) / geometricParams.cellPerRowCount + return CGSize(width: width, height: geometricParams.cellHeight) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + 20 + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + 0 + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + UIEdgeInsets(top: 20, left: geometricParams.cellLeftInset, bottom: 0, right: geometricParams.cellRightInset) + } +} + +// MARK: - FavoritesNFTCellDelegateProtocol + +extension FavoritesNFTViewController: FavoritesNFTCellDelegateProtocol { + func didTapHeartButton(in cell: FavoritesNFTCell) { + guard let indexPath = nftCollectionView.indexPath(for: cell) else { return } + let nft = viewModel.favoritesNFT[indexPath.row] + viewModel.dislike(for: nft) + } +} diff --git a/FakeNFT/Scenes /Profile/FavoritesNFTScreen/FavoritesNFTViewModel.swift b/FakeNFT/Scenes /Profile/FavoritesNFTScreen/FavoritesNFTViewModel.swift new file mode 100755 index 0000000000..7a3c7a37cf --- /dev/null +++ b/FakeNFT/Scenes /Profile/FavoritesNFTScreen/FavoritesNFTViewModel.swift @@ -0,0 +1,105 @@ +import Foundation + +protocol FavoritesNFTViewModelProtocol { + var favoritesNFT: [NFTProfile] { get } + var state: LoadingState { get } + + func observeFavoritesNFT(_ handler: @escaping ([NFTProfile]?) -> Void) + func observeState(_ handler: @escaping (LoadingState) -> Void) + + func viewDidLoad(nftList: [String]) + func viewWillDisappear() + func fetchNFT(nftList: [String]) + func dislike(for: NFTProfile) +} + +final class FavoritesNFTViewModel: FavoritesNFTViewModelProtocol { + @Observ + private (set) var favoritesNFT: [NFTProfile] = [] + + @Observ + private (set) var state: LoadingState = .idle + + private let nftService: NFTServiceProfile + private let profileService: ProfileService + + init(nftService: NFTServiceProfile, profileService: ProfileService) { + self.nftService = nftService + self.profileService = profileService + } + + func observeFavoritesNFT(_ handler: @escaping ([NFTProfile]?) -> Void) { + $favoritesNFT.observe(handler) + } + + func observeState(_ handler: @escaping (LoadingState) -> Void) { + $state.observe(handler) + } + + func viewDidLoad(nftList: [String]) { + self.fetchNFT(nftList: nftList) + } + + func viewWillDisappear() { + nftService.stopAllTasks() + NotificationCenter.default.post(name: NSNotification.Name("profileUpdated"), object: nil) + + } + + func dislike(for nft: NFTProfile) { + profileService.fetchProfile { [weak self] result in + guard let self = self else { return } + + switch result { + case .success(let userProfile): + let updatedLikes = userProfile.likes.filter { $0 != nft.id } + self.fetchNFT(nftList: updatedLikes) + let updatedProfile = UserProfile( + name: userProfile.name, + avatar: userProfile.avatar, + description: userProfile.description, + website: userProfile.website, + nfts: userProfile.nfts, + likes: updatedLikes, + id: userProfile.id + ) + self.profileService.updateProfile(with: updatedProfile) { result in + switch result { + case .success: + break + case .failure(let error): + print(error) + } + } + case.failure(let error): + print(error) + } + } + } + + func fetchNFT(nftList: [String]) { + state = .loading + + var fetchedNFTs: [NFTProfile] = [] + let group = DispatchGroup() + + for element in nftList { + group.enter() + + nftService.fetchNFT(nftID: element) { result in + switch result { + case .success(let nft): + fetchedNFTs.append(nft) + case .failure(let error): + self.state = .error(error) + } + group.leave() + } + } + + group.notify(queue: .main) { + self.favoritesNFT = fetchedNFTs + self.state = .loaded(hasData: !fetchedNFTs.isEmpty) + } + } +} diff --git a/FakeNFT/Scenes /Profile/ProfileScreen/ProfileViewModel.swift b/FakeNFT/Scenes /Profile/ProfileScreen/ProfileViewModel.swift new file mode 100755 index 0000000000..5da1bfd797 --- /dev/null +++ b/FakeNFT/Scenes /Profile/ProfileScreen/ProfileViewModel.swift @@ -0,0 +1,45 @@ +import Foundation + +protocol ProfileViewModelProtocol { + var userProfile: UserProfile? { get } + func observeUserProfileChanges(_ handler: @escaping (UserProfile?) -> Void) + + func viewDidLoad() +} + +final class ProfileViewModel: ProfileViewModelProtocol { + @Observ + private(set) var userProfile: UserProfile? + + private let profileService: ProfileService + + init(service: ProfileService) { + self.profileService = service + NotificationCenter.default.addObserver(self, selector: #selector(profileUpdated), name: NSNotification.Name("profileUpdated"), object: nil) + } + + @objc + private func profileUpdated() { + fetchUserProfile() + } + + func observeUserProfileChanges(_ handler: @escaping (UserProfile?) -> Void) { + $userProfile.observe(handler) + } + + func viewDidLoad() { + fetchUserProfile() + } + + private func fetchUserProfile() { + profileService.fetchProfile { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let userProfile): + self.userProfile = userProfile + case .failure(let error): + print(error) + } + } + } +} diff --git a/FakeNFT/Scenes /Profile/ProfileScreen/Views/ProfileCell.swift b/FakeNFT/Scenes /Profile/ProfileScreen/Views/ProfileCell.swift new file mode 100755 index 0000000000..6371f7450f --- /dev/null +++ b/FakeNFT/Scenes /Profile/ProfileScreen/Views/ProfileCell.swift @@ -0,0 +1,44 @@ +import UIKit + +final class ProfileCell: UITableViewCell, ReuseIdentifying { + static let reuseIdentifier = "ProfileCell" + + private let titleLabel: UILabel = { + let label = UILabel() + label.font = .bodyBold + return label + }() + + private let arrowImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "forward") + imageView.tintColor = .nftBlack + return imageView + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(title: String) { + self.titleLabel.text = title + } + + private func setupViews() { + [arrowImageView, titleLabel].forEach { contentView.addViewWithNoTAMIC($0) } + + NSLayoutConstraint.activate([ + + arrowImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + arrowImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + + titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16) + ]) + } +} diff --git a/FakeNFT/Scenes /Profile/ProfileScreen/Views/ProfileViewController.swift b/FakeNFT/Scenes /Profile/ProfileScreen/Views/ProfileViewController.swift new file mode 100755 index 0000000000..6f80b158d8 --- /dev/null +++ b/FakeNFT/Scenes /Profile/ProfileScreen/Views/ProfileViewController.swift @@ -0,0 +1,237 @@ +import UIKit +import Kingfisher +import ProgressHUD + +final class ProfileViewController: UIViewController { + + // MARK: - UI properties + + private let userNameLabel: UILabel = { + let label = UILabel() + label.font = .headline2 + label.numberOfLines = 2 + return label + }() + + private let userDescriptionLabel: UILabel = { + let label = UILabel() + label.font = .caption2 + label.numberOfLines = .zero + return label + }() + + private let userWebSiteTextView: UITextView = { + let textView = UITextView() + let text = "" + let attributedString = NSMutableAttributedString(string: text) + let linkRange = NSRange(location: 0, length: text.count) + attributedString.addAttribute(.link, value: text, range: linkRange) + textView.attributedText = attributedString + textView.isEditable = false + textView.isSelectable = true + textView.isScrollEnabled = false + textView.dataDetectorTypes = .link + textView.font = .caption1 + textView.textContainer.lineFragmentPadding = 16 + return textView + }() + + private lazy var editButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(named: "edit"), for: .normal) + button.addTarget(self, action: #selector(editButtonTapped), for: .touchUpInside) + return button + }() + + private lazy var profileImageView: UIImageView = { + let imageView = UIImageView() + imageView.clipsToBounds = true + imageView.contentMode = .scaleAspectFill + imageView.layer.cornerRadius = 35 + return imageView + }() + + private lazy var profileTableView: UITableView = { + let tableView = UITableView() + tableView.register(ProfileCell.self) + tableView.isScrollEnabled = false + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + return tableView + }() + + // MARK: - Properties + + private let viewModel: ProfileViewModelProtocol + private lazy var router = ProfileRouter(viewController: self) + + // MARK: - Lifecycle + + init(viewModel: ProfileViewModelProtocol) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + self.bind() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Actions + + @objc + private func editButtonTapped() { + router.routeToEditingViewController() + } + + // MARK: - Methods + + private func bind() { + viewModel.observeUserProfileChanges { [weak self] profileModel in + guard let self = self else { return } + + if profileModel == nil { + // Показываем индикатор загрузки, если данные еще не загружены + ProgressHUD.show(NSLocalizedString("ProgressHUD.loading", comment: "")) + } else { + // Обновление UI и скрытие индикатора загрузки + self.updateUI(with: profileModel) + } + } + } + + private func updateUI(with model: UserProfile?) { + guard let model = model else { + // Обработка ошибки или отсутствия данных + ProgressHUD.showError(NSLocalizedString("ProfileViewController.errorLoadingProfile", comment: "")) + return + } + DispatchQueue.main.async { [weak self] in + self?.profileImageView.kf.setImage(with: URL(string: model.avatar)) + self?.userNameLabel.text = model.name + self?.userDescriptionLabel.text = model.description + self?.userWebSiteTextView.text = model.website + self?.tabBarController?.tabBar.isHidden = false + self?.profileTableView.reloadData() + [self?.editButton, self?.profileImageView, self?.userNameLabel, self?.userDescriptionLabel, self?.userWebSiteTextView, self?.profileTableView].forEach { $0?.isHidden = false } + + // Скрываем индикатор загрузки + ProgressHUD.dismiss() + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.navigationController?.delegate = self + self.tabBarController?.tabBar.isHidden = true + + setupViews() + bind() // Устанавливаем привязку данных + + viewModel.viewDidLoad() // Запускаем процесс загрузки данных + } + + // MARK: - Layout methods + + private func setupViews() { + view.backgroundColor = .nftWhite + + [editButton, profileImageView, userNameLabel, userDescriptionLabel, userWebSiteTextView, profileTableView].forEach { + view.addViewWithNoTAMIC($0) + $0.isHidden = true + } + + NSLayoutConstraint.activate([ + editButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 2), + editButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -9), + editButton.heightAnchor.constraint(equalToConstant: 42), + editButton.widthAnchor.constraint(equalTo: editButton.heightAnchor, multiplier: 1.0), + + profileImageView.widthAnchor.constraint(equalToConstant: 70), + profileImageView.heightAnchor.constraint(equalToConstant: 70), + profileImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + profileImageView.topAnchor.constraint(equalTo: editButton.bottomAnchor, constant: 20), + + userNameLabel.leadingAnchor.constraint(equalTo: profileImageView.trailingAnchor, constant: 16), + userNameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + userNameLabel.centerYAnchor.constraint(equalTo: profileImageView.centerYAnchor), + + userDescriptionLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 20), + userDescriptionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + userDescriptionLabel.trailingAnchor.constraint(equalTo: editButton.leadingAnchor), + + userWebSiteTextView.topAnchor.constraint(equalTo: userDescriptionLabel.bottomAnchor, constant: 8), + userWebSiteTextView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + userWebSiteTextView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + profileTableView.topAnchor.constraint(equalTo: userWebSiteTextView.bottomAnchor, constant: 40), + profileTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + profileTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + profileTableView.heightAnchor.constraint(equalToConstant: 162) + ]) + } +} + +// MARK: - UITableViewDataSource + +extension ProfileViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 3 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: ProfileCell = tableView.dequeueReusableCell() + var cellTitle = "" + switch indexPath.row { + case 0: + cellTitle = NSLocalizedString("ProfileViewController.myNFT", comment: "") + "(\(viewModel.userProfile?.nfts.count ?? 0))" + case 1: + cellTitle = NSLocalizedString("ProfileViewController.favouritesNFT", comment: "") + "(\(viewModel.userProfile?.likes.count ?? 0))" + case 2: + cellTitle = NSLocalizedString("ProfileViewController.aboutDeveloper", comment: "") + default: + break + } + + cell.configure(title: cellTitle) + cell.selectionStyle = .none + return cell + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 54 + } +} + +// MARK: - UITableViewDelegate + +extension ProfileViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch indexPath.row { + case 0: + router.routeToUserNFT(nftList: viewModel.userProfile?.nfts ?? []) + case 1: + router.routeToFavoritesNFT(nftList: viewModel.userProfile?.likes ?? []) + case 2: + if let url = URL(string: userWebSiteTextView.text) { + router.routeToWebView(url: url) + } + default: + break + } + } +} + +// MARK: - UINavigationControllerDelegate + +extension ProfileViewController: UINavigationControllerDelegate { + func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { + if viewController is ProfileViewController { + navigationController.setNavigationBarHidden(true, animated: animated) + } else if viewController is UserNFTViewController || viewController is FavoritesNFTViewController || viewController is WebViewViewController { + navigationController.setNavigationBarHidden(false, animated: animated) + } + } +} diff --git a/FakeNFT/Scenes /Profile/UserNFTScreen/Views/NFTCell.swift b/FakeNFT/Scenes /Profile/UserNFTScreen/Views/NFTCell.swift new file mode 100755 index 0000000000..6c7d0c0910 --- /dev/null +++ b/FakeNFT/Scenes /Profile/UserNFTScreen/Views/NFTCell.swift @@ -0,0 +1,151 @@ +import UIKit +import Kingfisher + +final class NFTCell: UITableViewCell, ReuseIdentifying { + + private var nftImageView: UIImageView = { + let imageView = UIImageView() + imageView.layer.cornerRadius = 12 + imageView.clipsToBounds = true + return imageView + }() + + private var likeImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "favouritesIcons") + return imageView + }() + + private var name: UILabel = { + let label = UILabel() + label.font = .bodyBold + return label + }() + + private lazy var starsImage: [UIImageView] = { + (1...5).map { _ in + let view = UIImageView() + view.image = UIImage() + return view + } + }() + + private lazy var starsView: UIStackView = { + let view = UIStackView(arrangedSubviews: starsImage) + view.axis = .horizontal + view.spacing = CGFloat(2) + view.distribution = .fillEqually + view.alignment = .fill + return view + }() + + private let authorPrefix: UILabel = { + let label = UILabel() + label.font = .caption1 + label.text = NSLocalizedString("NFTCell.from", comment: "") + return label + }() + + private let author: UILabel = { + let label = UILabel() + label.font = .caption2 + return label + }() + + private let authorStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.alignment = .firstBaseline + stackView.spacing = 4 + return stackView + }() + + private let nftDetailsStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 4 + return stackView + }() + + private let priceLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("NFTCell.price", comment: "") + label.font = .caption2 + return label + }() + + private let currentPriceLabel: UILabel = { + let label = UILabel() + label.font = .bodyBold + return label + }() + + private let priceStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 2 + return stackView + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setStarsState(_ state: Int) { + starsImage.enumerated().forEach { position, star in + let color = position < state ? UIColor.nftYellowUniversal : UIColor.nftLightgrey + star.image = UIImage(named: "stars")?.withTintColor(color, renderingMode: .alwaysOriginal) + } + } + + func configure(nft: NFTProfile, authorName: String) { + self.nftImageView.kf.setImage(with: URL(string: nft.images[0]), + placeholder: UIImage(named: "nullImage")) + self.name.text = nft.name + self.setStarsState(nft.rating) + self.author.text = authorName + self.currentPriceLabel.text = formatPrice(nft.price) + } + + func formatPrice(_ price: Float) -> String? { + let formatedPrice = NumberFormatter.defaultPriceFormatter.string(from: NSNumber(value: price)) ?? "" + let priceString = formatedPrice + " ETH" + return priceString + } + + private func setupViews() { + nftImageView.addViewWithNoTAMIC(likeImageView) + [authorPrefix, author].forEach { authorStackView.addArrangedSubview($0) } + [name, starsView, authorStackView].forEach { nftDetailsStackView.addArrangedSubview($0) } + [priceLabel, currentPriceLabel].forEach { priceStackView.addArrangedSubview($0) } + [nftImageView, nftDetailsStackView, priceStackView].forEach { contentView.addViewWithNoTAMIC($0) } + + NSLayoutConstraint.activate([ + likeImageView.topAnchor.constraint(equalTo: nftImageView.topAnchor), + likeImageView.trailingAnchor.constraint(equalTo: nftImageView.trailingAnchor), + + nftImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + nftImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + nftImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16), + nftImageView.heightAnchor.constraint(equalToConstant: 108), + nftImageView.widthAnchor.constraint(equalTo: nftImageView.heightAnchor), + + nftDetailsStackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + nftDetailsStackView.leadingAnchor.constraint(equalTo: nftImageView.trailingAnchor, constant: 20), + nftDetailsStackView.trailingAnchor.constraint(equalTo: priceStackView.leadingAnchor, constant: -60), + + author.trailingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: trailingAnchor, multiplier: 988), + + priceStackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + priceStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -3), + + starsView.heightAnchor.constraint(equalToConstant: 12), + starsView.widthAnchor.constraint(equalToConstant: 68) + ]) + } +} diff --git a/FakeNFT/Scenes /Profile/UserNFTScreen/Views/UserNFTViewController.swift b/FakeNFT/Scenes /Profile/UserNFTScreen/Views/UserNFTViewController.swift new file mode 100755 index 0000000000..cfc893697d --- /dev/null +++ b/FakeNFT/Scenes /Profile/UserNFTScreen/Views/UserNFTViewController.swift @@ -0,0 +1,202 @@ +import UIKit +import ProgressHUD + +final class UserNFTViewController: UIViewController { + + // MARK: - UI properties + + private lazy var alertService: AlertServiceProtocol = { + return AlertService(viewController: self) + }() + + private lazy var nftTableView: UITableView = { + let tableView = UITableView() + tableView.register(NFTCell.self) + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + return tableView + }() + + private lazy var sortButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(UIImage(named: "sort_button"), for: .normal) + button.addTarget(self, action: #selector(sortButtonTapped), for: .touchUpInside) + return button + }() + + private lazy var noNFTLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("UserNFTViewController.nonft", comment: "") + label.font = .bodyBold + label.isHidden = true + return label + }() + + // MARK: - Properties + + private let nftList: [String] + private let viewModel: UserNFTViewModelProtocol + + // MARK: - Lifecycle + + init(nftList: [String], viewModel: UserNFTViewModelProtocol) { + self.nftList = nftList + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + self.bind() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + viewModel.viewDidLoad(nftList: self.nftList) + setupViews() + configNavigationBar() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewModel.viewWillDisappear() + } + + // MARK: - Actions + + @objc + private func sortButtonTapped() { + let priceAction = AlertActionModel(title: SortOption.price.description, style: .default) { [weak self] _ in + guard let self = self else { return } + self.viewModel.userSelectedSorting(by: .price) + } + + let ratingAction = AlertActionModel(title: SortOption.rating.description, style: .default) { [weak self] _ in + guard let self = self else { return } + self.viewModel.userSelectedSorting(by: .rating) + } + + let titleAction = AlertActionModel(title: SortOption.title.description, style: .default) { [weak self] _ in + guard let self = self else { return } + self.viewModel.userSelectedSorting(by: .title) + } + + let cancelAction = AlertActionModel(title: NSLocalizedString("AlertAction.close", comment: ""), + style: .cancel, + handler: nil) + + let alertModel = AlertProfileModel(title: NSLocalizedString("AlertAction.sort", comment: ""), + message: nil, + style: .actionSheet, + actions: [priceAction, ratingAction, titleAction, cancelAction], + textFieldPlaceholder: nil) + + alertService.showAlert(model: alertModel) + } + + // MARK: - Methods + + private func sortData(by option: SortOption) { + viewModel.userSelectedSorting(by: option) + } + + private func bind() { + viewModel.observeUserNFT { [weak self] _ in + guard let self = self else { return } + self.nftTableView.reloadData() + } + + viewModel.observeState { [weak self] state in + guard let self = self else { return } + + switch state { + case .loading: + ProgressHUD.show(NSLocalizedString("ProgressHUD.loading", comment: "")) + self.setUIInteraction(false) + case .loaded(let hasData): + ProgressHUD.dismiss() + if hasData { + self.updateUIBasedOnNFTData() + } else { + self.noNFTLabel.isHidden = false + } + case .error(let error): + DispatchQueue.main.async { + let alertController = UIAlertController(title: "Ошибка", message: error.localizedDescription, preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) + alertController.addAction(okAction) + self.present(alertController, animated: true, completion: nil) + } + default: + break + } + } + + } + + private func updateUIBasedOnNFTData() { + let barButtonItem = UIBarButtonItem(customView: sortButton) + navigationItem.rightBarButtonItem = barButtonItem + navigationItem.title = NSLocalizedString("ProfileViewController.myNFT", comment: "") + setUIInteraction(true) + } + + private func setUIInteraction(_ enabled: Bool) { + DispatchQueue.main.async { [weak self] in + self?.navigationItem.leftBarButtonItem?.isEnabled = enabled + } + } + + private func setupViews() { + view.backgroundColor = .nftWhite + + [nftTableView, noNFTLabel].forEach { view.addViewWithNoTAMIC($0) } + + NSLayoutConstraint.activate([ + nftTableView .topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + nftTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + nftTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + nftTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + + noNFTLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + noNFTLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ]) + } + + private func configNavigationBar() { + setupCustomBackButton() + } +} + +// MARK: - UITableViewDataSource + +extension UserNFTViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + viewModel.userNFT?.count ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: NFTCell = tableView.dequeueReusableCell() + cell.selectionStyle = .none + + guard let nft = viewModel.userNFT?[indexPath.row] else { + return cell + } + + if let author = viewModel.authors[nft.author] { + cell.configure(nft: nft, authorName: author.name) + } else { + cell.configure(nft: nft, authorName: "Unknown author") + } + + return cell + } +} + +// MARK: - UITableViewDelegate + +extension UserNFTViewController: UITableViewDelegate { + +} diff --git a/FakeNFT/Scenes /Profile/UserNFTScreen/Views/UserNFTViewModel.swift b/FakeNFT/Scenes /Profile/UserNFTScreen/Views/UserNFTViewModel.swift new file mode 100755 index 0000000000..f9461ef7ac --- /dev/null +++ b/FakeNFT/Scenes /Profile/UserNFTScreen/Views/UserNFTViewModel.swift @@ -0,0 +1,115 @@ +import Foundation + +protocol UserNFTViewModelProtocol { + var userNFT: [NFTProfile]? { get } + var authors: [String: Author] { get } + var state: LoadingState { get } + + func observeUserNFT(_ handler: @escaping ([NFTProfile]?) -> Void) + func observeState(_ handler: @escaping (LoadingState) -> Void) + + func viewDidLoad(nftList: [String]) + func viewWillDisappear() + + func userSelectedSorting(by option: SortOption) +} + +final class UserNFTViewModel: UserNFTViewModelProtocol { + @Observ + private (set) var userNFT: [NFTProfile]? + + @Observ + private (set) var state: LoadingState = .idle + + private (set) var authors: [String: Author] = [:] + private let service: NFTServiceProfile + + init(nftService: NFTServiceProfile) { + self.service = nftService + } + + func observeUserNFT(_ handler: @escaping ([NFTProfile]?) -> Void) { + $userNFT.observe(handler) + } + + func observeState(_ handler: @escaping (LoadingState) -> Void) { + $state.observe(handler) + } + + func viewDidLoad(nftList: [String]) { + state = .loading + + var fetchedNFTs: [NFTProfile] = [] + let group = DispatchGroup() + + for element in nftList { + group.enter() + + service.fetchNFT(nftID: element) { (result) in + switch result { + case .success(let nft): + fetchedNFTs.append(nft) + case .failure(let error): + print("Failed to fetch NFT with ID \(element): \(error)") + } + group.leave() + } + } + + group.notify(queue: .main) { + self.fetchAuthorList(nfts: fetchedNFTs) + } + } + + func viewWillDisappear() { + service.stopAllTasks() + } + + func userSelectedSorting(by option: SortOption) { + guard var nfts = userNFT else { + return + } + + switch option { + case .price: + nfts.sort(by: { $0.price > $1.price }) + case .rating: + nfts.sort(by: { $0.rating > $1.rating }) + case .title: + nfts.sort(by: { $0.name.lowercased() < $1.name.lowercased() }) + } + self.userNFT = nfts + } + + private func fetchAuthorList(nfts: [NFTProfile]) { + let authorGroup = DispatchGroup() + + for nft in nfts { + authorGroup.enter() + self.fetchAuthor(authorID: nft.author) { result in + switch result { + case .success(let author): + self.authors[nft.author] = author + case .failure(let error): + print("Failed to fetch author with ID \(nft.author): \(error)") + } + authorGroup.leave() + } + } + authorGroup.notify(queue: .main) { + self.userNFT = nfts + self.state = .loaded(hasData: !nfts.isEmpty) + } + } + + private func fetchAuthor(authorID: String, completion: @escaping (Result) -> Void) { + service.fetchAuthor(authorID: authorID) { result in + switch result { + case .success(let author): + completion(.success(author)) + case .failure(let error): + completion(.failure(error)) + } + } + } +} diff --git a/FakeNFT/Scenes /Profile/WebViewViewController.swift b/FakeNFT/Scenes /Profile/WebViewViewController.swift new file mode 100755 index 0000000000..707f6fdffd --- /dev/null +++ b/FakeNFT/Scenes /Profile/WebViewViewController.swift @@ -0,0 +1,66 @@ +import UIKit +import WebKit + +final class WebViewViewController: UIViewController { + private var targetURL: URL + private var observation: NSKeyValueObservation? + + private lazy var webView: WKWebView = { + let webView = WKWebView() + return webView + }() + + private lazy var progressView: UIProgressView = { + let progressView = UIProgressView(progressViewStyle: .default) + return progressView + }() + + init(url: URL) { + self.targetURL = url + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + observation?.invalidate() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupViews() + configNavigationBar() + webView.load(URLRequest(url: targetURL)) + observation = webView.observe(\.estimatedProgress, options: .new) { [weak self] (_, change) in + guard + let progress = change.newValue, + let self = self + else { return } + self.progressView.progress = Float(progress) + self.progressView.isHidden = progress == 1.0 + } + } + + private func setupViews() { + view.backgroundColor = .nftWhite + [webView, progressView].forEach { view.addViewWithNoTAMIC($0) } + + NSLayoutConstraint.activate([ + webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + + progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + } + + private func configNavigationBar() { + setupCustomBackButton() + } +} diff --git a/FakeNFT/Scenes /Statistic/StatisticsViewController.swift b/FakeNFT/Scenes /Statistic/StatisticsViewController.swift new file mode 100755 index 0000000000..63a2ba2e3a --- /dev/null +++ b/FakeNFT/Scenes /Statistic/StatisticsViewController.swift @@ -0,0 +1,8 @@ +import UIKit + +final class StatisticsViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .nftWhite + } +} diff --git a/FakeNFT/Scenes /Statistics/StatisticsViewController.swift b/FakeNFT/Scenes /Statistics/StatisticsViewController.swift new file mode 100755 index 0000000000..63a2ba2e3a --- /dev/null +++ b/FakeNFT/Scenes /Statistics/StatisticsViewController.swift @@ -0,0 +1,8 @@ +import UIKit + +final class StatisticsViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .nftWhite + } +} diff --git a/FakeNFT/Scenes /TabBarController/TabBarController.swift b/FakeNFT/Scenes /TabBarController/TabBarController.swift old mode 100644 new mode 100755 index 99931a214a..6e9b3fb984 --- a/FakeNFT/Scenes /TabBarController/TabBarController.swift +++ b/FakeNFT/Scenes /TabBarController/TabBarController.swift @@ -1,25 +1,137 @@ import UIKit +// final class TabBarController: UITabBarController { +// +// var servicesAssembly: ServicesAssembly! +// +// init() { +// super.init(nibName: nil, bundle: nil) +// setupView() +// } +// +// required init?(coder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// private func setupView() { +// view.backgroundColor = UIColor.nftWhite +// +// servicesAssembly = ServicesAssembly( +// networkClient: DefaultNetworkClient(), +// nftStorage: NftStorageImpl(), +// cartStorage: CartStorageImpl(), +// currencyStorage: CurrencyStorageImpl() +// ) +// +// configureTabBar() +// +// let catalogNavigationController = setupCatalogNavController() +// let cartViewController = setupCartViewController() +// +// viewControllers = [catalogNavigationController, cartViewController] +// } +// +// private func setupCatalogNavController() -> UINavigationController { +// let catalogController = CatalogViewController() +// +// catalogController.tabBarItem = UITabBarItem( +// title: L10n.Tabbar.catalogTitle, +// image: UIImage(named: "tabbar_catalogue"), +// selectedImage: nil) +// +// let catalogNavigationController = UINavigationController(rootViewController: catalogController) +// +// catalogNavigationController.navigationBar.setBackgroundImage(UIImage(), for: .default) +// catalogNavigationController.navigationBar.shadowImage = UIImage() +// catalogNavigationController.navigationBar.isTranslucent = true +// +// return catalogNavigationController +// } +// +// private func setupCartViewController() -> UINavigationController { +// let cartController = CartViewController(viewModel: CartViewModel(servicesAssembly: servicesAssembly)) +// +// cartController.tabBarItem = UITabBarItem( +// title: L10n.Tabbar.cartTitle, +// image: UIImage(named: "tabbar_basket"), +// selectedImage: nil) +// +// let cartNavigationController = UINavigationController(rootViewController: cartController) +// +// return cartNavigationController +// } +// +// private func configureTabBar() { +// let appearance = tabBar.standardAppearance +// appearance.shadowImage = nil +// appearance.shadowColor = nil +// appearance.backgroundEffect = nil +// appearance.backgroundColor = UIColor.nftWhite +// appearance.stackedLayoutAppearance.normal.iconColor = UIColor.nftBlack +// appearance.stackedLayoutAppearance.normal.titleTextAttributes = [ +// NSAttributedString.Key.foregroundColor: UIColor.nftBlack +// ] +// +// tabBar.standardAppearance = appearance +// } +// } + final class TabBarController: UITabBarController { var servicesAssembly: ServicesAssembly! - private let catalogTabBarItem = UITabBarItem( - title: NSLocalizedString("Tab.catalog", comment: ""), - image: UIImage(systemName: "square.stack.3d.up.fill"), - tag: 0 - ) - override func viewDidLoad() { super.viewDidLoad() - let catalogController = TestCatalogViewController( - servicesAssembly: servicesAssembly + self.view.backgroundColor = UIColor.nftWhite + self.tabBar.backgroundColor = .nftWhite + + servicesAssembly = ServicesAssembly( + networkClient: DefaultNetworkClient(), + nftStorage: NftStorageImpl(), + cartStorage: CartStorageImpl(), + currencyStorage: CurrencyStorageImpl() + ) + + let profileViewController = ProfileViewController(viewModel: ProfileViewModel(service: ProfileService.shared)) + let profileNavigationController = UINavigationController(rootViewController: profileViewController) + profileNavigationController.tabBarItem = UITabBarItem( + title: NSLocalizedString("TabBarController.Profile", comment: ""), + image: UIImage(named: "tabbar_profile"), + tag: 0 + ) + + let catalogViewController = CatalogViewController() + let catalogNavigationController = UINavigationController(rootViewController: catalogViewController) + catalogNavigationController.tabBarItem = UITabBarItem( + title: NSLocalizedString("Tabbar.catalogTitle", comment: ""), + image: UIImage(named: "tabbar_catalogue"), + tag: 1 ) - catalogController.tabBarItem = catalogTabBarItem - viewControllers = [catalogController] + let cartController = CartViewController(viewModel: CartViewModel(servicesAssembly: servicesAssembly)) - view.backgroundColor = .systemBackground + let basketNavigationController = UINavigationController(rootViewController: cartController) + basketNavigationController.tabBarItem = UITabBarItem( + title: NSLocalizedString("TabBarController.Basket", comment: ""), + image: UIImage(named: "tabbar_basket"), + tag: 2 + ) + + let statisticsViewController = StatisticsViewController() + let statisticsNavigationController = UINavigationController(rootViewController: statisticsViewController) + statisticsNavigationController.tabBarItem = UITabBarItem( + title: NSLocalizedString("TabBarController.Statistics", comment: ""), + image: UIImage(named: "tabbar_statistics"), + tag: 3 + ) + + self.viewControllers = [profileNavigationController, + catalogNavigationController, + basketNavigationController, + statisticsNavigationController] + + tabBar.unselectedItemTintColor = UIColor.nftBlack } + } diff --git a/FakeNFT/Services/CartService.swift b/FakeNFT/Services/CartService.swift new file mode 100755 index 0000000000..4c61d5f627 --- /dev/null +++ b/FakeNFT/Services/CartService.swift @@ -0,0 +1,64 @@ +import Foundation + +typealias CartCompletion = (Result) -> Void +typealias NftsCompletion = (Result<[Nft], Error>) -> Void + +protocol CartService { + func loadNFTs(completion: @escaping NftsCompletion) + func deleteNftFromCart(cartId: String, nfts: [String], completion: @escaping CartCompletion) +} + +final class CartServiceImpl: CartService { + private let networkClient: NetworkClient + private let storage: CartStorage + + init(networkClient: NetworkClient, storage: CartStorage) { + self.storage = storage + self.networkClient = networkClient + } + + func loadCart(completion: @escaping CartCompletion) { + let request = CartRequest() + networkClient.send(request: request, type: CartModel.self, onResponse: completion) + } + + func loadNft(id: String, completion: @escaping NftCompletion) { + let request = NFTRequest(id: id) + networkClient.send(request: request, type: Nft.self, onResponse: completion) + } + + func loadNFTs(completion: @escaping NftsCompletion) { + let group = DispatchGroup() + var nfts: [Nft] = [] + loadCart { result in + switch result { + case .success(let cart): + for nftId in cart.nfts { + group.enter() + self.loadNft(id: nftId) { result in + defer { group.leave() } + switch result { + case .success(let nft): + nfts.append(nft) + case .failure(let error): + completion(.failure(error)) + return + } + } + } + case .failure(let error): + completion(.failure(error)) + } + group.notify(queue: .main) { + completion(.success(nfts)) + } + } + } + + func deleteNftFromCart(cartId: String = "1", nfts: [String], completion: @escaping CartCompletion) { + let request = NFTNetworkRequest(endpoint: CartRequest().endpoint, + httpMethod: .put, + dto: CartModel(id: cartId, nfts: nfts)) + networkClient.send(request: request, type: CartModel.self, onResponse: completion) + } +} diff --git a/FakeNFT/Services/CartService/CartFilterStorage.swift b/FakeNFT/Services/CartService/CartFilterStorage.swift new file mode 100755 index 0000000000..2280cf2c5f --- /dev/null +++ b/FakeNFT/Services/CartService/CartFilterStorage.swift @@ -0,0 +1,31 @@ +import Foundation + +enum CartSortType: String { + case price + case rating + case name + } + +final class CartFilterStorage { + static let shared = CartFilterStorage() + + var cartSortType: CartSortType { + get { + if let sortType = userDefaults.string(forKey: sortTypeKey) { + return CartSortType(rawValue: sortType) ?? .name + } else { + return .name + } + } + set { + userDefaults.set(newValue.rawValue, forKey: sortTypeKey) + } + } + + private let sortTypeKey = "SortTypeKey" + private var userDefaults: UserDefaults + + private init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } +} diff --git a/FakeNFT/Services/CartService/ModuleFactory.swift b/FakeNFT/Services/CartService/ModuleFactory.swift new file mode 100755 index 0000000000..08d4be3048 --- /dev/null +++ b/FakeNFT/Services/CartService/ModuleFactory.swift @@ -0,0 +1,18 @@ +import UIKit + +class ModuleFactory { + let servicesAssembly: ServicesAssembly + + init(servicesAssembly: ServicesAssembly) { + self.servicesAssembly = servicesAssembly + } + + func makeCurrencyModule() -> UIViewController { + let currencyViewModel = CurrencyViewModel(servicesAssembly: servicesAssembly) + let currencyViewController = CurrencyScreenViewController(viewModel: currencyViewModel) + let navigationController = UINavigationController(rootViewController: currencyViewController) + navigationController.modalPresentationStyle = .fullScreen + navigationController.hidesBottomBarWhenPushed = true + return navigationController + } +} diff --git a/FakeNFT/Services/CatalogServices/CatalogAssembly.swift b/FakeNFT/Services/CatalogServices/CatalogAssembly.swift new file mode 100755 index 0000000000..47e543c12e --- /dev/null +++ b/FakeNFT/Services/CatalogServices/CatalogAssembly.swift @@ -0,0 +1,27 @@ +// +// CatalogAssembly.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 08.11.2023. +// + +import Foundation + +final class CatalogAssembly { + + private init() {} + + static func buildCatalogViewModel() -> CatalogViewModelProtocol { + let network = DefaultNetworkClient() + let service = CatalogService(networkClient: network) + let viewModel = CatalogViewModel(catalogService: service) + return viewModel + } + + static func buildCatalogCollectionViewModel(catalog: Catalog) -> CatalogCollectionViewModelProtocol { + let network = DefaultNetworkClient() + let service = CatalogCollectionService(networkClient: network) + let viewModel = CatalogCollectionViewModel(catalogCollection: catalog, service: service) + return viewModel + } +} diff --git a/FakeNFT/Services/CatalogServices/CatalogCollectionService.swift b/FakeNFT/Services/CatalogServices/CatalogCollectionService.swift new file mode 100755 index 0000000000..0533f7dc12 --- /dev/null +++ b/FakeNFT/Services/CatalogServices/CatalogCollectionService.swift @@ -0,0 +1,122 @@ +// +// CatalogCollectionService.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 13.11.2023. +// + +import Foundation + +protocol CatalogCollectionServiceProtocol { + func loadNftForCollection(id: String, completion: @escaping (Result) -> Void) + func fetchAuthorProfile(id: String, completion: @escaping (Result) -> Void) + func putProfileLikes(profile: ProfileLike, completion: @escaping (Result) -> Void) + func putNftsToCart(cart: PurchaseCart, completion: @escaping (Result) -> Void) +} + +final class CatalogCollectionService: CatalogCollectionServiceProtocol { + + // MARK: - Public properties + private var catalog: [Catalog] = [] + + // MARK: - Private properties + private let networkClient: NetworkClient + private let storage: NftStorage = NftStorageImpl.shared + private lazy var queue = DispatchQueue.global(qos: .userInitiated) + + init(networkClient: NetworkClient) { + self.networkClient = networkClient + } + + // MARK: - Public methods + func loadNftForCollection(id: String, completion: @escaping (Result) -> Void) { + if let nft = storage.getNft(with: id) { + completion(.success(nft)) + return + } + + let request = NFTRequest(id: id) + + queue.async { [weak self] in + guard let self = self else { return } + self.networkClient.send( + request: request, + type: Nft.self, + onResponse: { (result: Result) in + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + switch result { + case .success(let nft): + self.storage.saveNft(nft) + completion(.success(nft)) + case .failure(let error): + completion(.failure(error)) + } + } + } + ) + } + } + + func fetchAuthorProfile(id: String, completion: @escaping (Result) -> Void) { + let request = AuthorRequest(id: id) + + queue.async { [weak self] in + guard let self = self else { return } + self.networkClient.send( + request: request, + type: Author.self, + onResponse: { (result: Result) in + DispatchQueue.main.async { + switch result { + case .success(let author): + completion(.success(author)) + case .failure(let error): + completion(.failure(error)) + } + } + } + ) + } + } + + func putProfileLikes(profile: ProfileLike, completion: @escaping (Result) -> Void) { + let request = ProfileRequest(httpMethod: .put, dto: profile) + + queue.async { [weak self] in + guard let self = self else { return } + self.networkClient.send( + request: request, + type: ProfileLike.self) { (result: Result) in + DispatchQueue.main.async { + switch result { + case .success(let profile): + completion(.success(profile)) + case .failure(let error): + completion(.failure(error)) + } + } + } + } + } + + func putNftsToCart(cart: PurchaseCart, completion: @escaping (Result) -> Void) { + let request = OrdersRequest(httpMethod: .put, dto: cart) + + queue.async { [weak self] in + guard let self = self else { return } + self.networkClient.send( + request: request, + type: PurchaseCart.self) { (result: Result) in + DispatchQueue.main.async { + switch result { + case .success(let cart): + completion(.success(cart)) + case .failure(let error): + completion(.failure(error)) + } + } + } + } + } +} diff --git a/FakeNFT/Services/CatalogServices/CatalogFilterStorage.swift b/FakeNFT/Services/CatalogServices/CatalogFilterStorage.swift new file mode 100755 index 0000000000..e552f36cea --- /dev/null +++ b/FakeNFT/Services/CatalogServices/CatalogFilterStorage.swift @@ -0,0 +1,34 @@ +// +// CatalogFilterStorage.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 07.11.2023. +// + +import Foundation + +final class CatalogFilterStorage { + + // MARK: - public properties + static let shared = CatalogFilterStorage() + var filterDescriptor: String? { + get { + userDefaults.string(forKey: L10n.CatalogFilterStorage.key) + } + set { + if let newValue = newValue { + userDefaults.set(newValue, forKey: L10n.CatalogFilterStorage.key) + } + } + } + + // MARK: - private properties + private var userDefaults = UserDefaults.standard + + private init() {} +} + +enum CatalogFilter: String { + case filterName = "name" + case filterQuantity = "quantity" +} diff --git a/FakeNFT/Services/CatalogServices/CatalogService.swift b/FakeNFT/Services/CatalogServices/CatalogService.swift new file mode 100755 index 0000000000..90e03c9b4c --- /dev/null +++ b/FakeNFT/Services/CatalogServices/CatalogService.swift @@ -0,0 +1,104 @@ +// +// CatalogService.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 07.11.2023. +// + +import Foundation + +protocol CatalogServiceProtocol { + func fetchCatalog(completion: @escaping (Result<[Catalog], Error>) -> Void) + func fetchProfileLikes(completion: @escaping (Result) -> Void) + func fetchAddedToBasketNfts(completion: @escaping (Result) -> Void) +} + +final class CatalogService: CatalogServiceProtocol { + + // MARK: - Public properties + private var catalog: [Catalog] = [] + + // MARK: - Private properties + private let request = CatalogRequest() + private let networkClient: NetworkClient + private lazy var queue = DispatchQueue.global(qos: .userInitiated) + + init(networkClient: NetworkClient) { + self.networkClient = DefaultNetworkClient() + } + + // MARK: - Public methods + func fetchCatalog(completion: @escaping (Result<[Catalog], Error>) -> Void) { + + queue.async { [weak self] in + guard let self = self else { return } + self.networkClient.send( + request: self.request, + type: [CatalogResult].self, + onResponse: { [weak self] (result: Result<[CatalogResult], Error>) in + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + switch result { + case .success(let catalogRes): + self.catalog += catalogRes.map { + return Catalog( + name: $0.name, + coverURL: URL(string: $0.cover.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed) ?? ""), + nfts: $0.nfts, + desription: $0.description, + authorID: $0.author, + id: $0.id) + } + completion(.success(self.catalog)) + case .failure(let error): + completion(.failure(error)) + } + } + } + ) + } + } + + // methods implemented for temporary use in catalog epic to handle likes and addToBasket interaction + // when the full project is merged - these two methods will be removed + // both methods should be implemented in Profile epic + func fetchProfileLikes(completion: @escaping (Result) -> Void) { + let request = ProfileRequest(httpMethod: .get) + + queue.async { [weak self] in + guard let self = self else { return } + self.networkClient.send( + request: request, + type: ProfileLike.self) { (result: Result) in + DispatchQueue.main.async { + switch result { + case .success(let profile): + completion(.success(profile)) + case .failure(let error): + completion(.failure(error)) + } + } + } + } + } + + func fetchAddedToBasketNfts(completion: @escaping (Result) -> Void) { + let request = OrdersRequest(httpMethod: .get) + + queue.async { [weak self] in + guard let self = self else { return } + self.networkClient.send( + request: request, + type: PurchaseCart.self) { (result: Result) in + DispatchQueue.main.async { + switch result { + case .success(let order): + completion(.success(order)) + case .failure(let error): + completion(.failure(error)) + } + } + } + } + } +} diff --git a/FakeNFT/Services/CatalogServices/UIBlockingProgressHUD.swift b/FakeNFT/Services/CatalogServices/UIBlockingProgressHUD.swift new file mode 100755 index 0000000000..196e39b905 --- /dev/null +++ b/FakeNFT/Services/CatalogServices/UIBlockingProgressHUD.swift @@ -0,0 +1,29 @@ +// +// UIBlockingProgressHUD.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 08.11.2023. +// + +import UIKit +import ProgressHUD + +final class UIBlockingProgressHUD { + static var shared = UIBlockingProgressHUD() + + private init() {} + + private static var window: UIWindow? { + UIApplication.shared.windows.first + } + + static func show() { + window?.isUserInteractionEnabled = false + ProgressHUD.show() + } + + static func dismiss() { + window?.isUserInteractionEnabled = true + ProgressHUD.dismiss() + } +} diff --git a/FakeNFT/Services/CommonService/AlertPresenter.swift b/FakeNFT/Services/CommonService/AlertPresenter.swift new file mode 100755 index 0000000000..1df9a82d4c --- /dev/null +++ b/FakeNFT/Services/CommonService/AlertPresenter.swift @@ -0,0 +1,106 @@ +// +// AlertPresenter.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 04.11.2023. +// + +import UIKit + +final class AlertPresenter { + + private init() {} + + static func show(in viewController: UIViewController, model: AlertModel) { + let alert = UIAlertController( + title: nil, + message: model.message, + preferredStyle: .actionSheet) + + let nameSort = UIAlertAction(title: model.nameSortText, style: .default) { _ in + model.sortNameCompletion() + } + + alert.addAction(nameSort) + + let quantitySort = UIAlertAction(title: model.quantitySortText, style: .default) { _ in + model.sortQuantityCompletion() + } + + alert.addAction(quantitySort) + + let cancelAction = UIAlertAction(title: model.cancelButtonText, style: .cancel) + + alert.addAction(cancelAction) + + viewController.present(alert, animated: true) + } + + static func showError(in viewController: UIViewController, completion: @escaping () -> Void) { + let alert = UIAlertController( + title: L10n.NetworkErrorAlert.title, + message: L10n.NetworkErrorAlert.message, + preferredStyle: .alert + ) + + let action = UIAlertAction(title: L10n.NetworkErrorAlert.okButton, style: .default) { _ in + completion() + } + alert.addAction(action) + viewController.present(alert, animated: true) + } + + static func showNftInteractionError(in viewController: UIViewController) { + let alert = UIAlertController( + title: L10n.NftErrorAlert.title, message: L10n.NftErrorAlert.message, preferredStyle: .alert + ) + + let okAction = UIAlertAction(title: L10n.NftErrorAlert.okButton, style: .default) + alert.addAction(okAction) + viewController.present(alert, animated: true) + } + + static func showCartFiltersAlert(on viewController: UIViewController, viewModel: CartViewModel) { + let alertController = UIAlertController(title: nil, message: L10n.CartFilterAlert.title, + preferredStyle: .actionSheet) + let addAction: (String, CartSortType) -> UIAlertAction = { title, sortType in + return UIAlertAction(title: title, style: .default) { _ in + viewModel.sort(by: sortType) + } + } + alertController.addAction(addAction(L10n.CartFilterAlert.price, .price)) + alertController.addAction(addAction(L10n.CartFilterAlert.rating, .rating)) + alertController.addAction(addAction(L10n.CartFilterAlert.name, .name)) + let cancelAction = UIAlertAction(title: L10n.Cart.closeButtonText, style: .cancel) + alertController.addAction(cancelAction) + viewController.present(alertController, animated: true) + } + + static func showDataError(on viewController: UIViewController, completion: @escaping () -> Void) { + let alert = UIAlertController(title: nil, message: L10n.Cart.loadDataErrorText, preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Payment.okText, style: .default) { _ in + completion() + } + alert.addAction(okAction) + viewController.present(alert, animated: true) + } + + static func showError(on viewController: UIViewController) { + let alert = UIAlertController(title: nil, message: L10n.Currency.paymentTypeText, preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Payment.okText, style: .default) + alert.addAction(okAction) + viewController.present(alert, animated: true) + } + + static func showPaymentError(on viewController: UIViewController, completion: @escaping () -> Void) { + let alert = UIAlertController(title: nil, message: L10n.Payment.errorText, preferredStyle: .alert) + alert.overrideUserInterfaceStyle = .light + let cancelAction = UIAlertAction(title: L10n.Payment.cancelText, style: .default) + alert.addAction(cancelAction) + let retryAction = UIAlertAction(title: L10n.Payment.retryText, style: .default) { _ in + completion() + } + alert.addAction(retryAction) + viewController.present(alert, animated: true) + } +} diff --git a/FakeNFT/Services/CurrencyService.swift b/FakeNFT/Services/CurrencyService.swift new file mode 100755 index 0000000000..80e9a2907d --- /dev/null +++ b/FakeNFT/Services/CurrencyService.swift @@ -0,0 +1,29 @@ +import Foundation + +typealias CurrencyCompletion = (Result<[CurrencyModel], Error>) -> Void +typealias PaymentCompletion = (Result) -> Void + +protocol CurrencyService { + func loadCurrencies(completion: @escaping CurrencyCompletion) + func getPaymentResult(currencyId: String, completion: @escaping PaymentCompletion) +} + +final class CurrencyServiceImpl: CurrencyService { + private let networkClient: NetworkClient + private let storage: CurrencyStorage + + init(networkClient: NetworkClient, storage: CurrencyStorage) { + self.networkClient = networkClient + self.storage = storage + } + + func loadCurrencies(completion: @escaping CurrencyCompletion) { + let request = CurrencyRequest() + networkClient.send(request: request, type: [CurrencyModel].self, onResponse: completion) + } + + func getPaymentResult(currencyId: String, completion: @escaping PaymentCompletion) { + let request = PaymentRequest(id: currencyId) + networkClient.send(request: request, type: PaymentModel.self, onResponse: completion) + } +} diff --git a/FakeNFT/Services/NftService.swift b/FakeNFT/Services/NFTService.swift old mode 100644 new mode 100755 similarity index 99% rename from FakeNFT/Services/NftService.swift rename to FakeNFT/Services/NFTService.swift index 3a9b21711c..c46536a274 --- a/FakeNFT/Services/NftService.swift +++ b/FakeNFT/Services/NFTService.swift @@ -21,7 +21,6 @@ final class NftServiceImpl: NftService { completion(.success(nft)) return } - let request = NFTRequest(id: id) networkClient.send(request: request, type: Nft.self) { [weak storage] result in switch result { diff --git a/FakeNFT/Services/NFTServiceProfile.swift b/FakeNFT/Services/NFTServiceProfile.swift new file mode 100755 index 0000000000..c736d0f112 --- /dev/null +++ b/FakeNFT/Services/NFTServiceProfile.swift @@ -0,0 +1,25 @@ +import Foundation + +final class NFTServiceProfile { + static let shared = NFTServiceProfile(networkHelper: NetworkServiceHelper(networkClient: DefaultNetworkClient())) + + private let networkHelper: NetworkServiceHelper + + init(networkHelper: NetworkServiceHelper) { + self.networkHelper = networkHelper + } + + func fetchNFT(nftID: String, completion: @escaping (Result) -> Void) { + let request = FetchNFTNetworkRequest(nftID: nftID) + networkHelper.fetchData(request: request, type: NFTProfile.self, completion: completion) + } + + func fetchAuthor(authorID: String, completion: @escaping (Result) -> Void) { + let request = FetchAuthorNetworkRequest(authorID: authorID) + networkHelper.fetchData(request: request, type: Author.self, completion: completion) + } + + func stopAllTasks() { + networkHelper.stopAllTasks() + } +} diff --git a/FakeNFT/Services/ProfileRouter.swift b/FakeNFT/Services/ProfileRouter.swift new file mode 100755 index 0000000000..180d5106d1 --- /dev/null +++ b/FakeNFT/Services/ProfileRouter.swift @@ -0,0 +1,47 @@ +import UIKit + +protocol ProfileRouting { + func routeToEditingViewController() + func routeToWebView(url: URL) + func routeToUserNFT(nftList: [String]) + func routeToFavoritesNFT(nftList: [String]) +} + +class ProfileRouter: ProfileRouting { + private let factory = ViewControllerFactory() + weak var viewController: UIViewController? + + init(viewController: UIViewController) { + self.viewController = viewController + } + + func routeToUserNFT(nftList: [String]) { + let destinationVC = factory.makeUserNFTViewController(nftList: nftList) + pushController(destinationVC) + } + + func routeToFavoritesNFT(nftList: [String]) { + let destinationVC = factory.makeFavoritesNFTViewController(nftList: nftList) + pushController(destinationVC) + } + + func routeToEditingViewController() { + let editingViewController = factory.makeEditingViewController() + presentController(editingViewController) + } + + func routeToWebView(url: URL) { + let webView = factory.makeWebView(url: url) + pushController(webView) + } + + private func pushController(_ controller: UIViewController) { + controller.hidesBottomBarWhenPushed = true + viewController?.navigationController?.pushViewController(controller, animated: true) + } + + private func presentController(_ controller: UIViewController) { + controller.hidesBottomBarWhenPushed = true + viewController?.present(controller, animated: true) + } +} diff --git a/FakeNFT/Services/ProfileService.swift b/FakeNFT/Services/ProfileService.swift new file mode 100755 index 0000000000..0eb8b77416 --- /dev/null +++ b/FakeNFT/Services/ProfileService.swift @@ -0,0 +1,40 @@ +import Foundation + +final class ProfileService { + static let shared = ProfileService(networkHelper: NetworkServiceHelper(networkClient: DefaultNetworkClient())) + + private let networkHelper: NetworkServiceHelper + + init(networkHelper: NetworkServiceHelper) { + self.networkHelper = networkHelper + } + + func fetchProfile(completion: @escaping (Result) -> Void) { + let request = FetchProfileNetworkRequest() + networkHelper.fetchData(request: request, type: UserProfile.self) { result in + switch result { + case .success(let profile): + completion(.success(profile)) + case .failure(let error): + completion(.failure(error)) + } + } + } + + func updateProfile(with userProfileModel: UserProfile, + completion: @escaping (Result) -> Void) { + let request = UpdateProfileNetworkRequest(userProfile: userProfileModel) + networkHelper.fetchData(request: request, type: UserProfile.self) { result in + switch result { + case .success(let updatedProfile): + completion(.success(updatedProfile)) + case .failure(let error): + completion(.failure(error)) + } + } + } + + func stopAllTasks() { + networkHelper.stopAllTasks() + } +} diff --git a/FakeNFT/Services/Requests/AuthorRequest.swift b/FakeNFT/Services/Requests/AuthorRequest.swift new file mode 100755 index 0000000000..431828c14d --- /dev/null +++ b/FakeNFT/Services/Requests/AuthorRequest.swift @@ -0,0 +1,17 @@ +// +// AuthorRequest.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 15.11.2023. +// + +import Foundation + +struct AuthorRequest: NetworkRequest { + + let id: String + + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/users/\(id)") + } +} diff --git a/FakeNFT/Services/Requests/CartByldRequest.swift b/FakeNFT/Services/Requests/CartByldRequest.swift new file mode 100755 index 0000000000..24e06e2e89 --- /dev/null +++ b/FakeNFT/Services/Requests/CartByldRequest.swift @@ -0,0 +1,7 @@ +import Foundation + +struct CartRequest: NetworkRequest { + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/orders/1") + } +} diff --git a/FakeNFT/Services/Requests/CatalogRequest.swift b/FakeNFT/Services/Requests/CatalogRequest.swift new file mode 100755 index 0000000000..16d4bb52de --- /dev/null +++ b/FakeNFT/Services/Requests/CatalogRequest.swift @@ -0,0 +1,15 @@ +// +// CatalogRequest.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 07.11.2023. +// + +import Foundation + +struct CatalogRequest: NetworkRequest { + + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/collections/") + } +} diff --git a/FakeNFT/Services/Requests/CurrencyByldRequest.swift b/FakeNFT/Services/Requests/CurrencyByldRequest.swift new file mode 100755 index 0000000000..23c61e654a --- /dev/null +++ b/FakeNFT/Services/Requests/CurrencyByldRequest.swift @@ -0,0 +1,7 @@ +import Foundation + +struct CurrencyRequest: NetworkRequest { + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/currencies") + } +} diff --git a/FakeNFT/Services/Requests/ExampleRequest.swift b/FakeNFT/Services/Requests/ExampleRequest.swift old mode 100644 new mode 100755 index 569d94aca2..a9d2cfd880 --- a/FakeNFT/Services/Requests/ExampleRequest.swift +++ b/FakeNFT/Services/Requests/ExampleRequest.swift @@ -2,6 +2,12 @@ import Foundation struct ExampleRequest: NetworkRequest { var endpoint: URL? { - URL(string: "INSERT_URL_HERE") + URL(string: "https://65450baa5a0b4b04436d87e6.mockapi.io/api/v1/:endpoint") } } + +struct NFTNetworkRequest: NetworkRequest { + var endpoint: URL? + var httpMethod: HttpMethod + var dto: Encodable? +} diff --git a/FakeNFT/Services/Requests/NftByIdRequest.swift b/FakeNFT/Services/Requests/NftByIdRequest.swift old mode 100644 new mode 100755 diff --git a/FakeNFT/Services/Requests/OrdersRequest.swift b/FakeNFT/Services/Requests/OrdersRequest.swift new file mode 100755 index 0000000000..cd0bf8e1ec --- /dev/null +++ b/FakeNFT/Services/Requests/OrdersRequest.swift @@ -0,0 +1,16 @@ +// +// OrdersRequest.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 21.11.2023. +// + +import Foundation + +struct OrdersRequest: NetworkRequest { + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/orders/1") + } + var httpMethod: HttpMethod + var dto: Encodable? +} diff --git a/FakeNFT/Services/Requests/PaymentRequest.swift b/FakeNFT/Services/Requests/PaymentRequest.swift new file mode 100755 index 0000000000..120cddbaeb --- /dev/null +++ b/FakeNFT/Services/Requests/PaymentRequest.swift @@ -0,0 +1,10 @@ +import Foundation + +struct PaymentRequest: NetworkRequest { + + let id: String + + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/orders/1/payment/\(id)") + } +} diff --git a/FakeNFT/Services/Requests/ProfileRequest.swift b/FakeNFT/Services/Requests/ProfileRequest.swift new file mode 100755 index 0000000000..bbd81e1285 --- /dev/null +++ b/FakeNFT/Services/Requests/ProfileRequest.swift @@ -0,0 +1,17 @@ +// +// ProfileRequest.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 17.11.2023. +// + +import Foundation + +struct ProfileRequest: NetworkRequest { + + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/profile/1") + } + var httpMethod: HttpMethod + var dto: Encodable? +} diff --git a/FakeNFT/Services/Requests/RequestConstants.swift b/FakeNFT/Services/Requests/RequestConstants.swift old mode 100644 new mode 100755 index cbc97dab5e..63175cf818 --- a/FakeNFT/Services/Requests/RequestConstants.swift +++ b/FakeNFT/Services/Requests/RequestConstants.swift @@ -1,4 +1,4 @@ enum RequestConstants { - #warning("insert your baseUrl .mockapi.io") - static let baseURL = "" + static let baseURL = "https://65450baa5a0b4b04436d87e6.mockapi.io" + static let cartUserAgreementLink = "https://yandex.ru/legal/practicum_termsofuse/" } diff --git a/FakeNFT/Services/Requests/UserRequest.swift b/FakeNFT/Services/Requests/UserRequest.swift new file mode 100755 index 0000000000..a1838a10c5 --- /dev/null +++ b/FakeNFT/Services/Requests/UserRequest.swift @@ -0,0 +1,16 @@ +// +// UserRequest.swift +// FakeNFT +// +// Created by Eugene Kolesnikov on 21.11.2023. +// + +import Foundation + +struct UserRequest: NetworkRequest { + let id: String + + var endpoint: URL? { + URL(string: "\(RequestConstants.baseURL)/api/v1/users/\(id)") + } +} diff --git a/FakeNFT/Services/ServicesAssemly.swift b/FakeNFT/Services/ServicesAssemly.swift old mode 100644 new mode 100755 index 033f8bdf61..ed705915d4 --- a/FakeNFT/Services/ServicesAssemly.swift +++ b/FakeNFT/Services/ServicesAssemly.swift @@ -2,13 +2,19 @@ final class ServicesAssembly { private let networkClient: NetworkClient private let nftStorage: NftStorage + private let cartStorage: CartStorage + private let currencyStorage: CurrencyStorage init( networkClient: NetworkClient, - nftStorage: NftStorage + nftStorage: NftStorage, + cartStorage: CartStorage, + currencyStorage: CurrencyStorage ) { self.networkClient = networkClient self.nftStorage = nftStorage + self.cartStorage = cartStorage + self.currencyStorage = currencyStorage } var nftService: NftService { @@ -17,4 +23,18 @@ final class ServicesAssembly { storage: nftStorage ) } + + var cartService: CartService { + CartServiceImpl( + networkClient: networkClient, + storage: cartStorage + ) + } + + var currencyService: CurrencyService { + CurrencyServiceImpl( + networkClient: networkClient, + storage: currencyStorage + ) + } } diff --git a/FakeNFT/ru.lproj/LaunchScreen.storyboard b/FakeNFT/ru.lproj/LaunchScreen.storyboard new file mode 100755 index 0000000000..b56dc4a8f3 --- /dev/null +++ b/FakeNFT/ru.lproj/LaunchScreen.storyboard @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FakeNFT/swiftgen.yml b/FakeNFT/swiftgen.yml new file mode 100755 index 0000000000..d5ffe2f7fa --- /dev/null +++ b/FakeNFT/swiftgen.yml @@ -0,0 +1,63 @@ +## Note: all of the config entries below are just examples with placeholders. Be sure to edit and adjust to your needs when uncommenting. + +## In case your config entries all use a common input/output parent directory, you can specify those here. +## Every input/output paths in the rest of the config will then be expressed relative to these. +## Those two top-level keys are optional and default to "." (the directory of the config file). +# input_dir: MyLib/Sources/ +# output_dir: MyLib/Generated/ + + +## Generate constants for your localized strings. +## Be sure that SwiftGen only parses ONE locale (typically Base.lproj, or en.lproj, or whichever your development region is); otherwise it will generate the same keys multiple times. +## SwiftGen will parse all `.strings` files found in that folder. +# strings: +# inputs: +# - Resources/Base.lproj +# outputs: +# - templateName: structured-swift5 +# output: Strings+Generated.swift +strings: + inputs: + - Resources/ru.lproj + outputs: + - templateName: structured-swift5 + output: Generated/Strings.swift + + +## Generate constants for your Assets Catalogs, including constants for images, colors, ARKit resources, etc. +## This example also shows how to provide additional parameters to your template to customize the output. +## - Especially the `forceProvidesNamespaces: true` param forces to create sub-namespace for each folder/group used in your Asset Catalogs, even the ones without "Provides Namespace". Without this param, SwiftGen only generates sub-namespaces for folders/groups which have the "Provides Namespace" box checked in the Inspector pane. +## - To know which params are supported for a template, use `swiftgen template doc xcassets swift5` to open the template documentation on GitHub. +# xcassets: +# inputs: +# - Main.xcassets +# - ProFeatures.xcassets +# outputs: +# - templateName: swift5 +# params: +# forceProvidesNamespaces: true +# output: XCAssets+Generated.swift + +## Generate constants for your storyboards and XIBs. +## This one generates 2 output files, one containing the storyboard scenes, and another for the segues. +## (You can remove the segues entry if you don't use segues in your IB files). +## For `inputs` we can use "." here (aka "current directory", at least relative to `input_dir` = "MyLib/Sources"), +## and SwiftGen will recursively find all `*.storyboard` and `*.xib` files in there. +# ib: +# inputs: +# - . +# outputs: +# - templateName: scenes-swift5 +# output: IB-Scenes+Generated.swift +# - templateName: segues-swift5 +# output: IB-Segues+Generated.swift + + +## There are other parsers available for you to use depending on your needs, for example: +## - `fonts` (if you have custom ttf/ttc font files) +## - `coredata` (for CoreData models) +## - `json`, `yaml` and `plist` (to parse custom JSON/YAML/Plist files and generate code from their content) +## … +## +## For more info, use `swiftgen config doc` to open the full documentation on GitHub. +## https://github.com/SwiftGen/SwiftGen/tree/6.6.2/Documentation/ diff --git a/FakeNFTTests/ExampleUnitTests.swift b/FakeNFTTests/ExampleUnitTests.swift old mode 100644 new mode 100755 index 302ce89bd5..17b87977cc --- a/FakeNFTTests/ExampleUnitTests.swift +++ b/FakeNFTTests/ExampleUnitTests.swift @@ -3,6 +3,5 @@ import XCTest final class ExampleUnitTests: XCTestCase { func testExample() { - // TODO: - Не забудьте написать unit-тесты } } diff --git a/FakeNFTUITests/FakeNFTUITests.swift b/FakeNFTUITests/FakeNFTUITests.swift old mode 100644 new mode 100755 index 5b3e5f88ac..b09e28a7ca --- a/FakeNFTUITests/FakeNFTUITests.swift +++ b/FakeNFTUITests/FakeNFTUITests.swift @@ -4,7 +4,5 @@ final class FakeNFTUITests: XCTestCase { func testExample() throws { let app = XCUIApplication() app.launch() - - // TODO: - Не забудьте написать UI-тесты } } diff --git a/Helpers/AlertService.swift b/Helpers/AlertService.swift new file mode 100755 index 0000000000..48b967ba48 --- /dev/null +++ b/Helpers/AlertService.swift @@ -0,0 +1,54 @@ +import UIKit + +struct AlertActionModel { + let title: String + let style: UIAlertAction.Style + let handler: ((String?) -> Void)? +} + +struct AlertProfileModel { + let title: String? + let message: String? + let style: UIAlertController.Style + let actions: [AlertActionModel] + let textFieldPlaceholder: String? +} + +protocol AlertServiceProtocol { + func showAlert(model: AlertProfileModel) +} + +class AlertService: AlertServiceProtocol { + private weak var viewController: UIViewController? + + init(viewController: UIViewController) { + self.viewController = viewController + } + + func showAlert(model: AlertProfileModel) { + let alertController = UIAlertController( + title: model.title, + message: model.message, + preferredStyle: model.style + ) + + if let placeholder = model.textFieldPlaceholder { + alertController.addTextField { textField in + textField.placeholder = placeholder + } + } + + model.actions.forEach { actionModel in + let action = UIAlertAction(title: actionModel.title, style: actionModel.style) { _ in + if let textField = alertController.textFields?.first { + actionModel.handler?(textField.text) + } else { + actionModel.handler?(nil) + } + } + alertController.addAction(action) + } + + viewController?.present(alertController, animated: true, completion: nil) + } +} diff --git a/Helpers/GeometricProfileParams.swift b/Helpers/GeometricProfileParams.swift new file mode 100755 index 0000000000..a75df9c1d9 --- /dev/null +++ b/Helpers/GeometricProfileParams.swift @@ -0,0 +1,25 @@ +import Foundation + +struct GeometricProfileParams { + let cellPerRowCount: CGFloat + let cellSpacing: CGFloat + let cellLeftInset: CGFloat + let cellRightInset: CGFloat + let cellHeight: CGFloat + let paddingWight: CGFloat + + init( + cellPerRowCount: CGFloat, + cellSpacing: CGFloat, + cellLeftInset: CGFloat, + cellRightInset: CGFloat, + cellHeight: CGFloat + ) { + self.cellPerRowCount = cellPerRowCount + self.cellSpacing = cellSpacing + self.cellLeftInset = cellLeftInset + self.cellRightInset = cellRightInset + self.cellHeight = cellHeight + self.paddingWight = (cellPerRowCount - 1) * cellSpacing + cellLeftInset + cellRightInset + } +} diff --git a/Helpers/ImageValidator.swift b/Helpers/ImageValidator.swift new file mode 100755 index 0000000000..d28e9019bf --- /dev/null +++ b/Helpers/ImageValidator.swift @@ -0,0 +1,19 @@ +import Foundation +import Kingfisher + +protocol ImageValidatorProtocol { + func isValidImageURL(_ url: URL, completion: @escaping (Bool) -> Void) +} + +final class ImageValidator: ImageValidatorProtocol { + func isValidImageURL(_ url: URL, completion: @escaping (Bool) -> Void) { + KingfisherManager.shared.retrieveImage(with: url) { result in + switch result { + case .success: + completion(true) + case .failure: + completion(false) + } + } + } +} diff --git a/Helpers/LoadingState.swift b/Helpers/LoadingState.swift new file mode 100755 index 0000000000..c6a569396e --- /dev/null +++ b/Helpers/LoadingState.swift @@ -0,0 +1,8 @@ +import Foundation + +enum LoadingState { + case idle + case loading + case loaded(hasData: Bool) + case error(Error) +} diff --git a/Helpers/NetworkServiceHelper.swift b/Helpers/NetworkServiceHelper.swift new file mode 100755 index 0000000000..9510ea827c --- /dev/null +++ b/Helpers/NetworkServiceHelper.swift @@ -0,0 +1,57 @@ +import Foundation + +final class NetworkServiceHelper { + private let networkClient: NetworkClient + private var currentTasks: [NetworkTask] = [] + + init(networkClient: NetworkClient) { + self.networkClient = networkClient + } + + func fetchData(request: NetworkRequest, type: T.Type, completion: @escaping (Result) -> Void) { + fetchData(request: request, type: T.self, retryCount: 0, delayInterval: 3.0, completion: completion) + } + + func stopAllTasks() { + currentTasks.forEach { $0.cancel() } + currentTasks.removeAll() + } + + private func fetchData( + request: NetworkRequest, + type: T.Type, + retryCount: Int, + delayInterval: Double, + completion: @escaping (Result) -> Void + ) { + let task = networkClient.send(request: request, type: T.self) { [weak self] result in + guard let self = self else { return } + + switch result { + case .success(let data): + completion(.success(data)) + case .failure(let error): + if retryCount < 3 { + if case NetworkClientError.httpStatusCode(429) = error { + let newRetryCount = retryCount + 1 + let newDelayInterval = delayInterval * 2 + DispatchQueue.global().asyncAfter(deadline: .now() + delayInterval) { + self.fetchData(request: request, type: T.self, retryCount: newRetryCount, delayInterval: newDelayInterval, completion: completion) + } + } else { + completion(.failure(error)) + } + } else { + completion(.failure(error)) + } + } + } + + guard let unwrappedTask = task else { + completion(.failure(NetworkClientError.taskCreationFailed)) + return + } + + self.currentTasks.append(unwrappedTask) + } +} diff --git a/Helpers/SortOptions.swift b/Helpers/SortOptions.swift new file mode 100755 index 0000000000..6a6a4efdb6 --- /dev/null +++ b/Helpers/SortOptions.swift @@ -0,0 +1,18 @@ +import Foundation + +enum SortOption { + case price + case rating + case title + + var description: String { + switch self { + case .price: + return NSLocalizedString("SortOptions.price", comment: "") + case .rating: + return NSLocalizedString("SortOptions.rate", comment: "") + case .title: + return NSLocalizedString("SortOptions.name", comment: "") + } + } +} diff --git a/Helpers/ViewControllerFactory.swift b/Helpers/ViewControllerFactory.swift new file mode 100755 index 0000000000..7fa88acbb1 --- /dev/null +++ b/Helpers/ViewControllerFactory.swift @@ -0,0 +1,23 @@ +import UIKit + +final class ViewControllerFactory { + func makeWebView(url: URL) -> WebViewViewController { + let controller = WebViewViewController(url: url) + return controller + } + + func makeUserNFTViewController(nftList: [String]) -> UserNFTViewController { + return UserNFTViewController(nftList: nftList, + viewModel: UserNFTViewModel(nftService: NFTServiceProfile.shared)) + } + + func makeFavoritesNFTViewController(nftList: [String]) -> FavoritesNFTViewController { + return FavoritesNFTViewController(nftList: nftList, + viewModel: FavoritesNFTViewModel(nftService: NFTServiceProfile.shared, + profileService: ProfileService.shared)) + } + + func makeEditingViewController() -> EditingViewController { + return EditingViewController(viewModel: EditingViewModel(profileService: ProfileService.shared)) + } +} diff --git a/Readme.md b/Readme.md index 2a3e177e38..2eee070daf 100644 --- a/Readme.md +++ b/Readme.md @@ -2,6 +2,12 @@ # Ссылки +[Запись Catalog](https://www.veed.io/view/deb6963e-2c54-4e4d-b803-49a67d1121ec?panel=share) + +[Запись Cart](https://disk.yandex.com/i/et6WGMWac4sWUA) + +[Запись Profile](https://disk.yandex.ru/i/Hc2CiEsLX2X1RA) + [Дизайн Figma](https://www.figma.com/file/k1LcgXHGTHIeiCv4XuPbND/FakeNFT-(YP)?node-id=96-5542&t=YdNbOI8EcqdYmDeg-0) # Назначение и цели приложения