diff --git a/Decompositions/.gitkeep b/Decompositions/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Decompositions/statistic-decompositions.md b/Decompositions/statistic-decompositions.md new file mode 100644 index 0000000000..026c0e630f --- /dev/null +++ b/Decompositions/statistic-decompositions.md @@ -0,0 +1,16 @@ +# Архетектура +MVC +# Способ верски +Верстка кодом +## 1. экран со списком пользователей + верстка (4 часа) + разработка логики/сеть (7 часов) + добавление сортировки (3 часа) +## 2. экран профиля + верстка (3,5 часа) + разработка логики/сеть (6 часов) +## 3. экран коллекции + верстка (3,5 часа) + разработка логики/сеть (6 часов) + +# Decomposition board: https://github.com/users/ilyanikitash/projects/2/views/1 diff --git a/FakeNFT.xcodeproj/project.pbxproj b/FakeNFT.xcodeproj/project.pbxproj index e9ad82fe6f..ef616b7a6f 100644 --- a/FakeNFT.xcodeproj/project.pbxproj +++ b/FakeNFT.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -43,6 +43,26 @@ 3FC8C39129D2453B0081F015 /* ExamplePutRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC8C39029D2453B0081F015 /* ExamplePutRequest.swift */; }; 3FC8C39329D246BA0081F015 /* DateFormatters+Presets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC8C39229D246BA0081F015 /* DateFormatters+Presets.swift */; }; 558E39E72C68CE0A00FB86AC /* NftService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558E39E62C68CE0900FB86AC /* NftService.swift */; }; + D31966082D955E6600B18038 /* NFTCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31966072D955E5700B18038 /* NFTCollectionViewCell.swift */; }; + D3309ADB2D856E2C0026A80A /* StatisticUsersListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3309ADA2D856E1D0026A80A /* StatisticUsersListTableViewCell.swift */; }; + D3309B132D8584480026A80A /* SortCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3309B122D8584420026A80A /* SortCases.swift */; }; + D3309B152D85852E0026A80A /* SortAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3309B142D8585220026A80A /* SortAlertPresenter.swift */; }; + D39512412D9E9BDA00E5D9A0 /* NFTCollectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39512402D9E9BD000E5D9A0 /* NFTCollectionService.swift */; }; + D39512462D9EA1A400E5D9A0 /* NFTCollectionServiceErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39512452D9EA19900E5D9A0 /* NFTCollectionServiceErrors.swift */; }; + D3BB5F102D843C76007C84DF /* StatisticUsersListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3BB5F0F2D843C59007C84DF /* StatisticUsersListViewController.swift */; }; + D3BB5F122D843CB7007C84DF /* StatisticUsersListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3BB5F112D843CAB007C84DF /* StatisticUsersListView.swift */; }; + D3D75EB32D8ADCFC0099E9E4 /* SortStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D75EB22D8ADCF40099E9E4 /* SortStorage.swift */; }; + D3D75EBD2D8AEC9B0099E9E4 /* UsersListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D75EBC2D8AEC940099E9E4 /* UsersListService.swift */; }; + D3D75EBF2D8AEF990099E9E4 /* UsersListServiceErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D75EBE2D8AEF8B0099E9E4 /* UsersListServiceErrors.swift */; }; + D3D75EC12D8AF6A00099E9E4 /* URLSession + data.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D75EC02D8AF68D0099E9E4 /* URLSession + data.swift */; }; + D3D75EC32D8AF6EE0099E9E4 /* SnakeCaseJSONDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D75EC22D8AF6EB0099E9E4 /* SnakeCaseJSONDecoder.swift */; }; + D3E561BB2D90687000BCFF33 /* WebViewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E561BA2D90686500BCFF33 /* WebViewViewController.swift */; }; + D3E561BD2D90687A00BCFF33 /* WebViewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E561BC2D90687500BCFF33 /* WebViewView.swift */; }; + D3ED08E92D943DBF00EDD9FF /* NFTCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3ED08E82D943DB500EDD9FF /* NFTCollectionViewController.swift */; }; + D3ED08EB2D943E2900EDD9FF /* NFTCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3ED08EA2D943E2300EDD9FF /* NFTCollectionView.swift */; }; + D3FE781B2D8D3D32002208CD /* UserCardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3FE781A2D8D3D2A002208CD /* UserCardViewController.swift */; }; + D3FE781D2D8D3D43002208CD /* UserCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3FE781C2D8D3D3A002208CD /* UserCardView.swift */; }; + D3FE781F2D8D6F71002208CD /* UserCardTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3FE781E2D8D6F65002208CD /* UserCardTableViewCell.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 */; }; @@ -104,6 +124,26 @@ 3FC8C39029D2453B0081F015 /* ExamplePutRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExamplePutRequest.swift; sourceTree = ""; }; 3FC8C39229D246BA0081F015 /* DateFormatters+Presets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatters+Presets.swift"; sourceTree = ""; }; 558E39E62C68CE0900FB86AC /* NftService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftService.swift; sourceTree = ""; }; + D31966072D955E5700B18038 /* NFTCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFTCollectionViewCell.swift; sourceTree = ""; }; + D3309ADA2D856E1D0026A80A /* StatisticUsersListTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticUsersListTableViewCell.swift; sourceTree = ""; }; + D3309B122D8584420026A80A /* SortCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortCases.swift; sourceTree = ""; }; + D3309B142D8585220026A80A /* SortAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortAlertPresenter.swift; sourceTree = ""; }; + D39512402D9E9BD000E5D9A0 /* NFTCollectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFTCollectionService.swift; sourceTree = ""; }; + D39512452D9EA19900E5D9A0 /* NFTCollectionServiceErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFTCollectionServiceErrors.swift; sourceTree = ""; }; + D3BB5F0F2D843C59007C84DF /* StatisticUsersListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticUsersListViewController.swift; sourceTree = ""; }; + D3BB5F112D843CAB007C84DF /* StatisticUsersListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticUsersListView.swift; sourceTree = ""; }; + D3D75EB22D8ADCF40099E9E4 /* SortStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortStorage.swift; sourceTree = ""; }; + D3D75EBC2D8AEC940099E9E4 /* UsersListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersListService.swift; sourceTree = ""; }; + D3D75EBE2D8AEF8B0099E9E4 /* UsersListServiceErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersListServiceErrors.swift; sourceTree = ""; }; + D3D75EC02D8AF68D0099E9E4 /* URLSession + data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession + data.swift"; sourceTree = ""; }; + D3D75EC22D8AF6EB0099E9E4 /* SnakeCaseJSONDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnakeCaseJSONDecoder.swift; sourceTree = ""; }; + D3E561BA2D90686500BCFF33 /* WebViewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewViewController.swift; sourceTree = ""; }; + D3E561BC2D90687500BCFF33 /* WebViewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewView.swift; sourceTree = ""; }; + D3ED08E82D943DB500EDD9FF /* NFTCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFTCollectionViewController.swift; sourceTree = ""; }; + D3ED08EA2D943E2300EDD9FF /* NFTCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFTCollectionView.swift; sourceTree = ""; }; + D3FE781A2D8D3D2A002208CD /* UserCardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCardViewController.swift; sourceTree = ""; }; + D3FE781C2D8D3D3A002208CD /* UserCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCardView.swift; sourceTree = ""; }; + D3FE781E2D8D6F65002208CD /* UserCardTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCardTableViewCell.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 = ""; }; @@ -111,6 +151,10 @@ E1CD40DB2A96BECC00BE7FE8 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + D3309B0C2D8579010026A80A /* Statistic */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Statistic; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 3F68069029CBBAF100B4F915 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -224,6 +268,7 @@ 3F68069529CBBAF100B4F915 /* FakeNFT */ = { isa = PBXGroup; children = ( + D3BB5F0D2D843C0F007C84DF /* Statistic */, 3F68069629CBBAF100B4F915 /* AppDelegate.swift */, 3F68069829CBBAF100B4F915 /* SceneDelegate.swift */, E1CD40DA2A96BE9B00BE7FE8 /* Resources */, @@ -288,6 +333,7 @@ 3F6806C929CBBCAF00B4F915 /* Models */ = { isa = PBXGroup; children = ( + D3309B0C2D8579010026A80A /* Statistic */, 3F6806D829CC979D00B4F915 /* Network */, ); path = Models; @@ -296,6 +342,8 @@ 3F6806CE29CBBD1B00B4F915 /* Foundation */ = { isa = PBXGroup; children = ( + D3D75EC22D8AF6EB0099E9E4 /* SnakeCaseJSONDecoder.swift */, + D3D75EC02D8AF68D0099E9E4 /* URLSession + data.swift */, 0CFCB74C2A7817C30009A829 /* MemoryStorage */, 3F6806CF29CBBDB100B4F915 /* NetworkClient */, 3F6806D629CBC50A00B4F915 /* CellsReusingUtils.swift */, @@ -333,6 +381,95 @@ path = Requests; sourceTree = ""; }; + D3309B102D8584290026A80A /* Sort */ = { + isa = PBXGroup; + children = ( + D3D75EB22D8ADCF40099E9E4 /* SortStorage.swift */, + D3309B142D8585220026A80A /* SortAlertPresenter.swift */, + D3309B122D8584420026A80A /* SortCases.swift */, + ); + path = Sort; + sourceTree = ""; + }; + D395123F2D9E9BB600E5D9A0 /* NFTCollection */ = { + isa = PBXGroup; + children = ( + D39512452D9EA19900E5D9A0 /* NFTCollectionServiceErrors.swift */, + D39512402D9E9BD000E5D9A0 /* NFTCollectionService.swift */, + ); + path = NFTCollection; + sourceTree = ""; + }; + D3BB5F0D2D843C0F007C84DF /* Statistic */ = { + isa = PBXGroup; + children = ( + D3DE9C9F2D943D63004F578D /* NFTCollection */, + D3FE78192D8D3D1C002208CD /* UserCard */, + D3D75EB42D8AEBE40099E9E4 /* Network */, + D3309B102D8584290026A80A /* Sort */, + D3BB5F0E2D843C2A007C84DF /* StatisticUsersList */, + ); + path = Statistic; + sourceTree = ""; + }; + D3BB5F0E2D843C2A007C84DF /* StatisticUsersList */ = { + isa = PBXGroup; + children = ( + D3309ADA2D856E1D0026A80A /* StatisticUsersListTableViewCell.swift */, + D3BB5F112D843CAB007C84DF /* StatisticUsersListView.swift */, + D3BB5F0F2D843C59007C84DF /* StatisticUsersListViewController.swift */, + ); + path = StatisticUsersList; + sourceTree = ""; + }; + D3D75EB42D8AEBE40099E9E4 /* Network */ = { + isa = PBXGroup; + children = ( + D395123F2D9E9BB600E5D9A0 /* NFTCollection */, + D3D75EB52D8AEBF70099E9E4 /* UsersList */, + ); + path = Network; + sourceTree = ""; + }; + D3D75EB52D8AEBF70099E9E4 /* UsersList */ = { + isa = PBXGroup; + children = ( + D3D75EBE2D8AEF8B0099E9E4 /* UsersListServiceErrors.swift */, + D3D75EBC2D8AEC940099E9E4 /* UsersListService.swift */, + ); + path = UsersList; + sourceTree = ""; + }; + D3DE9C9F2D943D63004F578D /* NFTCollection */ = { + isa = PBXGroup; + children = ( + D31966072D955E5700B18038 /* NFTCollectionViewCell.swift */, + D3ED08EA2D943E2300EDD9FF /* NFTCollectionView.swift */, + D3ED08E82D943DB500EDD9FF /* NFTCollectionViewController.swift */, + ); + path = NFTCollection; + sourceTree = ""; + }; + D3E561B92D90685800BCFF33 /* WebView */ = { + isa = PBXGroup; + children = ( + D3E561BC2D90687500BCFF33 /* WebViewView.swift */, + D3E561BA2D90686500BCFF33 /* WebViewViewController.swift */, + ); + path = WebView; + sourceTree = ""; + }; + D3FE78192D8D3D1C002208CD /* UserCard */ = { + isa = PBXGroup; + children = ( + D3E561B92D90685800BCFF33 /* WebView */, + D3FE781E2D8D6F65002208CD /* UserCardTableViewCell.swift */, + D3FE781C2D8D3D3A002208CD /* UserCardView.swift */, + D3FE781A2D8D3D2A002208CD /* UserCardViewController.swift */, + ); + path = UserCard; + sourceTree = ""; + }; E19CD5A92A98B55900CA39A5 /* Cell */ = { isa = PBXGroup; children = ( @@ -376,6 +513,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + D3309B0C2D8579010026A80A /* Statistic */, + ); name = FakeNFT; packageProductDependencies = ( 3F603CCC29DB4A53000C43D7 /* ProgressHUD */, @@ -503,27 +643,43 @@ buildActionMask = 2147483647; files = ( 3FC8C38B29D242E90081F015 /* ProductDetailsTableViewCell.swift in Sources */, + D3D75EB32D8ADCFC0099E9E4 /* SortStorage.swift in Sources */, + D3309B132D8584480026A80A /* SortCases.swift in Sources */, 3F478ECF29DB474E00F6D39E /* Colors.swift in Sources */, + D3D75EBD2D8AEC9B0099E9E4 /* UsersListService.swift in Sources */, + D39512412D9E9BDA00E5D9A0 /* NFTCollectionService.swift in Sources */, 0C79EE662A76DDFF00EE90EA /* NftDetailViewController.swift in Sources */, E1A1B9DA2AA01CE400C3AFBC /* TabBarController.swift in Sources */, 3F478ED129DB476500F6D39E /* Fonts.swift in Sources */, 0C79EE6A2A76DE1000EE90EA /* NftDetailAssembly.swift in Sources */, + D3BB5F102D843C76007C84DF /* StatisticUsersListViewController.swift in Sources */, 3FC8C39329D246BA0081F015 /* DateFormatters+Presets.swift in Sources */, + D3FE781B2D8D3D32002208CD /* UserCardViewController.swift in Sources */, 0CFCB7422A78013E0009A829 /* Nft.swift in Sources */, 0CF2C2DD2A783CE600FDC837 /* ErrorView.swift in Sources */, 0C79EE682A76DE0900EE90EA /* NftDetailPresenter.swift in Sources */, 3F68069B29CBBAF100B4F915 /* ProductDetailsTableViewController.swift in Sources */, 0CFCB7402A78002A0009A829 /* ExamplePutService.swift in Sources */, 3F6806D529CBBEC700B4F915 /* NetworkTask.swift in Sources */, + D3FE781F2D8D6F71002208CD /* UserCardTableViewCell.swift in Sources */, 0CF2C2DB2A783C1B00FDC837 /* LoadingView.swift in Sources */, 558E39E72C68CE0A00FB86AC /* NftService.swift in Sources */, + D3E561BD2D90687A00BCFF33 /* WebViewView.swift in Sources */, + D3ED08EB2D943E2900EDD9FF /* NFTCollectionView.swift in Sources */, + D3E561BB2D90687000BCFF33 /* WebViewViewController.swift in Sources */, + D3D75EC12D8AF6A00099E9E4 /* URLSession + data.swift in Sources */, + D3D75EBF2D8AEF990099E9E4 /* UsersListServiceErrors.swift in Sources */, + D3BB5F122D843CB7007C84DF /* StatisticUsersListView.swift in Sources */, 3F68069729CBBAF100B4F915 /* AppDelegate.swift in Sources */, 3F68069929CBBAF100B4F915 /* SceneDelegate.swift in Sources */, 0C79EE6C2A76DE2E00EE90EA /* ServicesAssemly.swift in Sources */, 0CFCB74E2A7817DC0009A829 /* NftStorage.swift in Sources */, + D39512462D9EA1A400E5D9A0 /* NFTCollectionServiceErrors.swift in Sources */, 3FC8C39129D2453B0081F015 /* ExamplePutRequest.swift in Sources */, 0CFCB7462A78064B0009A829 /* NftDetailCellModel.swift in Sources */, + D3309ADB2D856E2C0026A80A /* StatisticUsersListTableViewCell.swift in Sources */, 0CFCB74B2A780EA80009A829 /* UIView+Constraints.swift in Sources */, + D3309B152D85852E0026A80A /* SortAlertPresenter.swift in Sources */, 3F6806D729CBC50A00B4F915 /* CellsReusingUtils.swift in Sources */, 0C79EE612A76DCD600EE90EA /* NftByIdRequest.swift in Sources */, E19CD5AB2A98B56600CA39A5 /* NftImageCollectionViewCell.swift in Sources */, @@ -531,7 +687,11 @@ 3F6806D329CBBE9600B4F915 /* NetworkRequest.swift in Sources */, 0CFCB7492A7808900009A829 /* TestCatalogController.swift in Sources */, 3F6806D129CBBE6B00B4F915 /* NetworkClient.swift in Sources */, + D3D75EC32D8AF6EE0099E9E4 /* SnakeCaseJSONDecoder.swift in Sources */, 0CFCB7442A7802440009A829 /* NftDetailInput.swift in Sources */, + D3ED08E92D943DBF00EDD9FF /* NFTCollectionViewController.swift in Sources */, + D31966082D955E6600B18038 /* NFTCollectionViewCell.swift in Sources */, + D3FE781D2D8D3D43002208CD /* UserCardView.swift in Sources */, 0C79EE632A76DD1900EE90EA /* RequestConstants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -712,7 +872,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6H2GQX47PU; + DEVELOPMENT_TEAM = 8HV589XKFV; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = FakeNFT/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -726,7 +886,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.practicum.FakeNFT; + PRODUCT_BUNDLE_IDENTIFIER = ilyanikitash.FakeNFT; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -741,7 +901,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6H2GQX47PU; + DEVELOPMENT_TEAM = 8HV589XKFV; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = FakeNFT/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -755,7 +915,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.practicum.FakeNFT; + PRODUCT_BUNDLE_IDENTIFIER = ilyanikitash.FakeNFT; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -770,6 +930,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8HV589XKFV; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.2; MARKETING_VERSION = 1.0; @@ -789,6 +950,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8HV589XKFV; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.2; MARKETING_VERSION = 1.0; @@ -807,6 +969,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8HV589XKFV; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.practicum.FakeNFTUITests; @@ -824,6 +987,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8HV589XKFV; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.practicum.FakeNFTUITests; diff --git a/FakeNFT/DesignSystem/Colors.swift b/FakeNFT/DesignSystem/Colors.swift index 8f95ac024d..583e812eff 100644 --- a/FakeNFT/DesignSystem/Colors.swift +++ b/FakeNFT/DesignSystem/Colors.swift @@ -34,7 +34,8 @@ extension UIColor { static let secondary = UIColor(red: 255 / 255, green: 193 / 255, blue: 7 / 255, alpha: 1.0) // Background Colors - static let background = UIColor.white + private static let backgroundLight = UIColor.white + private static let backgroundDark = UIColor.black // Text Colors static let textPrimary = UIColor.black @@ -46,7 +47,13 @@ extension UIColor { private static let yaBlackDark = UIColor.white private static let yaLightGrayLight = UIColor(hexString: "#F7F7F8") private static let yaLightGrayDark = UIColor(hexString: "#2C2C2E") - + + static let background = UIColor { traits in + return traits.userInterfaceStyle == .dark + ? .backgroundDark + : .backgroundLight + } + static let segmentActive = UIColor { traits in return traits.userInterfaceStyle == .dark ? .yaBlackDark diff --git a/FakeNFT/Foundation/SnakeCaseJSONDecoder.swift b/FakeNFT/Foundation/SnakeCaseJSONDecoder.swift new file mode 100644 index 0000000000..d3a7ec9d94 --- /dev/null +++ b/FakeNFT/Foundation/SnakeCaseJSONDecoder.swift @@ -0,0 +1,14 @@ +// +// SnakeCaseJSONDecoder.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/19/25. +// +import Foundation + +final class SnakeCaseJSONDecoder: JSONDecoder { + override init() { + super.init() + keyDecodingStrategy = .convertFromSnakeCase + } +} diff --git a/FakeNFT/Foundation/URLSession + data.swift b/FakeNFT/Foundation/URLSession + data.swift new file mode 100644 index 0000000000..73566db53a --- /dev/null +++ b/FakeNFT/Foundation/URLSession + data.swift @@ -0,0 +1,71 @@ +// +// URLSession.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/19/25. +// +import Foundation + + +enum NetworkError: Error { + case httpStatusCode(Int) + case urlRequestError(Error) + case urlSessionError +} + +extension URLSession { + func data( + for request: URLRequest, + completion: @escaping (Result) -> Void + ) -> URLSessionTask { + let fulfillCompletionOnTheMainThread: (Result) -> Void = { result in + DispatchQueue.main.async { + completion(result) + } + } + + let task = dataTask(with: request, completionHandler: { data, response, error in + if let data = data, let response = response, let statusCode = (response as? HTTPURLResponse)?.statusCode { + if 200 ..< 300 ~= statusCode { + fulfillCompletionOnTheMainThread(.success(data)) + } else { + print("[\(String(describing: self)).\(#function)]: \(NetworkError.httpStatusCode(statusCode)) - Network error with status code \(statusCode)") + fulfillCompletionOnTheMainThread(.failure(NetworkError.httpStatusCode(statusCode))) + } + } else if let error = error { + print("[\(String(describing: self)).\(#function)]: \(NetworkError.urlRequestError(error)) - URL request error, \(error.localizedDescription)") + fulfillCompletionOnTheMainThread(.failure(NetworkError.urlRequestError(error))) + } else { + print("[\(String(describing: self)).\(#function)]: \(NetworkError.urlSessionError) - URL session error") + fulfillCompletionOnTheMainThread(.failure(NetworkError.urlSessionError)) + } + }) + + return task + } +} + +extension URLSession { + func objectTask( + for request: URLRequest, + completion: @escaping (Result) -> Void + ) -> URLSessionTask { + let decoder = SnakeCaseJSONDecoder() + let task = data(for: request) { (result: Result) in + switch result { + case .success(let data): + do { + let object = try decoder.decode(T.self, from: data) + completion(.success(object)) + } catch { + print("Error decoding: \(error.localizedDescription), Data: \(String(data: data, encoding: .utf8) ?? "")") + completion(.failure(error)) + } + case .failure(let error): + print("[\(String(describing: self)).\(#function)]: - Error getting data: \(error.localizedDescription)") + completion(.failure(error)) + } + } + return task + } +} diff --git a/FakeNFT/Models/Statistic/NFTCollectionModel.swift b/FakeNFT/Models/Statistic/NFTCollectionModel.swift new file mode 100644 index 0000000000..15fcea4812 --- /dev/null +++ b/FakeNFT/Models/Statistic/NFTCollectionModel.swift @@ -0,0 +1,51 @@ +// +// NFTCollectionModel.swift +// FakeNFT +// +// Created by Ilya Nikitash on 4/3/25. +// +import UIKit + +struct NFTCollectionModel { + let createdAt: String + let name: String + let images: [String] + let rating: Int + let description: String + let price: Float + let author: String + let id: String + + init(createdAt: String, name: String, images: [String], rating: Int, description: String, price: Float, author: String, id: String) { + self.createdAt = createdAt + self.name = name + self.images = images + self.rating = rating + self.description = description + self.price = price + self.author = author + self.id = id + } + + init(result nft: NFTCollectionResult) { + self.createdAt = nft.createdAt + self.name = nft.name + self.images = nft.images + self.rating = nft.rating + self.description = nft.description + self.price = nft.price + self.author = nft.author + self.id = nft.id + } +} + +struct NFTCollectionResult: Decodable { + 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/Statistic/UsersListModel.swift b/FakeNFT/Models/Statistic/UsersListModel.swift new file mode 100644 index 0000000000..4aa29f8861 --- /dev/null +++ b/FakeNFT/Models/Statistic/UsersListModel.swift @@ -0,0 +1,47 @@ +// +// StatisticUsersListCellModel.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/15/25. +// +import UIKit + +struct UsersListModel { + let name: String + let avatar: String + let description: String? + let website: String + let nfts: [String] + let rating: String + let id: String + + init(name: String, avatar: String, description: String?, website: String, nfts: [String], rating: String, id: String) { + self.name = name + self.avatar = avatar + self.description = description + self.website = website + self.nfts = nfts + self.rating = rating + self.id = id + } + + init(result user: UsersListResult) { + self.name = user.name + self.avatar = user.avatar + self.description = user.description + self.website = user.website + self.nfts = user.nfts + self.rating = user.rating + self.id = user.id + } +} + +struct UsersListResult: Decodable { + let name: String + let avatar: String + let description: String? + let website: String + let nfts: [String] + let rating: String + let id: String +} diff --git a/FakeNFT/Resources/Assets.xcassets/Images/Contents.json b/FakeNFT/Resources/Assets.xcassets/Images/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/FakeNFT/Resources/Assets.xcassets/Images/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Resources/Assets.xcassets/Images/active_like.imageset/Contents.json b/FakeNFT/Resources/Assets.xcassets/Images/active_like.imageset/Contents.json new file mode 100644 index 0000000000..1815ba098d --- /dev/null +++ b/FakeNFT/Resources/Assets.xcassets/Images/active_like.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Favourites icons@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Favourites icons.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Resources/Assets.xcassets/Images/active_like.imageset/Favourites icons.png b/FakeNFT/Resources/Assets.xcassets/Images/active_like.imageset/Favourites icons.png new file mode 100644 index 0000000000..ef16675134 Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/active_like.imageset/Favourites icons.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/active_like.imageset/Favourites icons@2x.png b/FakeNFT/Resources/Assets.xcassets/Images/active_like.imageset/Favourites icons@2x.png new file mode 100644 index 0000000000..fb3fd72bd0 Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/active_like.imageset/Favourites icons@2x.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/active_star.imageset/Contents.json b/FakeNFT/Resources/Assets.xcassets/Images/active_star.imageset/Contents.json new file mode 100644 index 0000000000..483af9f37e --- /dev/null +++ b/FakeNFT/Resources/Assets.xcassets/Images/active_star.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Stars@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Stars.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Resources/Assets.xcassets/Images/active_star.imageset/Stars.png b/FakeNFT/Resources/Assets.xcassets/Images/active_star.imageset/Stars.png new file mode 100644 index 0000000000..cbbf3cb1b5 Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/active_star.imageset/Stars.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/active_star.imageset/Stars@2x.png b/FakeNFT/Resources/Assets.xcassets/Images/active_star.imageset/Stars@2x.png new file mode 100644 index 0000000000..ca86fd4796 Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/active_star.imageset/Stars@2x.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/add_cart.imageset/Contents.json b/FakeNFT/Resources/Assets.xcassets/Images/add_cart.imageset/Contents.json new file mode 100644 index 0000000000..fd9ed3cc88 --- /dev/null +++ b/FakeNFT/Resources/Assets.xcassets/Images/add_cart.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Resources/Assets.xcassets/Images/add_cart.imageset/Frame.png b/FakeNFT/Resources/Assets.xcassets/Images/add_cart.imageset/Frame.png new file mode 100644 index 0000000000..f34a767620 Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/add_cart.imageset/Frame.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/add_cart.imageset/Frame@2x.png b/FakeNFT/Resources/Assets.xcassets/Images/add_cart.imageset/Frame@2x.png new file mode 100644 index 0000000000..701ccaedfd Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/add_cart.imageset/Frame@2x.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/cart.imageset/Cart.png b/FakeNFT/Resources/Assets.xcassets/Images/cart.imageset/Cart.png new file mode 100644 index 0000000000..ba8e806ec8 Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/cart.imageset/Cart.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/cart.imageset/Cart@2x.png b/FakeNFT/Resources/Assets.xcassets/Images/cart.imageset/Cart@2x.png new file mode 100644 index 0000000000..e47599c1a3 Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/cart.imageset/Cart@2x.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/cart.imageset/Contents.json b/FakeNFT/Resources/Assets.xcassets/Images/cart.imageset/Contents.json new file mode 100644 index 0000000000..6eae778425 --- /dev/null +++ b/FakeNFT/Resources/Assets.xcassets/Images/cart.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Cart@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Cart.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Resources/Assets.xcassets/close.imageset/Contents.json b/FakeNFT/Resources/Assets.xcassets/Images/close.imageset/Contents.json similarity index 100% rename from FakeNFT/Resources/Assets.xcassets/close.imageset/Contents.json rename to FakeNFT/Resources/Assets.xcassets/Images/close.imageset/Contents.json diff --git a/FakeNFT/Resources/Assets.xcassets/close.imageset/Dark-1.png b/FakeNFT/Resources/Assets.xcassets/Images/close.imageset/Dark-1.png similarity index 100% rename from FakeNFT/Resources/Assets.xcassets/close.imageset/Dark-1.png rename to FakeNFT/Resources/Assets.xcassets/Images/close.imageset/Dark-1.png diff --git a/FakeNFT/Resources/Assets.xcassets/close.imageset/Dark.png b/FakeNFT/Resources/Assets.xcassets/Images/close.imageset/Dark.png similarity index 100% rename from FakeNFT/Resources/Assets.xcassets/close.imageset/Dark.png rename to FakeNFT/Resources/Assets.xcassets/Images/close.imageset/Dark.png diff --git a/FakeNFT/Resources/Assets.xcassets/Images/no_active_like.imageset/Contents.json b/FakeNFT/Resources/Assets.xcassets/Images/no_active_like.imageset/Contents.json new file mode 100644 index 0000000000..d4f6f6837c --- /dev/null +++ b/FakeNFT/Resources/Assets.xcassets/Images/no_active_like.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "No Active@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "No Active.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Resources/Assets.xcassets/Images/no_active_like.imageset/No Active.png b/FakeNFT/Resources/Assets.xcassets/Images/no_active_like.imageset/No Active.png new file mode 100644 index 0000000000..bbdc07cd70 Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/no_active_like.imageset/No Active.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/no_active_like.imageset/No Active@2x.png b/FakeNFT/Resources/Assets.xcassets/Images/no_active_like.imageset/No Active@2x.png new file mode 100644 index 0000000000..53add90895 Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/no_active_like.imageset/No Active@2x.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/no_active_star.imageset/Contents.json b/FakeNFT/Resources/Assets.xcassets/Images/no_active_star.imageset/Contents.json new file mode 100644 index 0000000000..483af9f37e --- /dev/null +++ b/FakeNFT/Resources/Assets.xcassets/Images/no_active_star.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Stars@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Stars.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Resources/Assets.xcassets/Images/no_active_star.imageset/Stars.png b/FakeNFT/Resources/Assets.xcassets/Images/no_active_star.imageset/Stars.png new file mode 100644 index 0000000000..6f5f43ed3c Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/no_active_star.imageset/Stars.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/no_active_star.imageset/Stars@2x.png b/FakeNFT/Resources/Assets.xcassets/Images/no_active_star.imageset/Stars@2x.png new file mode 100644 index 0000000000..65993e74b3 Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/no_active_star.imageset/Stars@2x.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/sort_button.imageset/Contents.json b/FakeNFT/Resources/Assets.xcassets/Images/sort_button.imageset/Contents.json new file mode 100644 index 0000000000..676c40bcc7 --- /dev/null +++ b/FakeNFT/Resources/Assets.xcassets/Images/sort_button.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Light.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Light-1.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Resources/Assets.xcassets/Images/sort_button.imageset/Light-1.png b/FakeNFT/Resources/Assets.xcassets/Images/sort_button.imageset/Light-1.png new file mode 100644 index 0000000000..094900bba6 Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/sort_button.imageset/Light-1.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/sort_button.imageset/Light.png b/FakeNFT/Resources/Assets.xcassets/Images/sort_button.imageset/Light.png new file mode 100644 index 0000000000..152b20fdf4 Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/sort_button.imageset/Light.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/stub_avatar.imageset/Contents.json b/FakeNFT/Resources/Assets.xcassets/Images/stub_avatar.imageset/Contents.json new file mode 100644 index 0000000000..830cc05868 --- /dev/null +++ b/FakeNFT/Resources/Assets.xcassets/Images/stub_avatar.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Userpick@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Userpick.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Resources/Assets.xcassets/Images/stub_avatar.imageset/Userpick.png b/FakeNFT/Resources/Assets.xcassets/Images/stub_avatar.imageset/Userpick.png new file mode 100644 index 0000000000..07649d8165 Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/stub_avatar.imageset/Userpick.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/stub_avatar.imageset/Userpick@2x.png b/FakeNFT/Resources/Assets.xcassets/Images/stub_avatar.imageset/Userpick@2x.png new file mode 100644 index 0000000000..c5b58e519b Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/stub_avatar.imageset/Userpick@2x.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/tab_statistic.imageset/Contents.json b/FakeNFT/Resources/Assets.xcassets/Images/tab_statistic.imageset/Contents.json new file mode 100644 index 0000000000..d4f6f6837c --- /dev/null +++ b/FakeNFT/Resources/Assets.xcassets/Images/tab_statistic.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "No Active@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "No Active.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/FakeNFT/Resources/Assets.xcassets/Images/tab_statistic.imageset/No Active.png b/FakeNFT/Resources/Assets.xcassets/Images/tab_statistic.imageset/No Active.png new file mode 100644 index 0000000000..c941a065bf Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/tab_statistic.imageset/No Active.png differ diff --git a/FakeNFT/Resources/Assets.xcassets/Images/tab_statistic.imageset/No Active@2x.png b/FakeNFT/Resources/Assets.xcassets/Images/tab_statistic.imageset/No Active@2x.png new file mode 100644 index 0000000000..e4a98cda4c Binary files /dev/null and b/FakeNFT/Resources/Assets.xcassets/Images/tab_statistic.imageset/No Active@2x.png differ diff --git a/FakeNFT/Resources/Localizable.strings b/FakeNFT/Resources/Localizable.strings index c498776710..0a0490f0c0 100644 --- a/FakeNFT/Resources/Localizable.strings +++ b/FakeNFT/Resources/Localizable.strings @@ -1,4 +1,5 @@ "Tab.catalog" = "Каталог"; +"Tab.statistic" = "Статистика"; "Catalog.openNft" = "Открыть Nft"; "Error.network" = "Произошла ошибка сети"; "Error.unknown" = "Произошла неизвестная ошибка"; diff --git a/FakeNFT/Scenes /TabBarController/TabBarController.swift b/FakeNFT/Scenes /TabBarController/TabBarController.swift index 99931a214a..659db1a996 100644 --- a/FakeNFT/Scenes /TabBarController/TabBarController.swift +++ b/FakeNFT/Scenes /TabBarController/TabBarController.swift @@ -9,6 +9,11 @@ final class TabBarController: UITabBarController { image: UIImage(systemName: "square.stack.3d.up.fill"), tag: 0 ) + + private let statisticTabBarItem = UITabBarItem( + title: NSLocalizedString("Tab.statistic", comment: ""), + image: UIImage(named: "tab_statistic"), + tag: 1) override func viewDidLoad() { super.viewDidLoad() @@ -16,9 +21,13 @@ final class TabBarController: UITabBarController { let catalogController = TestCatalogViewController( servicesAssembly: servicesAssembly ) + let statisticUsersListVC = StatisticUsersListViewController() + let statisticUsersListVCNavController = UINavigationController(rootViewController: statisticUsersListVC) + statisticUsersListVCNavController.setNavigationBarHidden(false, animated: false) + catalogController.tabBarItem = catalogTabBarItem - - viewControllers = [catalogController] + statisticUsersListVCNavController.tabBarItem = statisticTabBarItem + viewControllers = [catalogController, statisticUsersListVCNavController] view.backgroundColor = .systemBackground } diff --git a/FakeNFT/Services/Requests/RequestConstants.swift b/FakeNFT/Services/Requests/RequestConstants.swift index 9d3529893b..103f324eaf 100644 --- a/FakeNFT/Services/Requests/RequestConstants.swift +++ b/FakeNFT/Services/Requests/RequestConstants.swift @@ -1,5 +1,4 @@ enum RequestConstants { static let baseURL = "https://d5dn3j2ouj72b0ejucbl.apigw.yandexcloud.net" - #warning("Instert your token here") - static let token = "" + static let token = "59db9adc-fa26-4e11-9e0d-0cd1ccdb3512" } diff --git a/FakeNFT/Statistic/NFTCollection/NFTCollectionView.swift b/FakeNFT/Statistic/NFTCollection/NFTCollectionView.swift new file mode 100644 index 0000000000..ce0d39ec24 --- /dev/null +++ b/FakeNFT/Statistic/NFTCollection/NFTCollectionView.swift @@ -0,0 +1,57 @@ +// +// NFTCollectionView.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/26/25. +// +import UIKit + +final class NFTCollectionView: UIView { + // MARK: - UI Elements + lazy var collectionView: UICollectionView = { + let flowLayout = UICollectionViewFlowLayout() + flowLayout.minimumLineSpacing = 16 + flowLayout.minimumInteritemSpacing = 16 + flowLayout.scrollDirection = .vertical + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) + collectionView.backgroundColor = .background + collectionView.translatesAutoresizingMaskIntoConstraints = false + return collectionView + }() + lazy var activityIndicator: UIActivityIndicatorView = { + let activityIndicator = UIActivityIndicatorView(style: .large) + activityIndicator.color = .segmentActive + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + return activityIndicator + }() + lazy var emptyCollectionTitle: UILabel = { + let label = UILabel() + label.isHidden = true + label.text = "Нет NFT в коллекции" + label.font = .systemFont(ofSize: 18, weight: .bold) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + // MARK: - Public Methods + func configure() { + backgroundColor = .background + addSubview(collectionView) + addSubview(activityIndicator) + addSubview(emptyCollectionTitle) + + collectionView.register(NFTCollectionViewCell.self, forCellWithReuseIdentifier: NFTCollectionViewCell.reuseIdentifier) + + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: bottomAnchor), + collectionView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), + + activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor), + + emptyCollectionTitle.centerXAnchor.constraint(equalTo: centerXAnchor), + emptyCollectionTitle.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + } +} diff --git a/FakeNFT/Statistic/NFTCollection/NFTCollectionViewCell.swift b/FakeNFT/Statistic/NFTCollection/NFTCollectionViewCell.swift new file mode 100644 index 0000000000..82477ed152 --- /dev/null +++ b/FakeNFT/Statistic/NFTCollection/NFTCollectionViewCell.swift @@ -0,0 +1,150 @@ +// +// NFTCollectionViewCell.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/27/25. +// +import UIKit + +final class NFTCollectionViewCell: UICollectionViewCell { + // MARK: - UI Elements + private lazy var nftImage: UIImageView = { + let imageView = UIImageView() + // TODO: - Add stub image or animating + imageView.image = UIImage(named: "stub_avatar") + imageView.layer.cornerRadius = 12 + imageView.layer.masksToBounds = true + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + private lazy var likeButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(named: "no_active_like"), for: .normal) + button.addTarget(self, action: #selector(tapLikeButton), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + private lazy var rateView: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + private lazy var nftNameLabel: UILabel = { + let label = UILabel() + label.text = "Name Test" + label.font = .systemFont(ofSize: 17, weight: .bold) + label.numberOfLines = 1 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + private lazy var priceLabel: UILabel = { + let label = UILabel() + label.text = "13.5 TST" + label.font = .systemFont(ofSize: 10, weight: .medium) + label.numberOfLines = 1 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + private lazy var cartButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(named: "cart"), for: .normal) + button.addTarget(self, action: #selector(tapCartButton), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + // MARK: - Public Properties + static let reuseIdentifier = "NFTCollectionViewCell" + // MARK: - Private Properties + private var isLiked = false + private var isAddToCart = false + // MARK: - Initializers + override init(frame: CGRect) { + super.init(frame: frame) + contentView.addSubview(nftImage) + contentView.addSubview(likeButton) + contentView.addSubview(rateView) + contentView.addSubview(nftNameLabel) + contentView.addSubview(priceLabel) + contentView.addSubview(cartButton) + + setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + //MARK: - Selectors + @objc private func tapLikeButton() { + isLiked = !isLiked + likeButton.setImage(UIImage(named: isLiked ? "active_like" : "no_active_like"), for: .normal) + } + @objc private func tapCartButton() { + isAddToCart = !isAddToCart + cartButton.setImage(UIImage(named: isAddToCart ? "add_cart" : "cart"), for: .normal) + } + // MARK: - Public methods + func configure(with nft: NFTCollectionModel) { + if let nftUrl = URL(string: nft.images[1]) { + nftImage.kf.setImage(with: nftUrl, placeholder: UIImage(named: "stub_avatar")) + } + setup(rating: nft.rating) + nftNameLabel.text = nft.name + priceLabel.text = String("\(nft.price) ETH") + + } + // MARK: - Private methods + private func setup(rating: Int) { + var previousStar: UIImageView? + for i in 0..<5 { + let star = UIImageView() + star.image = UIImage(named: i < rating ? "active_star" : "no_active_star") + star.translatesAutoresizingMaskIntoConstraints = false + rateView.addSubview(star) + + NSLayoutConstraint.activate([ + star.heightAnchor.constraint(equalToConstant: 12), + star.widthAnchor.constraint(equalToConstant: 12), + star.centerYAnchor.constraint(equalTo: rateView.centerYAnchor) + ]) + + if let previous = previousStar { + star.leadingAnchor.constraint(equalTo: previous.trailingAnchor, constant: 2).isActive = true + } else { + star.leadingAnchor.constraint(equalTo: rateView.leadingAnchor).isActive = true + } + + previousStar = star + } + } + private func setupConstraints() { + NSLayoutConstraint.activate([ + nftImage.topAnchor.constraint(equalTo: contentView.topAnchor), + nftImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + nftImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + nftImage.heightAnchor.constraint(equalTo: nftImage.widthAnchor), + + likeButton.topAnchor.constraint(equalTo: nftImage.topAnchor), + likeButton.trailingAnchor.constraint(equalTo: nftImage.trailingAnchor), + likeButton.heightAnchor.constraint(equalToConstant: 40), + likeButton.widthAnchor.constraint(equalToConstant: 40), + + rateView.topAnchor.constraint(equalTo: nftImage.bottomAnchor, constant: 8), + rateView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + rateView.heightAnchor.constraint(equalToConstant: 12), + rateView.widthAnchor.constraint(equalToConstant: 68), + + cartButton.trailingAnchor.constraint(equalTo: nftImage.trailingAnchor), + cartButton.topAnchor.constraint(equalTo: nftImage.bottomAnchor, constant: 24), + cartButton.heightAnchor.constraint(equalToConstant: 40), + cartButton.widthAnchor.constraint(equalToConstant: 40), + + nftNameLabel.topAnchor.constraint(equalTo: rateView.bottomAnchor, constant: 5), + nftNameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + nftNameLabel.trailingAnchor.constraint(equalTo: cartButton.leadingAnchor), + + priceLabel.topAnchor.constraint(equalTo: nftNameLabel.bottomAnchor, constant: 4), + priceLabel.leadingAnchor.constraint(equalTo: nftNameLabel.leadingAnchor) + ]) + } +} diff --git a/FakeNFT/Statistic/NFTCollection/NFTCollectionViewController.swift b/FakeNFT/Statistic/NFTCollection/NFTCollectionViewController.swift new file mode 100644 index 0000000000..4937182860 --- /dev/null +++ b/FakeNFT/Statistic/NFTCollection/NFTCollectionViewController.swift @@ -0,0 +1,133 @@ +// +// NFTCollectionViewController.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/26/25. +// +import UIKit + +final class NFTCollectionViewController: UIViewController { + // MARK: - Private properties + private let nftCollectionView = NFTCollectionView() + private var nfts: [NFTCollectionModel] = [] + private let nftCollectionService = NFTCollectionService.shared + private var nftCollectionServiceObserver: NSObjectProtocol? + // MARK: - Lifecycle + override func loadView() { + self.view = nftCollectionView + } + + override func viewDidLoad() { + super.viewDidLoad() + nftCollectionView.configure() + setupNavigationBar() + setupCollectionView() + setupObserver() + } + // MARK: - Selectors + @objc private func goBack() { + self.dismiss(animated: true, completion: nil) + } + // MARK: - Private Methods + private func setupNavigationBar() { + let backButton = UIBarButtonItem( + image: UIImage(systemName: "chevron.backward"), + style: .plain, + target: self, + action: #selector(goBack) + ) + backButton.tintColor = .segmentActive + self.navigationItem.leftBarButtonItem = backButton + navigationItem.title = "Коллекция NFT" + navigationItem.largeTitleDisplayMode = .never + } + + private func setupCollectionView() { + nftCollectionView.collectionView.delegate = self + nftCollectionView.collectionView.dataSource = self + } + + private func setupObserver() { + nftCollectionServiceObserver = NotificationCenter.default + .addObserver(forName: NFTCollectionService.didChangeNotification, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self else { return print("error") } + nfts = nftCollectionService.nfts + nftCollectionView.collectionView.reloadData() + hideActivityIndicator() + } + } + private func showActivityIndicator() { + nftCollectionView.activityIndicator.startAnimating() + nftCollectionView.collectionView.isHidden = true + nftCollectionView.activityIndicator.isHidden = false + } + + private func hideActivityIndicator() { + nftCollectionView.activityIndicator.stopAnimating() + nftCollectionView.collectionView.isHidden = false + nftCollectionView.activityIndicator.isHidden = true + } + private func setupEmptyCollection() { + nftCollectionView.activityIndicator.stopAnimating() + nftCollectionView.collectionView.isHidden = true + nftCollectionView.activityIndicator.isHidden = true + nftCollectionView.emptyCollectionTitle.isHidden = false + } +} + +extension NFTCollectionViewController: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let spacing: CGFloat = 9 + let sideInset: CGFloat = 16 + let itemsPerRow: CGFloat = 3 + + let totalCellWidth = collectionView.bounds.width - 2 * sideInset + let totalSpacingWidth = spacing * CGFloat(itemsPerRow - 1) + + let itemWidth = (totalCellWidth - totalSpacingWidth) / itemsPerRow + + return CGSize(width: itemWidth, height: itemWidth * 1.78) + } + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) + } + 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 + } +} + +extension NFTCollectionViewController: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return nfts.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NFTCollectionViewCell.reuseIdentifier, for: indexPath) as? NFTCollectionViewCell else { + return UICollectionViewCell() + } + let nft = nfts[indexPath.row] + cell.configure(with: nft) + return cell + } +} + +extension NFTCollectionViewController: GetNFTsCollectionDelegate { + func getNFTs(of user: UsersListModel) { + guard !user.nfts.isEmpty else { + setupEmptyCollection() + return + } + showActivityIndicator() + nftCollectionService.fetchNFT(with: user.nfts) + } +} diff --git a/FakeNFT/Statistic/Network/NFTCollection/NFTCollectionService.swift b/FakeNFT/Statistic/Network/NFTCollection/NFTCollectionService.swift new file mode 100644 index 0000000000..2d0e5df2e5 --- /dev/null +++ b/FakeNFT/Statistic/Network/NFTCollection/NFTCollectionService.swift @@ -0,0 +1,77 @@ +// +// NFTCollectionService.swift +// FakeNFT +// +// Created by Ilya Nikitash on 4/3/25. +// +import Foundation + +final class NFTCollectionService { + // MARK: - Public Properties + static let shared = NFTCollectionService() + static let didChangeNotification = Notification.Name(rawValue: "NFTCollectionServiceDidChange") + // MARK: - Private Properties + private(set) var nfts: [NFTCollectionModel] = [] + private let urlSession = URLSession.shared + private var task: URLSessionTask? + // MARK: - Initializers + private init() {} + // MARK: - Public Methods + func fetchNFT(with ids: [String]) { + assert(Thread.isMainThread) + + // Очищаем предыдущие данные + self.nfts.removeAll() + + let dispatchGroup = DispatchGroup() + + for id in ids { + dispatchGroup.enter() + + guard let request = try? makeNFTRequest(with: id) else { + dispatchGroup.leave() + continue + } + + let task = urlSession.objectTask(for: request) { [weak self] (result: Result) in + defer { dispatchGroup.leave() } + + switch result { + case .success(let response): + let newNFT = NFTCollectionModel(result: response) + DispatchQueue.main.async { + self?.nfts.append(newNFT) + } + case .failure(let error): + print("Error fetching NFT \(id): \(error)") + } + } + task.resume() + } + + dispatchGroup.notify(queue: .main) { [weak self] in + self?.nfts.sort { $0.id < $1.id } // Сортируем по ID + NotificationCenter.default.post(name: Self.didChangeNotification, object: nil) + } + } + // MARK: - Private Methods + private func makeNFTRequest(with id: String) throws -> URLRequest? { + guard let baseUrl = URL(string: RequestConstants.baseURL) else { + throw NFTCollectionServiceErrors.invalidURL + } + guard var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true) else { + throw NFTCollectionServiceErrors.invalidURL + } + + components.path = "/api/v1/nft/\(id)" + + guard let url = components.url else { + throw NFTCollectionServiceErrors.invalidURL + } + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.addValue(RequestConstants.token, forHTTPHeaderField: "X-Practicum-Mobile-Token") + + return request + } +} diff --git a/FakeNFT/Statistic/Network/NFTCollection/NFTCollectionServiceErrors.swift b/FakeNFT/Statistic/Network/NFTCollection/NFTCollectionServiceErrors.swift new file mode 100644 index 0000000000..443213ac27 --- /dev/null +++ b/FakeNFT/Statistic/Network/NFTCollection/NFTCollectionServiceErrors.swift @@ -0,0 +1,13 @@ +// +// NFTCollectionServiceErrors.swift +// FakeNFT +// +// Created by Ilya Nikitash on 4/3/25. +// +import Foundation + +enum NFTCollectionServiceErrors: Error { + case invalidRequest + case invalidURL + case invalidFetchingNFTsList +} diff --git a/FakeNFT/Statistic/Network/UsersList/UsersListService.swift b/FakeNFT/Statistic/Network/UsersList/UsersListService.swift new file mode 100644 index 0000000000..96c0fadadc --- /dev/null +++ b/FakeNFT/Statistic/Network/UsersList/UsersListService.swift @@ -0,0 +1,81 @@ +// +// UsersListService.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/19/25. +// +import Foundation + +final class UsersListService { + // MARK: - Public Properties + static let shared = UsersListService() + static let didChangeNotification = Notification.Name(rawValue: "UsersListServiceDidChange") + // MARK: - Private Properties + private(set) var users: [UsersListModel] = [] + private let size: Int = 10 + private var lastLoadedPage: Int? + private let urlSession = URLSession.shared + private var task: URLSessionTask? + // MARK: - Initializers + private init() {} + // MARK: - Public Methods + func fetchUsersNextPage() { + assert(Thread.isMainThread) + guard task == nil else { return } + let nextPage = (lastLoadedPage ?? -1) + 1 + + guard let request = try? makeUsersNextPageRequest(page: nextPage, size: size) else { + return + } + task?.cancel() + let task = urlSession.objectTask(for: request) { [weak self] (result: Result<[UsersListResult], Error>) in + guard let self else { return } + switch result { + case .success(let response): + let newUser = response.map { UsersListModel(result: $0)} + DispatchQueue.main.async { + self.users.append(contentsOf: newUser) + self.lastLoadedPage = nextPage + NotificationCenter.default.post(name: UsersListService.didChangeNotification, object: nil) + } + case .failure(let error): + print("[\(String(describing: self)).\(#function)]: \(UsersListServiceErrors.invalidFetchingUsersList) - Error fetching users List, \(error.localizedDescription)") + } + self.task = nil + } + self.task = task + task.resume() + } + + func deleteUsersList() { + users = [] + lastLoadedPage = nil + } + // MARK: - Private Methods + private func makeUsersNextPageRequest(page: Int, size: Int) throws -> URLRequest? { + guard let baseUrl = URL(string: RequestConstants.baseURL) else { + throw UsersListServiceErrors.invalidURL + } + + guard var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true) else { + throw UsersListServiceErrors.invalidURL + } + + components.path = "/api/v1/users" + + components.queryItems = [ + URLQueryItem(name: "page", value: "\(page)"), + URLQueryItem(name: "size", value: "\(size)"), + URLQueryItem(name: "sortBy", value: "\(SortStorage.shared.selectedSort?.rawValue ?? "name")") + ] + + guard let url = components.url else { + throw UsersListServiceErrors.invalidURL + } + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.addValue(RequestConstants.token, forHTTPHeaderField: "X-Practicum-Mobile-Token") + + return request + } +} diff --git a/FakeNFT/Statistic/Network/UsersList/UsersListServiceErrors.swift b/FakeNFT/Statistic/Network/UsersList/UsersListServiceErrors.swift new file mode 100644 index 0000000000..070d6a5675 --- /dev/null +++ b/FakeNFT/Statistic/Network/UsersList/UsersListServiceErrors.swift @@ -0,0 +1,12 @@ +// +// UsersListServiceErrors.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/19/25. +// + +enum UsersListServiceErrors: Error { + case invalidRequest + case invalidURL + case invalidFetchingUsersList +} diff --git a/FakeNFT/Statistic/Sort/SortAlertPresenter.swift b/FakeNFT/Statistic/Sort/SortAlertPresenter.swift new file mode 100644 index 0000000000..e162fe1c97 --- /dev/null +++ b/FakeNFT/Statistic/Sort/SortAlertPresenter.swift @@ -0,0 +1,30 @@ +// +// SortAlertController.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/15/25. +// +import UIKit + +struct SortAlertPresenter { + static func present( + title: String?, + message: String, + actions: UIAlertAction..., + from controller: UIViewController + ) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) + + for action in actions { + alertController.addAction(action) + } + + controller.present(alertController, animated: true, completion: nil) + } + static func createAction(title: String, + style: UIAlertAction.Style, + handler: ((UIAlertAction) -> Void)? = nil + ) -> UIAlertAction { + return UIAlertAction(title: title, style: style, handler: handler) + } +} diff --git a/FakeNFT/Statistic/Sort/SortCases.swift b/FakeNFT/Statistic/Sort/SortCases.swift new file mode 100644 index 0000000000..709a0d2061 --- /dev/null +++ b/FakeNFT/Statistic/Sort/SortCases.swift @@ -0,0 +1,19 @@ +// +// SortCases.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/15/25. +// +import Foundation + +enum SortCases: String, CaseIterable { + case name + case rating + + var title: String { + switch self { + case .name: "По имени" + case .rating: "По рейтингу" + } + } +} diff --git a/FakeNFT/Statistic/Sort/SortStorage.swift b/FakeNFT/Statistic/Sort/SortStorage.swift new file mode 100644 index 0000000000..28232e6a5c --- /dev/null +++ b/FakeNFT/Statistic/Sort/SortStorage.swift @@ -0,0 +1,29 @@ +// +// SortStorage.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/19/25. +// +import Foundation + +final class SortStorage { + static let shared = SortStorage() + private let userDefaults = UserDefaults.standard + + private enum Keys: String { + case selectedSort + } + + var selectedSort: SortCases? { + get { + guard let selectedSort = userDefaults.string(forKey: Keys.selectedSort.rawValue) else { + return nil + } + + return SortCases(rawValue: selectedSort) + } + set { + userDefaults.set(newValue?.rawValue, forKey: Keys.selectedSort.rawValue) + } + } +} diff --git a/FakeNFT/Statistic/StatisticUsersList/StatisticUsersListTableViewCell.swift b/FakeNFT/Statistic/StatisticUsersList/StatisticUsersListTableViewCell.swift new file mode 100644 index 0000000000..e6a6ac720b --- /dev/null +++ b/FakeNFT/Statistic/StatisticUsersList/StatisticUsersListTableViewCell.swift @@ -0,0 +1,102 @@ +// +// StatisticUsersListTableViewCell.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/15/25. +// +import UIKit +import Kingfisher + +final class StatisticUsersListTableViewCell: UITableViewCell { + // MARK: - UI Elements + lazy var placeLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 1 + label.textColor = .segmentActive + label.font = .systemFont(ofSize: 15) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + lazy var avatarImage: UIImageView = { + let imageView = UIImageView() + imageView.layer.cornerRadius = 14 + imageView.layer.masksToBounds = true + imageView.image = UIImage(named: "stub_avatar") + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + lazy var nameLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 1 + label.textColor = .segmentActive + label.font = .systemFont(ofSize: 22, weight: .bold) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + lazy var nftCountLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 1 + label.textColor = .segmentActive + label.font = .systemFont(ofSize: 22, weight: .bold) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + lazy var cellView: UIView = { + let view = UIView() + view.backgroundColor = .segmentInactive + view.layer.cornerRadius = 12 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + // MARK: - Public Properties + static let identifier = "StatisticUsersListTableViewCell" + // MARK: - Initializers + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + contentView.addSubview(cellView) + contentView.addSubview(placeLabel) + cellView.addSubview(avatarImage) + cellView.addSubview(nameLabel) + cellView.addSubview(nftCountLabel) + + NSLayoutConstraint.activate([ + cellView.topAnchor.constraint(equalTo: contentView.topAnchor), + cellView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8), + cellView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 51), + cellView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + + placeLabel.centerYAnchor.constraint(equalTo: cellView.centerYAnchor), + placeLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + placeLabel.widthAnchor.constraint(equalToConstant: 27), + + avatarImage.leadingAnchor.constraint(equalTo: cellView.leadingAnchor, constant: 16), + avatarImage.topAnchor.constraint(equalTo: cellView.topAnchor, constant: 26), + avatarImage.heightAnchor.constraint(equalToConstant: 28), + avatarImage.widthAnchor.constraint(equalToConstant: 28), + + nftCountLabel.topAnchor.constraint(equalTo: cellView.topAnchor, constant: 26), + nftCountLabel.trailingAnchor.constraint(equalTo: cellView.trailingAnchor, constant: -16), + + nameLabel.centerYAnchor.constraint(equalTo: cellView.centerYAnchor), + nameLabel.leadingAnchor.constraint(equalTo: avatarImage.trailingAnchor, constant: 8), + nameLabel.trailingAnchor.constraint(equalTo: nftCountLabel.trailingAnchor, constant: -26) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Public Methods + func configure(with user: UsersListModel, place: Int) { + if let avatarUrl = URL(string: user.avatar) { + avatarImage.kf.setImage(with: avatarUrl, placeholder: UIImage(named: "stub_avatar")) + } + placeLabel.text = String(place) + nameLabel.text = user.name + nftCountLabel.text = String(user.nfts.count) + } +} diff --git a/FakeNFT/Statistic/StatisticUsersList/StatisticUsersListView.swift b/FakeNFT/Statistic/StatisticUsersList/StatisticUsersListView.swift new file mode 100644 index 0000000000..677b3b8b7a --- /dev/null +++ b/FakeNFT/Statistic/StatisticUsersList/StatisticUsersListView.swift @@ -0,0 +1,66 @@ +// +// StatisticUsersListView.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/14/25. +// +import UIKit + +protocol StatisticUsersListViewDelegate: AnyObject { + func clickSortButton() +} + +final class StatisticUsersListView: UIView { + // MARK: - UI Elements + lazy var sortButton: UIBarButtonItem = { + let button = UIBarButtonItem(image: UIImage(named: "sort_button"), + style: .plain, + target: self, + action: #selector(didTapSortButton)) + button.tintColor = .segmentActive + return button + }() + + lazy var usersListTableView: UITableView = { + let tableView = UITableView() + tableView.backgroundColor = .background + tableView.translatesAutoresizingMaskIntoConstraints = false + return tableView + }() + + lazy var activityIndicator: UIActivityIndicatorView = { + let activityIndicator = UIActivityIndicatorView(style: .large) + activityIndicator.color = .segmentActive + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + return activityIndicator + }() + // MARK: - Public Properties + weak var statisticUsersListViewDelegate: StatisticUsersListViewDelegate? + // MARK: - Selectors + @objc private func didTapSortButton() { + guard let statisticUsersListViewDelegate else { return } + statisticUsersListViewDelegate.clickSortButton() + } + // MARK: - Public Methods + func configure() { + backgroundColor = .background + + usersListTableView.register( + StatisticUsersListTableViewCell.self, + forCellReuseIdentifier: StatisticUsersListTableViewCell.identifier + ) + addSubview(usersListTableView) + addSubview(activityIndicator) + + usersListTableView.separatorStyle = .none + NSLayoutConstraint.activate([ + usersListTableView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20), + usersListTableView.leadingAnchor.constraint(equalTo: leadingAnchor), + usersListTableView.trailingAnchor.constraint(equalTo: trailingAnchor), + usersListTableView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), + + activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + } +} diff --git a/FakeNFT/Statistic/StatisticUsersList/StatisticUsersListViewController.swift b/FakeNFT/Statistic/StatisticUsersList/StatisticUsersListViewController.swift new file mode 100644 index 0000000000..c0df80badc --- /dev/null +++ b/FakeNFT/Statistic/StatisticUsersList/StatisticUsersListViewController.swift @@ -0,0 +1,153 @@ +// +// StatisticUsersListViewController.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/14/25. +// +import UIKit + +protocol StatisticUsersListVCDelegate: AnyObject { + func didTapCell(with user: UsersListModel) +} + +final class StatisticUsersListViewController: UIViewController { + // MARK: - Public Properties + weak var statisticUsersListVCDelegate: StatisticUsersListVCDelegate? + // MARK: - Private Properties + private var sort: SortCases? { + didSet { + sortStorage.selectedSort = sort + } + } + private let statisticUsersListView = StatisticUsersListView() + private let sortStorage = SortStorage.shared + private let usersListService = UsersListService.shared + private var usersListServiceObserver: NSObjectProtocol? + private var users: [UsersListModel] = [] + // MARK: - Lifecycle + override func loadView() { + self.view = statisticUsersListView + } + override func viewDidLoad() { + super.viewDidLoad() + statisticUsersListView.configure() + showActivityIndicator() + setupNavigationBar() + setupTableView() + setupObserver() + statisticUsersListView.statisticUsersListViewDelegate = self + usersListService.fetchUsersNextPage() + } + // MARK: - Private Methods + private func setupTableView() { + statisticUsersListView.usersListTableView.dataSource = self + statisticUsersListView.usersListTableView.delegate = self + } + + private func setupNavigationBar() { + navigationController?.navigationBar.backgroundColor = .background + navigationItem.rightBarButtonItem = statisticUsersListView.sortButton + } + + private func setupObserver() { + usersListServiceObserver = NotificationCenter.default + .addObserver(forName: UsersListService.didChangeNotification, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self else { return } + self.updateTableViewAnimated() + self.hideActivityIndicator() + } + } + + private func updateTableViewAnimated() { + let oldCount = users.count + let newCount = usersListService.users.count + users = usersListService.users + if oldCount != newCount { + statisticUsersListView.usersListTableView.performBatchUpdates { + var indexPaths: [IndexPath] = [] + for i in oldCount.. CGFloat { + 88 + } +} + // MARK: - UITableViewDataSource +extension StatisticUsersListViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + users.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: StatisticUsersListTableViewCell.identifier, for: indexPath) as? StatisticUsersListTableViewCell else { + return UITableViewCell() + } + cell.selectionStyle = .none + let user = users[indexPath.row] + cell.configure(with: user, place: indexPath.row + 1) + return cell + } + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + if indexPath.row + 1 == users.count { + usersListService.fetchUsersNextPage() + } + } +} + // MARK: - StatisticUsersListViewDelegate +extension StatisticUsersListViewController: StatisticUsersListViewDelegate { + func clickSortButton() { + let nameAction = SortAlertPresenter.createAction(title: SortCases.name.title, style: .default) { [weak self] _ in + guard let self else { return } + self.sort = .name + self.users = [] + statisticUsersListView.usersListTableView.reloadData() + usersListService.deleteUsersList() + usersListService.fetchUsersNextPage() + } + let rateAction = SortAlertPresenter.createAction(title: SortCases.rating.title, style: .default) { [weak self] _ in + guard let self else { return } + self.sort = .rating + self.users = [] + statisticUsersListView.usersListTableView.reloadData() + usersListService.deleteUsersList() + usersListService.fetchUsersNextPage() + } + let cancelAction = SortAlertPresenter.createAction(title: "Закрыть", style: .cancel) + + SortAlertPresenter.present(title: nil, message: "Сортировка", actions: nameAction, rateAction, cancelAction, from: self) + } +} diff --git a/FakeNFT/Statistic/Untitled.swift b/FakeNFT/Statistic/Untitled.swift new file mode 100644 index 0000000000..e69de29bb2 diff --git a/FakeNFT/Statistic/UserCard/UserCardTableViewCell.swift b/FakeNFT/Statistic/UserCard/UserCardTableViewCell.swift new file mode 100644 index 0000000000..6429841158 --- /dev/null +++ b/FakeNFT/Statistic/UserCard/UserCardTableViewCell.swift @@ -0,0 +1,48 @@ +// +// UserCartTableViewCell.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/21/25. +// +import UIKit + +final class UserCardTableViewCell: UITableViewCell { + // MARK: - UI Elements + private lazy var cellLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 17, weight: .bold) + label.numberOfLines = 1 + label.text = "TEST" + label.textColor = .segmentActive + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var customDisclosureIndicator: UIImageView = { + let customDisclosureIndicator = UIImageView(image: UIImage(systemName: "chevron.forward")) + customDisclosureIndicator.tintColor = .segmentActive + customDisclosureIndicator.frame = CGRect(x: 0, y: 0, width: 8, height: 14) + return customDisclosureIndicator + }() + // MARK: - Public Properties + static let identifier = "UserCardTableViewCell" + // MARK: - Initializers + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + accessoryView = customDisclosureIndicator + contentView.addSubview(cellLabel) + NSLayoutConstraint.activate([ + cellLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + cellLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Public Methods + func configure(with user: UsersListModel) { + cellLabel.text = "Коллекция NFT (\(user.nfts.count))" + } +} diff --git a/FakeNFT/Statistic/UserCard/UserCardView.swift b/FakeNFT/Statistic/UserCard/UserCardView.swift new file mode 100644 index 0000000000..dd6cb13f52 --- /dev/null +++ b/FakeNFT/Statistic/UserCard/UserCardView.swift @@ -0,0 +1,114 @@ +// +// UserCardView.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/21/25. +// +import UIKit + +protocol OpenUserWebsiteDelegate: AnyObject { + func openUserWebsite() +} + +final class UserCardView: UIView { + // MARK: - UI Elements + private lazy var avatar: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "stub_avatar") + imageView.layer.cornerRadius = 35 + imageView.layer.masksToBounds = true + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private lazy var nameLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 22, weight: .bold) + label.numberOfLines = 1 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.font = .systemFont(ofSize: 13, weight: .regular) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var siteButton: UIButton = { + let button = UIButton() + button.layer.cornerRadius = 16 + button.backgroundColor = .background + button.layer.borderColor = UIColor.segmentActive.cgColor + button.layer.borderWidth = 1 + button.setTitle("Перейти на сайт пользователя", for: .normal) + button.setTitleColor(.segmentActive, for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 15, weight: .regular) + button.addTarget(self, action: #selector(didTapSiteButton), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + return tableView + }() + // MARK: - Public Properties + weak var openUserWebsiteDelegate: OpenUserWebsiteDelegate? + // MARK: - Selectors + @objc private func didTapSiteButton() { + openUserWebsiteDelegate?.openUserWebsite() + } + // MARK: - Public Methods + func configure() { + backgroundColor = .background + + tableView.register( + UserCardTableViewCell.self, + forCellReuseIdentifier: UserCardTableViewCell.identifier + ) + + addSubview(avatar) + addSubview(nameLabel) + addSubview(descriptionLabel) + addSubview(siteButton) + addSubview(tableView) + tableView.separatorStyle = .none + + NSLayoutConstraint.activate([ + avatar.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20), + avatar.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + avatar.heightAnchor.constraint(equalToConstant: 70), + avatar.widthAnchor.constraint(equalToConstant: 70), + + nameLabel.leadingAnchor.constraint(equalTo: avatar.trailingAnchor, constant: 16), + nameLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + nameLabel.centerYAnchor.constraint(equalTo: avatar.centerYAnchor), + + descriptionLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + descriptionLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + descriptionLabel.topAnchor.constraint(equalTo: avatar.bottomAnchor, constant: 20), + + siteButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 28), + siteButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + siteButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + siteButton.heightAnchor.constraint(equalToConstant: 40), + + tableView.topAnchor.constraint(equalTo: siteButton.bottomAnchor, constant: 40), + tableView.leadingAnchor.constraint(equalTo: leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor) + ]) + } + + func updateProfile(of user: UsersListModel) { + if let avatarUrl = URL(string: user.avatar) { + avatar.kf.setImage(with: avatarUrl, placeholder: UIImage(named: "stub_avatar")) + } + nameLabel.text = user.name + descriptionLabel.text = user.description + } +} diff --git a/FakeNFT/Statistic/UserCard/UserCardViewController.swift b/FakeNFT/Statistic/UserCard/UserCardViewController.swift new file mode 100644 index 0000000000..de4ac21433 --- /dev/null +++ b/FakeNFT/Statistic/UserCard/UserCardViewController.swift @@ -0,0 +1,109 @@ +// +// UserCardViewController.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/21/25. +// +import UIKit + +protocol LoadUserWebsiteDelegate: AnyObject { + func loadWebsite(of userWebsite: String) +} + +protocol GetNFTsCollectionDelegate: AnyObject { + func getNFTs(of user: UsersListModel) +} + +final class UserCardViewController: UIViewController { + // MARK: - Public Properties + weak var loadUserWebsiteDelegate: LoadUserWebsiteDelegate? + weak var getNFTsCollectionDelegate: GetNFTsCollectionDelegate? + // MARK: - Private Properties + private let userCardView = UserCardView() + private let statisticUsersListViewController: StatisticUsersListViewController + private var user: UsersListModel = UsersListModel(name: "", avatar: "", description: "", website: "", nfts: [], rating: "", id: "") + // MARK: - Initializers + init(statisticUsersListViewController: StatisticUsersListViewController) { + self.statisticUsersListViewController = statisticUsersListViewController + super.init(nibName: nil, bundle: nil) + statisticUsersListViewController.statisticUsersListVCDelegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - View Life Cycles + override func loadView() { + self.view = userCardView + } + + override func viewDidLoad() { + super.viewDidLoad() + userCardView.configure() + userCardView.openUserWebsiteDelegate = self + setupTableView() + setupNavigationBar() + } + // MARK: - Selectors + @objc private func goBack() { + self.dismiss(animated: true, completion: nil) + } + // MARK: - Private Methods + private func setupTableView() { + userCardView.tableView.isScrollEnabled = false + userCardView.tableView.delegate = self + userCardView.tableView.dataSource = self + } + + private func setupNavigationBar() { + let backButton = UIBarButtonItem(image: UIImage(systemName: "chevron.backward"), style: .plain, target: self, action: #selector(goBack)) + backButton.tintColor = .segmentActive + self.navigationItem.leftBarButtonItem = backButton + } +} +// MARK: - UITableViewDelegate +extension UserCardViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let nftCollectionVC = NFTCollectionViewController() + self.getNFTsCollectionDelegate = nftCollectionVC + getNFTsCollectionDelegate?.getNFTs(of: user) + let nftCollectionNavController = UINavigationController(rootViewController: nftCollectionVC) + nftCollectionNavController.setNavigationBarHidden(false, animated: false) + nftCollectionNavController.modalPresentationStyle = .fullScreen + present(nftCollectionNavController,animated: true, completion: nil) + } + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 54 + } +} +// MARK: - UITableViewDataSource +extension UserCardViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + 1 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: UserCardTableViewCell.identifier, for: indexPath) as? UserCardTableViewCell else { return UITableViewCell() } + cell.configure(with: user) + return cell + } +} +// MARK: - StatisticUsersListVCDelegate +extension UserCardViewController: StatisticUsersListVCDelegate { + func didTapCell(with user: UsersListModel) { + self.user = user + userCardView.updateProfile(of: user) + } +} +// MARK: - OpenUserWebsiteDelegate +extension UserCardViewController: OpenUserWebsiteDelegate { + func openUserWebsite() { + let webViewViewController = WebViewViewController() + webViewViewController.modalPresentationStyle = .popover + self.loadUserWebsiteDelegate = webViewViewController + loadUserWebsiteDelegate?.loadWebsite(of: user.website) + present(webViewViewController, animated: true, completion: nil) + } +} + diff --git a/FakeNFT/Statistic/UserCard/WebView/WebViewView.swift b/FakeNFT/Statistic/UserCard/WebView/WebViewView.swift new file mode 100644 index 0000000000..0d10106d4c --- /dev/null +++ b/FakeNFT/Statistic/UserCard/WebView/WebViewView.swift @@ -0,0 +1,41 @@ +// +// WebViewView.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/23/25. +// +import UIKit +import WebKit + +final class WebViewView: UIView { + // MARK: - UI Elements + lazy var webView: WKWebView = { + let webView = WKWebView() + webView.translatesAutoresizingMaskIntoConstraints = false + return webView + }() + + lazy var progressView: UIProgressView = { + let progressView = UIProgressView(progressViewStyle: .default) + progressView.translatesAutoresizingMaskIntoConstraints = false + return progressView + }() + // MARK: - Public Methods + func configure() { + backgroundColor = .background + + addSubview(webView) + addSubview(progressView) + NSLayoutConstraint.activate([ + webView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + webView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), + webView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + webView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), + + progressView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + progressView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + progressView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), + progressView.heightAnchor.constraint(equalToConstant: 2) + ]) + } +} diff --git a/FakeNFT/Statistic/UserCard/WebView/WebViewViewController.swift b/FakeNFT/Statistic/UserCard/WebView/WebViewViewController.swift new file mode 100644 index 0000000000..16264e4320 --- /dev/null +++ b/FakeNFT/Statistic/UserCard/WebView/WebViewViewController.swift @@ -0,0 +1,64 @@ +// +// WebViewViewController.swift +// FakeNFT +// +// Created by Ilya Nikitash on 3/23/25. +// +import UIKit +import WebKit + +final class WebViewViewController: UIViewController { + // MARK: - Private Properties + private let webViewView = WebViewView() + // MARK: - Lifecycle + override func loadView() { + self.view = webViewView + } + + override func viewDidLoad() { + super.viewDidLoad() + webViewView.configure() + webViewView.webView.navigationDelegate = self + webViewView.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil) + } + // MARK: - Initializers + deinit { + webViewView.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == #keyPath(WKWebView.estimatedProgress) { + webViewView.progressView.progress = Float(webViewView.webView.estimatedProgress) + } + } +} +// MARK: - WKNavigationDelegate +extension WebViewViewController: WKNavigationDelegate { + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + webViewView.progressView.isHidden = false + webViewView.progressView.progress = 0 + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + webViewView.progressView.isHidden = true + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + webViewView.progressView.isHidden = true + print("Error page loading: \(error.localizedDescription)") + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + webViewView.progressView.isHidden = true + print("Error page preload: \(error.localizedDescription)") + } +} +// MARK: - LoadUserWebsiteDelegate +extension WebViewViewController: LoadUserWebsiteDelegate { + func loadWebsite(of userWebsite: String) { + if let url = URL(string: userWebsite) { + let request = URLRequest(url: url) + webViewView.webView.load(request) + } + } +} diff --git a/FakeNFTTests/ExampleUnitTests.swift b/FakeNFTTests/ExampleUnitTests.swift index 302ce89bd5..0f368f6da4 100644 --- a/FakeNFTTests/ExampleUnitTests.swift +++ b/FakeNFTTests/ExampleUnitTests.swift @@ -1,8 +1,156 @@ @testable import FakeNFT import XCTest -final class ExampleUnitTests: XCTestCase { - func testExample() { - // TODO: - Не забудьте написать unit-тесты +final class StatisticUsersListTests: XCTestCase { + + func testStatisticLoadingView() { + let statisticUsersListVCSpy = StatisticUsersListViewControllerSpy() + + statisticUsersListVCSpy.loadView() + + XCTAssertTrue(statisticUsersListVCSpy.view == statisticUsersListVCSpy.statisticUsersListView) + } + + func testClickSortButtonTap() { + let statisticUsersListViewControllerSpy = StatisticUsersListViewControllerSpy() + + _ = statisticUsersListViewControllerSpy.view + statisticUsersListViewControllerSpy.statisticUsersListView.statisticUsersListViewDelegate?.clickSortButton() + + XCTAssertTrue(statisticUsersListViewControllerSpy.sortButtonClicked) + } + + func testViewConfiguration() { + let statisticUsersListViewControllerSpy = StatisticUsersListViewControllerSpy() + + _ = statisticUsersListViewControllerSpy.view + + XCTAssertTrue(statisticUsersListViewControllerSpy.view.backgroundColor == .background) + } +} + +final class StatisticUsersListViewControllerSpy: UIViewController & StatisticUsersListViewDelegate { + let statisticUsersListView = StatisticUsersListView() + var sortButtonClicked = false + + override func loadView() { + self.view = statisticUsersListView + } + override func viewDidLoad() { + statisticUsersListView.configure() + statisticUsersListView.statisticUsersListViewDelegate = self + } + + func clickSortButton() { + sortButtonClicked = true + } +} + +final class UserCardTests: XCTestCase { + let sut = StatisticUsersListViewController() + + func testUserCardLoadingView() { + let userCardViewControllerSpy = UserCardViewControllerSpy(statisticUsersListViewController: sut) + + userCardViewControllerSpy.loadView() + + XCTAssertTrue(userCardViewControllerSpy.view == userCardViewControllerSpy.userCardView) + } + + func testUserWebsiteOpen() { + let userCardViewControllerSpy = UserCardViewControllerSpy(statisticUsersListViewController: sut) + + _ = userCardViewControllerSpy.view + userCardViewControllerSpy.userCardView.openUserWebsiteDelegate?.openUserWebsite() + + XCTAssertTrue(userCardViewControllerSpy.websiteIsOpen) + } + + func testUserCardConfiguration() { + let userCardViewControllerSpy = UserCardViewControllerSpy(statisticUsersListViewController: sut) + + _ = userCardViewControllerSpy.view + + XCTAssertTrue(userCardViewControllerSpy.view.backgroundColor == .background) + } + + func testUpdateProfile() { + let userCardViewControllerSpy = UserCardViewControllerSpy(statisticUsersListViewController: sut) + let testUser = UsersListModel(name: "testUser", avatar: "", description: "", website: "", nfts: [], rating: "", id: "") + + sut.statisticUsersListVCDelegate?.didTapCell(with: testUser) + + XCTAssertTrue(userCardViewControllerSpy.cellTapped) + } +} + +final class UserCardViewControllerSpy: UIViewController & OpenUserWebsiteDelegate & StatisticUsersListVCDelegate { + let sut: StatisticUsersListViewController + let userCardView = UserCardView() + var websiteIsOpen = false + var cellTapped = false + + init(statisticUsersListViewController: StatisticUsersListViewController) { + self.sut = statisticUsersListViewController + super.init(nibName: nil, bundle: nil) + statisticUsersListViewController.statisticUsersListVCDelegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + self.view = userCardView + } + override func viewDidLoad() { + userCardView.configure() + userCardView.openUserWebsiteDelegate = self + } + + func didTapCell(with user: FakeNFT.UsersListModel) { + cellTapped = true + } + + func openUserWebsite() { + websiteIsOpen = true + } +} + +final class WebViewTests: XCTestCase { + func testWebViewLoadingView() { + let webViewViewControllerSpy = WebViewViewControllerSpy() + + webViewViewControllerSpy.loadView() + + XCTAssertTrue(webViewViewControllerSpy.view == webViewViewControllerSpy.webViewView) + } + + func testWebViewViewConfiguration() { + let webViewViewControllerSpy = WebViewViewControllerSpy() + + _ = webViewViewControllerSpy.view + + XCTAssertTrue(webViewViewControllerSpy.view.backgroundColor == .background) + } + + func testShowProgressView() { + let webViewView = WebViewView() + + webViewView.progressView.progress = 0.5 + + XCTAssertFalse(webViewView.progressView.isHidden) + } +} + +final class WebViewViewControllerSpy: UIViewController { + let webViewView = WebViewView() + + override func loadView() { + self.view = webViewView + } + + override func viewDidLoad() { + webViewView.configure() } }