Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 274 additions & 0 deletions ImageFeed.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

53 changes: 0 additions & 53 deletions ImageFeed/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
//

import UIKit
import CoreData

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}

Expand All @@ -21,60 +19,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let sceneConfiguration = UISceneConfiguration(name: "Main", sessionRole: connectingSceneSession.role)
sceneConfiguration.delegateClass = SceneDelegate.self
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return sceneConfiguration
}

func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}

// MARK: - Core Data stack

lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "ImageFeed")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()

// MARK: - Core Data Saving support

func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}

61 changes: 26 additions & 35 deletions ImageFeed/Auth/ WebViewViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,87 +8,78 @@
import UIKit
import WebKit

private struct APIConstants {
static let authorizeURLString = "https://unsplash.com/oauth/authorize"
static let code = "code"
static let authorizathionPath = "/oauth/authorize/native"
public protocol WebViewViewControllerProtocol: AnyObject {
var presenter: WebViewPresenterProtocol? { get set }
func load(request: URLRequest)
func setProgressValue(_ newValue: Float)
func setProgressHidden(_ isHidden: Bool)
}

protocol WebViewViewControllerDelegate: AnyObject {
func webViewViewController(_ vc: WebViewViewController, didAuthenticateWithCode code: String)
func webViewViewControllerDidCancel(_ vc:WebViewViewController)
}

final class WebViewViewController: UIViewController {
final class WebViewViewController: UIViewController, WebViewViewControllerProtocol {

@IBOutlet private var progressView: UIProgressView!
@IBOutlet private var webView: WKWebView!

weak var delegate: WebViewViewControllerDelegate?
var presenter: WebViewPresenterProtocol?

private var estimatedProgressObservation: NSKeyValueObservation?

weak var delegate: WebViewViewControllerDelegate?

override func viewDidLoad() {
super.viewDidLoad()

webView.navigationDelegate = self

loadWebView()
presenter?.viewDidLoad()
webView.accessibilityIdentifier = "UnsplashWebView"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно выносить константы в private enum Constants


estimatedProgressObservation = webView.observe(
\.estimatedProgress,
options: [],
changeHandler: { [weak self] _, _ in
guard let self = self else { return }
self.updateProgress()
self.presenter?.didUpdateProgressValue(self.webView.estimatedProgress)
})
}

@IBAction private func didTapBackButton(_ sender: Any) {
@IBAction private func didTapBackButton(_ sender: Any?) {
delegate?.webViewViewControllerDidCancel(self)
print("Press button")
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateProgress()
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}

private func updateProgress() {
progressView.progress = Float(webView.estimatedProgress)
progressView.isHidden = fabs(webView.estimatedProgress - 1.0) <= 0.0001
func load(request: URLRequest) {
webView.load(request)
}

func setProgressValue(_ newValue: Float) {
progressView.progress = newValue
}

func setProgressHidden(_ isHidden: Bool) {
progressView.isHidden = isHidden
}
}

private extension WebViewViewController {
func loadWebView() {
var urlComponents = URLComponents(string: APIConstants.authorizeURLString)
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)
]
guard let url = urlComponents?.url else { return }
let request = URLRequest(url: url)
webView.load(request)
updateProgress()
}

func fetchCode(from navigationAction: WKNavigationAction) -> String? {
if let url = navigationAction.request.url,
let components = URLComponents(string: url.absoluteString),
components.path == APIConstants.authorizathionPath,
let items = components.queryItems,
let codeItem = items.first(where: { $0.name == APIConstants.code }) {
return codeItem.value
} else {
return nil
if let url = navigationAction.request.url {
return presenter?.code(from: url)
}
return nil
}
}

Expand Down
48 changes: 48 additions & 0 deletions ImageFeed/Auth/AuthHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// AuthHelper.swift
// ImageFeed
//
// Created by admin on 16.09.2023.
//

import Foundation

protocol AuthHelperProtocol {
func authRequest() -> URLRequest
func code(from url: URL) -> String?
}

class AuthHelper: AuthHelperProtocol {
let configuration: AuthConfiguration

init(configuration: AuthConfiguration = .standart) {
self.configuration = configuration
}

func authRequest() -> URLRequest {
let url = authURL()
return URLRequest(url: url)
}

func authURL() -> URL {
var urlComponents = URLComponents(string: configuration.authURLString)!
urlComponents.queryItems = [
URLQueryItem(name: "client_id", value: configuration.accessKey),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут также можно вынести константы в отдельный enum

URLQueryItem(name: "redirect_uri", value: configuration.redirectURI),
URLQueryItem(name: "response_type", value: "code"),
URLQueryItem(name: "scope", value: configuration.accessScope)
]
return urlComponents.url!
}

func code(from url: URL) -> String? {
if let components = URLComponents(string: url.absoluteString),
components.path == "/oauth/authorize/native",
let items = components.queryItems,
let codeItem = items.first(where: { $0.name == "code" }) {
return codeItem.value
} else {
return nil
}
}
}
5 changes: 5 additions & 0 deletions ImageFeed/Auth/AuthViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ final class AuthViewController: UIViewController {
if segue.identifier == showWebViewSegueIdentifier {
guard let webViewViewController = segue.destination as? WebViewViewController
else { fatalError("Failed to prepare for \(showWebViewSegueIdentifier)") }
let authHelper = AuthHelper()
let webViewPresenter = WebViewPresenter(authHelper: authHelper)
webViewViewController.presenter = webViewPresenter
webViewPresenter.view = webViewViewController
webViewViewController.delegate = self
} else {
super.prepare(for: segue, sender: sender)
Expand All @@ -38,3 +42,4 @@ extension AuthViewController: WebViewViewControllerDelegate {
}
}


47 changes: 47 additions & 0 deletions ImageFeed/Auth/WebViewPresenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// WebViewPresenter.swift
// ImageFeed
//
// Created by admin on 16.09.2023.
//

import Foundation

public protocol WebViewPresenterProtocol {
var view: WebViewViewControllerProtocol? { get set }
func viewDidLoad()
func didUpdateProgressValue(_ newValue: Double)
func code(from url: URL) -> String?
}

final class WebViewPresenter: WebViewPresenterProtocol {

weak var view: WebViewViewControllerProtocol?
var authHelper: AuthHelperProtocol

init(authHelper: AuthHelperProtocol) {
self.authHelper = authHelper
}

func viewDidLoad() {
let request = authHelper.authRequest()
view?.load(request: request)
didUpdateProgressValue(0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно использовать синтаксический сахар
didUpdateProgressValue(.zero)

}

func code(from url: URL) -> String? {
authHelper.code(from: url)
}

func didUpdateProgressValue(_ newValue: Double) {
let newProgressValue = Float(newValue)
view?.setProgressValue(newProgressValue)

let shouldHideProgress = shouldHideProgress(for: newProgressValue)
view?.setProgressHidden(shouldHideProgress)
}

func shouldHideProgress(for value: Float) -> Bool {
abs(value - 1.0) <= 0.0001
}
}
26 changes: 26 additions & 0 deletions ImageFeed/Auth/WebViewPresenterSpy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// WebViewPresenterSpy.swift
// ImageFeed
//
// Created by admin on 16.09.2023.
//

import ImageFeed
import Foundation

final class WebViewPresenterSpy: WebViewPresenterProtocol {
var viewDidLoadCalled: Bool = false
var view: WebViewViewControllerProtocol?

func viewDidLoad() {
viewDidLoadCalled = true
}

func didUpdateProgressValue(_ newValue: Double) {

}

func code(from url: URL) -> String? {
return nil
}
}
25 changes: 25 additions & 0 deletions ImageFeed/Auth/WebViewViewControllerSpy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// WebViewViewControllerSpy.swift
// ImageFeed
//
// Created by admin on 16.09.2023.
//

import ImageFeed
import Foundation
import UIKit

final class WebViewViewControllerSpy: UIViewController, WebViewViewControllerProtocol {
var loadRequestCalled: Bool = false
var presenter: ImageFeed.WebViewPresenterProtocol?

func load(request: URLRequest) {
loadRequestCalled = true
}

func setProgressValue(_ newValue: Float) {
}

func setProgressHidden(_ isHidden: Bool) {
}
}
10 changes: 9 additions & 1 deletion ImageFeed/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
<!--Web View View Controller-->
<scene sceneID="DSy-Ag-yen">
<objects>
<viewController id="mNl-Ee-A6h" customClass="WebViewViewController" customModule="ImageFeed" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="WebViewViewController" id="mNl-Ee-A6h" customClass="WebViewViewController" customModule="ImageFeed" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="oak-6D-1Vw">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
Expand All @@ -62,6 +62,9 @@
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
<wkPreferences key="preferences"/>
</wkWebViewConfiguration>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="accessibilityIdentifier" value="UnsplashWebView"/>
</userDefinedRuntimeAttributes>
</wkWebView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CQh-0L-ERS">
<rect key="frame" x="0.0" y="44" width="64" height="44"/>
Expand Down Expand Up @@ -121,6 +124,7 @@
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mzx-2O-dKO">
<rect key="frame" x="16" y="724" width="382" height="48"/>
<color key="backgroundColor" name="YP White"/>
<accessibility key="accessibilityConfiguration" identifier="Authenticate"/>
<constraints>
<constraint firstAttribute="height" constant="48" id="vNs-4e-NQc"/>
</constraints>
Expand Down Expand Up @@ -191,8 +195,12 @@
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="z2Z-pF-zIP">
<rect key="frame" x="356" y="4" width="42" height="42"/>
<accessibility key="accessibilityConfiguration" identifier="likeButton"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" image="like_button_on"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="accessibilityIdentifier" value="likeButton"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="likeButtonTapped:" destination="gB4-ew-bQb" eventType="touchUpInside" id="hn6-9T-P6a"/>
</connections>
Expand Down
Loading