diff --git a/ImageFeed.xcodeproj/project.pbxproj b/ImageFeed.xcodeproj/project.pbxproj index c21b349..33b7035 100644 --- a/ImageFeed.xcodeproj/project.pbxproj +++ b/ImageFeed.xcodeproj/project.pbxproj @@ -7,6 +7,19 @@ 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 */; }; + 223FDB28295DD00800FDCAE1 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223FDB27295DD00800FDCAE1 /* Constants.swift */; }; + 223FDB2B295DD3AB00FDCAE1 /* WebViewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223FDB2A295DD3AB00FDCAE1 /* WebViewViewController.swift */; }; + 223FDB2D295DD49F00FDCAE1 /* AuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223FDB2C295DD49F00FDCAE1 /* AuthViewController.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 */; }; 50A856762931FDA700E476DF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A856752931FDA700E476DF /* AppDelegate.swift */; }; 50A856782931FDA700E476DF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A856772931FDA700E476DF /* SceneDelegate.swift */; }; @@ -14,9 +27,24 @@ 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 */; }; + 50EEBC02293B6E2B00E950B5 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EEBC01293B6E2B00E950B5 /* ProfileViewController.swift */; }; /* End PBXBuildFile 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 = ""; }; + 223FDB27295DD00800FDCAE1 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.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 = ""; }; + 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 = ""; }; 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 = ""; }; @@ -26,6 +54,9 @@ 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 = ""; }; + 50EEBC01293B6E2B00E950B5 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -33,12 +64,60 @@ 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 */, + ); + path = Auth; + 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 = ( @@ -67,6 +146,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,10 +159,29 @@ 50A8567E2931FDA800E476DF /* Assets.xcassets */, 50A856802931FDA800E476DF /* LaunchScreen.storyboard */, 50A856832931FDA800E476DF /* Info.plist */, + 223FDB27295DD00800FDCAE1 /* Constants.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 */, + ); + path = Profile; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -94,6 +198,11 @@ dependencies = ( ); name = ImageFeed; + packageProductDependencies = ( + 50B8D4342961F15500508CF6 /* ProgressHUD */, + 50397DAD296362FF0078D6BE /* Kingfisher */, + 50397DB029646CE00078D6BE /* SwiftKeychainWrapper */, + ); productName = ImageFeed; productReference = 50A856722931FDA700E476DF /* ImageFeed.app */; productType = "com.apple.product-type.application"; @@ -122,6 +231,11 @@ Base, ); mainGroup = 50A856692931FDA700E476DF; + packageReferences = ( + 50B8D4332961F15500508CF6 /* XCRemoteSwiftPackageReference "ProgressHUD" */, + 50397DAC296362FF0078D6BE /* XCRemoteSwiftPackageReference "Kingfisher" */, + 50397DAF29646CE00078D6BE /* XCRemoteSwiftPackageReference "SwiftKeychainWrapper" */, + ); productRefGroup = 50A856732931FDA700E476DF /* Products */; projectDirPath = ""; projectRoot = ""; @@ -150,9 +264,23 @@ buildActionMask = 2147483647; files = ( 50A8567A2931FDA700E476DF /* ImagesListControllerViewController.swift in Sources */, + 22227A9A295F1B6B0014DF14 /* WebViewViewControllerDelegate.swift in Sources */, + 50487E6529609E7700F41745 /* OAuth2Service.swift in Sources */, + 222804C8297A688400D6B54A /* TabBarController.swift in Sources */, + 50397DAB296348080078D6BE /* URLSession.swift in Sources */, 506BFC6E2933D6160058728A /* ImagesListCell.swift in Sources */, + 50EEBC02293B6E2B00E950B5 /* ProfileViewController.swift in Sources */, 50A856762931FDA700E476DF /* AppDelegate.swift in Sources */, + 50B8D4322961AC7200508CF6 /* SplashViewController.swift in Sources */, + 223FDB28295DD00800FDCAE1 /* Constants.swift in Sources */, + 5044C5CD2940FC4200E65D8F /* SingleImageViewController.swift in Sources */, + 50397DA82963119B0078D6BE /* ProfileImageService.swift in Sources */, + 223FDB2B295DD3AB00FDCAE1 /* WebViewViewController.swift in Sources */, + 223FDB2D295DD49F00FDCAE1 /* AuthViewController.swift in Sources */, + 50B8D42F29619E7800508CF6 /* OAuth2TokenStorage.swift in Sources */, 50A856782931FDA700E476DF /* SceneDelegate.swift in Sources */, + 50397DA42962CB750078D6BE /* UIBlockingProgressHUD.swift in Sources */, + 50397DA62962D2730078D6BE /* ProfileService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -376,6 +504,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..a0b1679 100644 --- a/ImageFeed/AppDelegate.swift +++ b/ImageFeed/AppDelegate.swift @@ -21,13 +21,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { 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/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/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/AuthViewController.swift b/ImageFeed/Auth/AuthViewController.swift new file mode 100644 index 0000000..fbb293c --- /dev/null +++ b/ImageFeed/Auth/AuthViewController.swift @@ -0,0 +1,40 @@ +// +// AuthViewController.swift +// ImageFeed +// +// Created by macOS on 29.12.2022. +// + +import UIKit + +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)") } + 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..2453433 --- /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" +} + +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..1bb1fce --- /dev/null +++ b/ImageFeed/Auth/OAuth2Service.swift @@ -0,0 +1,83 @@ +// +// OAuth2Service.swift +// ImageFeed +// +// Created by macOS on 31.12.2022. +// + + +import Foundation + +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 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") + ] + let url = urlComponents.url! + + 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..f42b756 --- /dev/null +++ b/ImageFeed/Auth/UIBlockingProgressHUD.swift @@ -0,0 +1,26 @@ +// +// UIBlockingProgressHUD.swift +// ImageFeed +// +// Created by macOS on 02.01.2023. +// + +import Foundation +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/WebViewViewController.swift b/ImageFeed/Auth/WebViewViewController.swift new file mode 100644 index 0000000..18fb22f --- /dev/null +++ b/ImageFeed/Auth/WebViewViewController.swift @@ -0,0 +1,94 @@ +// +// WebViewViewController.swift +// ImageFeed +// +// Created by macOS on 29.12.2022. +// + +import UIKit +import WebKit + +fileprivate let UnsplashAuthorizeURLString = "https://unsplash.com/oauth/authorize" + +final class WebViewViewController: UIViewController { + + @IBOutlet private weak var webView: WKWebView! + @IBOutlet weak var progressView: UIProgressView! + + weak var delegate: WebViewViewControllerDelegate? + + private var estimatedProgressObservation: NSKeyValueObservation? + + override func viewDidLoad() { + super.viewDidLoad() + + webView.navigationDelegate = self + + var urlComponents = URLComponents(string: UnsplashAuthorizeURLString)! + urlComponents.queryItems = [ + URLQueryItem(name: "client_id", value: AccessKey), + URLQueryItem(name: "redirect_uri", value: RedirectURI), + URLQueryItem(name: "response_type", value: "code"), + URLQueryItem(name: "scope", value: AccessScope) + ] + let url = urlComponents.url! + + let request = URLRequest(url: url) + webView.load(request) + + estimatedProgressObservation = webView.observe( + \.estimatedProgress, + options: [], + changeHandler: { [weak self] _, _ in + guard let self = self else { return } + self.updateProgress() + }) + + updateProgress() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + updateProgress() + } + + + private func updateProgress() { + progressView.progress = Float(webView.estimatedProgress) + progressView.isHidden = fabs(webView.estimatedProgress - 1.0) <= 0.0001 + } + + @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, + 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/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/Base.lproj/Main.storyboard b/ImageFeed/Base.lproj/Main.storyboard index 849958b..1ca3c5c 100644 --- a/ImageFeed/Base.lproj/Main.storyboard +++ b/ImageFeed/Base.lproj/Main.storyboard @@ -1,18 +1,19 @@ - + + - + @@ -40,7 +41,7 @@ - + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + - + + + + + + + diff --git a/ImageFeed/Constants.swift b/ImageFeed/Constants.swift new file mode 100644 index 0000000..07164b1 --- /dev/null +++ b/ImageFeed/Constants.swift @@ -0,0 +1,14 @@ +// +// 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")! 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/ImagesListControllerViewController.swift b/ImageFeed/ImagesList/ImagesListControllerViewController.swift index 85c6ebb..1c513e4 100644 --- a/ImageFeed/ImagesList/ImagesListControllerViewController.swift +++ b/ImageFeed/ImagesList/ImagesListControllerViewController.swift @@ -12,6 +12,7 @@ class ImagesListViewController: UIViewController { @IBOutlet private var tableViewImage: UITableView! private var photosName = [String]() + private let showSingleImageSegueIdentifier = "ShowSingleImage" private lazy var dateFormatter: DateFormatter = { let formatter = DateFormatter() @@ -25,11 +26,25 @@ class ImagesListViewController: UIViewController { photosName = Array(0..<20).map{"\($0)"} } + + 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 image = UIImage(named: photosName[indexPath.row]) + viewController.image = image + } + } + + } else { + super.prepare(for: segue, sender: sender) + } + } } extension ImagesListViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - + performSegue(withIdentifier: showSingleImageSegueIdentifier, sender: indexPath) } } @@ -60,10 +75,11 @@ extension ImagesListViewController: UITableViewDataSource { cell.date.text = dateFormatter.string(from: Date()) if indexPath.row % 2 == 0 { - cell.favoriteButton.setImage(UIImage(named: "Active"), for: .normal) + cell.favoriteButton.setImage(UIImage(named: "liked"), for: .normal) } else{ - cell.favoriteButton.setImage(UIImage(named: "No Active"), for: .normal) + cell.favoriteButton.setImage(UIImage(named: "no liked"), for: .normal) } } } + diff --git a/ImageFeed/Profile/ProfileImageService.swift b/ImageFeed/Profile/ProfileImageService.swift new file mode 100644 index 0000000..e36ee8b --- /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 + completion(.success(body.profileImage.small)) + NotificationCenter.default + .post( + name: ProfileImageService.didChangeNotification, + object: self, + userInfo: ["URL": self.avatarURL!]) + 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/ProfileService.swift b/ImageFeed/Profile/ProfileService.swift new file mode 100644 index 0000000..7c02766 --- /dev/null +++ b/ImageFeed/Profile/ProfileService.swift @@ -0,0 +1,71 @@ +// +// ProfileService.swift +// ImageFeed +// +// Created by macOS on 02.01.2023. +// + +import Foundation + +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? +} + +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..c8fed97 --- /dev/null +++ b/ImageFeed/Profile/ProfileViewController.swift @@ -0,0 +1,103 @@ +// +// ProfileViewController.swift +// ImageFeed +// +// Created by Albert on 03.12.2022. +// + +import UIKit +import Kingfisher + +class ProfileViewController: UIViewController { + + 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 profileService = ProfileService.shared + private var profileImageServiceObserver: NSObjectProtocol? + + override func viewDidLoad() { + super.viewDidLoad() + + if let profile = profileService.profile { + updateProfileDetails(profile: profile) + } + + profileImageServiceObserver = NotificationCenter.default + .addObserver( + forName: ProfileImageService.didChangeNotification, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self = self else { return } + self.updateAvatar() + } + updateAvatar() + + addSubviews() + setViewConfiguration() + activateConstraints() + } + + private func updateAvatar() { + guard + let profileImageURL = ProfileImageService.shared.avatarURL, + let url = URL(string: profileImageURL) + else { return } + + let processor = RoundCornerImageProcessor(cornerRadius: 35) + image.kf.setImage(with: url, options: [.processor(processor), .cacheSerializer(FormatIndicatedCacheSerializer.png)]) + } + + private func updateProfileDetails(profile: Profile) { + self.labelName.text = profile.name + self.labelNickname.text = profile.loginName + self.labelStatus.text = profile.bio + } + + 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), + labelNickname.topAnchor.constraint(equalTo: labelName.bottomAnchor, constant: 8), + labelNickname.leadingAnchor.constraint(equalTo: labelName.leadingAnchor), + labelStatus.topAnchor.constraint(equalTo: labelNickname.bottomAnchor, constant: 8), + labelStatus.leadingAnchor.constraint(equalTo: labelName.leadingAnchor), + button.centerYAnchor.constraint(equalTo: image.centerYAnchor), + button.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20) + ]) + } +} 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..c4561b9 --- /dev/null +++ b/ImageFeed/SingleImage/SingleImageViewController.swift @@ -0,0 +1,71 @@ +// +// SingleImageViewController.swift +// ImageFeed +// +// Created by Albert on 07.12.2022. +// + +import UIKit + +class SingleImageViewController: UIViewController { + + + @IBOutlet weak private var buttonBack: UIButton! + @IBOutlet weak private var imageView: UIImageView! + @IBOutlet weak private var scrollView: UIScrollView! + + @IBOutlet weak var sharingButton: UIButton! + + 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) + imageView.image = image + rescaleAndCenterImageInScrollView(image: image) + scrollView.minimumZoomScale = 0.1 + scrollView.maximumZoomScale = 1.25 + } + + @IBAction private func clickBackButton(_ sender: Any) { + self.dismiss(animated: true) + } + + 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) + } + + @IBAction private func clickSharingButton(_ sender: Any) { + let share = UIActivityViewController( + activityItems: [image], + applicationActivities: nil + ) + present(share, 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..3f2e23e --- /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 authViewController = 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 } + UIBlockingProgressHUD.dismiss() + self.switchToTabBarController() + case .failure: + UIBlockingProgressHUD.dismiss() + 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/TabBar/TabBarController.swift b/ImageFeed/TabBar/TabBarController.swift new file mode 100644 index 0000000..8111eeb --- /dev/null +++ b/ImageFeed/TabBar/TabBarController.swift @@ -0,0 +1,27 @@ +// +// 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") + + let profileViewController = ProfileViewController() + profileViewController.tabBarItem = UITabBarItem( + title: NSLocalizedString("Profile", comment: ""), + image: UIImage(named: "tab_profile_active"), + selectedImage: nil) + + self.viewControllers = [imagesListViewController, profileViewController] + } + +}