Repository: GBCreat0r/ImageFeed
Pull request #3: Сделал авторизацию и аутентификацию и добавлен SplashView
Author: login: GBC-SR
Created: 2025-04-08T10:40:16Z | Merged: 2025-04-11T17:19:01Z



Diff:
diff --git a/Image Feed.xcodeproj/project.pbxproj b/Image Feed.xcodeproj/project.pbxproj
index 07e4d3b..1b80e37 100644
--- a/Image Feed.xcodeproj/project.pbxproj	
+++ b/Image Feed.xcodeproj/project.pbxproj	
@@ -29,6 +29,8 @@
 /* End PBXFileReference section */
 
 /* Begin PBXFileSystemSynchronizedRootGroup section */
+		CD6134062D941F170039AD5D /* Auth */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Auth; sourceTree = "<group>"; };
+		CD61345E2DA52A130039AD5D /* SplashView */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SplashView; sourceTree = "<group>"; };
 		CDF7370E2D7EEB69006C7E0A /* Service */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Service; sourceTree = "<group>"; };
 		CDF7371D2D8ABE83006C7E0A /* SingleImage */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SingleImage; sourceTree = "<group>"; };
 		CDF7371E2D8ABEA2006C7E0A /* Profile */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Profile; sourceTree = "<group>"; };
@@ -64,6 +66,8 @@
 		7D9CFA4128DDEE7F00D971A7 /* Image Feed */ = {
 			isa = PBXGroup;
 			children = (
+				CD61345E2DA52A130039AD5D /* SplashView */,
+				CD6134062D941F170039AD5D /* Auth */,
 				CDF7371E2D8ABEA2006C7E0A /* Profile */,
 				CDF7371D2D8ABE83006C7E0A /* SingleImage */,
 				7D9CFA5A28DDF66900D971A7 /* ImagesList */,
@@ -103,6 +107,8 @@
 			dependencies = (
 			);
 			fileSystemSynchronizedGroups = (
+				CD6134062D941F170039AD5D /* Auth */,
+				CD61345E2DA52A130039AD5D /* SplashView */,
 				CDF7370E2D7EEB69006C7E0A /* Service */,
 				CDF7371D2D8ABE83006C7E0A /* SingleImage */,
 				CDF7371E2D8ABEA2006C7E0A /* Profile */,
diff --git a/Image Feed/Assets.xcassets/Active.imageset/Active.png b/Image Feed/Assets.xcassets/Active.imageset/Active.png
new file mode 100644
index 0000000..a2f07f6
Binary files /dev/null and b/Image Feed/Assets.xcassets/Active.imageset/Active.png differ
diff --git a/Image Feed/Assets.xcassets/Active.imageset/Contents.json b/Image Feed/Assets.xcassets/Active.imageset/Contents.json
new file mode 100644
index 0000000..0fe7a56
--- /dev/null
+++ b/Image Feed/Assets.xcassets/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/Image Feed/Assets.xcassets/Logo_of_Unsplash.imageset/Contents.json b/Image Feed/Assets.xcassets/Logo_of_Unsplash.imageset/Contents.json
new file mode 100644
index 0000000..51a1070
--- /dev/null
+++ b/Image Feed/Assets.xcassets/Logo_of_Unsplash.imageset/Contents.json	
@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "Logo_of_Unsplash.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/Image Feed/Assets.xcassets/Logo_of_Unsplash.imageset/Logo_of_Unsplash.png b/Image Feed/Assets.xcassets/Logo_of_Unsplash.imageset/Logo_of_Unsplash.png
new file mode 100644
index 0000000..385d328
Binary files /dev/null and b/Image Feed/Assets.xcassets/Logo_of_Unsplash.imageset/Logo_of_Unsplash.png differ
diff --git a/Image Feed/Assets.xcassets/nav_back_button.imageset/Backward.png b/Image Feed/Assets.xcassets/nav_back_button.imageset/Backward.png
new file mode 100644
index 0000000..73bd422
Binary files /dev/null and b/Image Feed/Assets.xcassets/nav_back_button.imageset/Backward.png differ
diff --git a/Image Feed/Assets.xcassets/nav_back_button.imageset/Contents.json b/Image Feed/Assets.xcassets/nav_back_button.imageset/Contents.json
new file mode 100644
index 0000000..a410a9b
--- /dev/null
+++ b/Image Feed/Assets.xcassets/nav_back_button.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/Image Feed/Auth/AuthViewController.swift b/Image Feed/Auth/AuthViewController.swift
new file mode 100644
index 0000000..e3b5c5c
--- /dev/null
+++ b/Image Feed/Auth/AuthViewController.swift	
@@ -0,0 +1,67 @@
+//
+//  AuthViewController.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 25.03.2025.
+//
+
+import UIKit
+
+protocol AuthViewControllerDelegate: AnyObject {
+    func didAuthenticate(_ vc: AuthViewController)
+}
+
+final class AuthViewController: UIViewController {
+    let showWebViewSegueIdentifier = "showWebView"
+    weak var delegate: AuthViewControllerDelegate?
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        configureBackButton()
+    }
+    
+    private func  configureBackButton() {
+        navigationController?.navigationBar.backIndicatorImage = UIImage(named: "nav_back_button")
+        navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "nav_back_button")
+        navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
+        navigationItem.backBarButtonItem?.tintColor = #colorLiteral(red: 0.1019607843, green: 0.1058823529, blue: 0.1333333333, alpha: 1)
+    }
+    
+    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+        super.prepare(for: segue, sender: sender)
+        
+        guard segue.identifier == showWebViewSegueIdentifier,
+              let webViewViewController = segue.destination as? WebViewViewController else {
+            assertionFailure("Failed to prepare for \(String(describing: segue.identifier))")
+            return
+        }
+        segue.destination.modalPresentationStyle = .fullScreen
+        webViewViewController.delegate = self
+    }
+}
+
+extension AuthViewController: WebViewViewControllerDelegate {
+    func webViewViewControllerDidCancel(_ vc: WebViewViewController) {
+        vc.dismiss(animated: true)
+    }
+    
+    func webViewViewController(_ vc: WebViewViewController, didAuthenticateWithCode code: String) {
+        vc.dismiss(animated: true)
+        OAuth2Service.shared.fetchOAuthToken(code: code) { [weak self] result in
+            guard let self else {
+                print("Error: AuthViewController deallocated")
+                return
+            }
+            switch result {
+            case .success(let oAuthTokenResponse):
+                let token = OAuth2TokenStorage()
+                token.token = oAuthTokenResponse.accessToken
+                delegate?.didAuthenticate(self)
+                print("Token saved successfully.")
+                
+            case .failure(let error):
+                print("Failed to fetch OAuth token: \(error)")
+            }
+        }
+    }
+}
diff --git a/Image Feed/Auth/Constants.swift b/Image Feed/Auth/Constants.swift
new file mode 100644
index 0000000..52f0706
--- /dev/null
+++ b/Image Feed/Auth/Constants.swift	
@@ -0,0 +1,16 @@
+//
+//  Constants.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 25.03.2025.
+//
+
+import Foundation
+
+enum Constants {
+    static let accessKey = "Tm0f7JJO27QSk2C4V9Z5crBkowECKdDoZyOfmTW7klI"
+    static let secretKey = "BTMPNrX01IvyyQ7Y9TKH-Yz8kHjQ4QfUcwFJQhuiYTw"
+    static let redirectURI = "urn:ietf:wg:oauth:2.0:oob"
+    static let accessScope = "public+read_user+write_likes"
+    static var defaultBaseURL = URL(string: "https://api.unsplash.com") ?? nil
+}
diff --git a/Image Feed/Auth/OAuth2Service.swift b/Image Feed/Auth/OAuth2Service.swift
new file mode 100644
index 0000000..2e81eb3
--- /dev/null
+++ b/Image Feed/Auth/OAuth2Service.swift	
@@ -0,0 +1,95 @@
+//
+//  OAuth2Service.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 07.04.2025.
+//
+
+import UIKit
+
+protocol OAuth2ServiceProtocol {
+    func make0AuthTokenRequest(code: String) -> URLRequest?
+    func fetchOAuthToken(code: String, completion: @escaping (Result<OAuthTokenResponseBody, Error>) -> Void)
+}
+
+final class OAuth2Service: OAuth2ServiceProtocol {
+    static let shared = OAuth2Service()
+    private init() {}
+    
+    private enum NetworkError: Error {
+        case codeError
+    }
+    func make0AuthTokenRequest(code: String) -> URLRequest? {
+        guard var urlComponents = URLComponents(string: "https://unsplash.com/oauth/token")
+        else { return nil }
+        urlComponents.queryItems = [
+            URLQueryItem(name: "client_id", value: Constants.accessKey),
+            URLQueryItem(name: "client_secret", value: Constants.secretKey),
+            URLQueryItem(name: "redirect_uri", value: Constants.redirectURI),
+            URLQueryItem(name: "code", value: code),
+            URLQueryItem(name: "grant_type", value: "authorization_code")
+        ]
+        
+        guard let url = urlComponents.url else {
+            print("Error: Failed to create URL")
+            return nil
+        }
+        var request = URLRequest(url: url)
+        request.httpMethod = "POST"
+        return request
+    }
+    
+    func fetchOAuthToken(code: String, completion: @escaping (Result<OAuthTokenResponseBody, Error>) -> Void) {
+        guard let request = make0AuthTokenRequest(code: code) else {
+            DispatchQueue.main.async {
+                completion(.failure(NetworkError.codeError))
+            }
+            return
+        }
+
+        let task = URLSession.shared.dataTask(with: request) { data, response, error in
+            if let error = error {
+                print("Network request failed with error: \(error.localizedDescription)")
+                DispatchQueue.main.async {
+                    completion(.failure(error))
+                }
+                return
+            }
+
+            guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
+                if let httpResponse = response as? HTTPURLResponse {
+                    print("Server returned status code \(httpResponse.statusCode)")
+                } else {
+                    print("Failed to cast response to HTTPURLResponse")
+                }
+                
+                DispatchQueue.main.async {
+                    completion(.failure(NetworkError.codeError))
+                }
+                return
+            }
+
+            guard let data = data else {
+                print("Received no data in response")
+                DispatchQueue.main.async {
+                    completion(.failure(NetworkError.codeError))
+                }
+                return
+            }
+
+            do {
+                let decoder = JSONDecoder()
+                let oauthTokenResponse = try decoder.decode(OAuthTokenResponseBody.self, from: data)
+                DispatchQueue.main.async {
+                    completion(.success(oauthTokenResponse))
+                }
+            } catch {
+                print("Failed to decode JSON: \(error.localizedDescription)")
+                DispatchQueue.main.async {
+                    completion(.failure(error))
+                }
+            }
+        }
+        task.resume()
+    }
+}
diff --git a/Image Feed/Auth/OAuth2TokenStorage.swift b/Image Feed/Auth/OAuth2TokenStorage.swift
new file mode 100644
index 0000000..b7d3435
--- /dev/null
+++ b/Image Feed/Auth/OAuth2TokenStorage.swift	
@@ -0,0 +1,15 @@
+//
+//  OAuth2TokenStorage.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 07.04.2025.
+//
+
+import Foundation
+
+final class OAuth2TokenStorage {
+    var token: String? {
+        get {UserDefaults.standard.string(forKey: "bearerToken")}
+        set {UserDefaults.standard.set(newValue, forKey:"bearerToken")}
+    }
+}
diff --git a/Image Feed/Auth/OAuthTokenResponseBody.swift b/Image Feed/Auth/OAuthTokenResponseBody.swift
new file mode 100644
index 0000000..b0b4df5
--- /dev/null
+++ b/Image Feed/Auth/OAuthTokenResponseBody.swift	
@@ -0,0 +1,23 @@
+//
+//  OAuthTokenResponseBody.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 07.04.2025.
+//
+
+import UIKit
+
+
+struct OAuthTokenResponseBody: Decodable {
+    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/Image Feed/Auth/WebViewViewController.swift b/Image Feed/Auth/WebViewViewController.swift
new file mode 100644
index 0000000..200a46d
--- /dev/null
+++ b/Image Feed/Auth/WebViewViewController.swift	
@@ -0,0 +1,99 @@
+//
+//  WebViewViewController.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 26.03.2025.
+//
+
+import UIKit
+import WebKit
+
+protocol WebViewViewControllerDelegate: AnyObject {
+    func webViewViewController(_ vc: WebViewViewController, didAuthenticateWithCode code: String)
+    
+    func webViewViewControllerDidCancel(_ vc: WebViewViewController)
+}
+
+final class WebViewViewController: UIViewController {
+    
+    @IBOutlet private weak var progressView: UIProgressView!
+    @IBOutlet private weak var webView: WKWebView!
+    weak var delegate: WebViewViewControllerDelegate?
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        webView.navigationDelegate = self
+        if let request = makeRequest() {
+            webView.load(request)
+        }
+        updateProgress()
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        webView.addObserver(self,
+                            forKeyPath: #keyPath(WKWebView.estimatedProgress),
+                            options: .new,
+                            context: nil)
+    }
+    
+    override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+        webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), context: nil)
+    }
+    
+    override func observeValue(
+        forKeyPath keyPath: String?,
+        of object: Any?,
+        change: [NSKeyValueChangeKey : Any]?,
+        context: UnsafeMutableRawPointer?
+    ) {
+        if keyPath == #keyPath(WKWebView.estimatedProgress) {
+            updateProgress()
+        } else {
+            super.observeValue(forKeyPath: keyPath,
+                               of: object,
+                               change: change,
+                               context: context)
+        }
+    }
+    
+    private func updateProgress() {
+        progressView.progress = Float(webView.estimatedProgress)
+        progressView.isHidden = fabs(webView.estimatedProgress - 1.0) <= 0.0001
+    }
+    
+    func makeRequest() -> URLRequest? {
+        guard var uRLComponents = URLComponents(string: "https://unsplash.com/oauth/authorize") else { return nil }
+        
+        let queryItems: [URLQueryItem] = [
+            URLQueryItem(name: "client_id", value: Constants.accessKey),
+            URLQueryItem(name: "redirect_uri", value: Constants.redirectURI),
+            URLQueryItem(name: "response_type", value: "code"),
+            URLQueryItem(name: "scope", value: Constants.accessScope)
+        ]
+        
+        uRLComponents.queryItems = queryItems
+        guard let url = uRLComponents.url else { return nil }
+        return .init(url: url)
+    }
+}
+
+extension WebViewViewController: WKNavigationDelegate {
+    func webView(_ webView: WKWebView,
+                 decidePolicyFor navigationAction: WKNavigationAction,
+                 decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) {
+        guard
+            let url = navigationAction.request.url,
+            let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false),
+            let code = urlComponents.queryItems?.first(where: { item in item.name == "code"})?.value
+        else {
+            print ("No key value")
+            return decisionHandler(.allow)
+        }
+        print(">>>>", code)
+        guard let delegate else { print("23232"); return}
+        delegate.webViewViewController(self, didAuthenticateWithCode: code)
+        decisionHandler(.cancel)
+    }
+}
diff --git a/Image Feed/Base.lproj/Main.storyboard b/Image Feed/Base.lproj/Main.storyboard
index 0eb3c42..7b6ebb4 100644
--- a/Image Feed/Base.lproj/Main.storyboard	
+++ b/Image Feed/Base.lproj/Main.storyboard	
@@ -1,17 +1,18 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="yFP-Rc-xcZ">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="QDy-R4-ped">
     <device id="retina6_1" orientation="portrait" appearance="light"/>
     <dependencies>
         <deployment version="5120" identifier="iOS"/>
         <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="System colors in document resources" minToolsVersion="11.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
     <scenes>
         <!--Tab Bar Controller-->
         <scene sceneID="JJl-qI-nX2">
             <objects>
-                <tabBarController id="yFP-Rc-xcZ" sceneMemberID="viewController">
+                <tabBarController storyboardIdentifier="TabBarViewController" id="yFP-Rc-xcZ" sceneMemberID="viewController">
                     <tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="hUU-My-CaX">
                         <rect key="frame" x="0.0" y="0.0" width="393" height="49"/>
                         <autoresizingMask key="autoresizingMask"/>
@@ -28,7 +29,7 @@
                 </tabBarController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="mAY-Ou-VdB" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
             </objects>
-            <point key="canvasLocation" x="-946" y="110"/>
+            <point key="canvasLocation" x="-993" y="544"/>
         </scene>
         <!--ImageList-->
         <scene sceneID="tne-QT-ifu">
@@ -203,7 +204,39 @@
                 </viewController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="lsQ-Rg-fKC" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
             </objects>
-            <point key="canvasLocation" x="1027.536231884058" y="133.92857142857142"/>
+            <point key="canvasLocation" x="883" y="-135"/>
+        </scene>
+        <!--Splash View Controller-->
+        <scene sceneID="G1S-Tm-4Eu">
+            <objects>
+                <viewController id="QDy-R4-ped" userLabel="Splash View Controller" customClass="SplashViewController" customModule="Image_Feed" customModuleProvider="target" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleAspectFit" id="XQx-Su-t30">
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="886"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="splash_screen_logo" translatesAutoresizingMaskIntoConstraints="NO" id="z6O-3y-1EZ">
+                                <rect key="frame" x="171" y="405.5" width="72" height="75"/>
+                                <constraints>
+                                    <constraint firstAttribute="width" constant="72" id="m5R-IB-ruu"/>
+                                    <constraint firstAttribute="height" constant="75" id="zlv-OI-4cz"/>
+                                </constraints>
+                            </imageView>
+                        </subviews>
+                        <viewLayoutGuide key="safeArea" id="Oz7-BM-fBg"/>
+                        <color key="backgroundColor" red="0.10196078431372549" green="0.10588235294117647" blue="0.13333333333333333" alpha="1" colorSpace="calibratedRGB"/>
+                        <constraints>
+                            <constraint firstItem="z6O-3y-1EZ" firstAttribute="centerX" secondItem="XQx-Su-t30" secondAttribute="centerX" id="rEz-6g-sOO"/>
+                            <constraint firstItem="z6O-3y-1EZ" firstAttribute="centerY" secondItem="XQx-Su-t30" secondAttribute="centerY" id="tj9-Tf-CSK"/>
+                        </constraints>
+                    </view>
+                    <navigationItem key="navigationItem" id="ts3-85-dEF"/>
+                    <connections>
+                        <segue destination="YRR-5p-jAR" kind="show" identifier="showAuthenticationScreen" id="sXY-gr-joW"/>
+                    </connections>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="W93-b1-vXN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="-1913.0434782608697" y="963.61607142857133"/>
         </scene>
         <!--Profile View Controller-->
         <scene sceneID="459-AN-faS">
@@ -216,19 +249,143 @@
                         <color key="backgroundColor" red="0.10196078431372549" green="0.10588235294117647" blue="0.13333333333333333" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
                     </view>
                     <tabBarItem key="tabBarItem" title="" image="tab_profile_active" id="RJh-2O-gXP"/>
+                    <connections>
+                        <segue destination="QDy-R4-ped" kind="show" identifier="logoutSegue" id="nr7-zM-NfK"/>
+                    </connections>
                 </viewController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="lM9-T6-2V3" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
             </objects>
             <point key="canvasLocation" x="16" y="544"/>
         </scene>
+        <!--Auth View Controller-->
+        <scene sceneID="Aft-5u-Cuk">
+            <objects>
+                <viewController id="PQF-54-cg8" customClass="AuthViewController" customModule="Image_Feed" customModuleProvider="target" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleToFill" id="Uig-dy-5Eu">
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="886"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Logo_of_Unsplash" translatesAutoresizingMaskIntoConstraints="NO" id="qhL-NH-e5T">
+                                <rect key="frame" x="177" y="413" width="60" height="60"/>
+                                <constraints>
+                                    <constraint firstAttribute="width" constant="60" id="F94-nW-fIT"/>
+                                    <constraint firstAttribute="height" constant="60" id="ePd-0Q-e9f"/>
+                                </constraints>
+                            </imageView>
+                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wfH-DM-fzm">
+                                <rect key="frame" x="16" y="714" width="382" height="48"/>
+                                <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="48" id="3k7-NV-mLX"/>
+                                </constraints>
+                                <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
+                                <color key="tintColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
+                                <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                                <state key="normal" title="Войти">
+                                    <color key="titleColor" red="0.10196078431372549" green="0.10588235294117647" blue="0.13333333333333333" alpha="0.89803921568627454" colorSpace="calibratedRGB"/>
+                                </state>
+                                <userDefinedRuntimeAttributes>
+                                    <userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
+                                        <integer key="value" value="16"/>
+                                    </userDefinedRuntimeAttribute>
+                                    <userDefinedRuntimeAttribute type="boolean" keyPath="layer.masksToBounds" value="YES"/>
+                                </userDefinedRuntimeAttributes>
+                                <connections>
+                                    <segue destination="BGV-1i-ida" kind="presentation" identifier="showWebView" id="MZs-pQ-E6V"/>
+                                </connections>
+                            </button>
+                        </subviews>
+                        <viewLayoutGuide key="safeArea" id="xXl-pD-UMi"/>
+                        <color key="backgroundColor" red="0.10196078431372549" green="0.10588235294117647" blue="0.13333333333333333" alpha="1" colorSpace="calibratedRGB"/>
+                        <constraints>
+                            <constraint firstItem="qhL-NH-e5T" firstAttribute="centerY" secondItem="Uig-dy-5Eu" secondAttribute="centerY" id="29Y-zb-1Ao"/>
+                            <constraint firstItem="wfH-DM-fzm" firstAttribute="leading" secondItem="xXl-pD-UMi" secondAttribute="leading" constant="16" id="HoB-ge-96p"/>
+                            <constraint firstItem="xXl-pD-UMi" firstAttribute="trailing" secondItem="wfH-DM-fzm" secondAttribute="trailing" constant="16" id="MNR-h3-DQZ"/>
+                            <constraint firstItem="qhL-NH-e5T" firstAttribute="centerX" secondItem="Uig-dy-5Eu" secondAttribute="centerX" id="vdI-CY-OoB"/>
+                            <constraint firstAttribute="bottom" secondItem="wfH-DM-fzm" secondAttribute="bottom" constant="124" id="yar-iq-fZH"/>
+                        </constraints>
+                    </view>
+                    <navigationItem key="navigationItem" id="aFo-Km-gGx"/>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="Jqj-F6-2zV" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="-210" y="1266"/>
+        </scene>
+        <!--Web View View Controller-->
+        <scene sceneID="oov-Cj-l79">
+            <objects>
+                <viewController id="BGV-1i-ida" customClass="WebViewViewController" customModule="Image_Feed" customModuleProvider="target" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleToFill" id="8Ht-zu-pdx">
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="886"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <wkWebView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Bv5-Ba-ICT">
+                                <rect key="frame" x="0.0" y="0.0" width="414" height="886"/>
+                                <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                <wkWebViewConfiguration key="configuration">
+                                    <audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
+                                    <wkPreferences key="preferences"/>
+                                </wkWebViewConfiguration>
+                            </wkWebView>
+                            <progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progress="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Yyu-xZ-y2C">
+                                <rect key="frame" x="0.0" y="0.0" width="414" height="4"/>
+                                <color key="progressTintColor" red="0.10196078431372549" green="0.10588235294117647" blue="0.13333333333333333" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                            </progressView>
+                        </subviews>
+                        <viewLayoutGuide key="safeArea" id="KxP-cj-oHA"/>
+                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
+                        <constraints>
+                            <constraint firstItem="Yyu-xZ-y2C" firstAttribute="top" secondItem="KxP-cj-oHA" secondAttribute="top" id="13B-Om-xK5"/>
+                            <constraint firstItem="KxP-cj-oHA" firstAttribute="trailing" secondItem="Yyu-xZ-y2C" secondAttribute="trailing" id="UlW-AF-b6m"/>
+                            <constraint firstAttribute="bottom" secondItem="Bv5-Ba-ICT" secondAttribute="bottom" id="Vbj-FB-7dL"/>
+                            <constraint firstItem="Bv5-Ba-ICT" firstAttribute="top" secondItem="KxP-cj-oHA" secondAttribute="top" id="XHN-d3-uuD"/>
+                            <constraint firstItem="Yyu-xZ-y2C" firstAttribute="leading" secondItem="KxP-cj-oHA" secondAttribute="leading" id="ajO-Gq-Cem"/>
+                            <constraint firstAttribute="trailing" secondItem="Bv5-Ba-ICT" secondAttribute="trailing" id="dmS-LV-kJ8"/>
+                            <constraint firstItem="Bv5-Ba-ICT" firstAttribute="leading" secondItem="8Ht-zu-pdx" secondAttribute="leading" id="txR-fg-qkq"/>
+                        </constraints>
+                    </view>
+                    <navigationItem key="navigationItem" id="Amz-BP-Mhm"/>
+                    <connections>
+                        <outlet property="WebView" destination="Bv5-Ba-ICT" id="oWT-in-kwh"/>
+                        <outlet property="progressView" destination="Yyu-xZ-y2C" id="vSB-35-Dec"/>
+                    </connections>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="D7v-fM-hQ1" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="554" y="1266"/>
+        </scene>
+        <!--Navigation Controller-->
+        <scene sceneID="rnP-Mb-P5o">
+            <objects>
+                <navigationController automaticallyAdjustsScrollViewInsets="NO" id="YRR-5p-jAR" sceneMemberID="viewController">
+                    <toolbarItems/>
+                    <navigationItem key="navigationItem" id="L1v-v8-6TY"/>
+                    <navigationBar key="navigationBar" contentMode="scaleToFill" id="35E-Kt-MEb">
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="56"/>
+                        <autoresizingMask key="autoresizingMask"/>
+                    </navigationBar>
+                    <nil name="viewControllers"/>
+                    <connections>
+                        <segue destination="PQF-54-cg8" kind="relationship" relationship="rootViewController" id="f5i-h6-3Qp"/>
+                    </connections>
+                </navigationController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="nLa-oT-Yps" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="-994" y="1266"/>
+        </scene>
     </scenes>
     <resources>
         <image name="0" width="1600" height="1059"/>
         <image name="Backward" width="24" height="24"/>
+        <image name="Logo_of_Unsplash" width="60" height="60"/>
         <image name="Sharing" width="50" height="50"/>
         <image name="like_button_on" width="42" height="42"/>
+        <image name="splash_screen_logo" width="73" height="75.5"/>
         <image name="tab_editorial_active" width="30" height="30"/>
         <image name="tab_profile_active" width="30" height="30"/>
         <image name="Сircle" width="51" height="51"/>
+        <systemColor name="systemBackgroundColor">
+            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+        </systemColor>
     </resources>
 </document>
diff --git a/Image Feed/Profile/ProfileViewController.swift b/Image Feed/Profile/ProfileViewController.swift
index 839ace2..2122a29 100644
--- a/Image Feed/Profile/ProfileViewController.swift	
+++ b/Image Feed/Profile/ProfileViewController.swift	
@@ -24,11 +24,16 @@ final class ProfileViewController: UIViewController {
     }
     
     @objc
-    func didTapLogoutButton() {}
+    func didTapLogoutButton() {
+        let logoutToken = OAuth2TokenStorage()
+        logoutToken.token = nil
+        performSegue(withIdentifier: "logoutSegue" , sender: nil)
+    }
     
     private func addSubviews(anyView: UIView) {
         anyView.translatesAutoresizingMaskIntoConstraints = false
         view.addSubview(anyView)
+        
     }
     
     private func addLabels() {
diff --git a/Image Feed/Service/GradientExtension.swift b/Image Feed/Service/GradientExtension.swift
index c5c4ada..ff97050 100644
--- a/Image Feed/Service/GradientExtension.swift	
+++ b/Image Feed/Service/GradientExtension.swift	
@@ -45,7 +45,8 @@ extension UILabel {
         let textLayer = CATextLayer()
         textLayer.frame = self.bounds
         textLayer.string = self.text
-        textLayer.fontSize = self.font!.pointSize
+        guard let font else { return }
+        textLayer.fontSize = self.font.pointSize
         textLayer.alignmentMode = .center
         textLayer.foregroundColor = UIColor.white.cgColor
 
diff --git a/Image Feed/SingleImage/SingleImageViewController.swift b/Image Feed/SingleImage/SingleImageViewController.swift
index a187b28..ee50e49 100644
--- a/Image Feed/SingleImage/SingleImageViewController.swift	
+++ b/Image Feed/SingleImage/SingleImageViewController.swift	
@@ -46,7 +46,6 @@ final class SingleImageViewController: UIViewController {
         present(shareViewController, animated: true, completion: nil)
     }
     
-    
     func rescaleAndCenterImageInScrollView (image: UIImage) {
         view.layoutIfNeeded()
         let visibleRectSize = scrollView.bounds.size
diff --git a/Image Feed/SplashView/SplashViewController.swift b/Image Feed/SplashView/SplashViewController.swift
new file mode 100644
index 0000000..fe33063
--- /dev/null
+++ b/Image Feed/SplashView/SplashViewController.swift	
@@ -0,0 +1,62 @@
+//
+//  SplashViewController.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 08.04.2025.
+//
+
+import UIKit
+
+
+final class SplashViewController: UIViewController {
+    
+    private let storage = OAuth2TokenStorage()
+    private let authenticationSegueIdentifier = "showAuthenticationScreen"
+    
+    override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+        if let token = storage.token {
+            switchToTabBarController()
+        }
+        else {
+            performSegue(withIdentifier: authenticationSegueIdentifier, sender: nil)
+        }
+    }
+    
+    private func switchToTabBarController() {
+        DispatchQueue.main.async {
+            guard let window = UIApplication.shared.windows.first else {
+                print("Invalid window configuration")
+                assertionFailure("Invalid window configuration")
+                return
+            }
+            let tabBarController = UIStoryboard(name: "Main", bundle: .main)
+                .instantiateViewController(withIdentifier: "TabBarViewController")
+            window.rootViewController = tabBarController
+        }
+    }
+}
+
+extension SplashViewController {
+    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+        super.prepare(for: segue, sender: sender)
+        
+        guard segue.identifier == authenticationSegueIdentifier,
+              let navigationController = segue.destination as? UINavigationController,
+              let viewController = navigationController.viewControllers.first as? AuthViewController else {
+            assertionFailure("Failed to prepare for \(String(describing: segue.identifier))")
+            return
+        }
+        segue.destination.modalPresentationStyle = .fullScreen
+        viewController.delegate = self
+    }
+}
+
+extension SplashViewController: AuthViewControllerDelegate {
+    func didAuthenticate(_ vc: AuthViewController) {
+        DispatchQueue.main.async {
+            vc.dismiss(animated: true)
+        }
+        switchToTabBarController()
+    }
+}


Reviews:
user: login: ValeriUst
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-09T03:53:41Z
user: login: ValeriUst
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-10T06:38:31Z

Review comments:
diff_hunk: @@ -0,0 +1,66 @@
+//
+//  AuthViewController.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 25.03.2025.
+//
+
+import UIKit
+
+protocol AuthViewControllerDelegate: AnyObject {
+    func didAuthenticate(_ vc: AuthViewController)
+}
+
+final class AuthViewController: UIViewController {
+    let showWebViewSegueIdentifier = "showWebView"
+    weak var delegate: AuthViewControllerDelegate?
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        configureBackButton()
+    }
+    
+    private func  configureBackButton() {
+        navigationController?.navigationBar.backIndicatorImage = UIImage(named: "nav_back_button")
+        navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "nav_back_button")
path: Image Feed/Auth/AuthViewController.swift
user: login: ValeriUst
body: Здесь есть лишние отступы. В Xcode можно быстро выровнять код с помощью удобной команды: Cmd + A (выделить весь текст) → Ctrl + I (автоматическое выравнивание). Перед отправкой кода рекомендуется использовать этот способ для удобного форматирования изменённых файлов.

created_at: 2025-04-09T03:25:28Z
author_association: NONE
diff_hunk: @@ -0,0 +1,16 @@
+//
+//  Constants.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 25.03.2025.
+//
+
+import Foundation
+
+enum Constants {
+    static let accessKey = "Tm0f7JJO27QSk2C4V9Z5crBkowECKdDoZyOfmTW7klI"
+    static let secretKey = "BTMPNrX01IvyyQ7Y9TKH-Yz8kHjQ4QfUcwFJQhuiYTw"
+    static let redirectURI = "urn:ietf:wg:oauth:2.0:oob"
+    static let accessScope = "public+read_user+write_likes"
+    static let defaultBaseURL = URL(string: "https://api.unsplash.com")!
path: Image Feed/Auth/Constants.swift
user: login: ValeriUst
body: ⚠️ Лучше избегать использования `!` при инициализации URL.
Если `URL(string:)` вдруг вернёт `nil`, приложение упадёт. 
```
static let defaultBaseURL: URL? = URL(string: "https://api.unsplash.com")
```
created_at: 2025-04-09T03:31:26Z
author_association: NONE
diff_hunk: @@ -0,0 +1,66 @@
+//
+//  AuthViewController.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 25.03.2025.
+//
+
+import UIKit
+
+protocol AuthViewControllerDelegate: AnyObject {
+    func didAuthenticate(_ vc: AuthViewController)
+}
+
+final class AuthViewController: UIViewController {
+    let showWebViewSegueIdentifier = "showWebView"
+    weak var delegate: AuthViewControllerDelegate?
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        configureBackButton()
+    }
+    
+    private func  configureBackButton() {
+        navigationController?.navigationBar.backIndicatorImage = UIImage(named: "nav_back_button")
+        navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "nav_back_button")
+        navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
+        navigationItem.backBarButtonItem?.tintColor = #colorLiteral(red: 0.1019607843, green: 0.1058823529, blue: 0.1333333333, alpha: 1)
+    }
+    
+    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+        if segue.identifier == showWebViewSegueIdentifier {
+            guard
+                let webViewViewController = segue.destination as? WebViewViewController
+            else {
+                assertionFailure("Failed to prepare for \(showWebViewSegueIdentifier)")
+                return
+            }
+            webViewViewController.delegate = self
+        } else {
+            super.prepare(for: segue, sender: sender)
+        }
+    }
+}
path: Image Feed/Auth/AuthViewController.swift
user: login: ValeriUst
body: Лучше использовать единый `guard` для проверки идентификатора и приведения типа — так код будет короче, чище и легче читаем.
```
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    super.prepare(for: segue, sender: sender)
    
    guard segue.identifier == showWebViewSegueIdentifier,
          let webViewViewController = segue.destination as? WebViewViewController else {
        assertionFailure("Failed to prepare for \(String(describing: segue.identifier))")
        return
    }
    
    webViewViewController.delegate = self
}
```
created_at: 2025-04-09T03:35:53Z
author_association: NONE
diff_hunk: @@ -0,0 +1,66 @@
+//
+//  AuthViewController.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 25.03.2025.
+//
+
+import UIKit
+
+protocol AuthViewControllerDelegate: AnyObject {
+    func didAuthenticate(_ vc: AuthViewController)
+}
+
+final class AuthViewController: UIViewController {
+    let showWebViewSegueIdentifier = "showWebView"
+    weak var delegate: AuthViewControllerDelegate?
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        configureBackButton()
+    }
+    
+    private func  configureBackButton() {
+        navigationController?.navigationBar.backIndicatorImage = UIImage(named: "nav_back_button")
+        navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "nav_back_button")
+        navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
+        navigationItem.backBarButtonItem?.tintColor = #colorLiteral(red: 0.1019607843, green: 0.1058823529, blue: 0.1333333333, alpha: 1)
+    }
+    
+    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+        if segue.identifier == showWebViewSegueIdentifier {
+            guard
+                let webViewViewController = segue.destination as? WebViewViewController
+            else {
+                assertionFailure("Failed to prepare for \(showWebViewSegueIdentifier)")
+                return
+            }
+            webViewViewController.delegate = self
+        } else {
+            super.prepare(for: segue, sender: sender)
+        }
+    }
+}
+
+extension AuthViewController: WebViewViewControllerDelegate {
+    func webViewViewControllerDidCancel(_ vc: WebViewViewController) {
+        vc.dismiss(animated: true)
+    }
+    
+    func webViewViewController(_ vc: WebViewViewController, didAuthenticateWithCode code: String) {
+        
+        vc.dismiss(animated: true)
+        OAuth2Service.shared.fetchOAuthToken(code: code) { result in
path: Image Feed/Auth/AuthViewController.swift
user: login: ValeriUst
body: ⚠️ Не забывай использовать `[weak self]` в замыканиях, где используется `self`. Это помогает избежать сильной ссылки и гарантирует, что объект сможет корректно освобождаться из памяти, когда он больше не нужен.
created_at: 2025-04-09T03:38:57Z
author_association: NONE
diff_hunk: @@ -0,0 +1,70 @@
+//
+//  OAuth2Service.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 07.04.2025.
+//
+
+import UIKit
+
+protocol OAuth2ServiceProtocol {
+    func make0AuthTokenRequest(code: String) -> URLRequest?
+    func fetchOAuthToken(code: String, completion: @escaping (Result<OAuthTokenResponseBody, Error>) -> Void)
+}
+
+final class OAuth2Service: OAuth2ServiceProtocol {
+    static let shared = OAuth2Service()
+    private init() {}
+    
+    private enum NetworkError: Error {
+        case codeError
+    }
+    func make0AuthTokenRequest(code: String) -> URLRequest? {
+        var urlComponents = URLComponents(string: "https://unsplash.com/oauth/token")!
+        urlComponents.queryItems = [
+            URLQueryItem(name: "client_id", value: Constants.accessKey),
+            URLQueryItem(name: "client_secret", value: Constants.secretKey),
+            URLQueryItem(name: "redirect_uri", value: Constants.redirectURI),
+            URLQueryItem(name: "code", value: code),
+            URLQueryItem(name: "grant_type", value: "authorization_code")
+        ]
+        
+        guard let url = urlComponents.url else { return nil }
+        var request = URLRequest(url: url)
+        request.httpMethod = "POST"
path: Image Feed/Auth/OAuth2Service.swift
user: login: ValeriUst
body: Можно создать отдельный `enum` типа `String` для `http` методов
created_at: 2025-04-09T03:41:20Z
author_association: NONE
diff_hunk: @@ -0,0 +1,19 @@
+//
+//  OAuth2TokenStorage.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 07.04.2025.
+//
+
+import Foundation
+
+final class OAuth2TokenStorage {
+    var token: String? {
+        get {
+            return UserDefaults.standard.string(forKey: "bearerToken")
path: Image Feed/Auth/OAuth2TokenStorage.swift
user: login: ValeriUst
body: В однострочных выражениях `return` лучше опускать
created_at: 2025-04-09T03:41:49Z
author_association: NONE
diff_hunk: @@ -0,0 +1,102 @@
+//
+//  WebViewViewController.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 26.03.2025.
+//
+
+import UIKit
+import WebKit
+
+protocol WebViewViewControllerDelegate: AnyObject {
+    func webViewViewController(_ vc: WebViewViewController, didAuthenticateWithCode code: String)
+    
+    func webViewViewControllerDidCancel(_ vc: WebViewViewController)
+}
+
+final class WebViewViewController: UIViewController {
+    
+    @IBOutlet private weak var progressView: UIProgressView!
+    @IBOutlet private weak var webView: WKWebView!
+    weak var delegate: WebViewViewControllerDelegate?
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        webView.navigationDelegate = self
+        if let request = makeRequest() {
+            webView.load(request)
+        }
+        updateProgress()
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        webView.addObserver(self,
+                            forKeyPath: #keyPath(WKWebView.estimatedProgress),
+                            options: .new,
+                            context: nil)
+    }
+    
+    override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+        webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), context: nil)
+    }
+    
+    override func observeValue(
+        forKeyPath keyPath: String?,
+        of object: Any?,
+        change: [NSKeyValueChangeKey : Any]?,
+        context: UnsafeMutableRawPointer?
+    ) {
+        if keyPath == #keyPath(WKWebView.estimatedProgress) {
+            updateProgress()
+        } else {
+            super.observeValue(forKeyPath: keyPath,
+                               of: object,
+                               change: change,
+                               context: context)
+        }
+    }
+    
+    private func updateProgress() {
+        progressView.progress = Float(webView.estimatedProgress)
+        progressView.isHidden = fabs(webView.estimatedProgress - 1.0) <= 0.0001
+    }
+    
+    func makeRequest() -> URLRequest? {
+        guard var uRLComponents = URLComponents(string: "https://unsplash.com/oauth/authorize") else { return nil }
+        
+        let queryItems: [URLQueryItem] = [
+            URLQueryItem(name: "client_id", value: Constants.accessKey),
+            URLQueryItem(name: "redirect_uri", value: Constants.redirectURI),
+            URLQueryItem(name: "response_type", value: "code"),
+            URLQueryItem(name: "scope", value: Constants.accessScope)
+        ]
+        
+        uRLComponents.queryItems = queryItems
+        guard let url = uRLComponents.url else { return nil }
+        return .init(url: url)
+    }
+}
+
+
path: Image Feed/Auth/WebViewViewController.swift
user: login: ValeriUst
body: Давай уберем лишние пробелы.
created_at: 2025-04-09T03:42:43Z
author_association: NONE
diff_hunk: @@ -0,0 +1,64 @@
+//
+//  SplashViewController.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 08.04.2025.
+//
+
+import UIKit
+
+
+final class SplashViewController: UIViewController {
+    
+    private let storage = OAuth2TokenStorage()
+    private let authenticationSegueIdentifier = "showAuthenticationScreen"
+    
+    
path: Image Feed/SplashView/SplashViewController.swift
user: login: ValeriUst
body: Лишние пробелы встречаются во всем коде. Рекомендую пройтись по файлам и удалить их, чтобы код выглядел аккуратнее и был легче для чтения.
created_at: 2025-04-09T03:43:08Z
author_association: NONE
diff_hunk: @@ -0,0 +1,64 @@
+//
+//  SplashViewController.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 08.04.2025.
+//
+
+import UIKit
+
+
+final class SplashViewController: UIViewController {
+    
+    private let storage = OAuth2TokenStorage()
+    private let authenticationSegueIdentifier = "showAuthenticationScreen"
+    
+    
+    override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+        
+        if let token = storage.token {
+            switchToTabBarController()
+        }
+        else {
+            performSegue(withIdentifier: authenticationSegueIdentifier, sender: nil)
+        }
+    }
+    
+    private func switchToTabBarController() {
+        DispatchQueue.main.async {
+            guard let window = UIApplication.shared.windows.first else {
+                assertionFailure("Invalid window configuration")
+                return
+            }
+            let tabBarController = UIStoryboard(name: "Main", bundle: .main)
+                .instantiateViewController(withIdentifier: "TabBarViewController")
+            window.rootViewController = tabBarController
+        }
+    }
+}
+
+extension SplashViewController {
+    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+        if segue.identifier == authenticationSegueIdentifier {guard
+            let navigationController = segue.destination as? UINavigationController,
+            let viewController = navigationController.viewControllers[0] as? AuthViewController
+            else {
+            assertionFailure("Failed to prepare for \(authenticationSegueIdentifier)")
+            return
+        }
+            viewController.delegate = self
+        } else {
+            super.prepare(for: segue, sender: sender)
+        }
+    }
+}
path: Image Feed/SplashView/SplashViewController.swift
user: login: ValeriUst
body: 
```
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    super.prepare(for: segue, sender: sender)

    guard segue.identifier == authenticationSegueIdentifier,
          let navigationController = segue.destination as? UINavigationController,
          let viewController = navigationController.viewControllers.first as? AuthViewController else {
        assertionFailure("Failed to prepare for \(String(describing: segue.identifier))")
        return
    }

    viewController.delegate = self
}
```
created_at: 2025-04-09T03:44:43Z
author_association: NONE
diff_hunk: @@ -0,0 +1,70 @@
+//
+//  OAuth2Service.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 07.04.2025.
+//
+
+import UIKit
+
+protocol OAuth2ServiceProtocol {
+    func make0AuthTokenRequest(code: String) -> URLRequest?
+    func fetchOAuthToken(code: String, completion: @escaping (Result<OAuthTokenResponseBody, Error>) -> Void)
+}
+
+final class OAuth2Service: OAuth2ServiceProtocol {
+    static let shared = OAuth2Service()
+    private init() {}
+    
+    private enum NetworkError: Error {
+        case codeError
+    }
+    func make0AuthTokenRequest(code: String) -> URLRequest? {
+        var urlComponents = URLComponents(string: "https://unsplash.com/oauth/token")!
path: Image Feed/Auth/OAuth2Service.swift
user: login: ValeriUst
body: ⚠️  Давай избавимся от форс-распаковки, используя безопасные методы, чтобы избежать возможных сбоев приложения.
``` 
guard let urlComponents = URLComponents(string: "https://unsplash.com/oauth/token") else {
        return nil
    }
```
created_at: 2025-04-09T03:50:01Z
author_association: NONE
diff_hunk: @@ -0,0 +1,87 @@
+//
+//  OAuth2Service.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 07.04.2025.
+//
+
+import UIKit
+
+protocol OAuth2ServiceProtocol {
+    func make0AuthTokenRequest(code: String) -> URLRequest?
+    func fetchOAuthToken(code: String, completion: @escaping (Result<OAuthTokenResponseBody, Error>) -> Void)
+}
+
+final class OAuth2Service: OAuth2ServiceProtocol {
+    static let shared = OAuth2Service()
+    private init() {}
+    
+    private enum NetworkError: Error {
+        case codeError
+    }
+    func make0AuthTokenRequest(code: String) -> URLRequest? {
+        guard var urlComponents = URLComponents(string: "https://unsplash.com/oauth/token")
+        else { return nil }
+        urlComponents.queryItems = [
+            URLQueryItem(name: "client_id", value: Constants.accessKey),
+            URLQueryItem(name: "client_secret", value: Constants.secretKey),
+            URLQueryItem(name: "redirect_uri", value: Constants.redirectURI),
+            URLQueryItem(name: "code", value: code),
+            URLQueryItem(name: "grant_type", value: "authorization_code")
+        ]
+        
+        guard let url = urlComponents.url else {
+            print("Error: Failed to create URL")
+            return nil
+        }
+        var request = URLRequest(url: url)
+        request.httpMethod = "POST"
+        return request
+    }
+    
+    func fetchOAuthToken(code: String, completion: @escaping (Result<OAuthTokenResponseBody, Error>) -> Void) {
+        guard let request = make0AuthTokenRequest(code: code) else {
+            DispatchQueue.main.async {
+                completion(.failure(NetworkError.codeError))
+            }
+            return
+        }
+
+        let task = URLSession.shared.dataTask(with: request) { data, response, error in
+            if let error = error {
+                print("Network request failed with error: \(error.localizedDescription)")
+                DispatchQueue.main.async {
+                    completion(.failure(error))
+                }
+                return
+            }
+
+            guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
+                DispatchQueue.main.async {
+                    completion(.failure(NetworkError.codeError))
+                }
+                return
+            }
+
+            guard let data = data else {
+                DispatchQueue.main.async {
+                    completion(.failure(NetworkError.codeError))
+                }
+                return
+            }
+
+            do {
+                let decoder = JSONDecoder()
+                let oauthTokenResponse = try decoder.decode(OAuthTokenResponseBody.self, from: data)
+                DispatchQueue.main.async {
+                    completion(.success(oauthTokenResponse))
+                }
+            } catch {
+                DispatchQueue.main.async {
+                    completion(.failure(error))
path: Image Feed/Auth/OAuth2Service.swift
user: login: ValeriUst
body: ⚠️ Необходимо добавить логирование ошибок в консоль (print) для улучшения отладки (Пункты 12.4)
created_at: 2025-04-10T06:37:34Z
author_association: NONE
diff_hunk: @@ -0,0 +1,87 @@
+//
+//  OAuth2Service.swift
+//  Image Feed
+//
+//  Created by semrumyantsev on 07.04.2025.
+//
+
+import UIKit
+
+protocol OAuth2ServiceProtocol {
+    func make0AuthTokenRequest(code: String) -> URLRequest?
+    func fetchOAuthToken(code: String, completion: @escaping (Result<OAuthTokenResponseBody, Error>) -> Void)
+}
+
+final class OAuth2Service: OAuth2ServiceProtocol {
+    static let shared = OAuth2Service()
+    private init() {}
+    
+    private enum NetworkError: Error {
+        case codeError
+    }
+    func make0AuthTokenRequest(code: String) -> URLRequest? {
+        guard var urlComponents = URLComponents(string: "https://unsplash.com/oauth/token")
+        else { return nil }
+        urlComponents.queryItems = [
+            URLQueryItem(name: "client_id", value: Constants.accessKey),
+            URLQueryItem(name: "client_secret", value: Constants.secretKey),
+            URLQueryItem(name: "redirect_uri", value: Constants.redirectURI),
+            URLQueryItem(name: "code", value: code),
+            URLQueryItem(name: "grant_type", value: "authorization_code")
+        ]
+        
+        guard let url = urlComponents.url else {
+            print("Error: Failed to create URL")
+            return nil
+        }
+        var request = URLRequest(url: url)
+        request.httpMethod = "POST"
+        return request
+    }
+    
+    func fetchOAuthToken(code: String, completion: @escaping (Result<OAuthTokenResponseBody, Error>) -> Void) {
+        guard let request = make0AuthTokenRequest(code: code) else {
+            DispatchQueue.main.async {
+                completion(.failure(NetworkError.codeError))
+            }
+            return
+        }
+
+        let task = URLSession.shared.dataTask(with: request) { data, response, error in
+            if let error = error {
+                print("Network request failed with error: \(error.localizedDescription)")
+                DispatchQueue.main.async {
+                    completion(.failure(error))
+                }
+                return
+            }
+
+            guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
+                DispatchQueue.main.async {
+                    completion(.failure(NetworkError.codeError))
path: Image Feed/Auth/OAuth2Service.swift
user: login: ValeriUst
body: ⚠️ Необходимо добавить логирование ошибок в консоль для улучшения отладки (Пункты 12.3)
created_at: 2025-04-10T06:37:47Z
author_association: NONE

PR comments:
user: login: ValeriUst
created_at: 2025-04-09T04:51:41Z
body: ⚠️ Сейчас при тапе на кнопку "Войти" приложение крашится. Требуется устранить эту проблему (пункт 2).
<img width="1440" alt="Снимок экрана 2025-04-09 в 11 30 27" src="https://github.com/user-attachments/assets/81ba9de4-d38d-44da-92a6-9aaf1343215f" />

author_association: NONE
user: login: ValeriUst
created_at: 2025-04-09T04:52:39Z
body: ⚠️ Дизайн проекта не соответствует макету в Figma.Требуется установить верный шрифт для кнопки "Войти" и сделать сплеш-скрин на весь экран. (пункт 13).

![Simulator Screenshot - iPhone 16 - 2025-04-09 at 10 24 39](https://github.com/user-attachments/assets/5d7c999b-877c-4922-ba1b-b07bb8879847)

author_association: NONE
user: login: ValeriUst
created_at: 2025-04-10T06:41:49Z
body: @GBCreat0r, отличная работа — ты исправил почти все критические замечания  и небольшие недочеты, молодец!  Осталось совсем немного — добавить логирование ошибок в консоль(указала где в пулл-реквесте). Это важно для удобства отладки и входит в наш чек-лист (⚠️ пункты 12.3–12.4), поэтому пока не могу финально принять работу.

Давай доделаем — у тебя всё уже на финальной прямой 💪 
author_association: NONE
user: login: ValeriUst
created_at: 2025-04-10T14:39:39Z
body: @GBCreat0r, молодец! Все критические замечания исправлены.
Удачи тебе и продолжай в том же духе!) 
author_association: NONE