diff --git a/ImageFeed.xcodeproj/project.pbxproj b/ImageFeed.xcodeproj/project.pbxproj index c21b349..572c5a9 100644 --- a/ImageFeed.xcodeproj/project.pbxproj +++ b/ImageFeed.xcodeproj/project.pbxproj @@ -7,51 +7,222 @@ objects = { /* Begin PBXBuildFile section */ + 22227A9A295F1B6B0014DF14 /* WebViewViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22227A99295F1B6B0014DF14 /* WebViewViewControllerDelegate.swift */; }; + 222804C8297A688400D6B54A /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222804C7297A688400D6B54A /* TabBarController.swift */; }; + 222804CA297A88B200D6B54A /* ImageListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222804C9297A88B200D6B54A /* ImageListService.swift */; }; + 223FDB28295DD00800FDCAE1 /* AuthConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223FDB27295DD00800FDCAE1 /* AuthConfiguration.swift */; }; + 223FDB2B295DD3AB00FDCAE1 /* WebViewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223FDB2A295DD3AB00FDCAE1 /* WebViewViewController.swift */; }; + 223FDB2D295DD49F00FDCAE1 /* AuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223FDB2C295DD49F00FDCAE1 /* AuthViewController.swift */; }; + 22437B07297AD6210026DE5D /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22437B06297AD6210026DE5D /* Photo.swift */; }; + 226DAD8829C1F7A700E5D7EF /* ImageFeedUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226DAD8729C1F7A700E5D7EF /* ImageFeedUITests.swift */; }; + 226DAD8A29C1F7A700E5D7EF /* ImageFeedUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226DAD8929C1F7A700E5D7EF /* ImageFeedUITestsLaunchTests.swift */; }; + 503717812986E1F10012E52C /* PhotoResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503717802986E1F10012E52C /* PhotoResult.swift */; }; + 50397DA42962CB750078D6BE /* UIBlockingProgressHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50397DA32962CB750078D6BE /* UIBlockingProgressHUD.swift */; }; + 50397DA62962D2730078D6BE /* ProfileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50397DA52962D2730078D6BE /* ProfileService.swift */; }; + 50397DA82963119B0078D6BE /* ProfileImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50397DA72963119B0078D6BE /* ProfileImageService.swift */; }; + 50397DAB296348080078D6BE /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50397DAA296348080078D6BE /* URLSession.swift */; }; + 50397DAE296362FF0078D6BE /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 50397DAD296362FF0078D6BE /* Kingfisher */; }; + 50397DB129646CE00078D6BE /* SwiftKeychainWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = 50397DB029646CE00078D6BE /* SwiftKeychainWrapper */; }; + 5044C5CD2940FC4200E65D8F /* SingleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5044C5CC2940FC4200E65D8F /* SingleImageViewController.swift */; }; + 50487E6529609E7700F41745 /* OAuth2Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50487E6429609E7700F41745 /* OAuth2Service.swift */; }; 506BFC6E2933D6160058728A /* ImagesListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506BFC6D2933D6160058728A /* ImagesListCell.swift */; }; + 506FAA1029A3E13A00E275C8 /* WebViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506FAA0F29A3E13A00E275C8 /* WebViewPresenter.swift */; }; + 506FAA1329A3E67200E275C8 /* AuthHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506FAA1229A3E67200E275C8 /* AuthHelper.swift */; }; + 506FAA1B29A3EFE400E275C8 /* WebViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506FAA1A29A3EFE400E275C8 /* WebViewTests.swift */; }; 50A856762931FDA700E476DF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A856752931FDA700E476DF /* AppDelegate.swift */; }; 50A856782931FDA700E476DF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A856772931FDA700E476DF /* SceneDelegate.swift */; }; - 50A8567A2931FDA700E476DF /* ImagesListControllerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A856792931FDA700E476DF /* ImagesListControllerViewController.swift */; }; + 50A8567A2931FDA700E476DF /* ImagesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A856792931FDA700E476DF /* ImagesListViewController.swift */; }; 50A8567D2931FDA700E476DF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A8567B2931FDA700E476DF /* Main.storyboard */; }; 50A8567F2931FDA800E476DF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A8567E2931FDA800E476DF /* Assets.xcassets */; }; 50A856822931FDA800E476DF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A856802931FDA800E476DF /* LaunchScreen.storyboard */; }; + 50B8D42F29619E7800508CF6 /* OAuth2TokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8D42E29619E7800508CF6 /* OAuth2TokenStorage.swift */; }; + 50B8D4322961AC7200508CF6 /* SplashViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8D4312961AC7200508CF6 /* SplashViewController.swift */; }; + 50B8D4352961F15500508CF6 /* ProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = 50B8D4342961F15500508CF6 /* ProgressHUD */; }; + 50B9138B29A5353A0093832D /* ProfilePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B9138A29A5353A0093832D /* ProfilePresenter.swift */; }; + 50B9138D29A549010093832D /* ImageListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B9138C29A549010093832D /* ImageListPresenter.swift */; }; + 50B9138F29A557AD0093832D /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B9138E29A557AD0093832D /* ProfileViewTests.swift */; }; + 50B9139129A557BF0093832D /* ImageListViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B9139029A557BF0093832D /* ImageListViewTests.swift */; }; + 50EEBC02293B6E2B00E950B5 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EEBC01293B6E2B00E950B5 /* ProfileViewController.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 226DAD8B29C1F7A700E5D7EF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 50A8566A2931FDA700E476DF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 50A856712931FDA700E476DF; + remoteInfo = ImageFeed; + }; + 506FAA1C29A3EFE400E275C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 50A8566A2931FDA700E476DF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 50A856712931FDA700E476DF; + remoteInfo = ImageFeed; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ + 22227A99295F1B6B0014DF14 /* WebViewViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewViewControllerDelegate.swift; sourceTree = ""; }; + 222804C7297A688400D6B54A /* TabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; }; + 222804C9297A88B200D6B54A /* ImageListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageListService.swift; sourceTree = ""; }; + 223FDB27295DD00800FDCAE1 /* AuthConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthConfiguration.swift; sourceTree = ""; }; + 223FDB2A295DD3AB00FDCAE1 /* WebViewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewViewController.swift; sourceTree = ""; }; + 223FDB2C295DD49F00FDCAE1 /* AuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewController.swift; sourceTree = ""; }; + 22437B06297AD6210026DE5D /* Photo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = ""; }; + 226DAD8529C1F7A700E5D7EF /* ImageFeedUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImageFeedUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 226DAD8729C1F7A700E5D7EF /* ImageFeedUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFeedUITests.swift; sourceTree = ""; }; + 226DAD8929C1F7A700E5D7EF /* ImageFeedUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFeedUITestsLaunchTests.swift; sourceTree = ""; }; + 503717802986E1F10012E52C /* PhotoResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoResult.swift; sourceTree = ""; }; + 50397DA32962CB750078D6BE /* UIBlockingProgressHUD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBlockingProgressHUD.swift; sourceTree = ""; }; + 50397DA52962D2730078D6BE /* ProfileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileService.swift; sourceTree = ""; }; + 50397DA72963119B0078D6BE /* ProfileImageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImageService.swift; sourceTree = ""; }; + 50397DAA296348080078D6BE /* URLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; }; + 5044C5CC2940FC4200E65D8F /* SingleImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleImageViewController.swift; sourceTree = ""; }; + 50487E6429609E7700F41745 /* OAuth2Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth2Service.swift; sourceTree = ""; }; 506BFC6D2933D6160058728A /* ImagesListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesListCell.swift; sourceTree = ""; }; + 506FAA0F29A3E13A00E275C8 /* WebViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewPresenter.swift; sourceTree = ""; }; + 506FAA1229A3E67200E275C8 /* AuthHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthHelper.swift; sourceTree = ""; }; + 506FAA1829A3EFE400E275C8 /* ImageFeedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImageFeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 506FAA1A29A3EFE400E275C8 /* WebViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewTests.swift; sourceTree = ""; }; 50A856722931FDA700E476DF /* ImageFeed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ImageFeed.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50A856752931FDA700E476DF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 50A856772931FDA700E476DF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 50A856792931FDA700E476DF /* ImagesListControllerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesListControllerViewController.swift; sourceTree = ""; }; + 50A856792931FDA700E476DF /* ImagesListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesListViewController.swift; sourceTree = ""; }; 50A8567C2931FDA700E476DF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 50A8567E2931FDA800E476DF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 50A856812931FDA800E476DF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 50A856832931FDA800E476DF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50B8D42E29619E7800508CF6 /* OAuth2TokenStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth2TokenStorage.swift; sourceTree = ""; }; + 50B8D4312961AC7200508CF6 /* SplashViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewController.swift; sourceTree = ""; }; + 50B9138A29A5353A0093832D /* ProfilePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePresenter.swift; sourceTree = ""; }; + 50B9138C29A549010093832D /* ImageListPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageListPresenter.swift; sourceTree = ""; }; + 50B9138E29A557AD0093832D /* ProfileViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewTests.swift; sourceTree = ""; }; + 50B9139029A557BF0093832D /* ImageListViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageListViewTests.swift; sourceTree = ""; }; + 50EEBC01293B6E2B00E950B5 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 226DAD8229C1F7A700E5D7EF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 506FAA1529A3EFE400E275C8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 50A8566F2931FDA700E476DF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 50397DAE296362FF0078D6BE /* Kingfisher in Frameworks */, + 50B8D4352961F15500508CF6 /* ProgressHUD in Frameworks */, + 50397DB129646CE00078D6BE /* SwiftKeychainWrapper in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 222804C6297A684400D6B54A /* TabBar */ = { + isa = PBXGroup; + children = ( + 222804C7297A688400D6B54A /* TabBarController.swift */, + ); + path = TabBar; + sourceTree = ""; + }; + 223FDB29295DD36600FDCAE1 /* Auth */ = { + isa = PBXGroup; + children = ( + 50487E6029609D3200F41745 /* Models */, + 223FDB2A295DD3AB00FDCAE1 /* WebViewViewController.swift */, + 223FDB2C295DD49F00FDCAE1 /* AuthViewController.swift */, + 22227A99295F1B6B0014DF14 /* WebViewViewControllerDelegate.swift */, + 50487E6429609E7700F41745 /* OAuth2Service.swift */, + 50397DA32962CB750078D6BE /* UIBlockingProgressHUD.swift */, + 506FAA0F29A3E13A00E275C8 /* WebViewPresenter.swift */, + 506FAA1229A3E67200E275C8 /* AuthHelper.swift */, + ); + path = Auth; + sourceTree = ""; + }; + 22437B05297AD6140026DE5D /* Models */ = { + isa = PBXGroup; + children = ( + 22437B06297AD6210026DE5D /* Photo.swift */, + 503717802986E1F10012E52C /* PhotoResult.swift */, + ); + path = Models; + sourceTree = ""; + }; + 226DAD8629C1F7A700E5D7EF /* ImageFeedUITests */ = { + isa = PBXGroup; + children = ( + 226DAD8729C1F7A700E5D7EF /* ImageFeedUITests.swift */, + 226DAD8929C1F7A700E5D7EF /* ImageFeedUITestsLaunchTests.swift */, + ); + path = ImageFeedUITests; + sourceTree = ""; + }; + 50397DA9296347EC0078D6BE /* Extensions */ = { + isa = PBXGroup; + children = ( + 50397DAA296348080078D6BE /* URLSession.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 5044C5CB2940FC1F00E65D8F /* SingleImage */ = { + isa = PBXGroup; + children = ( + 5044C5CC2940FC4200E65D8F /* SingleImageViewController.swift */, + ); + path = SingleImage; + sourceTree = ""; + }; + 50487E6029609D3200F41745 /* Models */ = { + isa = PBXGroup; + children = ( + 50B8D42E29619E7800508CF6 /* OAuth2TokenStorage.swift */, + ); + path = Models; + sourceTree = ""; + }; 506BFC6F2933D62F0058728A /* ImagesList */ = { isa = PBXGroup; children = ( - 50A856792931FDA700E476DF /* ImagesListControllerViewController.swift */, + 22437B05297AD6140026DE5D /* Models */, + 50A856792931FDA700E476DF /* ImagesListViewController.swift */, 506BFC6D2933D6160058728A /* ImagesListCell.swift */, + 222804C9297A88B200D6B54A /* ImageListService.swift */, + 50B9138C29A549010093832D /* ImageListPresenter.swift */, ); path = ImagesList; sourceTree = ""; }; + 506FAA1929A3EFE400E275C8 /* ImageFeedTests */ = { + isa = PBXGroup; + children = ( + 506FAA1A29A3EFE400E275C8 /* WebViewTests.swift */, + 50B9138E29A557AD0093832D /* ProfileViewTests.swift */, + 50B9139029A557BF0093832D /* ImageListViewTests.swift */, + ); + path = ImageFeedTests; + sourceTree = ""; + }; 50A856692931FDA700E476DF = { isa = PBXGroup; children = ( 50A856742931FDA700E476DF /* ImageFeed */, + 506FAA1929A3EFE400E275C8 /* ImageFeedTests */, + 226DAD8629C1F7A700E5D7EF /* ImageFeedUITests */, 50A856732931FDA700E476DF /* Products */, ); sourceTree = ""; @@ -60,6 +231,8 @@ isa = PBXGroup; children = ( 50A856722931FDA700E476DF /* ImageFeed.app */, + 506FAA1829A3EFE400E275C8 /* ImageFeedTests.xctest */, + 226DAD8529C1F7A700E5D7EF /* ImageFeedUITests.xctest */, ); name = Products; sourceTree = ""; @@ -67,6 +240,12 @@ 50A856742931FDA700E476DF /* ImageFeed */ = { isa = PBXGroup; children = ( + 222804C6297A684400D6B54A /* TabBar */, + 50397DA9296347EC0078D6BE /* Extensions */, + 50B8D4302961AC4F00508CF6 /* SplashViewController */, + 223FDB29295DD36600FDCAE1 /* Auth */, + 5044C5CB2940FC1F00E65D8F /* SingleImage */, + 50EEBC00293B6E1100E950B5 /* Profile */, 506BFC6F2933D62F0058728A /* ImagesList */, 50A856752931FDA700E476DF /* AppDelegate.swift */, 50A856772931FDA700E476DF /* SceneDelegate.swift */, @@ -74,13 +253,69 @@ 50A8567E2931FDA800E476DF /* Assets.xcassets */, 50A856802931FDA800E476DF /* LaunchScreen.storyboard */, 50A856832931FDA800E476DF /* Info.plist */, + 223FDB27295DD00800FDCAE1 /* AuthConfiguration.swift */, ); path = ImageFeed; sourceTree = ""; }; + 50B8D4302961AC4F00508CF6 /* SplashViewController */ = { + isa = PBXGroup; + children = ( + 50B8D4312961AC7200508CF6 /* SplashViewController.swift */, + ); + path = SplashViewController; + sourceTree = ""; + }; + 50EEBC00293B6E1100E950B5 /* Profile */ = { + isa = PBXGroup; + children = ( + 50EEBC01293B6E2B00E950B5 /* ProfileViewController.swift */, + 50397DA52962D2730078D6BE /* ProfileService.swift */, + 50397DA72963119B0078D6BE /* ProfileImageService.swift */, + 50B9138A29A5353A0093832D /* ProfilePresenter.swift */, + ); + path = Profile; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 226DAD8429C1F7A700E5D7EF /* ImageFeedUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 226DAD8F29C1F7A700E5D7EF /* Build configuration list for PBXNativeTarget "ImageFeedUITests" */; + buildPhases = ( + 226DAD8129C1F7A700E5D7EF /* Sources */, + 226DAD8229C1F7A700E5D7EF /* Frameworks */, + 226DAD8329C1F7A700E5D7EF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 226DAD8C29C1F7A700E5D7EF /* PBXTargetDependency */, + ); + name = ImageFeedUITests; + productName = ImageFeedUITests; + productReference = 226DAD8529C1F7A700E5D7EF /* ImageFeedUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 506FAA1729A3EFE400E275C8 /* ImageFeedTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 506FAA1E29A3EFE400E275C8 /* Build configuration list for PBXNativeTarget "ImageFeedTests" */; + buildPhases = ( + 506FAA1429A3EFE400E275C8 /* Sources */, + 506FAA1529A3EFE400E275C8 /* Frameworks */, + 506FAA1629A3EFE400E275C8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 506FAA1D29A3EFE400E275C8 /* PBXTargetDependency */, + ); + name = ImageFeedTests; + productName = ImageFeedTests; + productReference = 506FAA1829A3EFE400E275C8 /* ImageFeedTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 50A856712931FDA700E476DF /* ImageFeed */ = { isa = PBXNativeTarget; buildConfigurationList = 50A856862931FDA800E476DF /* Build configuration list for PBXNativeTarget "ImageFeed" */; @@ -94,6 +329,11 @@ dependencies = ( ); name = ImageFeed; + packageProductDependencies = ( + 50B8D4342961F15500508CF6 /* ProgressHUD */, + 50397DAD296362FF0078D6BE /* Kingfisher */, + 50397DB029646CE00078D6BE /* SwiftKeychainWrapper */, + ); productName = ImageFeed; productReference = 50A856722931FDA700E476DF /* ImageFeed.app */; productType = "com.apple.product-type.application"; @@ -105,9 +345,17 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1410; + LastSwiftUpdateCheck = 1420; LastUpgradeCheck = 1410; TargetAttributes = { + 226DAD8429C1F7A700E5D7EF = { + CreatedOnToolsVersion = 14.2; + TestTargetID = 50A856712931FDA700E476DF; + }; + 506FAA1729A3EFE400E275C8 = { + CreatedOnToolsVersion = 14.1; + TestTargetID = 50A856712931FDA700E476DF; + }; 50A856712931FDA700E476DF = { CreatedOnToolsVersion = 14.1; }; @@ -122,16 +370,37 @@ Base, ); mainGroup = 50A856692931FDA700E476DF; + packageReferences = ( + 50B8D4332961F15500508CF6 /* XCRemoteSwiftPackageReference "ProgressHUD" */, + 50397DAC296362FF0078D6BE /* XCRemoteSwiftPackageReference "Kingfisher" */, + 50397DAF29646CE00078D6BE /* XCRemoteSwiftPackageReference "SwiftKeychainWrapper" */, + ); productRefGroup = 50A856732931FDA700E476DF /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 50A856712931FDA700E476DF /* ImageFeed */, + 506FAA1729A3EFE400E275C8 /* ImageFeedTests */, + 226DAD8429C1F7A700E5D7EF /* ImageFeedUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 226DAD8329C1F7A700E5D7EF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 506FAA1629A3EFE400E275C8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 50A856702931FDA700E476DF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -145,19 +414,72 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 226DAD8129C1F7A700E5D7EF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 226DAD8A29C1F7A700E5D7EF /* ImageFeedUITestsLaunchTests.swift in Sources */, + 226DAD8829C1F7A700E5D7EF /* ImageFeedUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 506FAA1429A3EFE400E275C8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 50B9138F29A557AD0093832D /* ProfileViewTests.swift in Sources */, + 506FAA1B29A3EFE400E275C8 /* WebViewTests.swift in Sources */, + 50B9139129A557BF0093832D /* ImageListViewTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 50A8566E2931FDA700E476DF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 50A8567A2931FDA700E476DF /* ImagesListControllerViewController.swift in Sources */, + 50A8567A2931FDA700E476DF /* ImagesListViewController.swift in Sources */, + 22227A9A295F1B6B0014DF14 /* WebViewViewControllerDelegate.swift in Sources */, + 50B9138B29A5353A0093832D /* ProfilePresenter.swift in Sources */, + 50487E6529609E7700F41745 /* OAuth2Service.swift in Sources */, + 222804C8297A688400D6B54A /* TabBarController.swift in Sources */, + 22437B07297AD6210026DE5D /* Photo.swift in Sources */, + 50397DAB296348080078D6BE /* URLSession.swift in Sources */, 506BFC6E2933D6160058728A /* ImagesListCell.swift in Sources */, + 50EEBC02293B6E2B00E950B5 /* ProfileViewController.swift in Sources */, 50A856762931FDA700E476DF /* AppDelegate.swift in Sources */, + 503717812986E1F10012E52C /* PhotoResult.swift in Sources */, + 50B9138D29A549010093832D /* ImageListPresenter.swift in Sources */, + 50B8D4322961AC7200508CF6 /* SplashViewController.swift in Sources */, + 223FDB28295DD00800FDCAE1 /* AuthConfiguration.swift in Sources */, + 5044C5CD2940FC4200E65D8F /* SingleImageViewController.swift in Sources */, + 50397DA82963119B0078D6BE /* ProfileImageService.swift in Sources */, + 223FDB2B295DD3AB00FDCAE1 /* WebViewViewController.swift in Sources */, + 223FDB2D295DD49F00FDCAE1 /* AuthViewController.swift in Sources */, + 506FAA1029A3E13A00E275C8 /* WebViewPresenter.swift in Sources */, + 50B8D42F29619E7800508CF6 /* OAuth2TokenStorage.swift in Sources */, + 506FAA1329A3E67200E275C8 /* AuthHelper.swift in Sources */, 50A856782931FDA700E476DF /* SceneDelegate.swift in Sources */, + 50397DA42962CB750078D6BE /* UIBlockingProgressHUD.swift in Sources */, + 50397DA62962D2730078D6BE /* ProfileService.swift in Sources */, + 222804CA297A88B200D6B54A /* ImageListService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 226DAD8C29C1F7A700E5D7EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 50A856712931FDA700E476DF /* ImageFeed */; + targetProxy = 226DAD8B29C1F7A700E5D7EF /* PBXContainerItemProxy */; + }; + 506FAA1D29A3EFE400E275C8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 50A856712931FDA700E476DF /* ImageFeed */; + targetProxy = 506FAA1C29A3EFE400E275C8 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 50A8567B2931FDA700E476DF /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -178,6 +500,76 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 226DAD8D29C1F7A700E5D7EF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 725CQTTNVU; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.abanking.ImageFeedUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = ImageFeed; + }; + name = Debug; + }; + 226DAD8E29C1F7A700E5D7EF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 725CQTTNVU; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.abanking.ImageFeedUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = ImageFeed; + }; + name = Release; + }; + 506FAA1F29A3EFE400E275C8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ru.maxcode2023.ImageFeedTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ImageFeed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ImageFeed"; + }; + name = Debug; + }; + 506FAA2029A3EFE400E275C8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ru.maxcode2023.ImageFeedTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ImageFeed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ImageFeed"; + }; + name = Release; + }; 50A856842931FDA800E476DF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -302,7 +694,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ImageFeed/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UILaunchStoryboardName = Main.storyboard; INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; @@ -333,7 +725,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ImageFeed/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UILaunchStoryboardName = Main.storyboard; INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; @@ -357,6 +749,24 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 226DAD8F29C1F7A700E5D7EF /* Build configuration list for PBXNativeTarget "ImageFeedUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 226DAD8D29C1F7A700E5D7EF /* Debug */, + 226DAD8E29C1F7A700E5D7EF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 506FAA1E29A3EFE400E275C8 /* Build configuration list for PBXNativeTarget "ImageFeedTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 506FAA1F29A3EFE400E275C8 /* Debug */, + 506FAA2029A3EFE400E275C8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 50A8566D2931FDA700E476DF /* Build configuration list for PBXProject "ImageFeed" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -376,6 +786,51 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 50397DAC296362FF0078D6BE /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.0.0; + }; + }; + 50397DAF29646CE00078D6BE /* XCRemoteSwiftPackageReference "SwiftKeychainWrapper" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/jrendel/SwiftKeychainWrapper"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.0.0; + }; + }; + 50B8D4332961F15500508CF6 /* XCRemoteSwiftPackageReference "ProgressHUD" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/relatedcode/ProgressHUD"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 13.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 50397DAD296362FF0078D6BE /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 50397DAC296362FF0078D6BE /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; + 50397DB029646CE00078D6BE /* SwiftKeychainWrapper */ = { + isa = XCSwiftPackageProductDependency; + package = 50397DAF29646CE00078D6BE /* XCRemoteSwiftPackageReference "SwiftKeychainWrapper" */; + productName = SwiftKeychainWrapper; + }; + 50B8D4342961F15500508CF6 /* ProgressHUD */ = { + isa = XCSwiftPackageProductDependency; + package = 50B8D4332961F15500508CF6 /* XCRemoteSwiftPackageReference "ProgressHUD" */; + productName = ProgressHUD; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 50A8566A2931FDA700E476DF /* Project object */; } diff --git a/ImageFeed/AppDelegate.swift b/ImageFeed/AppDelegate.swift index d86e7ff..7017a12 100644 --- a/ImageFeed/AppDelegate.swift +++ b/ImageFeed/AppDelegate.swift @@ -6,28 +6,27 @@ // import UIKit +import ProgressHUD @main class AppDelegate: UIResponder, UIApplicationDelegate { - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - + ProgressHUD.animationType = .systemActivityIndicator + ProgressHUD.colorHUD = .black + ProgressHUD.colorAnimation = .lightGray return true } - // MARK: UISceneSession Lifecycle - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + let sceneConfiguration = UISceneConfiguration(name: "Main", sessionRole: connectingSceneSession.role) + + sceneConfiguration.delegateClass = SceneDelegate.self + return sceneConfiguration } func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { } - - } - diff --git a/ImageFeed/Assets.xcassets/Colors/Contents.json b/ImageFeed/Assets.xcassets/Colors/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/ImageFeed/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/Colors/YP Black.colorset/Contents.json b/ImageFeed/Assets.xcassets/Colors/YP Black.colorset/Contents.json new file mode 100644 index 0000000..1d65869 --- /dev/null +++ b/ImageFeed/Assets.xcassets/Colors/YP Black.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.133", + "green" : "0.106", + "red" : "0.102" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.133", + "green" : "0.106", + "red" : "0.102" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/Colors/YP Grey.colorset/Contents.json b/ImageFeed/Assets.xcassets/Colors/YP Grey.colorset/Contents.json new file mode 100644 index 0000000..a6ef923 --- /dev/null +++ b/ImageFeed/Assets.xcassets/Colors/YP Grey.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.706", + "green" : "0.686", + "red" : "0.682" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.706", + "green" : "0.686", + "red" : "0.682" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/YP Black.colorset/Contents.json b/ImageFeed/Assets.xcassets/Colors/YP Red.colorset/Contents.json similarity index 76% rename from ImageFeed/Assets.xcassets/YP Black.colorset/Contents.json rename to ImageFeed/Assets.xcassets/Colors/YP Red.colorset/Contents.json index b15ea4c..11976ca 100644 --- a/ImageFeed/Assets.xcassets/YP Black.colorset/Contents.json +++ b/ImageFeed/Assets.xcassets/Colors/YP Red.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x22", - "green" : "0x1B", - "red" : "0x1A" + "blue" : "0x6C", + "green" : "0x6B", + "red" : "0xF5" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x22", - "green" : "0x1B", - "red" : "0x1A" + "blue" : "0x6C", + "green" : "0x6B", + "red" : "0xF5" } }, "idiom" : "universal" diff --git a/ImageFeed/Assets.xcassets/Colors/YP White.colorset/Contents.json b/ImageFeed/Assets.xcassets/Colors/YP White.colorset/Contents.json new file mode 100644 index 0000000..22c4bb0 --- /dev/null +++ b/ImageFeed/Assets.xcassets/Colors/YP White.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/AccentColor.colorset/Contents.json b/ImageFeed/Assets.xcassets/Icons/AccentColor.colorset/Contents.json similarity index 100% rename from ImageFeed/Assets.xcassets/AccentColor.colorset/Contents.json rename to ImageFeed/Assets.xcassets/Icons/AccentColor.colorset/Contents.json diff --git a/ImageFeed/Assets.xcassets/AppIcon.appiconset/Contents.json b/ImageFeed/Assets.xcassets/Icons/AppIcon.appiconset/Contents.json similarity index 100% rename from ImageFeed/Assets.xcassets/AppIcon.appiconset/Contents.json rename to ImageFeed/Assets.xcassets/Icons/AppIcon.appiconset/Contents.json diff --git a/ImageFeed/Assets.xcassets/Icons/Backward.imageset/Backward.png b/ImageFeed/Assets.xcassets/Icons/Backward.imageset/Backward.png new file mode 100644 index 0000000..1e28c7e Binary files /dev/null and b/ImageFeed/Assets.xcassets/Icons/Backward.imageset/Backward.png differ diff --git a/ImageFeed/Assets.xcassets/Icons/Backward.imageset/Contents.json b/ImageFeed/Assets.xcassets/Icons/Backward.imageset/Contents.json new file mode 100644 index 0000000..a410a9b --- /dev/null +++ b/ImageFeed/Assets.xcassets/Icons/Backward.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Backward.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/Icons/Contents.json b/ImageFeed/Assets.xcassets/Icons/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/ImageFeed/Assets.xcassets/Icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/Icons/Exit.imageset/Contents.json b/ImageFeed/Assets.xcassets/Icons/Exit.imageset/Contents.json new file mode 100644 index 0000000..be12e6d --- /dev/null +++ b/ImageFeed/Assets.xcassets/Icons/Exit.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Exit.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/Icons/Exit.imageset/Exit.png b/ImageFeed/Assets.xcassets/Icons/Exit.imageset/Exit.png new file mode 100644 index 0000000..b461674 Binary files /dev/null and b/ImageFeed/Assets.xcassets/Icons/Exit.imageset/Exit.png differ diff --git a/ImageFeed/Assets.xcassets/Logo.imageset/Contents.json b/ImageFeed/Assets.xcassets/Icons/Logo.imageset/Contents.json similarity index 100% rename from ImageFeed/Assets.xcassets/Logo.imageset/Contents.json rename to ImageFeed/Assets.xcassets/Icons/Logo.imageset/Contents.json diff --git a/ImageFeed/Assets.xcassets/Logo.imageset/Vector.png b/ImageFeed/Assets.xcassets/Icons/Logo.imageset/Vector.png similarity index 100% rename from ImageFeed/Assets.xcassets/Logo.imageset/Vector.png rename to ImageFeed/Assets.xcassets/Icons/Logo.imageset/Vector.png diff --git a/ImageFeed/Assets.xcassets/Icons/Photo.imageset/Contents.json b/ImageFeed/Assets.xcassets/Icons/Photo.imageset/Contents.json new file mode 100644 index 0000000..b8b03a0 --- /dev/null +++ b/ImageFeed/Assets.xcassets/Icons/Photo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Photo.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/Icons/Photo.imageset/Photo.pdf b/ImageFeed/Assets.xcassets/Icons/Photo.imageset/Photo.pdf new file mode 100644 index 0000000..7e7e79e Binary files /dev/null and b/ImageFeed/Assets.xcassets/Icons/Photo.imageset/Photo.pdf differ diff --git a/ImageFeed/Assets.xcassets/Icons/Sharing.imageset/Contents.json b/ImageFeed/Assets.xcassets/Icons/Sharing.imageset/Contents.json new file mode 100644 index 0000000..3bfb24e --- /dev/null +++ b/ImageFeed/Assets.xcassets/Icons/Sharing.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Sharing.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/Icons/Sharing.imageset/Sharing.pdf b/ImageFeed/Assets.xcassets/Icons/Sharing.imageset/Sharing.pdf new file mode 100644 index 0000000..eee12c4 Binary files /dev/null and b/ImageFeed/Assets.xcassets/Icons/Sharing.imageset/Sharing.pdf differ diff --git a/ImageFeed/Assets.xcassets/Icons/image plug.imageset/Contents.json b/ImageFeed/Assets.xcassets/Icons/image plug.imageset/Contents.json new file mode 100644 index 0000000..6a88fbb --- /dev/null +++ b/ImageFeed/Assets.xcassets/Icons/image plug.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "scribble.variable.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/Icons/image plug.imageset/scribble.variable.pdf b/ImageFeed/Assets.xcassets/Icons/image plug.imageset/scribble.variable.pdf new file mode 100644 index 0000000..98e8227 Binary files /dev/null and b/ImageFeed/Assets.xcassets/Icons/image plug.imageset/scribble.variable.pdf differ diff --git a/ImageFeed/Assets.xcassets/Active.imageset/Active.png b/ImageFeed/Assets.xcassets/Icons/liked.imageset/Active.png similarity index 100% rename from ImageFeed/Assets.xcassets/Active.imageset/Active.png rename to ImageFeed/Assets.xcassets/Icons/liked.imageset/Active.png diff --git a/ImageFeed/Assets.xcassets/Active.imageset/Contents.json b/ImageFeed/Assets.xcassets/Icons/liked.imageset/Contents.json similarity index 100% rename from ImageFeed/Assets.xcassets/Active.imageset/Contents.json rename to ImageFeed/Assets.xcassets/Icons/liked.imageset/Contents.json diff --git a/ImageFeed/Assets.xcassets/Icons/nav_back_button.imageset/Backward.pdf b/ImageFeed/Assets.xcassets/Icons/nav_back_button.imageset/Backward.pdf new file mode 100644 index 0000000..4da8fe4 Binary files /dev/null and b/ImageFeed/Assets.xcassets/Icons/nav_back_button.imageset/Backward.pdf differ diff --git a/ImageFeed/Assets.xcassets/Icons/nav_back_button.imageset/Contents.json b/ImageFeed/Assets.xcassets/Icons/nav_back_button.imageset/Contents.json new file mode 100644 index 0000000..1a321b5 --- /dev/null +++ b/ImageFeed/Assets.xcassets/Icons/nav_back_button.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Backward.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/No Active.imageset/Contents.json b/ImageFeed/Assets.xcassets/Icons/no liked.imageset/Contents.json similarity index 100% rename from ImageFeed/Assets.xcassets/No Active.imageset/Contents.json rename to ImageFeed/Assets.xcassets/Icons/no liked.imageset/Contents.json diff --git a/ImageFeed/Assets.xcassets/No Active.imageset/No Active.png b/ImageFeed/Assets.xcassets/Icons/no liked.imageset/No Active.png similarity index 100% rename from ImageFeed/Assets.xcassets/No Active.imageset/No Active.png rename to ImageFeed/Assets.xcassets/Icons/no liked.imageset/No Active.png diff --git a/ImageFeed/Assets.xcassets/Icons/plug.imageset/Contents.json b/ImageFeed/Assets.xcassets/Icons/plug.imageset/Contents.json new file mode 100644 index 0000000..0d64d19 --- /dev/null +++ b/ImageFeed/Assets.xcassets/Icons/plug.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Снимок экрана 2023-02-14 в 18.39.51.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/ImageFeed/Assets.xcassets/Icons/plug.imageset/\320\241\320\275\320\270\320\274\320\276\320\272 \321\215\320\272\321\200\320\260\320\275\320\260 2023-02-14 \320\262 18.39.51.png" "b/ImageFeed/Assets.xcassets/Icons/plug.imageset/\320\241\320\275\320\270\320\274\320\276\320\272 \321\215\320\272\321\200\320\260\320\275\320\260 2023-02-14 \320\262 18.39.51.png" new file mode 100644 index 0000000..1a210ef Binary files /dev/null and "b/ImageFeed/Assets.xcassets/Icons/plug.imageset/\320\241\320\275\320\270\320\274\320\276\320\272 \321\215\320\272\321\200\320\260\320\275\320\260 2023-02-14 \320\262 18.39.51.png" differ diff --git a/ImageFeed/Assets.xcassets/Icons/tab_editorial_active.imageset/Active.png b/ImageFeed/Assets.xcassets/Icons/tab_editorial_active.imageset/Active.png new file mode 100644 index 0000000..d522e6f Binary files /dev/null and b/ImageFeed/Assets.xcassets/Icons/tab_editorial_active.imageset/Active.png differ diff --git a/ImageFeed/Assets.xcassets/Icons/tab_editorial_active.imageset/Contents.json b/ImageFeed/Assets.xcassets/Icons/tab_editorial_active.imageset/Contents.json new file mode 100644 index 0000000..0fe7a56 --- /dev/null +++ b/ImageFeed/Assets.xcassets/Icons/tab_editorial_active.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Active.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/Icons/tab_profile_active.imageset/Contents.json b/ImageFeed/Assets.xcassets/Icons/tab_profile_active.imageset/Contents.json new file mode 100644 index 0000000..effb505 --- /dev/null +++ b/ImageFeed/Assets.xcassets/Icons/tab_profile_active.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "tab_profile_active.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/Icons/tab_profile_active.imageset/tab_profile_active.png b/ImageFeed/Assets.xcassets/Icons/tab_profile_active.imageset/tab_profile_active.png new file mode 100644 index 0000000..cc36a93 Binary files /dev/null and b/ImageFeed/Assets.xcassets/Icons/tab_profile_active.imageset/tab_profile_active.png differ diff --git a/ImageFeed/Assets.xcassets/Icons/unsplash icon.imageset/Contents.json b/ImageFeed/Assets.xcassets/Icons/unsplash icon.imageset/Contents.json new file mode 100644 index 0000000..0d192b3 --- /dev/null +++ b/ImageFeed/Assets.xcassets/Icons/unsplash icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Vector.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ImageFeed/Assets.xcassets/Icons/unsplash icon.imageset/Vector.pdf b/ImageFeed/Assets.xcassets/Icons/unsplash icon.imageset/Vector.pdf new file mode 100644 index 0000000..4acf463 Binary files /dev/null and b/ImageFeed/Assets.xcassets/Icons/unsplash icon.imageset/Vector.pdf differ diff --git a/ImageFeed/Auth/AuthHelper.swift b/ImageFeed/Auth/AuthHelper.swift new file mode 100644 index 0000000..50f5cc9 --- /dev/null +++ b/ImageFeed/Auth/AuthHelper.swift @@ -0,0 +1,49 @@ +// +// AuthHelper.swift +// ImageFeed +// +// Created by Albert on 20.02.2023. +// + +import Foundation + +protocol AuthHelperProtocol { + func authRequest() -> URLRequest + func code(from url: URL) -> String? +} + +class AuthHelper: AuthHelperProtocol { + let configuration: AuthConfiguration + + init(configuration: AuthConfiguration = .standard) { + self.configuration = configuration + } + + func authRequest() -> URLRequest { + let url = authURL() + return URLRequest(url: url) + } + + func authURL() -> URL { + var urlComponents = URLComponents(string: configuration.authURLString)! + urlComponents.queryItems = [ + URLQueryItem(name: "client_id", value: configuration.accessKey), + URLQueryItem(name: "redirect_uri", value: configuration.redirectURI), + URLQueryItem(name: "response_type", value: "code"), + URLQueryItem(name: "scope", value: configuration.accessScope) + ] + return urlComponents.url! + } + + func code(from url: URL) -> String? { + if let urlComponents = URLComponents(string: url.absoluteString), + urlComponents.path == "/oauth/authorize/native", + let items = urlComponents.queryItems, + let codeItem = items.first(where: { $0.name == "code" }) + { + return codeItem.value + } else { + return nil + } + } +} diff --git a/ImageFeed/Auth/AuthViewController.swift b/ImageFeed/Auth/AuthViewController.swift new file mode 100644 index 0000000..c189613 --- /dev/null +++ b/ImageFeed/Auth/AuthViewController.swift @@ -0,0 +1,44 @@ +// +// AuthViewController.swift +// ImageFeed +// +// Created by macOS on 29.12.2022. +// + +import UIKit + +final class AuthViewController: UIViewController { + + private let segueID = "ShowWebView" + + weak var delegate: AuthViewControllerDelegate? + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == segueID { + guard + let webViewViewController = segue.destination as? WebViewViewController + else { fatalError("Failed to prepare for \(segueID)") } + let authHelper = AuthHelper() + let webViewPresenter = WebViewPresenter(authHelper: authHelper) + webViewViewController.presenter = webViewPresenter + webViewPresenter.view = webViewViewController + webViewViewController.delegate = self + } else { + super.prepare(for: segue, sender: sender) + } + } +} + +extension AuthViewController: WebViewViewControllerDelegate { + func webViewViewController(_ vc: WebViewViewController, didAuthenticateWithCode code: String) { + delegate?.authViewController(self, didAuthenticateWithCode: code) + } + + func webViewViewControllerDidCancel(_ vc: WebViewViewController) { + dismiss(animated: true) + } +} + +protocol AuthViewControllerDelegate: AnyObject { + func authViewController(_ vc: AuthViewController, didAuthenticateWithCode code: String) +} diff --git a/ImageFeed/Auth/Models/OAuth2TokenStorage.swift b/ImageFeed/Auth/Models/OAuth2TokenStorage.swift new file mode 100644 index 0000000..cbf58bc --- /dev/null +++ b/ImageFeed/Auth/Models/OAuth2TokenStorage.swift @@ -0,0 +1,32 @@ +// +// OAuth2TokenStorage.swift +// ImageFeed +// +// Created by macOS on 01.01.2023. +// + +import Foundation +import SwiftKeychainWrapper + +enum TokenStorageKeys: String { + case tokenKey = "BearerToken" +} + +final class OAuth2TokenStorage { + private let keychain = KeychainWrapper.standard + + var token: String? { + get { + return keychain.string(forKey: TokenStorageKeys.tokenKey.rawValue) + } + + set(newValue) { + if let token = newValue { + keychain.set(token, forKey: TokenStorageKeys.tokenKey.rawValue) + + } else { + keychain.removeObject(forKey: TokenStorageKeys.tokenKey.rawValue) + } + } + } +} diff --git a/ImageFeed/Auth/OAuth2Service.swift b/ImageFeed/Auth/OAuth2Service.swift new file mode 100644 index 0000000..783dbfb --- /dev/null +++ b/ImageFeed/Auth/OAuth2Service.swift @@ -0,0 +1,99 @@ +// +// OAuth2Service.swift +// ImageFeed +// +// Created by macOS on 31.12.2022. +// + + +import Foundation +import WebKit + +enum NetworkError: Error { + case httpStatusCode(Int) + case urlRequestError(Error) + case urlSessionError +} + +final class OAuth2Service { + static let shared = OAuth2Service() + + private let urlSession = URLSession.shared + private var task: URLSessionTask? + private var lastCode: String? + + private (set) var authToken: String? { + get { + return OAuth2TokenStorage().token + } + set { + OAuth2TokenStorage().token = newValue + } + } + + func logout(completion: @escaping () -> Void) { + authToken?.removeAll() + OAuth2Service.clean() + completion() + } + + static func clean() { + HTTPCookieStorage.shared.removeCookies(since: Date.distantPast) + WKWebsiteDataStore.default().fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in + records.forEach { record in + WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {}) + } + } + } + + func fetchOAuthToken(_ code: String, completion: @escaping (Result) -> Void) { + + assert(Thread.isMainThread) + + if lastCode == code { return } + task?.cancel() + lastCode = code + + var urlComponents = URLComponents(string: "https://unsplash.com/oauth/token")! + urlComponents.queryItems = [ + URLQueryItem(name: "client_id", value: AccessKey), + URLQueryItem(name: "client_secret", value: SecretKey), + URLQueryItem(name: "redirect_uri", value: RedirectURI), + URLQueryItem(name: "code", value: code), + URLQueryItem(name: "grant_type", value: "authorization_code") + ] + guard let url = urlComponents.url else { return } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + + + let task = urlSession.objectTask(for: request) { [weak self] (result: Result) in + guard let self = self else { return } + switch result { + case .success(let body): + let authToken = body.accessToken + self.authToken = authToken + completion(.success(authToken)) + case .failure(let error): + completion(.failure(error)) + } + } + self.task = task + task.resume() + } + + private struct OAuthTokenResponseBody: Codable { + let accessToken: String + let tokenType: String + let scope: String + let createdAt: Int + + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case tokenType = "token_type" + case scope + case createdAt = "created_at" + } + } +} diff --git a/ImageFeed/Auth/UIBlockingProgressHUD.swift b/ImageFeed/Auth/UIBlockingProgressHUD.swift new file mode 100644 index 0000000..4259c29 --- /dev/null +++ b/ImageFeed/Auth/UIBlockingProgressHUD.swift @@ -0,0 +1,25 @@ +// +// UIBlockingProgressHUD.swift +// ImageFeed +// +// Created by macOS on 02.01.2023. +// + +import UIKit +import ProgressHUD + +final class UIBlockingProgressHUD { + private static var window: UIWindow? { + return UIApplication.shared.windows.first + } + + static func show() { + window?.isUserInteractionEnabled = false + ProgressHUD.show() + } + + static func dismiss() { + window?.isUserInteractionEnabled = true + ProgressHUD.dismiss() + } +} diff --git a/ImageFeed/Auth/WebViewPresenter.swift b/ImageFeed/Auth/WebViewPresenter.swift new file mode 100644 index 0000000..bb7a405 --- /dev/null +++ b/ImageFeed/Auth/WebViewPresenter.swift @@ -0,0 +1,47 @@ +// +// WebViewPresenterProtocol.swift +// ImageFeed +// +// Created by Albert on 20.02.2023. +// + +import Foundation + +public protocol WebViewPresenterProtocol { + var view: WebViewViewControllerProtocol? { get set } + func viewDidLoad() + func didUpdateProgressValue(_ newValue: Double) + func code(from url: URL) -> String? +} + +final class WebViewPresenter: WebViewPresenterProtocol { + weak var view: WebViewViewControllerProtocol? + + var authHelper: AuthHelperProtocol + + init(authHelper: AuthHelperProtocol) { + self.authHelper = authHelper + } + + func viewDidLoad() { + let request = authHelper.authRequest() + view?.load(request: request) + didUpdateProgressValue(0) + } + + func didUpdateProgressValue(_ newValue: Double) { + let newProgressValue = Float(newValue) + view?.setProgressValue(newProgressValue) + + let shouldHideProgress = shouldHideProgress(for: newProgressValue) + view?.setProgressHidden(shouldHideProgress) + } + + func shouldHideProgress(for value: Float) -> Bool { + abs(value - 1.0) <= 0.0001 + } + + func code(from url: URL) -> String? { + authHelper.code(from: url) + } +} diff --git a/ImageFeed/Auth/WebViewViewController.swift b/ImageFeed/Auth/WebViewViewController.swift new file mode 100644 index 0000000..f25f4f1 --- /dev/null +++ b/ImageFeed/Auth/WebViewViewController.swift @@ -0,0 +1,85 @@ +// +// WebViewViewController.swift +// ImageFeed +// +// Created by macOS on 29.12.2022. +// + +import UIKit +import WebKit + +public protocol WebViewViewControllerProtocol: AnyObject { + var presenter: WebViewPresenterProtocol? { get set } + func load(request: URLRequest) + func setProgressValue(_ newValue: Float) + func setProgressHidden(_ isHidden: Bool) +} + +final class WebViewViewController: UIViewController, WebViewViewControllerProtocol { + + @IBOutlet private weak var webView: WKWebView! + @IBOutlet private weak var progressView: UIProgressView! + + private var estimatedProgressObservation: NSKeyValueObservation? + weak var delegate: WebViewViewControllerDelegate? + var presenter: WebViewPresenterProtocol? + +// func configure(_ presenter: WebViewPresenterProtocol) { +// self.presenter = presenter +// self.presenter?.view = self +// } + + override func viewDidLoad() { + super.viewDidLoad() + webView.navigationDelegate = self + webView.accessibilityActivate() + // configure(WebViewPresenter(authHelper: AuthHelper())) + presenter?.viewDidLoad() + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == #keyPath(WKWebView.estimatedProgress) { + presenter?.didUpdateProgressValue(webView.estimatedProgress) + } else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + } + } + + func load(request: URLRequest) { + webView.load(request) + } + + func setProgressValue(_ newValue: Float) { + progressView.progress = newValue + } + + func setProgressHidden(_ isHidden: Bool) { + progressView.isHidden = isHidden + } + + @IBAction private func didTapBackButton(_ sender: Any) { + delegate?.webViewViewControllerDidCancel(self) + } +} + +extension WebViewViewController: WKNavigationDelegate { + func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + if let code = code(from: navigationAction) { + delegate?.webViewViewController(self, didAuthenticateWithCode: code) + decisionHandler(.cancel) + } else { + decisionHandler(.allow) + } + } + + private func code(from navigationAction: WKNavigationAction) -> String? { + if let url = navigationAction.request.url { + return presenter?.code(from: url) + } + return nil + } +} diff --git a/ImageFeed/Auth/WebViewViewControllerDelegate.swift b/ImageFeed/Auth/WebViewViewControllerDelegate.swift new file mode 100644 index 0000000..b8aef8f --- /dev/null +++ b/ImageFeed/Auth/WebViewViewControllerDelegate.swift @@ -0,0 +1,15 @@ +// +// WebViewViewControllerDelegate.swift +// ImageFeed +// +// Created by macOS on 30.12.2022. +// + +import Foundation + +protocol WebViewViewControllerDelegate: AnyObject { + + func webViewViewController(_ vc: WebViewViewController, didAuthenticateWithCode code: String) + + func webViewViewControllerDidCancel(_ vc: WebViewViewController) +} diff --git a/ImageFeed/AuthConfiguration.swift b/ImageFeed/AuthConfiguration.swift new file mode 100644 index 0000000..2a47d53 --- /dev/null +++ b/ImageFeed/AuthConfiguration.swift @@ -0,0 +1,44 @@ +// +// Constants.swift +// ImageFeed +// +// Created by macOS on 29.12.2022. +// + +import Foundation + +let AccessKey = "kLuw65wDZT845EyoYEfX2gWsq1ueUuX06gvSOEuPFT0" +let SecretKey = "b99lHBvBrUgVpF70lQFrvKJ3hyyCCQMA0tk28nVoTB4" +let RedirectURI = "urn:ietf:wg:oauth:2.0:oob" +let AccessScope = "public+read_user+write_likes" +let DefaultBaseURL = URL(string: "https://api.unsplash.com")! +let UnsplashAuthorizeURLString = "https://unsplash.com/oauth/authorize" + +struct AuthConfiguration { + let accessKey: String + let secretKey: String + let redirectURI: String + let accessScope: String + let defaultBaseURL: URL + let authURLString: String + + init(accessKey: String, secretKey: String, redirectURI: String, accessScope: String, authURLString: String, defaultBaseURL: URL) { + self.accessKey = accessKey + self.secretKey = secretKey + self.redirectURI = redirectURI + self.accessScope = accessScope + self.defaultBaseURL = defaultBaseURL + self.authURLString = authURLString + } + + static var standard: AuthConfiguration { + return AuthConfiguration(accessKey: AccessKey, + secretKey: SecretKey, + redirectURI: RedirectURI, + accessScope: AccessScope, + authURLString: UnsplashAuthorizeURLString, + defaultBaseURL: DefaultBaseURL) + } +} + +public struct VoidCodable: Codable {} diff --git a/ImageFeed/Base.lproj/Main.storyboard b/ImageFeed/Base.lproj/Main.storyboard index 849958b..47d424b 100644 --- a/ImageFeed/Base.lproj/Main.storyboard +++ b/ImageFeed/Base.lproj/Main.storyboard @@ -1,18 +1,19 @@ - + + - + @@ -28,11 +29,13 @@ - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + - + + + + + + + + + + diff --git a/ImageFeed/Extensions/URLSession.swift b/ImageFeed/Extensions/URLSession.swift new file mode 100644 index 0000000..190d580 --- /dev/null +++ b/ImageFeed/Extensions/URLSession.swift @@ -0,0 +1,47 @@ +// +// URLSession.swift +// ImageFeed +// +// Created by macOS on 02.01.2023. +// + +import Foundation + +extension URLSession { + + func objectTask( + for request: URLRequest, + completion: @escaping (Result) -> Void + ) -> URLSessionTask { + + let fulfillCompletionOnMainThread: (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 { + do { + let decoder = JSONDecoder() + let result = try decoder.decode(T.self, from: data) + + fulfillCompletionOnMainThread(.success(result)) + } catch { + fulfillCompletionOnMainThread(.failure(NetworkError.urlRequestError(error))) + } + } else { + fulfillCompletionOnMainThread(.failure(NetworkError.httpStatusCode(statusCode))) + } + } else if let error = error { + fulfillCompletionOnMainThread(.failure(NetworkError.urlRequestError(error))) + } else { + fulfillCompletionOnMainThread(.failure(NetworkError.urlSessionError)) + } + }) + task.resume() + return task + } +} diff --git a/ImageFeed/ImagesList/ImageListPresenter.swift b/ImageFeed/ImagesList/ImageListPresenter.swift new file mode 100644 index 0000000..6463bed --- /dev/null +++ b/ImageFeed/ImagesList/ImageListPresenter.swift @@ -0,0 +1,42 @@ +// +// ImageListPresenter.swift +// ImageFeed +// +// Created by macOS on 21.02.2023. +// + +import Foundation + +public protocol ImageListPresenterProtocol { + var view: ImagesListViewControllerProtocol? { get set } + var imagesListService: ImageListService { get set } + func viewDidLoad() + func fetchPhotosNextPage() +} + +final class ImageListPresenter: ImageListPresenterProtocol { + + weak var view: ImagesListViewControllerProtocol? + var imagesListService = ImageListService() + private var imageListServiceObserver: NSObjectProtocol? + + func viewDidLoad() { + imagesListService.fetchPhotosNextPage() + + imageListServiceObserver = NotificationCenter.default + .addObserver( + forName: ImageListService.didChangeNotification, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self = self else { return } + + let photos = self.imagesListService.photos + self.view?.updateTableViewAnimated(photos: photos) + } + } + + internal func fetchPhotosNextPage() { + imagesListService.fetchPhotosNextPage() + } +} diff --git a/ImageFeed/ImagesList/ImageListService.swift b/ImageFeed/ImagesList/ImageListService.swift new file mode 100644 index 0000000..0b2a75a --- /dev/null +++ b/ImageFeed/ImagesList/ImageListService.swift @@ -0,0 +1,110 @@ +// +// ImageListService.swift +// ImageFeed +// +// Created by macOS on 20.01.2023. +// + +import Foundation + +public final class ImageListService { + private (set) var photos: [Photo] = [] + static let didChangeNotification = Notification.Name(rawValue: "ImagesListServiceDidChange") + + private var lastLoadedPage: Int? + + private let urlSession = URLSession.shared + private var task: URLSessionTask? + + func fetchPhotosNextPage() { + let nextPage = lastLoadedPage == nil ? 1 : lastLoadedPage! + 1 + lastLoadedPage = (lastLoadedPage ?? 0) + 1 + + assert(Thread.isMainThread) + + task?.cancel() + + let request = makeRequest(path: "/photos?page=\(nextPage)", httpMethod: "GET", baseURL: DefaultBaseURL) + + let task = urlSession.objectTask(for: request) { [weak self] (result: Result, Error>) in + guard let self = self else { return } + switch result { + case .success(let photoResult): + print(photoResult) + for i in photoResult.indices { + let createdAt = photoResult[i].createdAt + let dateFormatter = ISO8601DateFormatter() + let date = dateFormatter.date(from: createdAt) + self.photos.append( + Photo( + id: photoResult[i].id, + size: CGSize(width: photoResult[i].width, height: photoResult[i].height), + createdAt: date, + welcomeDescription: photoResult[i].description, + thumbImageURL: photoResult[i].urls.thumb, + fullImageURL: photoResult[i].urls.full, + isLiked: photoResult[i].likedByUser)) + } + NotificationCenter.default + .post( + name: ImageListService.didChangeNotification, + object: self) + case .failure(let error): + print(error.localizedDescription) + } + } + + self.task = task + task.resume() + } + + func changeLike(photoId: String, isLike: Bool, _ completion: @escaping (Result) -> Void) { + assert(Thread.isMainThread) + task?.cancel() + + let request: URLRequest? + if isLike { + request = makeRequest(path: "/photos/\(photoId)/like", httpMethod: "POST", baseURL: DefaultBaseURL) + } else { + request = makeRequest(path: "/photos/\(photoId)/like", httpMethod: "DELETE", baseURL: DefaultBaseURL) + } + + let task = urlSession.objectTask(for: request!) { [weak self] (result: Result) in + guard let self = self else { return } + switch result { + case .success(_): + if let index = self.photos.firstIndex(where: { $0.id == photoId }) { + let photo = self.photos[index] + let newPhoto = Photo( + id: photo.id, + size: photo.size, + createdAt: photo.createdAt, + welcomeDescription: photo.welcomeDescription, + thumbImageURL: photo.thumbImageURL, + fullImageURL: photo.fullImageURL, + isLiked: !photo.isLiked + ) + self.photos[index] = newPhoto + completion(result) + } + case .failure(let error): + print(error.localizedDescription) + completion(result) + } + } + self.task = task + task.resume() + + } + + private func makeRequest( + path: String, + httpMethod: String, + baseURL: URL = DefaultBaseURL) -> URLRequest { + var request = URLRequest(url: URL(string: path, relativeTo: baseURL)!) + request.setValue("Bearer \(String(describing: OAuth2TokenStorage().token!))", forHTTPHeaderField: "Authorization") + request.httpMethod = httpMethod + + return request + } +} diff --git a/ImageFeed/ImagesList/ImagesListCell.swift b/ImageFeed/ImagesList/ImagesListCell.swift index 68706d2..421ef9d 100644 --- a/ImageFeed/ImagesList/ImagesListCell.swift +++ b/ImageFeed/ImagesList/ImagesListCell.swift @@ -6,10 +6,12 @@ // import UIKit +import Kingfisher -class ImagesListCell: UITableViewCell { +final class ImagesListCell: UITableViewCell { static let reuseIdentifier = "ImagesListCell" + weak var delegate: ImagesListCellDelegate? @IBOutlet weak var imageCell: UIImageView! @IBOutlet weak var favoriteButton: UIButton! @@ -17,14 +19,25 @@ class ImagesListCell: UITableViewCell { override func awakeFromNib() { super.awakeFromNib() - // Initialization code favoriteButton.setTitle("", for: .normal) } - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) + override func prepareForReuse() { + super.prepareForReuse() + + imageCell.kf.cancelDownloadTask() + } - // Configure the view for the selected state + @IBAction func likeButtonClicked(_ sender: Any) { + delegate?.imageListCellDidTapLike(self) + } + + func setIsLiked(isLiked: Bool) { + let imageLike = isLiked == true ? UIImage(named: "liked") : UIImage(named: "no liked") + favoriteButton.setImage(imageLike, for: .normal) } +} +protocol ImagesListCellDelegate: AnyObject { + func imageListCellDidTapLike(_ cell: ImagesListCell) } diff --git a/ImageFeed/ImagesList/ImagesListControllerViewController.swift b/ImageFeed/ImagesList/ImagesListControllerViewController.swift deleted file mode 100644 index 85c6ebb..0000000 --- a/ImageFeed/ImagesList/ImagesListControllerViewController.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// ViewController.swift -// ImageFeed -// -// Created by macOS on 26.11.2022. -// - -import UIKit - -class ImagesListViewController: UIViewController { - - @IBOutlet private var tableViewImage: UITableView! - - private var photosName = [String]() - - private lazy var dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .long - formatter.timeStyle = .none - return formatter - }() - - override func viewDidLoad() { - super.viewDidLoad() - - photosName = Array(0..<20).map{"\($0)"} - } -} - -extension ImagesListViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - - } -} - -extension ImagesListViewController: UITableViewDataSource { - - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return photosName.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - - let cell = tableView.dequeueReusableCell(withIdentifier: ImagesListCell.reuseIdentifier, for: indexPath) - - guard let imagesListCell = cell as? ImagesListCell else { - return UITableViewCell() - } - configCell(for: imagesListCell, with: indexPath) - - return imagesListCell - } - - func configCell(for cell: ImagesListCell, with indexPath: IndexPath) { - guard let image = UIImage(named: photosName[indexPath.row]) else { - return - } - cell.imageCell.image = image - cell.date.text = dateFormatter.string(from: Date()) - - if indexPath.row % 2 == 0 { - cell.favoriteButton.setImage(UIImage(named: "Active"), for: .normal) - } else{ - cell.favoriteButton.setImage(UIImage(named: "No Active"), for: .normal) - } - } -} - diff --git a/ImageFeed/ImagesList/ImagesListViewController.swift b/ImageFeed/ImagesList/ImagesListViewController.swift new file mode 100644 index 0000000..16ea4ab --- /dev/null +++ b/ImageFeed/ImagesList/ImagesListViewController.swift @@ -0,0 +1,173 @@ +// +// ViewController.swift +// ImageFeed +// +// Created by macOS on 26.11.2022. +// + +import UIKit +import Kingfisher + +public protocol ImagesListViewControllerProtocol: AnyObject { + var presenter: ImageListPresenterProtocol? { get set } + var photos: [Photo] { get set } + func updateTableViewAnimated(photos: [Photo]) +} + +final class ImagesListViewController: UIViewController, ImagesListViewControllerProtocol { + + @IBOutlet private weak var tableViewImage: UITableView! + + private let showSingleImageSegueIdentifier = "ShowSingleImage" + + var photos: [Photo] = [] + var presenter: ImageListPresenterProtocol? + private var imageListServiceObserver: NSObjectProtocol? + + func configure(_ presenter: ImageListPresenterProtocol) { + self.presenter = presenter + self.presenter?.view = self + } + + private lazy var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .long + formatter.timeStyle = .none + return formatter + }() + + override func viewDidLoad() { + super.viewDidLoad() + presenter?.viewDidLoad() + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == showSingleImageSegueIdentifier { + if let viewController = segue.destination as? SingleImageViewController { + if let indexPath = sender as? IndexPath { + let urlImage = URL(string: photos[indexPath.row].fullImageURL) + viewController.urlImage = urlImage + } + } + + } else { + super.prepare(for: segue, sender: sender) + } + } +} + +extension ImagesListViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + performSegue(withIdentifier: showSingleImageSegueIdentifier, sender: indexPath) + } +} + +extension ImagesListViewController: UITableViewDataSource { + + func updateTableViewAnimated(photos: [Photo]) { + let oldCount = self.photos.count + let newCount = photos.count + self.photos = photos + if oldCount != newCount { + tableViewImage.performBatchUpdates { + let indexPaths = (oldCount.. Int { + return photos.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let cell = tableView.dequeueReusableCell(withIdentifier: ImagesListCell.reuseIdentifier, for: indexPath) + + guard let imagesListCell = cell as? ImagesListCell else { + return UITableViewCell() + } + imagesListCell.delegate = self + configCell(for: imagesListCell, with: indexPath) + + return imagesListCell + } + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + if indexPath.row == photos.count-1 { + presenter?.fetchPhotosNextPage() + } + } + + func configCell(for cell: ImagesListCell, with indexPath: IndexPath) { + + cell.imageCell.image = UIImage(named: "plug") + + let gradientImage = CAGradientLayer() + cell.imageCell.layer.addSublayer(gradientImage) + + gradientImage.frame = CGRect(origin: .zero, size: CGSize(width: cell.imageCell.frame.width, height: cell.imageCell.frame.height)) + gradientImage.locations = [0, 0.1, 0.3] + gradientImage.colors = [ + UIColor(red: 0.682, green: 0.686, blue: 0.706, alpha: 1).cgColor, + UIColor(red: 0.531, green: 0.533, blue: 0.553, alpha: 1).cgColor, + UIColor(red: 0.431, green: 0.433, blue: 0.453, alpha: 1).cgColor + ] + gradientImage.startPoint = CGPoint(x: 0, y: 0.5) + gradientImage.endPoint = CGPoint(x: 1, y: 0.5) + gradientImage.cornerRadius = 16 + gradientImage.masksToBounds = true + + let gradientChangeAnimation = CABasicAnimation(keyPath: "locations") + gradientChangeAnimation.duration = 1.0 + gradientChangeAnimation.repeatCount = .infinity + gradientChangeAnimation.fromValue = [0, 0.1, 0.3] + gradientChangeAnimation.toValue = [0, 0.8, 1] + gradientImage.add(gradientChangeAnimation, forKey: "imageSkeleton") + + cell.imageCell.kf.indicatorType = .activity + let urlImage = URL(string: photos[indexPath.row].thumbImageURL) + + cell.imageCell.kf.setImage(with: urlImage, options: [.cacheSerializer(FormatIndicatedCacheSerializer.png)]) { _ in + gradientImage.removeFromSuperlayer() + self.tableViewImage.reloadRows(at: [indexPath], with: .automatic) + } + + cell.date.text = dateFormatter.string(from: photos[indexPath.row].createdAt ?? Date()) + } +} + +extension ImagesListViewController: ImagesListCellDelegate { + func imageListCellDidTapLike(_ cell: ImagesListCell) { + guard let indexPath = tableViewImage.indexPath(for: cell) else { return } + let photo = photos[indexPath.row] + + UIBlockingProgressHUD.show() + presenter?.imagesListService.changeLike(photoId: photo.id, isLike: !photo.isLiked) { result in + switch result { + case .success: + self.photos = self.presenter?.imagesListService.photos ?? [] + cell.setIsLiked(isLiked: !photo.isLiked) + UIBlockingProgressHUD.dismiss() + case .failure(let error): + UIBlockingProgressHUD.dismiss() + print(error) + self.showAlert() + } + } + } + + func showAlert() { + let alert = UIAlertController(title: "Упс, что-то пошло не так(", + message: "Не удалось лайкнуть картинку", + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: "OK", + style: .default, + handler: { _ in + })) + self.present(alert, animated: true, completion: nil) + } +} diff --git a/ImageFeed/ImagesList/Models/Photo.swift b/ImageFeed/ImagesList/Models/Photo.swift new file mode 100644 index 0000000..56ef308 --- /dev/null +++ b/ImageFeed/ImagesList/Models/Photo.swift @@ -0,0 +1,18 @@ +// +// Photo.swift +// ImageFeed +// +// Created by macOS on 20.01.2023. +// + +import Foundation + +public struct Photo { + let id: String + let size: CGSize + let createdAt: Date? + let welcomeDescription: String? + let thumbImageURL: String + let fullImageURL: String + let isLiked: Bool +} diff --git a/ImageFeed/ImagesList/Models/PhotoResult.swift b/ImageFeed/ImagesList/Models/PhotoResult.swift new file mode 100644 index 0000000..bfc84ad --- /dev/null +++ b/ImageFeed/ImagesList/Models/PhotoResult.swift @@ -0,0 +1,41 @@ +// +// PhotoResult.swift +// ImageFeed +// +// Created by Albert on 29.01.2023. +// + +import Foundation + +struct PhotoResult: Codable { + let id: String + let createdAt: String + let updatedAt: String + let width: Int + let height: Int + let color: String + let blurHash: String + let likes: Int + let likedByUser: Bool + let description: String? + let user: UserResult + let urls: UrlResult + + enum CodingKeys: String, CodingKey { + case createdAt = "created_at" + case updatedAt = "updated_at" + case blurHash = "blur_hash" + case likedByUser = "liked_by_user" + case description, user, urls, id, width, height, color, likes + } + +} + +struct UrlResult: Codable { + let raw: String + let full: String + let regular: String + let small: String + let thumb: String +} + diff --git a/ImageFeed/Profile/ProfileImageService.swift b/ImageFeed/Profile/ProfileImageService.swift new file mode 100644 index 0000000..ba61d26 --- /dev/null +++ b/ImageFeed/Profile/ProfileImageService.swift @@ -0,0 +1,72 @@ +// +// ProfileImageService.swift +// ImageFeed +// +// Created by macOS on 02.01.2023. +// + +import Foundation + +final class ProfileImageService { + + static let shared = ProfileImageService() + + static let didChangeNotification = Notification.Name(rawValue: "ProfileImageProviderDidChange") + + private(set) var avatarURL: String? + + private let urlSession = URLSession.shared + private var task: URLSessionTask? + + func fetchProfileImageURL(username: String, _ completion: @escaping (Result) -> Void) { + assert(Thread.isMainThread) + + task?.cancel() + + let request = makeRequest(path: "/users/\(username)", httpMethod: "GET", baseURL: DefaultBaseURL) + + let task = urlSession.objectTask(for: request) { [weak self] (result: Result) in + guard let self = self else { return } + switch result { + case .success(let body): + self.avatarURL = body.profileImage.small + NotificationCenter.default + .post( + name: ProfileImageService.didChangeNotification, + object: self, + userInfo: ["URL": self.avatarURL!]) + completion(.success(body.profileImage.small)) + case .failure(let error): + print(error) + completion(.failure(error)) + } + } + self.task = task + task.resume() + } + + private func makeRequest( + path: String, + httpMethod: String, + baseURL: URL = DefaultBaseURL) -> URLRequest { + var request = URLRequest(url: URL(string: path, relativeTo: baseURL)!) + request.setValue("Bearer \(String(describing: OAuth2TokenStorage().token!))", forHTTPHeaderField: "Authorization") + request.httpMethod = httpMethod + return request + } +} + +struct UserResult: Codable { + let profileImage: ImageResult + + enum CodingKeys: String, CodingKey { + case profileImage = "profile_image" + } +} + +struct ImageResult: Codable { + let small: String + let medium: String + let large: String +} + diff --git a/ImageFeed/Profile/ProfilePresenter.swift b/ImageFeed/Profile/ProfilePresenter.swift new file mode 100644 index 0000000..e93976e --- /dev/null +++ b/ImageFeed/Profile/ProfilePresenter.swift @@ -0,0 +1,52 @@ +// +// ProfilePresenter.swift +// ImageFeed +// +// Created by macOS on 21.02.2023. +// + +import Foundation + +public protocol ProfilePresenterProtocol { + var view: ProfileViewControllerProtocol? { get set } + func viewDidLoad() + func loadProfileImage() + var profileService: ProfileService { get set } +} + +final class ProfilePresenter: ProfilePresenterProtocol { + weak var view: ProfileViewControllerProtocol? + private var profileImageServiceObserver: NSObjectProtocol? + var profileService = ProfileService.shared + + func viewDidLoad() { + + if let profile = self.profileService.profile { + view?.updateProfileDetails(profile: profile) + } + + loadProfileImage() + } + + func loadProfileImage() { + guard + let profileImageURL = ProfileImageService.shared.avatarURL, + let url = URL(string: profileImageURL) + else { return } + + profileImageServiceObserver = NotificationCenter.default + .addObserver( + forName: ProfileImageService.didChangeNotification, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self = self else { return } + self.view?.updateAvatar(url: url) + if let profile = self.profileService.profile { + self.view?.updateProfileDetails(profile: profile) + } + } + + view?.updateAvatar(url: url) + } +} diff --git a/ImageFeed/Profile/ProfileService.swift b/ImageFeed/Profile/ProfileService.swift new file mode 100644 index 0000000..6070b66 --- /dev/null +++ b/ImageFeed/Profile/ProfileService.swift @@ -0,0 +1,71 @@ +// +// ProfileService.swift +// ImageFeed +// +// Created by macOS on 02.01.2023. +// + +import Foundation + +public final class ProfileService { + static let shared = ProfileService() + + private(set) var profile: Profile? + + private let urlSession = URLSession.shared + private var task: URLSessionTask? + + func fetchProfile(completion: @escaping (Result) -> Void) { + assert(Thread.isMainThread) + + task?.cancel() + + let request = makeRequest(path: "/me", httpMethod: "GET", baseURL: DefaultBaseURL) + + let task = urlSession.objectTask(for: request) { [weak self] (result: Result) in + guard let self = self else { return } + switch result { + case .success(let body): + self.profile = Profile(result: body) + completion(.success(body)) + case .failure(let error): + print(error) + completion(.failure(error)) + } + } + self.task = task + task.resume() + } + + private func makeRequest( + path: String, + httpMethod: String, + baseURL: URL = DefaultBaseURL) -> URLRequest { + + var request = URLRequest(url: URL(string: path, relativeTo: baseURL)!) + request.setValue("Bearer \(String(describing: OAuth2TokenStorage().token!))", forHTTPHeaderField: "Authorization") + request.httpMethod = httpMethod + return request + } +} + +struct ProfileResult: Codable { + let username: String + let first_name: String + let last_name: String + let bio: String? +} + +public struct Profile { + let username: String + let name: String + let loginName: String + let bio: String? + + init(result: ProfileResult) { + self.username = result.username + self.name = "\(result.first_name)" + " \(result.last_name)" + self.loginName = "@\(username)" + self.bio = result.bio + } +} diff --git a/ImageFeed/Profile/ProfileViewController.swift b/ImageFeed/Profile/ProfileViewController.swift new file mode 100644 index 0000000..6e699d8 --- /dev/null +++ b/ImageFeed/Profile/ProfileViewController.swift @@ -0,0 +1,167 @@ +// +// ProfileViewController.swift +// ImageFeed +// +// Created by Albert on 03.12.2022. +// + +import UIKit +import Kingfisher + +public protocol ProfileViewControllerProtocol: AnyObject { + var presenter: ProfilePresenterProtocol? { get set } + func updateAvatar(url: URL) + func updateProfileDetails(profile: Profile) +} + +final class ProfileViewController: UIViewController, ProfileViewControllerProtocol { + + private var image = UIImageView() + private var labelName = UILabel() + private var labelNickname = UILabel() + private var labelStatus = UILabel() + private let button = UIButton.systemButton(with: UIImage(systemName: "ipad.and.arrow.forward")!, target: ProfileViewController.self, action: nil) + + private let gradientImage = CAGradientLayer() + private let gradientLabelName = CAGradientLayer() + private let gradientLabelNickname = CAGradientLayer() + private let gradientLabelStatus = CAGradientLayer() + + var presenter: ProfilePresenterProtocol? + + override func viewDidLoad() { + super.viewDidLoad() + button.accessibilityIdentifier = "logoutButton" + button.addTarget(self, action: #selector(showExitAlert(sender:)), for: .touchUpInside) + setSkeleton() + addSubviews() + setViewConfiguration() + activateConstraints() + presenter?.viewDidLoad() + } + + internal func updateAvatar(url: URL) { + let processor = RoundCornerImageProcessor(cornerRadius: 35) + image.kf.setImage(with: url, options: [.processor(processor), .cacheSerializer(FormatIndicatedCacheSerializer.png)]) + } + + internal func updateProfileDetails(profile: Profile) { + self.labelName.text = profile.name + self.labelNickname.text = profile.loginName + self.labelStatus.text = profile.bio + endAnimateSkeleton() + } + + private func setSkeleton() { + image.layer.addSublayer(gradientImage) + labelName.layer.addSublayer(gradientLabelName) + labelNickname.layer.addSublayer(gradientLabelNickname) + labelStatus.layer.addSublayer(gradientLabelStatus) + + animateSkeleton(gradientName: gradientImage, size: CGSize(width: 70, height: 70), cornerRadius: 35, keyAnimation: "imageLocationChange") + animateSkeleton(gradientName: gradientLabelName, size: CGSize(width: 223, height: 18), cornerRadius: 9, keyAnimation: "labelNameLocationChange") + animateSkeleton(gradientName: gradientLabelNickname, size: CGSize(width: 89, height: 18), cornerRadius: 9, keyAnimation: "labelNicknameLocationChange") + animateSkeleton(gradientName: gradientLabelStatus, size: CGSize(width: 67, height: 18), cornerRadius: 9, keyAnimation: "labelStatusLocationChange") + } + + private func addSubviews() { + view.addSubview(image) + view.addSubview(labelName) + view.addSubview(labelNickname) + view.addSubview(labelStatus) + view.addSubview(button) + + image.translatesAutoresizingMaskIntoConstraints = false + labelName.translatesAutoresizingMaskIntoConstraints = false + labelNickname.translatesAutoresizingMaskIntoConstraints = false + labelStatus.translatesAutoresizingMaskIntoConstraints = false + button.translatesAutoresizingMaskIntoConstraints = false + } + + private func setViewConfiguration() { + labelName.textColor = UIColor(named: "YP White") + labelName.font = labelName.font.withSize(23) + + labelNickname.textColor = UIColor(named: "YP Grey") + labelNickname.font = labelNickname.font.withSize(13) + + labelStatus.textColor = UIColor(named: "YP White") + labelStatus.font = labelStatus.font.withSize(13) + + button.tintColor = UIColor(named: "YP Red") + } + + private func activateConstraints() { + NSLayoutConstraint.activate([ + image.heightAnchor.constraint(equalToConstant: 70), + image.widthAnchor.constraint(equalToConstant: 70), + image.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + image.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), + labelName.topAnchor.constraint(equalTo: image.bottomAnchor, constant: 8), + labelName.leadingAnchor.constraint(equalTo: image.leadingAnchor), + labelName.heightAnchor.constraint(equalToConstant: 25), + labelName.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + labelNickname.topAnchor.constraint(equalTo: labelName.bottomAnchor, constant: 8), + labelNickname.leadingAnchor.constraint(equalTo: labelName.leadingAnchor), + labelNickname.heightAnchor.constraint(equalToConstant: 18), + labelNickname.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + labelStatus.topAnchor.constraint(equalTo: labelNickname.bottomAnchor, constant: 8), + labelStatus.leadingAnchor.constraint(equalTo: labelName.leadingAnchor), + labelStatus.heightAnchor.constraint(equalToConstant: 18), + labelStatus.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + button.centerYAnchor.constraint(equalTo: image.centerYAnchor), + button.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20) + ]) + } + + private func animateSkeleton(gradientName: CAGradientLayer, size: CGSize, cornerRadius: CGFloat, keyAnimation: String) { + + gradientName.frame = CGRect(origin: .zero, size: size) + gradientName.locations = [0, 0.1, 0.3] + gradientName.colors = [ + UIColor(red: 0.682, green: 0.686, blue: 0.706, alpha: 1).cgColor, + UIColor(red: 0.531, green: 0.533, blue: 0.553, alpha: 1).cgColor, + UIColor(red: 0.431, green: 0.433, blue: 0.453, alpha: 1).cgColor + ] + gradientName.startPoint = CGPoint(x: 0, y: 0.5) + gradientName.endPoint = CGPoint(x: 1, y: 0.5) + gradientName.cornerRadius = cornerRadius + gradientName.masksToBounds = true + + let gradientChangeAnimation = CABasicAnimation(keyPath: "locations") + gradientChangeAnimation.duration = 1.0 + gradientChangeAnimation.repeatCount = .infinity + gradientChangeAnimation.fromValue = [0, 0.1, 0.3] + gradientChangeAnimation.toValue = [0, 0.8, 1] + gradientName.add(gradientChangeAnimation, forKey: keyAnimation) + } + + private func endAnimateSkeleton() { + gradientImage.removeFromSuperlayer() + gradientLabelStatus.removeFromSuperlayer() + gradientLabelName.removeFromSuperlayer() + gradientLabelNickname.removeFromSuperlayer() + } + + @objc func showExitAlert(sender: AnyObject) { + let alert = UIAlertController(title: "Пока, пока", + message: "Уверены, что хотите выйти?", + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: "Да", + style: .default, + handler: { _ in + OAuth2Service.shared.logout { + guard let window = UIApplication.shared.windows.first else { fatalError("Invalid Configuration") } + let splashViewController = SplashViewController() + window.rootViewController = splashViewController + } + })) + alert.addAction(UIAlertAction(title: "Нет", + style: .default, + handler: { _ in + alert.dismiss(animated: true) + })) + self.present(alert, animated: true, completion: nil) + } +} diff --git a/ImageFeed/SceneDelegate.swift b/ImageFeed/SceneDelegate.swift index 8d9eac7..c0bfc43 100644 --- a/ImageFeed/SceneDelegate.swift +++ b/ImageFeed/SceneDelegate.swift @@ -14,7 +14,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - guard let _ = (scene as? UIWindowScene) else { return } + guard let scene = (scene as? UIWindowScene) else { return } + window = UIWindow(windowScene: scene) + window?.rootViewController = SplashViewController() + window?.makeKeyAndVisible() } func sceneDidDisconnect(_ scene: UIScene) { diff --git a/ImageFeed/SingleImage/SingleImageViewController.swift b/ImageFeed/SingleImage/SingleImageViewController.swift new file mode 100644 index 0000000..3f45bbd --- /dev/null +++ b/ImageFeed/SingleImage/SingleImageViewController.swift @@ -0,0 +1,113 @@ +// +// SingleImageViewController.swift +// ImageFeed +// +// Created by Albert on 07.12.2022. +// + +import UIKit + +final class SingleImageViewController: UIViewController { + + + @IBOutlet private weak var buttonBack: UIButton! + @IBOutlet private weak var imageView: UIImageView! + @IBOutlet private weak var scrollView: UIScrollView! + + @IBOutlet private weak var sharingButton: UIButton! + + var urlImage: URL? + +// var image: UIImage! { +// didSet { +// guard isViewLoaded else { return } // 1 +// imageView.image = image // 2 +// rescaleAndCenterImageInScrollView(image: image) +// } +// } + + override func viewDidLoad() { + super.viewDidLoad() + buttonBack.setTitle("", for: .normal) + sharingButton.setTitle("", for: .normal) + + guard let urlImage = urlImage else { return } + setFullImage(url: urlImage) + + scrollView.minimumZoomScale = 0.1 + scrollView.maximumZoomScale = 1.25 + } + + override func viewWillAppear(_ animated: Bool) { + // rescaleAndCenterImageInScrollView(image: imageView.image!) + } + + @IBAction private func clickBackButton(_ sender: Any) { + self.dismiss(animated: true) + } + + @IBAction private func clickSharingButton(_ sender: Any) { + let share = UIActivityViewController( + activityItems: [imageView.image], + applicationActivities: nil + ) + present(share, animated: true, completion: nil) + } + + private func rescaleAndCenterImageInScrollView(image: UIImage) { + let minZoomScale = scrollView.minimumZoomScale + let maxZoomScale = scrollView.maximumZoomScale + view.layoutIfNeeded() + let visibleRectSize = scrollView.bounds.size + let imageSize = image.size + let hScale = visibleRectSize.width / imageSize.width + let vScale = visibleRectSize.height / imageSize.height + let scale = min(maxZoomScale, max(minZoomScale, max(hScale, vScale))) + scrollView.setZoomScale(scale, animated: false) + scrollView.layoutIfNeeded() + let newContentSize = scrollView.contentSize + let x = (newContentSize.width - visibleRectSize.width) / 2 + let y = (newContentSize.height - visibleRectSize.height) / 2 + scrollView.setContentOffset(CGPoint(x: x, y: y), animated: false) + } + + private func setFullImage(url: URL) { + UIBlockingProgressHUD.show() + imageView.kf.setImage(with: url) { [weak self] result in + UIBlockingProgressHUD.dismiss() + + guard let self = self else { return } + switch result { + case .success(let imageResult): + self.rescaleAndCenterImageInScrollView(image: imageResult.image) + case .failure: + self.showError() + } + } + } + + private func showError() { + let alert = UIAlertController(title: "Что-то пошло не так", + message: "Попробовать ещё раз?", + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: "Не надо", + style: .default, + handler: { _ in + })) + + alert.addAction(UIAlertAction(title: "Повторить ", + style: .default, + handler: { _ in + guard let urlImage = self.urlImage else { return } + self.setFullImage(url: urlImage) + })) + self.present(alert, animated: true, completion: nil) + } +} + +extension SingleImageViewController: UIScrollViewDelegate { + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + imageView + } +} diff --git a/ImageFeed/SplashViewController/SplashViewController.swift b/ImageFeed/SplashViewController/SplashViewController.swift new file mode 100644 index 0000000..1f4ab6b --- /dev/null +++ b/ImageFeed/SplashViewController/SplashViewController.swift @@ -0,0 +1,114 @@ +// +// SplashViewController.swift +// ImageFeed +// +// Created by macOS on 01.01.2023. +// + +import UIKit + +final class SplashViewController: UIViewController { + private let showAuthenticationScreenSegueIdentifier = "ShowAuthenticationScreen" + private let tabBarViewControllerIdentifier = "TabBarViewController" + + private let oauth2Service = OAuth2Service.shared + private let profileService = ProfileService.shared + + private let logoImage = UIImageView() + + override func viewDidLoad() { + setUI() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if oauth2Service.authToken != nil { + self.fetchProfile() + UIBlockingProgressHUD.show() + } else { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + let authViewController = storyboard.instantiateViewController(withIdentifier: "AuthViewController") as! AuthViewController + authViewController.delegate = self + authViewController.modalPresentationStyle = .fullScreen + self.present(authViewController, animated: true) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + setNeedsStatusBarAppearanceUpdate() + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + .lightContent + } + + func setUI() { + view.backgroundColor = UIColor(named: "YP Black") + logoImage.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(logoImage) + logoImage.image = UIImage(named: "Logo") + + NSLayoutConstraint.activate([ + logoImage.centerXAnchor.constraint(equalTo: view.centerXAnchor), + logoImage.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ]) + } + + private func switchToTabBarController() { + guard let window = UIApplication.shared.windows.first else { fatalError("Invalid Configuration") } + let tabBarController = UIStoryboard(name: "Main", bundle: .main) + .instantiateViewController(withIdentifier: tabBarViewControllerIdentifier) + window.rootViewController = tabBarController + } +} + +extension SplashViewController: AuthViewControllerDelegate { + func authViewController(_ vc: AuthViewController, didAuthenticateWithCode code: String) { + UIBlockingProgressHUD.show() + dismiss(animated: true) { [weak self] in + guard let self = self else { return } + self.fetchOAuthToken(code) + } + } + + private func fetchOAuthToken(_ code: String) { + oauth2Service.fetchOAuthToken(code) { [weak self] result in + guard let self = self else { return } + switch result { + case .success: + self.fetchProfile() + case .failure: + self.showAlert() + UIBlockingProgressHUD.dismiss() + } + } + } + + private func fetchProfile() { + profileService.fetchProfile { [weak self] result in + guard let self = self else { return } + switch result { + case .success: + ProfileImageService.shared.fetchProfileImageURL(username: self.profileService.profile?.username ?? "") { _ in } + self.switchToTabBarController() + case .failure: + self.showAlert() + } + UIBlockingProgressHUD.dismiss() + } + } + + func showAlert() { + let alert = UIAlertController(title: "Что-то пошло не так(", + message: "Не удалось войти в систему", + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: "OK", + style: .default, + handler: { _ in + })) + self.present(alert, animated: true, completion: nil) + } +} diff --git a/ImageFeed/TabBar/TabBarController.swift b/ImageFeed/TabBar/TabBarController.swift new file mode 100644 index 0000000..bbfbfbe --- /dev/null +++ b/ImageFeed/TabBar/TabBarController.swift @@ -0,0 +1,32 @@ +// +// TabBarController.swift +// ImageFeed +// +// Created by macOS on 20.01.2023. +// + +import UIKit + +final class TabBarController: UITabBarController { + + override func awakeFromNib() { + super.awakeFromNib() + let storyboard = UIStoryboard(name: "Main", bundle: .main) + + let imagesListViewController = storyboard.instantiateViewController(withIdentifier: "ImagesListViewController") as! ImagesListViewController + imagesListViewController.configure(ImageListPresenter()) + + let profileViewController = ProfileViewController() + let profilePresenter = ProfilePresenter() + profileViewController.presenter = profilePresenter + profilePresenter.view = profileViewController + + profileViewController.tabBarItem = UITabBarItem( + title: NSLocalizedString("Profile", comment: ""), + image: UIImage(named: "tab_profile_active"), + selectedImage: nil) + + self.viewControllers = [imagesListViewController, profileViewController] + } + +} diff --git a/ImageFeedTests/ImageListViewTests.swift b/ImageFeedTests/ImageListViewTests.swift new file mode 100644 index 0000000..9eeeaba --- /dev/null +++ b/ImageFeedTests/ImageListViewTests.swift @@ -0,0 +1,61 @@ +// +// ImageListViewTests.swift +// ImageFeedTests +// +// Created by macOS on 22.02.2023. +// + +import XCTest +@testable import ImageFeed + +final class ImageListViewTests: XCTestCase { + + func testViewControllerCallsViewDidLoad() { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + let viewController = storyboard.instantiateViewController(withIdentifier: "ImagesListViewController") as! ImagesListViewController + let presenter = ImageListPresenterSpy() + viewController.configure(presenter) + + _ = viewController.view + + XCTAssertTrue(presenter.viewDidLoadCalled) + } + + func testAddPhotos() { + let viewController = ImagesListViewControllerSpy() + let presenter = ImageListPresenter() + viewController.presenter = presenter + presenter.view = viewController + + presenter.viewDidLoad() + + let imagesListService = presenter.imagesListService + XCTAssertNotNil(imagesListService.photos) + } +} + +final class ImageListPresenterSpy: ImageListPresenterProtocol { + var viewDidLoadCalled: Bool = false + var view: ImagesListViewControllerProtocol? + var imagesListService: ImageListService = ImageListService() + + func viewDidLoad() { + viewDidLoadCalled = true + } + + func fetchPhotosNextPage() { + return + } +} + +final class ImagesListViewControllerSpy: ImagesListViewControllerProtocol { + var presenter: ImageListPresenterProtocol? + var updateTableViewAnimatedCalled = false + var photosAdded = false + + var photos: [Photo] = [] + + func updateTableViewAnimated(photos: [Photo]) { + updateTableViewAnimatedCalled = true + } +} diff --git a/ImageFeedTests/ProfileViewTests.swift b/ImageFeedTests/ProfileViewTests.swift new file mode 100644 index 0000000..66558cf --- /dev/null +++ b/ImageFeedTests/ProfileViewTests.swift @@ -0,0 +1,80 @@ +// +// ProfileViewTests.swift +// ImageFeedTests +// +// Created by macOS on 22.02.2023. +// + +import XCTest +@testable import ImageFeed + +final class ProfileViewTests: XCTestCase { + + func testViewControllerCallsViewDidLoad() { + let viewController = ProfileViewController() + let presenter = ProfilePresenterSpy() + viewController.presenter = presenter + presenter.view = viewController + + _ = viewController.view + + XCTAssertTrue(presenter.viewDidLoadCalled) + } + + func testPresenterCallsUpdates() { + let viewController = ProfileViewControllerSpy() + let presenter = ProfilePresenter() + viewController.presenter = presenter + presenter.view = viewController + + let expectation = expectation(description: "Test completed") + + ProfileService.shared.fetchProfile { result in + switch result { + case .success: + ProfileImageService.shared.fetchProfileImageURL(username: ProfileService.shared.profile?.username ?? "") { _ in + presenter.viewDidLoad() + + XCTAssertTrue(viewController.updateAvatarCalled) + XCTAssertTrue(viewController.updateProfileDetailsCalled) + + expectation.fulfill() + } + case .failure: + XCTFail() + } + } + + waitForExpectations(timeout: 10) + } +} + +final class ProfileViewControllerSpy: ProfileViewControllerProtocol { + var presenter: ImageFeed.ProfilePresenterProtocol? + + var updateAvatarCalled = false + var updateProfileDetailsCalled = false + + func updateAvatar(url: URL) { + updateAvatarCalled = true + } + + func updateProfileDetails(profile: ImageFeed.Profile) { + updateProfileDetailsCalled = true + } +} + +final class ProfilePresenterSpy: ProfilePresenterProtocol { + var viewDidLoadCalled: Bool = false + var view: ImageFeed.ProfileViewControllerProtocol? + + func viewDidLoad() { + viewDidLoadCalled = true + } + + func loadProfileImage() { + + } + + var profileService: ImageFeed.ProfileService = ImageFeed.ProfileService() +} diff --git a/ImageFeedTests/WebViewTests.swift b/ImageFeedTests/WebViewTests.swift new file mode 100644 index 0000000..571fb70 --- /dev/null +++ b/ImageFeedTests/WebViewTests.swift @@ -0,0 +1,116 @@ +// +// ImageFeedTests.swift +// ImageFeedTests +// +// Created by macOS on 20.02.2023. +// + +import XCTest +@testable import ImageFeed + +final class WebViewTests: XCTestCase { + + func testViewControllerCallsViewDidLoad() { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + let viewController = storyboard.instantiateViewController(withIdentifier: "WebViewViewController") as! WebViewViewController + let presenter = WebViewPresenterSpy() + viewController.presenter = presenter + presenter.view = viewController + + _ = viewController.view + + XCTAssertTrue(presenter.viewDidLoadCalled) + } + + func testPresenterCallsLoadRequest() { + let viewController = WebViewViewControllerSpy() + let authHelper = AuthHelper() + let presenter = WebViewPresenter(authHelper: authHelper) + viewController.presenter = presenter + presenter.view = viewController + + presenter.viewDidLoad() + + XCTAssertTrue(viewController.loadRequestCalled) + } + + func testProgressVisibleWhenLessThenOne() { + let authHelper = AuthHelper() + let presenter = WebViewPresenter(authHelper: authHelper) + let progress: Float = 0.6 + + let shouldHideProgress = presenter.shouldHideProgress(for: progress) + + XCTAssertFalse(shouldHideProgress) + } + + func testProgressHiddenWhenOne() { + let authHelper = AuthHelper() + let presenter = WebViewPresenter(authHelper: authHelper) + let progress: Float = 1.0 + + let shouldHideProgress = presenter.shouldHideProgress(for: progress) // return value verification + + XCTAssertTrue(shouldHideProgress) + } + + func testAuthHelperAuthURL() { + let configuration = AuthConfiguration.standard + let authHelper = AuthHelper(configuration: configuration) + + let url = authHelper.authURL() + let urlString = url.absoluteString + + XCTAssertTrue(urlString.contains(configuration.authURLString)) + XCTAssertTrue(urlString.contains(configuration.accessKey)) + XCTAssertTrue(urlString.contains(configuration.redirectURI)) + XCTAssertTrue(urlString.contains("code")) + XCTAssertTrue(urlString.contains(configuration.accessScope)) + } + + func testCodeFromURL() { + var urlComponents = URLComponents(string: "https://unsplash.com/oauth/authorize/native")! + urlComponents.queryItems = [URLQueryItem(name: "code", value: "test code")] + let url = urlComponents.url! + let authHelper = AuthHelper() + + let code = authHelper.code(from: url) + + XCTAssertEqual(code, "test code") + } +} + +final class WebViewPresenterSpy: WebViewPresenterProtocol { + var viewDidLoadCalled: Bool = false + var view: WebViewViewControllerProtocol? + + func viewDidLoad() { + viewDidLoadCalled = true + } + + func didUpdateProgressValue(_ newValue: Double) { + + } + + func code(from url: URL) -> String? { + return nil + } +} + +final class WebViewViewControllerSpy: WebViewViewControllerProtocol { + var presenter: ImageFeed.WebViewPresenterProtocol? + + var loadRequestCalled: Bool = false + + func load(request: URLRequest) { + loadRequestCalled = true + } + + func setProgressValue(_ newValue: Float) { + + } + + func setProgressHidden(_ isHidden: Bool) { + + } +} diff --git a/ImageFeedUITests/ImageFeedUITests.swift b/ImageFeedUITests/ImageFeedUITests.swift new file mode 100644 index 0000000..d38418b --- /dev/null +++ b/ImageFeedUITests/ImageFeedUITests.swift @@ -0,0 +1,81 @@ +// +// ImageFeedUITests.swift +// ImageFeedUITests +// +// Created by macOS on 15.03.2023. +// + +import XCTest + +final class ImageFeedUITests: XCTestCase { + + private let app = XCUIApplication() + + override func setUpWithError() throws { + continueAfterFailure = false + app.launch() + + } + + func testAuth() throws { + app.buttons["Authenticate"].tap() + let webView = app.webViews["UnsplashWebView"] + XCTAssertTrue(webView.waitForExistence(timeout: 5)) + + let loginTextField = webView.descendants(matching: .textField).element + XCTAssertTrue(loginTextField.waitForExistence(timeout: 5)) + loginTextField.tap() + loginTextField.typeText("course.ios@artsofte.ru") + webView.swipeUp() + + let passwordTextField = webView.descendants(matching: .secureTextField).element + XCTAssertTrue(passwordTextField.waitForExistence(timeout: 5)) + passwordTextField.tap() + passwordTextField.typeText("iosartsofte") + webView.swipeUp() + + webView.buttons["Login"].tap() + + let tablesQuery = app.tables + let cell = tablesQuery.children(matching: .cell).element(boundBy: 0) + XCTAssertTrue(cell.waitForExistence(timeout: 5)) + + print(app.debugDescription) + } + + func testFeed() throws { + let tablesQuery = app.tables + + let cell = tablesQuery.children(matching: .cell).element(boundBy: 0) + cell.swipeUp() + sleep(2) + + let cellToLike = tablesQuery.children(matching: .cell).element(boundBy: 1) + + cellToLike.buttons["likeButton"].tap() + sleep(2) + cellToLike.buttons["likeButton"].tap() + + cellToLike.tap() + + sleep(2) + + let image = app.scrollViews.images.element(boundBy: 0) + image.pinch(withScale: 3, velocity: 1) + image.pinch(withScale: 0.5, velocity: -1) + + let navBackButtonWhiteButton = app.buttons["navBackButton"] + navBackButtonWhiteButton.tap() + } + + func testProfile() throws { + app.tabBars.buttons.element(boundBy: 1).tap() + + XCTAssertTrue(app.staticTexts["Max Bayankin"].exists) + XCTAssertTrue(app.staticTexts["@maxcode2023"].exists) + + app.buttons["logoutButton"].tap() + + app.alerts["Пока, пока"].scrollViews.otherElements.buttons["Да"].tap() + } +} diff --git a/ImageFeedUITests/ImageFeedUITestsLaunchTests.swift b/ImageFeedUITests/ImageFeedUITestsLaunchTests.swift new file mode 100644 index 0000000..d3f18ed --- /dev/null +++ b/ImageFeedUITests/ImageFeedUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// ImageFeedUITestsLaunchTests.swift +// ImageFeedUITests +// +// Created by macOS on 15.03.2023. +// + +import XCTest + +final class ImageFeedUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +}