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
262 changes: 262 additions & 0 deletions ImageFeed.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions ImageFeed/Auth/AuthHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// AuthHelper.swift
// ImageFeed
//
// Created by Юрий Гриневич on 10.08.2024.
//

import Foundation

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

final class AuthHelper: AuthHelperProtocol {
let configuration: AuthConfiguration

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

func authRequest() -> URLRequest? {
guard let url = authURL() else { return nil }

return URLRequest(url: url)
}

func authURL() -> URL? {
guard var urlComponents = URLComponents(string: configuration.authURLString) else {
return nil
}

urlComponents.queryItems = [
URLQueryItem(name: "client_id", value: configuration.accessKey),
URLQueryItem(name: "redirect_uri", value: configuration.redirectURI),
URLQueryItem(name: "response_type", value: "code"),
URLQueryItem(name: "scope", value: configuration.accessScope)
]

return urlComponents.url
}

func code(from url: URL) -> String? {
if let urlComponents = URLComponents(string: url.absoluteString),
urlComponents.path == "/oauth/authorize/native",
let items = urlComponents.queryItems,
let codeItem = items.first(where: { $0.name == "code" })
{
return codeItem.value
} else {
return nil
}
}
}
4 changes: 4 additions & 0 deletions ImageFeed/Auth/AuthViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ final class AuthViewController: UIViewController {

@objc private func didTapLoginButton() {
let vc = WebViewViewController()
let authHelper = AuthHelper()
let webViewPresenter = WebViewPresenter(authHelper: authHelper)
vc.presenter = webViewPresenter
webViewPresenter.view = vc
vc.delegate = self
navigationController?.pushViewController(vc, animated: true)
}
Expand Down
48 changes: 48 additions & 0 deletions ImageFeed/Auth/WebViewPresenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// WebViewPresenter.swift
// ImageFeed
//
// Created by Юрий Гриневич on 09.08.2024.
//

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() {
guard let request = authHelper.authRequest() else { return }

view?.load(request: request)
didUpdateProgressValue(0)
}

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

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

func shouldHideProgress(for value: Float) -> Bool {
abs(value - 1.0) <= 0.0001
}

func code(from url: URL) -> String? {
authHelper.code(from: url)
}
}
52 changes: 21 additions & 31 deletions ImageFeed/Auth/WebViewViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@
import UIKit
import WebKit

public protocol WebViewViewControllerProtocol: AnyObject {
var presenter: WebViewPresenterProtocol? { get set }
func load(request: URLRequest)
func setProgressValue(_ newValue: Float)
func setProgressHidden(_ isHidden: Bool)
}

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

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

weak var delegate: WebViewViewControllerDelegate?
var presenter: WebViewPresenterProtocol?

private let progressBar: UIProgressView = {
let bar = UIProgressView()
Expand All @@ -26,6 +34,7 @@ final class WebViewViewController: UIViewController {

private let webView: WKWebView = {
let web = WKWebView()
web.accessibilityIdentifier = "UnsplashWebView"
web.translatesAutoresizingMaskIntoConstraints = false
return web
}()
Expand All @@ -35,41 +44,27 @@ final class WebViewViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
loadAuthView()
presenter?.viewDidLoad()

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

private func loadAuthView() {
guard var urlComponents = URLComponents(string: Constants.unsplashAuthorizeURLString) else {
return
}

urlComponents.queryItems = [
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)
]

guard let url = urlComponents.url else {
print("Function: \(#function), line \(#line) Failed to get URL")
return
}

let request = URLRequest(url: url)
func load(request: URLRequest) {
webView.load(request)
}

private func updateProgress() {
progressBar.setProgress(Float(webView.estimatedProgress), animated: true)
progressBar.isHidden = fabs(webView.estimatedProgress - 1.0) <= 0.0001
func setProgressValue(_ newValue: Float) {
progressBar.setProgress(newValue, animated: true)
}

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


Expand Down Expand Up @@ -105,13 +100,8 @@ extension WebViewViewController: WKNavigationDelegate {
}

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
if let url = navigationAction.request.url {
return presenter?.code(from: url)

} else {
return nil
Expand Down
27 changes: 27 additions & 0 deletions ImageFeed/Helpers/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,30 @@ enum Constants {
static let unsplashAuthorizeURLString = "https://unsplash.com/oauth/authorize"
static let tokenURL = "https://unsplash.com/oauth/token"
}

struct AuthConfiguration {
let accessKey: String
let secretKey: String
let redirectURI: String
let accessScope: String
let defaultBaseURL: URL
let authURLString: String

init(accessKey: String, secretKey: String, redirectURI: String, accessScope: String, authURLString: String, defaultBaseURL: URL) {
self.accessKey = accessKey
self.secretKey = secretKey
self.redirectURI = redirectURI
self.accessScope = accessScope
self.defaultBaseURL = defaultBaseURL
self.authURLString = authURLString
}

static var standard: AuthConfiguration {
return AuthConfiguration(accessKey: Constants.accessKey,
secretKey: Constants.secretKey,
redirectURI: Constants.redirectURI,
accessScope: Constants.accessScope,
authURLString: Constants.unsplashAuthorizeURLString,
defaultBaseURL: Constants.defaultBaseURL)
}
}
3 changes: 2 additions & 1 deletion ImageFeed/ImagesList/ImagesListCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ final class ImagesListCell: UITableViewCell {

private let likeButton: UIButton = {
let button = UIButton()
button.accessibilityIdentifier = "LikeButton"
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
Expand Down Expand Up @@ -64,7 +65,7 @@ final class ImagesListCell: UITableViewCell {
configureUI()
}

override func prepareForReuse() {
override func prepareForReuse() {
super.prepareForReuse()
imageFeed.kf.cancelDownloadTask()
}
Expand Down
80 changes: 80 additions & 0 deletions ImageFeed/ImagesList/ImagesListPresenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// ImageListPresenter.swift
// ImageFeed
//
// Created by Юрий Гриневич on 14.08.2024.
//

import Foundation

protocol ImagesListPresenterProtocol {
var view: ImagesListViewControllerProtocol? { get set }
var photos: [Photo] { get set }
func viewDidLoad()
func willDisplay(for indexPath: IndexPath)
func imageListCellDidTapLike(_ cell: ImagesListCell, indexPath: IndexPath)
func updateTableViewAnimated()
}

final class ImagesListPresenter: ImagesListPresenterProtocol {

weak var view: ImagesListViewControllerProtocol?

private let imagesListService = ImagesListService.shared
private var imagesListServiceObserver: NSObjectProtocol?

var photos: [Photo] = []

func viewDidLoad() {
imagesListService.fetchPhotosNextPage()
setupNotifications()
}

func updateTableViewAnimated() {
let oldCount = photos.count
let newCount = imagesListService.photos.count
photos = imagesListService.photos
if oldCount != newCount {
let indexPaths = (oldCount..<newCount).map { i in
IndexPath(row: i, section: 0)
}
view?.updateTableViewAnimated(indexPaths: indexPaths)
}
}

func willDisplay(for indexPath: IndexPath) {
if indexPath.row == photos.count - 1 {
imagesListService.fetchPhotosNextPage()
}
}

func imageListCellDidTapLike(_ cell: ImagesListCell, indexPath: IndexPath) {
let photo = photos[indexPath.row]

view?.showBlockingHud()

imagesListService.changeLike(photoId: photo.id, isLike: !photo.isLiked) { [weak self] result in
guard let self else { return }
switch result {
case .success:
self.photos = self.imagesListService.photos
cell.setIsLiked(isLiked: self.photos[indexPath.row].isLiked)

view?.dismissBlockingHud()
case .failure:
print("Function: \(#function), line \(#line) Failed to change Like")
view?.showBlockingHud()
}
}
Comment on lines +56 to +68

Choose a reason for hiding this comment

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

Неточности в реализации MVP. Presenter знает о ImagesListCell, умеет настраивать ячейку. Это обязанность View. Presenter не должен знать ничего о конкретной реализации UI.

}

private func setupNotifications() {
imagesListServiceObserver = NotificationCenter.default
.addObserver(
forName: ImagesListService.didChangeNotification,
object: nil,
queue: .main) { [weak self] _ in
self?.updateTableViewAnimated()
}
}
}
Loading