From 77b861249deb19156eb9652a9aa110a61163dd79 Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Sun, 22 Nov 2020 21:11:10 +0100 Subject: [PATCH 01/16] Added photo size parameter for photo fetching --- .../project.pbxproj | 4 +++ .../Scenes/Browser/BrowserInteractor.swift | 7 ++-- .../Scenes/Browser/BrowserModels.swift | 1 + .../Browser/BrowserViewController.swift | 33 ++++++++++--------- .../Photos/FlickrPhotoURLResolver.swift | 19 +++++++++-- .../Flickr/FlickrCollectionFetcher.swift | 22 +++++++------ .../PhotoCollectionFetching.swift | 1 + .../PhotoCollectionFetcher/PhotoSize.swift | 10 ++++++ 8 files changed, 66 insertions(+), 31 deletions(-) create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoSize.swift diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj index ce1fcd9..ef3ef68 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 62DE971FF8AB9DC74BA9B87C /* FlickrApiValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE987D3B885CA1A25FB693 /* FlickrApiValues.swift */; }; 62DE97B6EC739349BC9A6D2F /* FlickrPhotoServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE97953D19EE0C13151316 /* FlickrPhotoServiceTests.swift */; }; 62DE97CDCFCD2718C1BBC1E8 /* PhotoCollectionFetcherFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9A0F5F5E76B6AFE7159B /* PhotoCollectionFetcherFactory.swift */; }; + 62DE9823B2E92F91763DBF97 /* PhotoSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE91478FF013F59D0F522A /* PhotoSize.swift */; }; 62DE983EF92FD18305C80488 /* BrowserCellInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE997B53C61FE26593A99A /* BrowserCellInteractor.swift */; }; 62DE9866170F4BCE2B93E2E0 /* MockBrowserPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE944604E007DD383ABDE1 /* MockBrowserPresenter.swift */; }; 62DE98D6E300CD17DD7D23A3 /* BrowserCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9648DCA09C364860E8BB /* BrowserCellView.swift */; }; @@ -76,6 +77,7 @@ 62DE900EADCC52560B246392 /* BrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = ""; }; 62DE90B754DC827D73A31B9E /* PhotoCollectionFetching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionFetching.swift; sourceTree = ""; }; 62DE91396D5EDB77544792D3 /* ConfigurableBrowserCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurableBrowserCell.swift; sourceTree = ""; }; + 62DE91478FF013F59D0F522A /* PhotoSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoSize.swift; sourceTree = ""; }; 62DE915A672C1C41DCCF9C8D /* BrowserPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserPresenterTests.swift; sourceTree = ""; }; 62DE91C1A5EBF975C64A37B3 /* MockPhotoDataCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataCache.swift; sourceTree = ""; }; 62DE9225FFEBB4968F60A987 /* MockFlickrPhotoService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFlickrPhotoService.swift; sourceTree = ""; }; @@ -179,6 +181,7 @@ 62DE90B754DC827D73A31B9E /* PhotoCollectionFetching.swift */, 62DE9A0F5F5E76B6AFE7159B /* PhotoCollectionFetcherFactory.swift */, 62DE9F3D2015C40F75DEED96 /* Flickr */, + 62DE91478FF013F59D0F522A /* PhotoSize.swift */, ); path = PhotoCollectionFetcher; sourceTree = ""; @@ -600,6 +603,7 @@ 62DE966944A50351E4087F92 /* FlickrPhotosService.swift in Sources */, 62DE971FF8AB9DC74BA9B87C /* FlickrApiValues.swift in Sources */, 62DE95BAA18D1A52C35EF040 /* FlickrPhotos.swift in Sources */, + 62DE9823B2E92F91763DBF97 /* PhotoSize.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserInteractor.swift index fe631c8..1078266 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserInteractor.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserInteractor.swift @@ -28,9 +28,10 @@ final class BrowserInteractor: BrowserInteracting { currentRequest = request photoCollectionFetcher.fetchPhotos( - startingFrom: request.startFromPosition, - fetchAtMost: request.fetchAtMost, - matching: request.searchCriteria + startingFrom: request.startFromPosition, + fetchAtMost: request.fetchAtMost, + matching: request.searchCriteria, + withSize: request.size ) { result in switch result { case let .success(photos): diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserModels.swift index 71cf0e9..1e43eae 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserModels.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserModels.swift @@ -9,6 +9,7 @@ struct BrowserModels { let startFromPosition: Int let fetchAtMost: Int let searchCriteria: String + let size: PhotoSize } struct Response { diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift index 4b70c5e..e92206c 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift @@ -22,6 +22,7 @@ private struct LayoutConstants { final class BrowserViewController: UIViewController { private let photosPerFetchRequest = LayoutConstants.itemsPerRow * 15 + private let photoSize = PhotoSize.thumbSquare private let interactor: BrowserInteracting private let dataSource: BrowserDataSourcing @@ -39,10 +40,10 @@ final class BrowserViewController: UIViewController { let layout = UICollectionViewFlowLayout() layout.sectionInset = UIEdgeInsets( - top: LayoutConstants.padding, - left: LayoutConstants.padding, - bottom: LayoutConstants.padding, - right: LayoutConstants.padding + top: LayoutConstants.padding, + left: LayoutConstants.padding, + bottom: LayoutConstants.padding, + right: LayoutConstants.padding ) layout.minimumInteritemSpacing = LayoutConstants.interitemSpacing @@ -121,21 +122,23 @@ final class BrowserViewController: UIViewController { func requestMorePhotos() { interactor.fetch( - photos: BrowserModels.Photos.Request( - startFromPosition: dataSource.photoCount, - fetchAtMost: photosPerFetchRequest, - searchCriteria: searchController.searchBar.text ?? "" - ) + photos: BrowserModels.Photos.Request( + startFromPosition: dataSource.photoCount, + fetchAtMost: photosPerFetchRequest, + searchCriteria: searchController.searchBar.text ?? "", + size: photoSize + ) ) } func requestNewPhotos() { interactor.fetch( - photos: BrowserModels.Photos.Request( - startFromPosition: 0, - fetchAtMost: photosPerFetchRequest, - searchCriteria: searchController.searchBar.text ?? "" - ) + photos: BrowserModels.Photos.Request( + startFromPosition: 0, + fetchAtMost: photosPerFetchRequest, + searchCriteria: searchController.searchBar.text ?? "", + size: photoSize + ) ) } } @@ -157,7 +160,7 @@ extension BrowserViewController: BrowserDisplaying { dataSource.add(photos: photos.photos) - collectionView.insertItems(at: (0 ..< photos.photos.count).map { IndexPath(item: $0 + currentPhotoCount, section: 0) }) + collectionView.insertItems(at: (0.. URL { - guard let url = URL(string: "https://farm\(photo.farm).staticflickr.com/\(photo.server)/\(photo.id)_\(photo.secret)_q.jpg") - else { + static func resolveUrl(for photo: FlickrPhoto, withSize size: PhotoSize) -> URL { + let sizeSuffix = getSizeSuffix(for: size) + + guard let url = URL(string: "https://farm\(photo.farm).staticflickr.com/\(photo.server)/\(photo.id)_\(photo.secret)_\(sizeSuffix).jpg") + else { fatalError("Failed to resolve flickr photo url (input photo: \(photo))") } return url } + + private static func getSizeSuffix(for size: PhotoSize) -> String { + switch (size) { + case .thumbSquare: + return "q" + case .medium: + return "c" + case .large: + return "b" + } + } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift index 1c84205..886977c 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift @@ -12,37 +12,39 @@ final class FlickrCollectionFetcher: PhotoCollectionFetching { func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, + withSize size: PhotoSize, completion: @escaping Completion) { - fetchPhotos(startingFrom: position, fetchAtMost: maxFetchCount, matching: nil, completion: completion) + fetchPhotos(startingFrom: position, fetchAtMost: maxFetchCount, matching: nil, withSize: size, completion: completion) } func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, matching searchCriteria: String?, + withSize size: PhotoSize, completion: @escaping Completion) { let page = position / maxFetchCount + 1 if let searchCriteria = searchCriteria, !searchCriteria.isEmpty { flickrPhotosService.search( - matching: searchCriteria, - page: page, - photosPerPage: maxFetchCount, - completion: transformFetchResult(completion) + matching: searchCriteria, + page: page, + photosPerPage: maxFetchCount, + completion: transformFetchResult(withSize: size, completion) ) } else { flickrPhotosService.getRecent( - page: page, - photosPerPage: maxFetchCount, - completion: transformFetchResult(completion) + page: page, + photosPerPage: maxFetchCount, + completion: transformFetchResult(withSize: size, completion) ) } } - private func transformFetchResult(_ completion: @escaping Completion) -> FlickrPhotosService.Completion { + private func transformFetchResult(withSize size: PhotoSize, _ completion: @escaping Completion) -> FlickrPhotosService.Completion { { result in switch result { case let .success(photos): - completion(.success(photos.map { Photo(id: $0.id, imageURL: FlickrPhotoURLResolver.resolveUrl(for: $0)) })) + completion(.success(photos.map { Photo(id: $0.id, imageURL: FlickrPhotoURLResolver.resolveUrl(for: $0, withSize: size)) })) case let .failure(error): completion(.failure(error)) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift index 0d9ad18..bab29a0 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift @@ -9,5 +9,6 @@ protocol PhotoCollectionFetching { func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, matching searchCriteria: String?, + withSize size: PhotoSize, completion: @escaping Completion) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoSize.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoSize.swift new file mode 100644 index 0000000..937d1c6 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoSize.swift @@ -0,0 +1,10 @@ +// +// Created by Maxim Berezhnoy on 22/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +enum PhotoSize { + case thumbSquare + case medium + case large +} \ No newline at end of file From 9b29882dac5da2df77b3cce17feb743eeeadacca Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Sun, 22 Nov 2020 21:46:14 +0100 Subject: [PATCH 02/16] Added meatadata to photo fetching services; added VIP stubs for Feed scene --- .../project.pbxproj | 32 ++++++++++++++++--- .../Scenes/Browser/BrowserInteractor.swift | 3 +- .../Scenes/Feed/FeedInteractor.swift | 5 +++ .../Scenes/Feed/FeedModels.swift | 5 +++ .../Scenes/Feed/FeedPresenter.swift | 5 +++ .../Scenes/Feed/FeedViewController.swift | 5 +++ .../FlickrApiServices/FlickrApiValues.swift | 14 ++++++++ .../Photos/FlickrPhotoURLResolver.swift | 10 +++--- .../Photos/FlickrPhotosService.swift | 25 ++++++++++++--- .../Flickr/FlickrCollectionFetcher.swift | 6 +++- .../PhotoCollectionFetching.swift | 4 ++- ...{PhotoSize.swift => PhotoParameters.swift} | 7 ++++ 12 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedPresenter.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift rename SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/{PhotoSize.swift => PhotoParameters.swift} (65%) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj index ef3ef68..4007fa9 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 62DE90F40E919CCFF876D1A6 /* MockFlickrPhotoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9225FFEBB4968F60A987 /* MockFlickrPhotoService.swift */; }; 62DE911E1EB4D78BCA9085CB /* FlickrCollectionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9A2EBADCA5EE9059C3DD /* FlickrCollectionFetcherTests.swift */; }; 62DE91E5B84C14466618EAC1 /* ConfigurableBrowserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE91396D5EDB77544792D3 /* ConfigurableBrowserCell.swift */; }; + 62DE91F2D0080AF13460E47C /* FeedInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9DB2850E842E22C4CC84 /* FeedInteractor.swift */; }; 62DE91F96D06BDB6AE8D40A1 /* XCTestCase+QueueAwait.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE924CEDF37B8D5BC86D48 /* XCTestCase+QueueAwait.swift */; }; 62DE927526937FB6BEDCE8BC /* MockBrowserCellPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9B3E9AAA8D0661DEEFFE /* MockBrowserCellPresenter.swift */; }; 62DE92A1A0D21DCC9FC97F7E /* NetworkPhotoDataRetriever.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9868FFC49BF89E9F9192 /* NetworkPhotoDataRetriever.swift */; }; @@ -27,13 +28,14 @@ 62DE946CFE4B0C1C5B9E1324 /* MockBrowserDisplaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9810EDB0220E74893A89 /* MockBrowserDisplaying.swift */; }; 62DE959CA829F11A51767E30 /* FlickrCollectionFetcherFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9B8BC72A4AB8A5A34B65 /* FlickrCollectionFetcherFactory.swift */; }; 62DE95BAA18D1A52C35EF040 /* FlickrPhotos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE96CE6B44327361D73D24 /* FlickrPhotos.swift */; }; + 62DE96046383936ED2BDE433 /* FeedModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9D31EAF1CFCCDE85B8DE /* FeedModels.swift */; }; 62DE966944A50351E4087F92 /* FlickrPhotosService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE954BE0BFF3D2EE91DE12 /* FlickrPhotosService.swift */; }; + 62DE96C60A976276F0E3D60E /* FeedPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9673FC376E2BC90087D6 /* FeedPresenter.swift */; }; 62DE971066062BAA9D8C9AE4 /* FlickrPhotoURLResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE986497C5FB223DA10256 /* FlickrPhotoURLResolver.swift */; }; 62DE97140AE23BE5CD384EF0 /* MockHttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9E19BC1900DC8648FD5E /* MockHttpClient.swift */; }; 62DE971FF8AB9DC74BA9B87C /* FlickrApiValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE987D3B885CA1A25FB693 /* FlickrApiValues.swift */; }; 62DE97B6EC739349BC9A6D2F /* FlickrPhotoServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE97953D19EE0C13151316 /* FlickrPhotoServiceTests.swift */; }; 62DE97CDCFCD2718C1BBC1E8 /* PhotoCollectionFetcherFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9A0F5F5E76B6AFE7159B /* PhotoCollectionFetcherFactory.swift */; }; - 62DE9823B2E92F91763DBF97 /* PhotoSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE91478FF013F59D0F522A /* PhotoSize.swift */; }; 62DE983EF92FD18305C80488 /* BrowserCellInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE997B53C61FE26593A99A /* BrowserCellInteractor.swift */; }; 62DE9866170F4BCE2B93E2E0 /* MockBrowserPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE944604E007DD383ABDE1 /* MockBrowserPresenter.swift */; }; 62DE98D6E300CD17DD7D23A3 /* BrowserCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9648DCA09C364860E8BB /* BrowserCellView.swift */; }; @@ -47,6 +49,7 @@ 62DE9C2CEA5B44E24BB2D6F5 /* PhotoCollectionFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE90B754DC827D73A31B9E /* PhotoCollectionFetching.swift */; }; 62DE9C8EC1EBD9B055EF5388 /* BrowserViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9817F61216E36044EA42 /* BrowserViewDataSource.swift */; }; 62DE9CBE1FB6013A700EB635 /* BrowserCellPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9591CAA01E3F3C91A977 /* BrowserCellPresenter.swift */; }; + 62DE9CD65849E14B67A5AF42 /* PhotoParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9031E5EEBEA8EBA4D644 /* PhotoParameters.swift */; }; 62DE9CE6366FD6ECE740C55C /* FlickrCollectionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C90249CCE2FAF835CF5 /* FlickrCollectionFetcher.swift */; }; 62DE9E63C0B530594A5B150B /* Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C8D791DA5418ED32FCF /* Style.swift */; }; 62DE9EBE33EDC3D97D929FF2 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE970A3FD09743A03DAF33 /* Photo.swift */; }; @@ -55,6 +58,7 @@ 62DE9F5B37C5FE4BED753FB2 /* PhotoDataNSCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9805D6D4DBDDBAD078E2 /* PhotoDataNSCache.swift */; }; 62DE9F69E85302202E6884B3 /* PhotoDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9FF6B55060E5204AC907 /* PhotoDataProvider.swift */; }; 62DE9FA335C6F1E04D690818 /* BrowserInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9EA9D6ACC68E32F8A681 /* BrowserInteractorTests.swift */; }; + 62DE9FAF86BD3911FC84BF72 /* FeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C09B259CEA839DAE2B9 /* FeedViewController.swift */; }; 62DE9FDF59C5BAB0F8D43494 /* PhotoDataCaching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9B49CABD228CCFBCE936 /* PhotoDataCaching.swift */; }; 944AC01E23CE0E9C009FD611 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944AC01D23CE0E9C009FD611 /* AppDelegate.swift */; }; 944AC02723CE0E9E009FD611 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 944AC02623CE0E9E009FD611 /* Assets.xcassets */; }; @@ -75,9 +79,9 @@ /* Begin PBXFileReference section */ 62DE9001FE62BE27C3D938B3 /* MockPhotoCollectionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoCollectionFetcher.swift; sourceTree = ""; }; 62DE900EADCC52560B246392 /* BrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = ""; }; + 62DE9031E5EEBEA8EBA4D644 /* PhotoParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoParameters.swift; sourceTree = ""; }; 62DE90B754DC827D73A31B9E /* PhotoCollectionFetching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionFetching.swift; sourceTree = ""; }; 62DE91396D5EDB77544792D3 /* ConfigurableBrowserCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurableBrowserCell.swift; sourceTree = ""; }; - 62DE91478FF013F59D0F522A /* PhotoSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoSize.swift; sourceTree = ""; }; 62DE915A672C1C41DCCF9C8D /* BrowserPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserPresenterTests.swift; sourceTree = ""; }; 62DE91C1A5EBF975C64A37B3 /* MockPhotoDataCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataCache.swift; sourceTree = ""; }; 62DE9225FFEBB4968F60A987 /* MockFlickrPhotoService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFlickrPhotoService.swift; sourceTree = ""; }; @@ -91,6 +95,7 @@ 62DE954BE0BFF3D2EE91DE12 /* FlickrPhotosService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotosService.swift; sourceTree = ""; }; 62DE9591CAA01E3F3C91A977 /* BrowserCellPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellPresenter.swift; sourceTree = ""; }; 62DE9648DCA09C364860E8BB /* BrowserCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellView.swift; sourceTree = ""; }; + 62DE9673FC376E2BC90087D6 /* FeedPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedPresenter.swift; sourceTree = ""; }; 62DE969E8F3F30D004171974 /* BrowserCellModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellModels.swift; sourceTree = ""; }; 62DE96B5EBB14213DC9E7804 /* MockPhotoDataRetriever.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataRetriever.swift; sourceTree = ""; }; 62DE96CE6B44327361D73D24 /* FlickrPhotos.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotos.swift; sourceTree = ""; }; @@ -112,11 +117,14 @@ 62DE9B3E9AAA8D0661DEEFFE /* MockBrowserCellPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBrowserCellPresenter.swift; sourceTree = ""; }; 62DE9B49CABD228CCFBCE936 /* PhotoDataCaching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataCaching.swift; sourceTree = ""; }; 62DE9B8BC72A4AB8A5A34B65 /* FlickrCollectionFetcherFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcherFactory.swift; sourceTree = ""; }; + 62DE9C09B259CEA839DAE2B9 /* FeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = ""; }; 62DE9C85502219A9858DCF22 /* PhotoDataProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProviderTests.swift; sourceTree = ""; }; 62DE9C8D791DA5418ED32FCF /* Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Style.swift; sourceTree = ""; }; 62DE9C90249CCE2FAF835CF5 /* FlickrCollectionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcher.swift; sourceTree = ""; }; 62DE9CC652A43A976EDE4FF6 /* BrowserFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserFactory.swift; sourceTree = ""; }; + 62DE9D31EAF1CFCCDE85B8DE /* FeedModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedModels.swift; sourceTree = ""; }; 62DE9D790ECF920DF2C9802B /* FlickrApiURLBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrApiURLBuilder.swift; sourceTree = ""; }; + 62DE9DB2850E842E22C4CC84 /* FeedInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedInteractor.swift; sourceTree = ""; }; 62DE9E19BC1900DC8648FD5E /* MockHttpClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockHttpClient.swift; sourceTree = ""; }; 62DE9E2159757D6020248C32 /* BrowserInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserInteractor.swift; sourceTree = ""; }; 62DE9E652D625ECC302D66F9 /* BrowserModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserModels.swift; sourceTree = ""; }; @@ -175,13 +183,24 @@ path = Scenes; sourceTree = ""; }; + 62DE923C7B6825D8B8E3001A /* Feed */ = { + isa = PBXGroup; + children = ( + 62DE9C09B259CEA839DAE2B9 /* FeedViewController.swift */, + 62DE9673FC376E2BC90087D6 /* FeedPresenter.swift */, + 62DE9DB2850E842E22C4CC84 /* FeedInteractor.swift */, + 62DE9D31EAF1CFCCDE85B8DE /* FeedModels.swift */, + ); + path = Feed; + sourceTree = ""; + }; 62DE932EFD760F664BE020DD /* PhotoCollectionFetcher */ = { isa = PBXGroup; children = ( 62DE90B754DC827D73A31B9E /* PhotoCollectionFetching.swift */, 62DE9A0F5F5E76B6AFE7159B /* PhotoCollectionFetcherFactory.swift */, 62DE9F3D2015C40F75DEED96 /* Flickr */, - 62DE91478FF013F59D0F522A /* PhotoSize.swift */, + 62DE9031E5EEBEA8EBA4D644 /* PhotoParameters.swift */, ); path = PhotoCollectionFetcher; sourceTree = ""; @@ -451,6 +470,7 @@ children = ( 944AC04523CFA643009FD611 /* Browser */, 62DE9C8D791DA5418ED32FCF /* Style.swift */, + 62DE923C7B6825D8B8E3001A /* Feed */, ); path = Scenes; sourceTree = ""; @@ -603,7 +623,11 @@ 62DE966944A50351E4087F92 /* FlickrPhotosService.swift in Sources */, 62DE971FF8AB9DC74BA9B87C /* FlickrApiValues.swift in Sources */, 62DE95BAA18D1A52C35EF040 /* FlickrPhotos.swift in Sources */, - 62DE9823B2E92F91763DBF97 /* PhotoSize.swift in Sources */, + 62DE9FAF86BD3911FC84BF72 /* FeedViewController.swift in Sources */, + 62DE96C60A976276F0E3D60E /* FeedPresenter.swift in Sources */, + 62DE91F2D0080AF13460E47C /* FeedInteractor.swift in Sources */, + 62DE96046383936ED2BDE433 /* FeedModels.swift in Sources */, + 62DE9CD65849E14B67A5AF42 /* PhotoParameters.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserInteractor.swift index 1078266..abce38c 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserInteractor.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserInteractor.swift @@ -31,7 +31,8 @@ final class BrowserInteractor: BrowserInteracting { startingFrom: request.startFromPosition, fetchAtMost: request.fetchAtMost, matching: request.searchCriteria, - withSize: request.size + withSize: request.size, + includeMetadata: [] ) { result in switch result { case let .success(photos): diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift new file mode 100644 index 0000000..df59ecc --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift @@ -0,0 +1,5 @@ +// +// Created by Maxim Berezhnoy on 22/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift new file mode 100644 index 0000000..df59ecc --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift @@ -0,0 +1,5 @@ +// +// Created by Maxim Berezhnoy on 22/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedPresenter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedPresenter.swift new file mode 100644 index 0000000..df59ecc --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedPresenter.swift @@ -0,0 +1,5 @@ +// +// Created by Maxim Berezhnoy on 22/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift new file mode 100644 index 0000000..df59ecc --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift @@ -0,0 +1,5 @@ +// +// Created by Maxim Berezhnoy on 22/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/FlickrApiValues.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/FlickrApiValues.swift index b263a14..3f3d7ab 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/FlickrApiValues.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/FlickrApiValues.swift @@ -20,5 +20,19 @@ enum FlickrApiValues { case format case text case noJsonCallback = "nojsoncallback" + case extras + } + + enum PhotoSizeSuffix: String { + case thumbSquare = "q" + case medium = "c" + case large = "b" + } + + enum PhotoMetadata: String { + case views + case tags + case ownerName = "owner_name" + case dateTaken = "date_taken" } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotoURLResolver.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotoURLResolver.swift index 84184ba..1fedca0 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotoURLResolver.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotoURLResolver.swift @@ -9,7 +9,7 @@ final class FlickrPhotoURLResolver { static func resolveUrl(for photo: FlickrPhoto, withSize size: PhotoSize) -> URL { let sizeSuffix = getSizeSuffix(for: size) - guard let url = URL(string: "https://farm\(photo.farm).staticflickr.com/\(photo.server)/\(photo.id)_\(photo.secret)_\(sizeSuffix).jpg") + guard let url = URL(string: "https://farm\(photo.farm).staticflickr.com/\(photo.server)/\(photo.id)_\(photo.secret)_\(sizeSuffix.rawValue).jpg") else { fatalError("Failed to resolve flickr photo url (input photo: \(photo))") } @@ -17,14 +17,14 @@ final class FlickrPhotoURLResolver { return url } - private static func getSizeSuffix(for size: PhotoSize) -> String { + private static func getSizeSuffix(for size: PhotoSize) -> FlickrApiValues.PhotoSizeSuffix { switch (size) { case .thumbSquare: - return "q" + return .thumbSquare case .medium: - return "c" + return .medium case .large: - return "b" + return .large } } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift index 478e376..8f97afd 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift @@ -8,9 +8,9 @@ import Foundation protocol FlickrPhotosFetching { typealias Completion = (Result<[FlickrPhoto], Error>) -> Void - func getRecent(page: Int, photosPerPage: Int, completion: @escaping Completion) + func getRecent(page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoMetadata], completion: @escaping Completion) - func search(matching text: String, page: Int, photosPerPage: Int, completion: @escaping Completion) + func search(matching text: String, page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoMetadata], completion: @escaping Completion) } final class FlickrPhotosService: FlickrPhotosFetching { @@ -22,7 +22,7 @@ final class FlickrPhotosService: FlickrPhotosFetching { self.httpClient = httpClient } - func getRecent(page: Int, photosPerPage: Int, completion: @escaping Completion) { + func getRecent(page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoMetadata], completion: @escaping Completion) { let url = FlickrApiURLResolver.build( method: .photosGetRecent, apiKey: apiKey, @@ -35,7 +35,7 @@ final class FlickrPhotosService: FlickrPhotosFetching { fetchFrom(url: url, completion: completion) } - func search(matching text: String, page: Int, photosPerPage: Int, completion: @escaping Completion) { + func search(matching text: String, page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoMetadata], completion: @escaping Completion) { let url = FlickrApiURLResolver.build( method: .photosSearch, apiKey: apiKey, @@ -43,6 +43,8 @@ final class FlickrPhotosService: FlickrPhotosFetching { .text: text, .page: String(page), .perPage: String(photosPerPage), + // todo: write test for this + .extras: getQueryParameters(for: metadata).map { $0.rawValue }.joined(separator: ",") ] ) @@ -79,4 +81,19 @@ final class FlickrPhotosService: FlickrPhotosFetching { return .failure(error) } } + + private func getQueryParameters(for metadata: [PhotoMetadata]) -> [FlickrApiValues.PhotoMetadata] { + metadata.map { + switch ($0) { + case .views: + return .views + case .tags: + return .tags + case .ownerName: + return .ownerName + case .dateTaken: + return .dateTaken + } + } + } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift index 886977c..4f0f728 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift @@ -13,14 +13,16 @@ final class FlickrCollectionFetcher: PhotoCollectionFetching { func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, withSize size: PhotoSize, + includeMetadata metadata: [PhotoMetadata], completion: @escaping Completion) { - fetchPhotos(startingFrom: position, fetchAtMost: maxFetchCount, matching: nil, withSize: size, completion: completion) + fetchPhotos(startingFrom: position, fetchAtMost: maxFetchCount, matching: nil, withSize: size, includeMetadata: metadata, completion: completion) } func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, matching searchCriteria: String?, withSize size: PhotoSize, + includeMetadata metadata: [PhotoMetadata], completion: @escaping Completion) { let page = position / maxFetchCount + 1 @@ -29,12 +31,14 @@ final class FlickrCollectionFetcher: PhotoCollectionFetching { matching: searchCriteria, page: page, photosPerPage: maxFetchCount, + includeMetadata: metadata, completion: transformFetchResult(withSize: size, completion) ) } else { flickrPhotosService.getRecent( page: page, photosPerPage: maxFetchCount, + includeMetadata: metadata, completion: transformFetchResult(withSize: size, completion) ) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift index bab29a0..1016563 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift @@ -10,5 +10,7 @@ protocol PhotoCollectionFetching { fetchAtMost maxFetchCount: Int, matching searchCriteria: String?, withSize size: PhotoSize, - completion: @escaping Completion) + includeMetadata metadata: [PhotoMetadata], + completion: @escaping Completion + ) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoSize.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift similarity index 65% rename from SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoSize.swift rename to SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift index 937d1c6..6b118cf 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoSize.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift @@ -7,4 +7,11 @@ enum PhotoSize { case thumbSquare case medium case large +} + +enum PhotoMetadata { + case views + case tags + case ownerName + case dateTaken } \ No newline at end of file From 304dd594875401d2eb1f77d61caba0c1b9d7434f Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Tue, 24 Nov 2020 23:58:10 +0100 Subject: [PATCH 03/16] Basic Feed VIP implementation and factories --- .../project.pbxproj | 16 ++++ .../SimpleFlickrBrowser/AppDelegate.swift | 5 +- .../SimpleFlickrBrowser/Models/Photo.swift | 9 +++ .../RootDependencies.swift | 9 +++ .../Scenes/Browser/BrowserModels.swift | 2 +- .../Browser/BrowserViewController.swift | 2 +- .../Scenes/Feed/FeedFactory.swift | 29 +++++++ .../Scenes/Feed/FeedInteractor.swift | 50 ++++++++++++ .../Scenes/Feed/FeedModels.swift | 20 +++++ .../Scenes/Feed/FeedPresenter.swift | 17 ++++ .../Scenes/Feed/FeedViewController.swift | 81 +++++++++++++++++++ .../Scenes/Feed/FeedViewDataSource.swift | 43 ++++++++++ .../Photos/FlickrPhotoURLResolver.swift | 4 +- .../Photos/FlickrPhotosService.swift | 11 ++- .../Flickr/FlickrCollectionFetcher.swift | 13 +-- .../PhotoCollectionFetching.swift | 4 +- .../PhotoParameters.swift | 22 ++--- .../Photos/FlickrPhotoServiceTests.swift | 4 +- 18 files changed, 309 insertions(+), 32 deletions(-) create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj index 4007fa9..307dac7 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ 62DE971066062BAA9D8C9AE4 /* FlickrPhotoURLResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE986497C5FB223DA10256 /* FlickrPhotoURLResolver.swift */; }; 62DE97140AE23BE5CD384EF0 /* MockHttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9E19BC1900DC8648FD5E /* MockHttpClient.swift */; }; 62DE971FF8AB9DC74BA9B87C /* FlickrApiValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE987D3B885CA1A25FB693 /* FlickrApiValues.swift */; }; + 62DE97615F6230CBFF9F92A4 /* FeedFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE96216E4D360CAB68AF75 /* FeedFactory.swift */; }; 62DE97B6EC739349BC9A6D2F /* FlickrPhotoServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE97953D19EE0C13151316 /* FlickrPhotoServiceTests.swift */; }; 62DE97CDCFCD2718C1BBC1E8 /* PhotoCollectionFetcherFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9A0F5F5E76B6AFE7159B /* PhotoCollectionFetcherFactory.swift */; }; 62DE983EF92FD18305C80488 /* BrowserCellInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE997B53C61FE26593A99A /* BrowserCellInteractor.swift */; }; @@ -51,6 +52,7 @@ 62DE9CBE1FB6013A700EB635 /* BrowserCellPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9591CAA01E3F3C91A977 /* BrowserCellPresenter.swift */; }; 62DE9CD65849E14B67A5AF42 /* PhotoParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9031E5EEBEA8EBA4D644 /* PhotoParameters.swift */; }; 62DE9CE6366FD6ECE740C55C /* FlickrCollectionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C90249CCE2FAF835CF5 /* FlickrCollectionFetcher.swift */; }; + 62DE9D29B400373915DB581A /* FeedViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE925A929D79068D7BE7CB /* FeedViewDataSource.swift */; }; 62DE9E63C0B530594A5B150B /* Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C8D791DA5418ED32FCF /* Style.swift */; }; 62DE9EBE33EDC3D97D929FF2 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE970A3FD09743A03DAF33 /* Photo.swift */; }; 62DE9EC2FE48A5A204531EEA /* HttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE93C78893DAA0E46DBD6A /* HttpClient.swift */; }; @@ -86,6 +88,7 @@ 62DE91C1A5EBF975C64A37B3 /* MockPhotoDataCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataCache.swift; sourceTree = ""; }; 62DE9225FFEBB4968F60A987 /* MockFlickrPhotoService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFlickrPhotoService.swift; sourceTree = ""; }; 62DE924CEDF37B8D5BC86D48 /* XCTestCase+QueueAwait.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCTestCase+QueueAwait.swift"; sourceTree = ""; }; + 62DE925A929D79068D7BE7CB /* FeedViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedViewDataSource.swift; sourceTree = ""; }; 62DE92C6C8EA6BC8A24E8EBC /* BrowserCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellFactory.swift; sourceTree = ""; }; 62DE92EE74C81312D3DB90B0 /* MockPhotoDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataProvider.swift; sourceTree = ""; }; 62DE93A95BC1DB3D74EC66D8 /* BrowserCellInteractorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellInteractorTests.swift; sourceTree = ""; }; @@ -94,6 +97,7 @@ 62DE94A9AA78F83D54078368 /* RootDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootDependencies.swift; sourceTree = ""; }; 62DE954BE0BFF3D2EE91DE12 /* FlickrPhotosService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotosService.swift; sourceTree = ""; }; 62DE9591CAA01E3F3C91A977 /* BrowserCellPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellPresenter.swift; sourceTree = ""; }; + 62DE96216E4D360CAB68AF75 /* FeedFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedFactory.swift; sourceTree = ""; }; 62DE9648DCA09C364860E8BB /* BrowserCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellView.swift; sourceTree = ""; }; 62DE9673FC376E2BC90087D6 /* FeedPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedPresenter.swift; sourceTree = ""; }; 62DE969E8F3F30D004171974 /* BrowserCellModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellModels.swift; sourceTree = ""; }; @@ -190,6 +194,9 @@ 62DE9673FC376E2BC90087D6 /* FeedPresenter.swift */, 62DE9DB2850E842E22C4CC84 /* FeedInteractor.swift */, 62DE9D31EAF1CFCCDE85B8DE /* FeedModels.swift */, + 62DE96216E4D360CAB68AF75 /* FeedFactory.swift */, + 62DE925A929D79068D7BE7CB /* FeedViewDataSource.swift */, + 62DE98914295E0F2A4403FBE /* Cell */, ); path = Feed; sourceTree = ""; @@ -279,6 +286,13 @@ path = PhotoDataRetriever; sourceTree = ""; }; + 62DE98914295E0F2A4403FBE /* Cell */ = { + isa = PBXGroup; + children = ( + ); + path = Cell; + sourceTree = ""; + }; 62DE98C735D3CC9E8C88071A /* FlickrApiServices */ = { isa = PBXGroup; children = ( @@ -628,6 +642,8 @@ 62DE91F2D0080AF13460E47C /* FeedInteractor.swift in Sources */, 62DE96046383936ED2BDE433 /* FeedModels.swift in Sources */, 62DE9CD65849E14B67A5AF42 /* PhotoParameters.swift in Sources */, + 62DE97615F6230CBFF9F92A4 /* FeedFactory.swift in Sources */, + 62DE9D29B400373915DB581A /* FeedViewDataSource.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/AppDelegate.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/AppDelegate.swift index e601bff..81d1065 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/AppDelegate.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/AppDelegate.swift @@ -11,8 +11,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let dependencies = RootDependencies() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - let viewController = dependencies.createBrowserViewController() - +// let viewController = dependencies.createBrowserViewController() + let viewController = dependencies.createFeedViewController() + let navigationController = UINavigationController(rootViewController: viewController) window = UIWindow(frame: UIScreen.main.bounds) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift index 1409964..d2fa3d2 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift @@ -4,10 +4,19 @@ // Copyright (c) 2020 rencevio. All rights reserved. import struct Foundation.URL +import struct Foundation.Date struct Photo { + struct Metadata { + let views: Int + let tags: [String] + let ownerName: String + let dateTaken: Date + } + typealias ID = String let id: ID let imageURL: URL + let metadata: Metadata? } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift index 7dc9fd2..abde65b 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift @@ -23,4 +23,13 @@ final class RootDependencies { photoCollectionFetcher: photoCollectionFetcher ) } + + func createFeedViewController() -> FeedViewController { + let feedFactory = FeedFactory() + + return feedFactory.createViewController( + photoDataProvider: photoDataProvider, + photoCollectionFetcher: photoCollectionFetcher + ) + } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserModels.swift index 1e43eae..66a3de5 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserModels.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserModels.swift @@ -9,7 +9,7 @@ struct BrowserModels { let startFromPosition: Int let fetchAtMost: Int let searchCriteria: String - let size: PhotoSize + let size: PhotoParameters.Size } struct Response { diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift index e92206c..b35ed02 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift @@ -22,7 +22,7 @@ private struct LayoutConstants { final class BrowserViewController: UIViewController { private let photosPerFetchRequest = LayoutConstants.itemsPerRow * 15 - private let photoSize = PhotoSize.thumbSquare + private let photoSize = PhotoParameters.Size.thumbSquare private let interactor: BrowserInteracting private let dataSource: BrowserDataSourcing diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift new file mode 100644 index 0000000..4488bb3 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift @@ -0,0 +1,29 @@ +// +// Created by Maxim Berezhnoy on 23/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +final class FeedFactory { + func createViewController(photoDataProvider: PhotoDataProviding, + photoCollectionFetcher: PhotoCollectionFetching) -> FeedViewController { + let presenter = FeedPresenter() + let interactor = FeedInteractor(presenter: presenter, photoCollectionFetcher: photoCollectionFetcher) + + let dataSource = createDataSource(photoDataProvider: photoDataProvider) + + let viewController = FeedViewController(interactor: interactor, dataSource: dataSource) + + presenter.view = viewController + + return viewController + } + + private func createDataSource(photoDataProvider: PhotoDataProviding) -> FeedDataSourcing { +// let cellFactory = BrowserCellFactory(photoDataProvider: photoDataProvider) +// let cellConfigurator = cellFactory.createConfigurator() + + return FeedViewDataSource( +// cellConfigurator: cellConfigurator + ) + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift index df59ecc..de2296f 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift @@ -3,3 +3,53 @@ // // Copyright (c) 2020 rencevio. All rights reserved. +import Foundation + +protocol FeedInteracting { + func fetch(photos request: FeedModels.Photos.Request) +} + +final class FeedInteractor: FeedInteracting { + private let presenter: FeedPresenting + private let photoCollectionFetcher: PhotoCollectionFetching + + private var currentRequest: FeedModels.Photos.Request? + + init(presenter: FeedPresenting, photoCollectionFetcher: PhotoCollectionFetching) { + self.presenter = presenter + self.photoCollectionFetcher = photoCollectionFetcher + } + + func fetch(photos request: FeedModels.Photos.Request) { + if currentRequest == request { + return + } + + currentRequest = request + + photoCollectionFetcher.fetchPhotos( + startingFrom: request.startFromPosition, + fetchAtMost: request.fetchAtMost, + matching: request.searchCriteria, + withSize: request.size, + includeMetadata: [] + ) { result in + switch result { + case let .success(photos): + DispatchQueue.main.async { [weak self, request] in + guard let self = self else { return } + + if self.currentRequest == request { + self.currentRequest = nil + } + + let response = FeedModels.Photos.Response(startingPosition: request.startFromPosition, photos: photos) + + self.presenter.present(photos: response) + } + case let .failure(error): + print("Error while retrieving photos (request: \(request)): \(error)") + } + } + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift index df59ecc..1089067 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift @@ -3,3 +3,23 @@ // // Copyright (c) 2020 rencevio. All rights reserved. +struct FeedModels { + struct Photos { + struct Request: Equatable { + let startFromPosition: Int + let fetchAtMost: Int + let searchCriteria: String + let size: PhotoParameters.Size + let metadata: [PhotoParameters.Metadata] + } + + struct Response { + let startingPosition: Int + let photos: [Photo] + } + + struct ViewModel { + let photos: [Photo] + } + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedPresenter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedPresenter.swift index df59ecc..1e93e16 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedPresenter.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedPresenter.swift @@ -3,3 +3,20 @@ // // Copyright (c) 2020 rencevio. All rights reserved. +protocol FeedPresenting: AnyObject { + func present(photos: FeedModels.Photos.Response) +} + +final class FeedPresenter: FeedPresenting { + weak var view: FeedDisplaying? + + func present(photos: FeedModels.Photos.Response) { + let viewModel = FeedModels.Photos.ViewModel(photos: photos.photos) + + if photos.startingPosition == 0 { + view?.displayNew(photos: viewModel) + } else { + view?.displayMore(photos: viewModel) + } + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift index df59ecc..366e805 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift @@ -3,3 +3,84 @@ // // Copyright (c) 2020 rencevio. All rights reserved. +import UIKit + +protocol FeedDisplaying: AnyObject { + func displayNew(photos: FeedModels.Photos.ViewModel) + func displayMore(photos: FeedModels.Photos.ViewModel) +} + +final class FeedViewController: UIViewController { + private let photoSize = PhotoParameters.Size.medium + + private let interactor: FeedInteracting + private let dataSource: FeedDataSourcing + + private lazy var tableView: UITableView = { + let view = UITableView(frame: .zero) + + return view + }() + + private lazy var refreshControl: UIRefreshControl = { + let control = UIRefreshControl(frame: .zero) + + return control + }() + + init(interactor: FeedInteracting, dataSource: FeedDataSourcing) { + self.interactor = interactor + self.dataSource = dataSource + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupTableView() + setupRefreshControl() + + requestNewPhotos() + } + + // MARK: - View Setup + + private func setupTableView() { + + } + + private func setupRefreshControl() { + tableView.refreshControl = refreshControl + + refreshControl.addTarget(self, action: #selector(handleRefreshControl), for: .valueChanged) + } + + // MARK: - Data Requesting + + private func requestNewPhotos() { + + } +} + +// MARK: - FeedDisplaying + +extension FeedViewController: FeedDisplaying { + func displayNew(photos: FeedModels.Photos.ViewModel) { + refreshControl.endRefreshing() + } + + func displayMore(photos: FeedModels.Photos.ViewModel) {} +} + +// MARK: - Refresh control + +extension FeedViewController { + @objc func handleRefreshControl() { + requestNewPhotos() + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift new file mode 100644 index 0000000..f304bd5 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift @@ -0,0 +1,43 @@ +// +// Created by Maxim Berezhnoy on 23/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import UIKit + +protocol FeedDataSourcing: UITableViewDataSource { +// var photoCount: Int { get } + +// func register(for collectionView: UICollectionView) +// func add(photos: [Photo]) +// func set(photos: [Photo]) +} + +final class FeedViewDataSource: NSObject { +// private let cellConfigurator: BrowserCellConfiguring + + private var photos = [Photo]() + + override init( +// cellConfigurator: BrowserCellConfiguring + ) { +// self.cellConfigurator = cellConfigurator + + super.init() + } +} + +// MARK: - UITableViewDataSource + +extension FeedViewDataSource: UITableViewDataSource { + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { fatalError("tableView(_:numberOfRowsInSection:) has not been implemented") } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { fatalError("tableView(_:cellForRowAt:) has not been implemented") } +} + +// MARK: - FeedDataSourcing + +extension FeedViewDataSource: FeedDataSourcing { + +} + diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotoURLResolver.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotoURLResolver.swift index 1fedca0..e16a347 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotoURLResolver.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotoURLResolver.swift @@ -6,7 +6,7 @@ import struct Foundation.URL final class FlickrPhotoURLResolver { - static func resolveUrl(for photo: FlickrPhoto, withSize size: PhotoSize) -> URL { + static func resolveUrl(for photo: FlickrPhoto, withSize size: PhotoParameters.Size) -> URL { let sizeSuffix = getSizeSuffix(for: size) guard let url = URL(string: "https://farm\(photo.farm).staticflickr.com/\(photo.server)/\(photo.id)_\(photo.secret)_\(sizeSuffix.rawValue).jpg") @@ -17,7 +17,7 @@ final class FlickrPhotoURLResolver { return url } - private static func getSizeSuffix(for size: PhotoSize) -> FlickrApiValues.PhotoSizeSuffix { + private static func getSizeSuffix(for size: PhotoParameters.Size) -> FlickrApiValues.PhotoSizeSuffix { switch (size) { case .thumbSquare: return .thumbSquare diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift index 8f97afd..5635b7d 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift @@ -8,9 +8,9 @@ import Foundation protocol FlickrPhotosFetching { typealias Completion = (Result<[FlickrPhoto], Error>) -> Void - func getRecent(page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoMetadata], completion: @escaping Completion) + func getRecent(page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion) - func search(matching text: String, page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoMetadata], completion: @escaping Completion) + func search(matching text: String, page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion) } final class FlickrPhotosService: FlickrPhotosFetching { @@ -22,7 +22,7 @@ final class FlickrPhotosService: FlickrPhotosFetching { self.httpClient = httpClient } - func getRecent(page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoMetadata], completion: @escaping Completion) { + func getRecent(page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion) { let url = FlickrApiURLResolver.build( method: .photosGetRecent, apiKey: apiKey, @@ -35,7 +35,7 @@ final class FlickrPhotosService: FlickrPhotosFetching { fetchFrom(url: url, completion: completion) } - func search(matching text: String, page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoMetadata], completion: @escaping Completion) { + func search(matching text: String, page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion) { let url = FlickrApiURLResolver.build( method: .photosSearch, apiKey: apiKey, @@ -43,7 +43,6 @@ final class FlickrPhotosService: FlickrPhotosFetching { .text: text, .page: String(page), .perPage: String(photosPerPage), - // todo: write test for this .extras: getQueryParameters(for: metadata).map { $0.rawValue }.joined(separator: ",") ] ) @@ -82,7 +81,7 @@ final class FlickrPhotosService: FlickrPhotosFetching { } } - private func getQueryParameters(for metadata: [PhotoMetadata]) -> [FlickrApiValues.PhotoMetadata] { + private func getQueryParameters(for metadata: [PhotoParameters.Metadata]) -> [FlickrApiValues.PhotoMetadata] { metadata.map { switch ($0) { case .views: diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift index 4f0f728..51b0dd5 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift @@ -12,8 +12,8 @@ final class FlickrCollectionFetcher: PhotoCollectionFetching { func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, - withSize size: PhotoSize, - includeMetadata metadata: [PhotoMetadata], + withSize size: PhotoParameters.Size, + includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion) { fetchPhotos(startingFrom: position, fetchAtMost: maxFetchCount, matching: nil, withSize: size, includeMetadata: metadata, completion: completion) } @@ -21,8 +21,8 @@ final class FlickrCollectionFetcher: PhotoCollectionFetching { func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, matching searchCriteria: String?, - withSize size: PhotoSize, - includeMetadata metadata: [PhotoMetadata], + withSize size: PhotoParameters.Size, + includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion) { let page = position / maxFetchCount + 1 @@ -44,11 +44,12 @@ final class FlickrCollectionFetcher: PhotoCollectionFetching { } } - private func transformFetchResult(withSize size: PhotoSize, _ completion: @escaping Completion) -> FlickrPhotosService.Completion { + private func transformFetchResult(withSize size: PhotoParameters.Size, _ completion: @escaping Completion) -> FlickrPhotosService.Completion { { result in switch result { case let .success(photos): - completion(.success(photos.map { Photo(id: $0.id, imageURL: FlickrPhotoURLResolver.resolveUrl(for: $0, withSize: size)) })) + // todo: metadata + completion(.success(photos.map { Photo(id: $0.id, imageURL: FlickrPhotoURLResolver.resolveUrl(for: $0, withSize: size), metadata: nil) })) case let .failure(error): completion(.failure(error)) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift index 1016563..02c4b03 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift @@ -9,8 +9,8 @@ protocol PhotoCollectionFetching { func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, matching searchCriteria: String?, - withSize size: PhotoSize, - includeMetadata metadata: [PhotoMetadata], + withSize size: PhotoParameters.Size, + includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion ) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift index 6b118cf..4165efc 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift @@ -3,15 +3,17 @@ // // Copyright (c) 2020 rencevio. All rights reserved. -enum PhotoSize { - case thumbSquare - case medium - case large -} +enum PhotoParameters { + enum Size { + case thumbSquare + case medium + case large + } -enum PhotoMetadata { - case views - case tags - case ownerName - case dateTaken + enum Metadata { + case views + case tags + case ownerName + case dateTaken + } } \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/FlickrPhotoServiceTests.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/FlickrPhotoServiceTests.swift index d6ed07a..ab963bb 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/FlickrPhotoServiceTests.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/FlickrPhotoServiceTests.swift @@ -59,7 +59,7 @@ class FlickrPhotoServiceTests: XCTestCase { var fetchedPhotos: [FlickrPhoto]! - sut.getRecent(page: 1, photosPerPage: 1) { result in + sut.getRecent(page: 1, photosPerPage: 1, includeMetadata: []) { result in switch result { case let .success(photos): fetchedPhotos = photos @@ -84,7 +84,7 @@ class FlickrPhotoServiceTests: XCTestCase { var fetchError: Error? - sut.getRecent(page: 1, photosPerPage: 1) { result in + sut.getRecent(page: 1, photosPerPage: 1, includeMetadata: []) { result in switch result { case let .success(photos): XCTFail("Unexpected success while fetching photos: \(photos)") From 4ff3acb9ed445ff11e1ba5608fafe599f1e32454 Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Thu, 26 Nov 2020 00:47:25 +0100 Subject: [PATCH 04/16] Feed cell VIP and configuration --- .../project.pbxproj | 56 ++++++ .../Configurator/ConfigurableFeedCell.swift | 8 + .../Configurator/FeedCellConfigurator.swift | 25 +++ .../Scenes/Feed/Cell/FeedCellFactory.swift | 20 ++ .../Scenes/Feed/Cell/FeedCellInteractor.swift | 45 +++++ .../Scenes/Feed/Cell/FeedCellModels.swift | 23 +++ .../Scenes/Feed/Cell/FeedCellPresenter.swift | 32 +++ .../Scenes/Feed/Cell/FeedCellView.swift | 190 ++++++++++++++++++ .../Scenes/Feed/Cell/Metadata/DateLabel.swift | 25 +++ .../Scenes/Feed/Cell/Metadata/TagsLabel.swift | 12 ++ .../Feed/Cell/Metadata/ViewsLabel.swift | 13 ++ 11 files changed, 449 insertions(+) create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/ConfigurableFeedCell.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/FeedCellConfigurator.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellInteractor.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellModels.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellPresenter.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/DateLabel.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/TagsLabel.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/ViewsLabel.swift diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj index 307dac7..482d68d 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 62DE90E73B9BFCA4A54B76C2 /* MockPhotoDataRetriever.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE96B5EBB14213DC9E7804 /* MockPhotoDataRetriever.swift */; }; 62DE90F40E919CCFF876D1A6 /* MockFlickrPhotoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9225FFEBB4968F60A987 /* MockFlickrPhotoService.swift */; }; 62DE911E1EB4D78BCA9085CB /* FlickrCollectionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9A2EBADCA5EE9059C3DD /* FlickrCollectionFetcherTests.swift */; }; + 62DE9150E7D8AE5820E891E1 /* FeedCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9779CE157EB936EF5A99 /* FeedCellFactory.swift */; }; 62DE91E5B84C14466618EAC1 /* ConfigurableBrowserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE91396D5EDB77544792D3 /* ConfigurableBrowserCell.swift */; }; 62DE91F2D0080AF13460E47C /* FeedInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9DB2850E842E22C4CC84 /* FeedInteractor.swift */; }; 62DE91F96D06BDB6AE8D40A1 /* XCTestCase+QueueAwait.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE924CEDF37B8D5BC86D48 /* XCTestCase+QueueAwait.swift */; }; @@ -21,11 +22,13 @@ 62DE92DECC87D7DACFB69CC9 /* PhotoDataRetrieving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99F1F322B9F470256B1E /* PhotoDataRetrieving.swift */; }; 62DE93261A41C5DDC9BACDB0 /* MockPhotoDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE91C1A5EBF975C64A37B3 /* MockPhotoDataCache.swift */; }; 62DE9387D309D2DD3F2AC102 /* BrowserPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE915A672C1C41DCCF9C8D /* BrowserPresenterTests.swift */; }; + 62DE93997644B299A146E0FF /* FeedCellPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE92C92B36ED5061F378E7 /* FeedCellPresenter.swift */; }; 62DE93E9065F0528B8577C0E /* BrowserCellInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE93A95BC1DB3D74EC66D8 /* BrowserCellInteractorTests.swift */; }; 62DE9427597853C3B0A5E661 /* BrowserCellModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE969E8F3F30D004171974 /* BrowserCellModels.swift */; }; 62DE944EF838C8C313469CCB /* MockPhotoDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE92EE74C81312D3DB90B0 /* MockPhotoDataProvider.swift */; }; 62DE9452E6FC1D01DD8976DA /* BrowserFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9CC652A43A976EDE4FF6 /* BrowserFactory.swift */; }; 62DE946CFE4B0C1C5B9E1324 /* MockBrowserDisplaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9810EDB0220E74893A89 /* MockBrowserDisplaying.swift */; }; + 62DE94F49D115431A2630DAB /* FeedCellInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99D07CD51D362F953646 /* FeedCellInteractor.swift */; }; 62DE959CA829F11A51767E30 /* FlickrCollectionFetcherFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9B8BC72A4AB8A5A34B65 /* FlickrCollectionFetcherFactory.swift */; }; 62DE95BAA18D1A52C35EF040 /* FlickrPhotos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE96CE6B44327361D73D24 /* FlickrPhotos.swift */; }; 62DE96046383936ED2BDE433 /* FeedModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9D31EAF1CFCCDE85B8DE /* FeedModels.swift */; }; @@ -37,26 +40,33 @@ 62DE97615F6230CBFF9F92A4 /* FeedFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE96216E4D360CAB68AF75 /* FeedFactory.swift */; }; 62DE97B6EC739349BC9A6D2F /* FlickrPhotoServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE97953D19EE0C13151316 /* FlickrPhotoServiceTests.swift */; }; 62DE97CDCFCD2718C1BBC1E8 /* PhotoCollectionFetcherFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9A0F5F5E76B6AFE7159B /* PhotoCollectionFetcherFactory.swift */; }; + 62DE9825AD3853FDB25206DB /* FeedCellModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9E7BAAEC8599D46BFE80 /* FeedCellModels.swift */; }; 62DE983EF92FD18305C80488 /* BrowserCellInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE997B53C61FE26593A99A /* BrowserCellInteractor.swift */; }; 62DE9866170F4BCE2B93E2E0 /* MockBrowserPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE944604E007DD383ABDE1 /* MockBrowserPresenter.swift */; }; 62DE98D6E300CD17DD7D23A3 /* BrowserCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9648DCA09C364860E8BB /* BrowserCellView.swift */; }; 62DE9921C1443189F640771E /* PhotoDataProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C85502219A9858DCF22 /* PhotoDataProviderTests.swift */; }; 62DE9927B234F05FAB311B8D /* RootDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE94A9AA78F83D54078368 /* RootDependencies.swift */; }; 62DE9934B5A84CB2A3736360 /* MockPhotoCollectionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9001FE62BE27C3D938B3 /* MockPhotoCollectionFetcher.swift */; }; + 62DE9A474D891C506B86BF8A /* ViewsLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE97EF27BCB2A06EB35FDF /* ViewsLabel.swift */; }; 62DE9A8C69E9E0A200181E5C /* FlickrApiURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9D790ECF920DF2C9802B /* FlickrApiURLBuilder.swift */; }; 62DE9A9250BD0AC541234841 /* PhotoDataProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99EC2B14DB9F0C554529 /* PhotoDataProviderFactory.swift */; }; 62DE9B36C2A72575C7802A24 /* BrowserInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9E2159757D6020248C32 /* BrowserInteractor.swift */; }; + 62DE9B3C2CAC899A108EFA70 /* DateLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE963F12E663C923326C82 /* DateLabel.swift */; }; 62DE9B6C8EDB44A25EEE77A0 /* BrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE900EADCC52560B246392 /* BrowserViewController.swift */; }; 62DE9C2CEA5B44E24BB2D6F5 /* PhotoCollectionFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE90B754DC827D73A31B9E /* PhotoCollectionFetching.swift */; }; 62DE9C8EC1EBD9B055EF5388 /* BrowserViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9817F61216E36044EA42 /* BrowserViewDataSource.swift */; }; + 62DE9CACADCC0438B0091A5C /* FeedCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE98ED3E76A320E0BF36D3 /* FeedCellView.swift */; }; 62DE9CBE1FB6013A700EB635 /* BrowserCellPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9591CAA01E3F3C91A977 /* BrowserCellPresenter.swift */; }; 62DE9CD65849E14B67A5AF42 /* PhotoParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9031E5EEBEA8EBA4D644 /* PhotoParameters.swift */; }; 62DE9CE6366FD6ECE740C55C /* FlickrCollectionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C90249CCE2FAF835CF5 /* FlickrCollectionFetcher.swift */; }; + 62DE9CF01CC66EDE19407A30 /* ConfigurableFeedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE948E9483F63E1CCCCCF5 /* ConfigurableFeedCell.swift */; }; 62DE9D29B400373915DB581A /* FeedViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE925A929D79068D7BE7CB /* FeedViewDataSource.swift */; }; + 62DE9DE0C4D4BD2598CA2C7F /* FeedCellConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C5C53014BB9513FCD95 /* FeedCellConfigurator.swift */; }; 62DE9E63C0B530594A5B150B /* Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C8D791DA5418ED32FCF /* Style.swift */; }; 62DE9EBE33EDC3D97D929FF2 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE970A3FD09743A03DAF33 /* Photo.swift */; }; 62DE9EC2FE48A5A204531EEA /* HttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE93C78893DAA0E46DBD6A /* HttpClient.swift */; }; 62DE9F2E46CDDC60B4A5DEAE /* BrowserModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9E652D625ECC302D66F9 /* BrowserModels.swift */; }; + 62DE9F3410EB4A39040BBC55 /* TagsLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE940D06ADFF0BDAC17ADC /* TagsLabel.swift */; }; 62DE9F5B37C5FE4BED753FB2 /* PhotoDataNSCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9805D6D4DBDDBAD078E2 /* PhotoDataNSCache.swift */; }; 62DE9F69E85302202E6884B3 /* PhotoDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9FF6B55060E5204AC907 /* PhotoDataProvider.swift */; }; 62DE9FA335C6F1E04D690818 /* BrowserInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9EA9D6ACC68E32F8A681 /* BrowserInteractorTests.swift */; }; @@ -90,29 +100,37 @@ 62DE924CEDF37B8D5BC86D48 /* XCTestCase+QueueAwait.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCTestCase+QueueAwait.swift"; sourceTree = ""; }; 62DE925A929D79068D7BE7CB /* FeedViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedViewDataSource.swift; sourceTree = ""; }; 62DE92C6C8EA6BC8A24E8EBC /* BrowserCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellFactory.swift; sourceTree = ""; }; + 62DE92C92B36ED5061F378E7 /* FeedCellPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellPresenter.swift; sourceTree = ""; }; 62DE92EE74C81312D3DB90B0 /* MockPhotoDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataProvider.swift; sourceTree = ""; }; 62DE93A95BC1DB3D74EC66D8 /* BrowserCellInteractorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellInteractorTests.swift; sourceTree = ""; }; 62DE93C78893DAA0E46DBD6A /* HttpClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpClient.swift; sourceTree = ""; }; + 62DE940D06ADFF0BDAC17ADC /* TagsLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TagsLabel.swift; sourceTree = ""; }; 62DE944604E007DD383ABDE1 /* MockBrowserPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBrowserPresenter.swift; sourceTree = ""; }; + 62DE948E9483F63E1CCCCCF5 /* ConfigurableFeedCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurableFeedCell.swift; sourceTree = ""; }; 62DE94A9AA78F83D54078368 /* RootDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootDependencies.swift; sourceTree = ""; }; 62DE954BE0BFF3D2EE91DE12 /* FlickrPhotosService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotosService.swift; sourceTree = ""; }; 62DE9591CAA01E3F3C91A977 /* BrowserCellPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellPresenter.swift; sourceTree = ""; }; 62DE96216E4D360CAB68AF75 /* FeedFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedFactory.swift; sourceTree = ""; }; + 62DE963F12E663C923326C82 /* DateLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateLabel.swift; sourceTree = ""; }; 62DE9648DCA09C364860E8BB /* BrowserCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellView.swift; sourceTree = ""; }; 62DE9673FC376E2BC90087D6 /* FeedPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedPresenter.swift; sourceTree = ""; }; 62DE969E8F3F30D004171974 /* BrowserCellModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellModels.swift; sourceTree = ""; }; 62DE96B5EBB14213DC9E7804 /* MockPhotoDataRetriever.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataRetriever.swift; sourceTree = ""; }; 62DE96CE6B44327361D73D24 /* FlickrPhotos.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotos.swift; sourceTree = ""; }; 62DE970A3FD09743A03DAF33 /* Photo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = ""; }; + 62DE9779CE157EB936EF5A99 /* FeedCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellFactory.swift; sourceTree = ""; }; 62DE97953D19EE0C13151316 /* FlickrPhotoServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotoServiceTests.swift; sourceTree = ""; }; + 62DE97EF27BCB2A06EB35FDF /* ViewsLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewsLabel.swift; sourceTree = ""; }; 62DE9805D6D4DBDDBAD078E2 /* PhotoDataNSCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataNSCache.swift; sourceTree = ""; }; 62DE9810EDB0220E74893A89 /* MockBrowserDisplaying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBrowserDisplaying.swift; sourceTree = ""; }; 62DE9817F61216E36044EA42 /* BrowserViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserViewDataSource.swift; sourceTree = ""; }; 62DE986497C5FB223DA10256 /* FlickrPhotoURLResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotoURLResolver.swift; sourceTree = ""; }; 62DE9868FFC49BF89E9F9192 /* NetworkPhotoDataRetriever.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkPhotoDataRetriever.swift; sourceTree = ""; }; 62DE987D3B885CA1A25FB693 /* FlickrApiValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrApiValues.swift; sourceTree = ""; }; + 62DE98ED3E76A320E0BF36D3 /* FeedCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellView.swift; sourceTree = ""; }; 62DE98FFE252C3D837867816 /* BrowserPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserPresenter.swift; sourceTree = ""; }; 62DE997B53C61FE26593A99A /* BrowserCellInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellInteractor.swift; sourceTree = ""; }; + 62DE99D07CD51D362F953646 /* FeedCellInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellInteractor.swift; sourceTree = ""; }; 62DE99EC2B14DB9F0C554529 /* PhotoDataProviderFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProviderFactory.swift; sourceTree = ""; }; 62DE99F1F322B9F470256B1E /* PhotoDataRetrieving.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataRetrieving.swift; sourceTree = ""; }; 62DE9A0F5F5E76B6AFE7159B /* PhotoCollectionFetcherFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionFetcherFactory.swift; sourceTree = ""; }; @@ -122,6 +140,7 @@ 62DE9B49CABD228CCFBCE936 /* PhotoDataCaching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataCaching.swift; sourceTree = ""; }; 62DE9B8BC72A4AB8A5A34B65 /* FlickrCollectionFetcherFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcherFactory.swift; sourceTree = ""; }; 62DE9C09B259CEA839DAE2B9 /* FeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = ""; }; + 62DE9C5C53014BB9513FCD95 /* FeedCellConfigurator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellConfigurator.swift; sourceTree = ""; }; 62DE9C85502219A9858DCF22 /* PhotoDataProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProviderTests.swift; sourceTree = ""; }; 62DE9C8D791DA5418ED32FCF /* Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Style.swift; sourceTree = ""; }; 62DE9C90249CCE2FAF835CF5 /* FlickrCollectionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcher.swift; sourceTree = ""; }; @@ -132,6 +151,7 @@ 62DE9E19BC1900DC8648FD5E /* MockHttpClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockHttpClient.swift; sourceTree = ""; }; 62DE9E2159757D6020248C32 /* BrowserInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserInteractor.swift; sourceTree = ""; }; 62DE9E652D625ECC302D66F9 /* BrowserModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserModels.swift; sourceTree = ""; }; + 62DE9E7BAAEC8599D46BFE80 /* FeedCellModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellModels.swift; sourceTree = ""; }; 62DE9EA9D6ACC68E32F8A681 /* BrowserInteractorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserInteractorTests.swift; sourceTree = ""; }; 62DE9FF6B55060E5204AC907 /* PhotoDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProvider.swift; sourceTree = ""; }; 944AC01A23CE0E9C009FD611 /* SimpleFlickrBrowser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimpleFlickrBrowser.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -289,6 +309,13 @@ 62DE98914295E0F2A4403FBE /* Cell */ = { isa = PBXGroup; children = ( + 62DE9B5801A7627FCBDA8DF6 /* Configurator */, + 62DE9779CE157EB936EF5A99 /* FeedCellFactory.swift */, + 62DE99D07CD51D362F953646 /* FeedCellInteractor.swift */, + 62DE9E7BAAEC8599D46BFE80 /* FeedCellModels.swift */, + 62DE92C92B36ED5061F378E7 /* FeedCellPresenter.swift */, + 62DE98ED3E76A320E0BF36D3 /* FeedCellView.swift */, + 62DE9EA0E878692CF74D79CB /* Metadata */, ); path = Cell; sourceTree = ""; @@ -337,6 +364,15 @@ path = Extensions; sourceTree = ""; }; + 62DE9B5801A7627FCBDA8DF6 /* Configurator */ = { + isa = PBXGroup; + children = ( + 62DE9C5C53014BB9513FCD95 /* FeedCellConfigurator.swift */, + 62DE948E9483F63E1CCCCCF5 /* ConfigurableFeedCell.swift */, + ); + path = Configurator; + sourceTree = ""; + }; 62DE9C11F52BBF38DEB44438 /* Network */ = { isa = PBXGroup; children = ( @@ -393,6 +429,16 @@ path = Workers; sourceTree = ""; }; + 62DE9EA0E878692CF74D79CB /* Metadata */ = { + isa = PBXGroup; + children = ( + 62DE963F12E663C923326C82 /* DateLabel.swift */, + 62DE97EF27BCB2A06EB35FDF /* ViewsLabel.swift */, + 62DE940D06ADFF0BDAC17ADC /* TagsLabel.swift */, + ); + path = Metadata; + sourceTree = ""; + }; 62DE9ED1568E25802A697C02 /* Cell */ = { isa = PBXGroup; children = ( @@ -644,6 +690,16 @@ 62DE9CD65849E14B67A5AF42 /* PhotoParameters.swift in Sources */, 62DE97615F6230CBFF9F92A4 /* FeedFactory.swift in Sources */, 62DE9D29B400373915DB581A /* FeedViewDataSource.swift in Sources */, + 62DE9DE0C4D4BD2598CA2C7F /* FeedCellConfigurator.swift in Sources */, + 62DE9CF01CC66EDE19407A30 /* ConfigurableFeedCell.swift in Sources */, + 62DE9150E7D8AE5820E891E1 /* FeedCellFactory.swift in Sources */, + 62DE94F49D115431A2630DAB /* FeedCellInteractor.swift in Sources */, + 62DE9825AD3853FDB25206DB /* FeedCellModels.swift in Sources */, + 62DE93997644B299A146E0FF /* FeedCellPresenter.swift in Sources */, + 62DE9CACADCC0438B0091A5C /* FeedCellView.swift in Sources */, + 62DE9B3C2CAC899A108EFA70 /* DateLabel.swift in Sources */, + 62DE9A474D891C506B86BF8A /* ViewsLabel.swift in Sources */, + 62DE9F3410EB4A39040BBC55 /* TagsLabel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/ConfigurableFeedCell.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/ConfigurableFeedCell.swift new file mode 100644 index 0000000..e5ccfdf --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/ConfigurableFeedCell.swift @@ -0,0 +1,8 @@ +// +// Created by Maxim Berezhnoy on 24/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +protocol ConfigurableFeedCell: FeedCellDisplaying { + func configure(interactor: FeedCellInteracting, photo: Photo) +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/FeedCellConfigurator.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/FeedCellConfigurator.swift new file mode 100644 index 0000000..630a88b --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/FeedCellConfigurator.swift @@ -0,0 +1,25 @@ +// +// Created by Maxim Berezhnoy on 24/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +protocol FeedCellConfiguring { + func configure(cell: ConfigurableFeedCell, with photo: Photo) +} + +final class FeedCellConfigurator: FeedCellConfiguring { + private let photoDataProvider: PhotoDataProviding + + init(photoDataProvider: PhotoDataProviding) { + self.photoDataProvider = photoDataProvider + } + + func configure(cell: ConfigurableFeedCell, with photo: Photo) { + let presenter = FeedCellPresenter() + let interactor = FeedCellInteractor(presenter: presenter, photoDataProvider: photoDataProvider) + + presenter.view = cell + + cell.configure(interactor: interactor, photo: photo) + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift new file mode 100644 index 0000000..2164cbd --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift @@ -0,0 +1,20 @@ +// +// Created by Maxim Berezhnoy on 25/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +protocol FeedCellCreating { + func createConfigurator() -> FeedCellConfiguring +} + +final class FeedCellFactory: FeedCellCreating { + private let photoDataProvider: PhotoDataProviding + + init(photoDataProvider: PhotoDataProviding) { + self.photoDataProvider = photoDataProvider + } + + func createConfigurator() -> FeedCellConfiguring { + FeedCellConfigurator(photoDataProvider: photoDataProvider) + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellInteractor.swift new file mode 100644 index 0000000..627284b --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellInteractor.swift @@ -0,0 +1,45 @@ +// +// Created by Maxim Berezhnoy on 25/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import Foundation + +protocol FeedCellInteracting { + func fetch(image: FeedCellModels.PhotoImage.Request) +} + +final class FeedCellInteractor: FeedCellInteracting { + private let presenter: FeedCellPresenting + private let photoDataProvider: PhotoDataProviding + + private var currentPhotoId: Photo.ID? + + init(presenter: FeedCellPresenting, photoDataProvider: PhotoDataProviding) { + self.presenter = presenter + self.photoDataProvider = photoDataProvider + } + + func fetch(image: FeedCellModels.PhotoImage.Request) { + presenter.presentLoading() + + currentPhotoId = image.photoID + + photoDataProvider.getPhotoData(from: image.url) { result in + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + // Do not send a response if the ID has changed in the meantime. + // Another option would be to make requests to provider cancelable + if let currentPhotoId = self.currentPhotoId, currentPhotoId == image.photoID { + switch result { + case let .success(data): + self.presenter.present(image: FeedCellModels.PhotoImage.Response(data: data)) + case .failure: + self.presenter.presentError() + } + } + } + } + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellModels.swift new file mode 100644 index 0000000..28c3cbc --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellModels.swift @@ -0,0 +1,23 @@ +// +// Created by Maxim Berezhnoy on 25/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import UIKit + +struct FeedCellModels { + struct PhotoImage { + struct Request { + let photoID: Photo.ID + let url: URL + } + + struct Response { + let data: Data + } + + struct ViewModel { + let image: UIImage + } + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellPresenter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellPresenter.swift new file mode 100644 index 0000000..cc23d12 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellPresenter.swift @@ -0,0 +1,32 @@ +// +// Created by Maxim Berezhnoy on 25/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import UIKit + +protocol FeedCellPresenting: AnyObject { + func present(image: FeedCellModels.PhotoImage.Response) + func presentLoading() + func presentError() +} + +final class FeedCellPresenter: FeedCellPresenting { + weak var view: FeedCellDisplaying? + + func present(image: FeedCellModels.PhotoImage.Response) { + guard let view = view, + let image = UIImage(data: image.data) + else { return } + + view.display(image: FeedCellModels.PhotoImage.ViewModel(image: image)) + } + + func presentLoading() { + view?.displayLoading() + } + + func presentError() { + view?.displayLoading() + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift new file mode 100644 index 0000000..3ce5d57 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift @@ -0,0 +1,190 @@ +// +// Created by Maxim Berezhnoy on 25/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import UIKit + +protocol FeedCellDisplaying: AnyObject { + func display(image: FeedCellModels.PhotoImage.ViewModel) + func displayLoading() +} + +private struct LayoutConstants { + static let ownerNameFontSize: CGFloat = 12 + static let tagsFontSize: CGFloat = 11 + static let viewsFontSize: CGFloat = 12 + static let dateTakenFontSize: CGFloat = 12 + + static let sideMarginRatio: CGFloat = 0.02 + static let tagsSideMarginRatio: CGFloat = 0.04 + static let topBottomMarginRatio: CGFloat = 0.2 + + static let dateTakenFormat = "MMM dd, yyyy" +} + +final class FeedViewCell: UITableViewCell { + private enum ImageViewState { + case loading + case image(UIImage) + } + + static let identifier = "\(FeedViewCell.self)" + + private var interactor: FeedCellInteracting? + + private var heightConstraint: NSLayoutConstraint? + + // MARK: - Subviews + lazy var cellImageView: UIImageView = { + let view = UIImageView() + view.contentMode = .scaleAspectFit + + return view + }() + + lazy var ownerNameView: UILabel = { + let view = UILabel() + + view.font = UIFont.systemFont(ofSize: LayoutConstants.ownerNameFontSize) + + return view + }() + + lazy var viewsView: ViewsLabel = { + let view = ViewsLabel() + + view.font = UIFont.systemFont(ofSize: LayoutConstants.viewsFontSize) + + return view + }() + + lazy var tagsView: TagsLabel = { + let view = TagsLabel() + + view.font = UIFont.boldSystemFont(ofSize: LayoutConstants.tagsFontSize) + view.lineBreakMode = .byWordWrapping + view.numberOfLines = 0 + + return view + }() + + lazy var dateTakenView: DateLabel = { + let view = DateLabel(dateFormat: LayoutConstants.dateTakenFormat) + + view.font = UIFont.systemFont(ofSize: LayoutConstants.dateTakenFontSize) + + return view + }() + + // MARK: - Init + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + setupImageView() + setupOwnerNameView() + setupViewsView() + setupTagsView() + setupDateTakenView() + + set(imageState: .loading) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + lazy var widthConstraint: NSLayoutConstraint = { + let constraint = contentView.widthAnchor.constraint(equalToConstant: bounds.size.width) + constraint.isActive = true + return constraint + }() + + override func systemLayoutSizeFitting(_ targetSize: CGSize, + withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, + verticalFittingPriority: UILayoutPriority) -> CGSize { + widthConstraint.constant = bounds.size.width + return contentView.systemLayoutSizeFitting(CGSize(width: targetSize.width, height: 1)) + } + + // MARK: - View Setup + private func setupImageView() { + contentView.addSubview(cellImageView) + + cellImageView.translatesAutoresizingMaskIntoConstraints = false + cellImageView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true + cellImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true + cellImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true + } + + private func setupOwnerNameView() { + contentView.addSubview(ownerNameView) + + ownerNameView.translatesAutoresizingMaskIntoConstraints = false + ownerNameView.topAnchor.constraint(equalTo: cellImageView.bottomAnchor, constant: frame.height * LayoutConstants.topBottomMarginRatio).isActive = true + ownerNameView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: frame.width * LayoutConstants.sideMarginRatio).isActive = true + } + + private func setupViewsView() { + contentView.addSubview(viewsView) + + viewsView.translatesAutoresizingMaskIntoConstraints = false + viewsView.topAnchor.constraint(equalTo: cellImageView.bottomAnchor, constant: frame.height * LayoutConstants.topBottomMarginRatio).isActive = true + viewsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -1 * frame.width * LayoutConstants.sideMarginRatio).isActive = true + } + + private func setupTagsView() { + contentView.addSubview(tagsView) + + tagsView.translatesAutoresizingMaskIntoConstraints = false + tagsView.topAnchor.constraint(equalTo: ownerNameView.bottomAnchor, constant: frame.height * LayoutConstants.topBottomMarginRatio).isActive = true + tagsView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: frame.width * LayoutConstants.tagsSideMarginRatio).isActive = true + tagsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -1 * frame.width * LayoutConstants.tagsSideMarginRatio).isActive = true + } + + private func setupDateTakenView() { + contentView.addSubview(dateTakenView) + + dateTakenView.translatesAutoresizingMaskIntoConstraints = false + dateTakenView.topAnchor.constraint(equalTo: tagsView.bottomAnchor, constant: frame.height * LayoutConstants.topBottomMarginRatio).isActive = true + dateTakenView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true + dateTakenView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: frame.width * LayoutConstants.sideMarginRatio).isActive = true + } + + private func set(imageState: ImageViewState) { + switch imageState { + case .loading: + cellImageView.isHidden = true + + case let .image(image): + cellImageView.isHidden = false + cellImageView.image = image + } + } +} + + +// MARK: - FeedCellDisplaying + +extension FeedViewCell: FeedCellDisplaying { + func display(image: FeedCellModels.PhotoImage.ViewModel) {} + + func displayLoading() {} +} + +// MARK: - ConfigurableFeedCell + +extension FeedViewCell: ConfigurableFeedCell { + func configure(interactor: FeedCellInteracting, photo: Photo) { + self.interactor = interactor + + interactor.fetch(image: FeedCellModels.PhotoImage.Request(photoID: photo.id, url: photo.imageURL)) + + if let metadata = photo.metadata { + ownerNameView.text = metadata.ownerName + dateTakenView.set(date: metadata.dateTaken) + viewsView.set(views: metadata.views) + tagsView.set(tags: metadata.tags) + } + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/DateLabel.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/DateLabel.swift new file mode 100644 index 0000000..adda6eb --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/DateLabel.swift @@ -0,0 +1,25 @@ +// +// Created by Maxim Berezhnoy on 26/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import UIKit + +final class DateLabel: UILabel { + private let dateFormatter: DateFormatter + + init(dateFormat: String, frame: CGRect = .zero) { + self.dateFormatter = DateFormatter() + dateFormatter.dateFormat = dateFormat + + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func set(date: Date) { + text = dateFormatter.string(from: date) + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/TagsLabel.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/TagsLabel.swift new file mode 100644 index 0000000..e561e02 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/TagsLabel.swift @@ -0,0 +1,12 @@ +// +// Created by Maxim Berezhnoy on 26/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import UIKit + +class TagsLabel: UILabel { + func set(tags: [String]) { + super.text = tags.joined(separator: ", ") + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/ViewsLabel.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/ViewsLabel.swift new file mode 100644 index 0000000..b351fe9 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/ViewsLabel.swift @@ -0,0 +1,13 @@ +// +// Created by Maxim Berezhnoy on 26/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import UIKit + +final class ViewsLabel: UILabel { + func set(views: Int) { + let description = views == 1 ? "view" : "views" + super.text = "\(views) \(description)" + } +} \ No newline at end of file From 7c134b90f43e7a8d72f8c779ba6a445e69104fb9 Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Thu, 26 Nov 2020 02:59:27 +0100 Subject: [PATCH 05/16] Bump swift version to 5.0 --- .../SimpleFlickrBrowser.xcodeproj/project.pbxproj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj index 482d68d..1aaaf04 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj @@ -594,7 +594,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1130; - LastUpgradeCheck = 1130; + LastUpgradeCheck = 1220; ORGANIZATIONNAME = rencevio; TargetAttributes = { 944AC01923CE0E9C009FD611 = { @@ -774,6 +774,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -805,6 +806,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -834,6 +836,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -858,6 +861,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; name = Release; From 41826b96cd0dc9a53234098899402245a7874960 Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Thu, 26 Nov 2020 03:00:17 +0100 Subject: [PATCH 06/16] Parse and layout photo metadata --- SimpleFlickrBrowser/Extensions/String.swift | 16 ++++ .../project.pbxproj | 12 +++ .../SimpleFlickrBrowser/Models/Photo.swift | 10 +-- .../Browser/BrowserViewController.swift | 7 +- .../Scenes/Feed/Cell/FeedCellView.swift | 57 +++++++++--- .../Scenes/Feed/FeedFactory.swift | 8 +- .../Scenes/Feed/FeedInteractor.swift | 4 +- .../Scenes/Feed/FeedModels.swift | 1 - .../Scenes/Feed/FeedViewController.swift | 87 ++++++++++++++++++- .../Scenes/Feed/FeedViewDataSource.swift | 62 ++++++++++--- .../Photos/Entities/FlickrPhotos.swift | 4 + .../Photos/FlickrPhotosService.swift | 1 + .../Flickr/FlickrCollectionFetcher.swift | 25 +++++- .../PhotoParameters.swift | 2 +- 14 files changed, 249 insertions(+), 47 deletions(-) create mode 100644 SimpleFlickrBrowser/Extensions/String.swift diff --git a/SimpleFlickrBrowser/Extensions/String.swift b/SimpleFlickrBrowser/Extensions/String.swift new file mode 100644 index 0000000..2406782 --- /dev/null +++ b/SimpleFlickrBrowser/Extensions/String.swift @@ -0,0 +1,16 @@ +// +// Created by Maxim Berezhnoy on 26/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import Foundation.NSDate + +extension String { + func toInt() -> Int? { + Int(self) + } + + func toDate(formatter: DateFormatter) -> Date? { + formatter.date(from: self) + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj index 1aaaf04..c76242e 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 62DE9452E6FC1D01DD8976DA /* BrowserFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9CC652A43A976EDE4FF6 /* BrowserFactory.swift */; }; 62DE946CFE4B0C1C5B9E1324 /* MockBrowserDisplaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9810EDB0220E74893A89 /* MockBrowserDisplaying.swift */; }; 62DE94F49D115431A2630DAB /* FeedCellInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99D07CD51D362F953646 /* FeedCellInteractor.swift */; }; + 62DE9521164585DF38F0C460 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99C6333390A5CB4F91B0 /* String.swift */; }; 62DE959CA829F11A51767E30 /* FlickrCollectionFetcherFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9B8BC72A4AB8A5A34B65 /* FlickrCollectionFetcherFactory.swift */; }; 62DE95BAA18D1A52C35EF040 /* FlickrPhotos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE96CE6B44327361D73D24 /* FlickrPhotos.swift */; }; 62DE96046383936ED2BDE433 /* FeedModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9D31EAF1CFCCDE85B8DE /* FeedModels.swift */; }; @@ -130,6 +131,7 @@ 62DE98ED3E76A320E0BF36D3 /* FeedCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellView.swift; sourceTree = ""; }; 62DE98FFE252C3D837867816 /* BrowserPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserPresenter.swift; sourceTree = ""; }; 62DE997B53C61FE26593A99A /* BrowserCellInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellInteractor.swift; sourceTree = ""; }; + 62DE99C6333390A5CB4F91B0 /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 62DE99D07CD51D362F953646 /* FeedCellInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellInteractor.swift; sourceTree = ""; }; 62DE99EC2B14DB9F0C554529 /* PhotoDataProviderFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProviderFactory.swift; sourceTree = ""; }; 62DE99F1F322B9F470256B1E /* PhotoDataRetrieving.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataRetrieving.swift; sourceTree = ""; }; @@ -258,6 +260,14 @@ path = Photos; sourceTree = ""; }; + 62DE9688CB88553090DF24CC /* Extensions */ = { + isa = PBXGroup; + children = ( + 62DE99C6333390A5CB4F91B0 /* String.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 62DE96CD430BE02619B01ABC /* Flickr */ = { isa = PBXGroup; children = ( @@ -476,6 +486,7 @@ 944AC01C23CE0E9C009FD611 /* SimpleFlickrBrowser */, 944AC03323CE0E9E009FD611 /* SimpleFlickrBrowserTests */, 944AC01B23CE0E9C009FD611 /* Products */, + 62DE9688CB88553090DF24CC /* Extensions */, ); sourceTree = ""; }; @@ -700,6 +711,7 @@ 62DE9B3C2CAC899A108EFA70 /* DateLabel.swift in Sources */, 62DE9A474D891C506B86BF8A /* ViewsLabel.swift in Sources */, 62DE9F3410EB4A39040BBC55 /* TagsLabel.swift in Sources */, + 62DE9521164585DF38F0C460 /* String.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift index d2fa3d2..cb0b877 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift @@ -8,15 +8,15 @@ import struct Foundation.Date struct Photo { struct Metadata { - let views: Int - let tags: [String] - let ownerName: String - let dateTaken: Date + let views: Int? + let tags: [String]? + let ownerName: String? + let dateTaken: Date? } typealias ID = String let id: ID let imageURL: URL - let metadata: Metadata? + let metadata: Metadata } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift index b35ed02..a7878d1 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift @@ -18,11 +18,12 @@ private struct LayoutConstants { static let lineSpacing: CGFloat = 10 static let heightRatio: CGFloat = 1.0 + + static let photoSize = PhotoParameters.Size.thumbSquare } final class BrowserViewController: UIViewController { private let photosPerFetchRequest = LayoutConstants.itemsPerRow * 15 - private let photoSize = PhotoParameters.Size.thumbSquare private let interactor: BrowserInteracting private let dataSource: BrowserDataSourcing @@ -126,7 +127,7 @@ final class BrowserViewController: UIViewController { startFromPosition: dataSource.photoCount, fetchAtMost: photosPerFetchRequest, searchCriteria: searchController.searchBar.text ?? "", - size: photoSize + size: LayoutConstants.photoSize ) ) } @@ -137,7 +138,7 @@ final class BrowserViewController: UIViewController { startFromPosition: 0, fetchAtMost: photosPerFetchRequest, searchCriteria: searchController.searchBar.text ?? "", - size: photoSize + size: LayoutConstants.photoSize ) ) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift index 3ce5d57..6cd8125 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift @@ -19,7 +19,7 @@ private struct LayoutConstants { static let sideMarginRatio: CGFloat = 0.02 static let tagsSideMarginRatio: CGFloat = 0.04 static let topBottomMarginRatio: CGFloat = 0.2 - + static let dateTakenFormat = "MMM dd, yyyy" } @@ -33,7 +33,7 @@ final class FeedViewCell: UITableViewCell { private var interactor: FeedCellInteracting? - private var heightConstraint: NSLayoutConstraint? + private var imageHeightConstraint: NSLayoutConstraint? // MARK: - Subviews lazy var cellImageView: UIImageView = { @@ -94,16 +94,16 @@ final class FeedViewCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } - lazy var widthConstraint: NSLayoutConstraint = { + lazy var viewWidthConstraint: NSLayoutConstraint = { let constraint = contentView.widthAnchor.constraint(equalToConstant: bounds.size.width) constraint.isActive = true return constraint }() - override func systemLayoutSizeFitting(_ targetSize: CGSize, - withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, + override func systemLayoutSizeFitting(_ targetSize: CGSize, + withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize { - widthConstraint.constant = bounds.size.width + viewWidthConstraint.constant = bounds.size.width return contentView.systemLayoutSizeFitting(CGSize(width: targetSize.width, height: 1)) } @@ -159,7 +159,19 @@ final class FeedViewCell: UITableViewCell { case let .image(image): cellImageView.isHidden = false cellImageView.image = image + layoutImage() + } + } + + private func layoutImage() { + guard let image = cellImageView.image else { + return } + + imageHeightConstraint?.isActive = false + imageHeightConstraint = cellImageView.heightAnchor.constraint(equalTo: cellImageView.widthAnchor, multiplier: image.size.height / image.size.width) + imageHeightConstraint?.priority = .defaultHigh + imageHeightConstraint?.isActive = true } } @@ -167,9 +179,13 @@ final class FeedViewCell: UITableViewCell { // MARK: - FeedCellDisplaying extension FeedViewCell: FeedCellDisplaying { - func display(image: FeedCellModels.PhotoImage.ViewModel) {} + func display(image: FeedCellModels.PhotoImage.ViewModel) { + set(imageState: .image(image.image)) + } - func displayLoading() {} + func displayLoading() { + set(imageState: .loading) + } } // MARK: - ConfigurableFeedCell @@ -179,12 +195,27 @@ extension FeedViewCell: ConfigurableFeedCell { self.interactor = interactor interactor.fetch(image: FeedCellModels.PhotoImage.Request(photoID: photo.id, url: photo.imageURL)) + + let metadata = photo.metadata + + ownerNameView.text = metadata.ownerName ?? "" + + if let dateTaken = metadata.dateTaken { + dateTakenView.set(date: dateTaken) + } else { + dateTakenView.text = "" + } + + if let views = metadata.views { + viewsView.set(views: views) + } else { + viewsView.text = "" + } - if let metadata = photo.metadata { - ownerNameView.text = metadata.ownerName - dateTakenView.set(date: metadata.dateTaken) - viewsView.set(views: metadata.views) - tagsView.set(tags: metadata.tags) + if let tags = metadata.tags { + tagsView.set(tags: tags) + } else { + tagsView.text = "" } } } \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift index 4488bb3..51d9f09 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift @@ -19,11 +19,9 @@ final class FeedFactory { } private func createDataSource(photoDataProvider: PhotoDataProviding) -> FeedDataSourcing { -// let cellFactory = BrowserCellFactory(photoDataProvider: photoDataProvider) -// let cellConfigurator = cellFactory.createConfigurator() + let cellFactory = FeedCellFactory(photoDataProvider: photoDataProvider) + let cellConfigurator = cellFactory.createConfigurator() - return FeedViewDataSource( -// cellConfigurator: cellConfigurator - ) + return FeedViewDataSource(cellConfigurator: cellConfigurator) } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift index de2296f..1ec254c 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift @@ -30,9 +30,9 @@ final class FeedInteractor: FeedInteracting { photoCollectionFetcher.fetchPhotos( startingFrom: request.startFromPosition, fetchAtMost: request.fetchAtMost, - matching: request.searchCriteria, + matching: "", withSize: request.size, - includeMetadata: [] + includeMetadata: request.metadata ) { result in switch result { case let .success(photos): diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift index 1089067..eec01fe 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift @@ -8,7 +8,6 @@ struct FeedModels { struct Request: Equatable { let startFromPosition: Int let fetchAtMost: Int - let searchCriteria: String let size: PhotoParameters.Size let metadata: [PhotoParameters.Metadata] } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift index 366e805..b19bc72 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift @@ -10,8 +10,19 @@ protocol FeedDisplaying: AnyObject { func displayMore(photos: FeedModels.Photos.ViewModel) } +private struct LayoutConstants { + static let photoSize = PhotoParameters.Size.medium + static let itemSpacing: CGFloat = 30 +} + final class FeedViewController: UIViewController { - private let photoSize = PhotoParameters.Size.medium + private let photosPerFetchRequest = 4 + private let metadataToFetch: [PhotoParameters.Metadata] = [ + .views, + .dateTaken, + .tags, + .ownerName + ] private let interactor: FeedInteracting private let dataSource: FeedDataSourcing @@ -19,6 +30,15 @@ final class FeedViewController: UIViewController { private lazy var tableView: UITableView = { let view = UITableView(frame: .zero) + view.alwaysBounceVertical = true + view.backgroundColor = Style.ScreenBackground.color + + view.tableFooterView = UIView() + view.separatorStyle = .none + view.sectionHeaderHeight = LayoutConstants.itemSpacing + + view.allowsSelection = false + return view }() @@ -51,7 +71,17 @@ final class FeedViewController: UIViewController { // MARK: - View Setup private func setupTableView() { - + view.addSubview(tableView) + + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + tableView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + tableView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true + tableView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true + + tableView.delegate = self + + dataSource.register(for: tableView) } private func setupRefreshControl() { @@ -63,7 +93,45 @@ final class FeedViewController: UIViewController { // MARK: - Data Requesting private func requestNewPhotos() { - + interactor.fetch( + photos: FeedModels.Photos.Request( + startFromPosition: dataSource.photoCount, + fetchAtMost: photosPerFetchRequest, + size: LayoutConstants.photoSize, + metadata: metadataToFetch + ) + ) + } + + func requestMorePhotos() { + interactor.fetch( + photos: FeedModels.Photos.Request( + startFromPosition: dataSource.photoCount, + fetchAtMost: photosPerFetchRequest, + size: LayoutConstants.photoSize, + metadata: metadataToFetch + ) + ) + } +} + +// MARK: - UITableViewDelegate + +extension FeedViewController: UITableViewDelegate { + public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + UIView() + } + + public func tableView(_ tableView: UITableView, + willDisplay cell: UITableViewCell, + forRowAt indexPath: IndexPath) { + let itemToDisplay = indexPath.section + + let loadedPhotosCount = dataSource.photoCount + + if itemToDisplay == loadedPhotosCount { + requestMorePhotos() + } } } @@ -71,10 +139,21 @@ final class FeedViewController: UIViewController { extension FeedViewController: FeedDisplaying { func displayNew(photos: FeedModels.Photos.ViewModel) { + dataSource.set(photos: photos.photos) + refreshControl.endRefreshing() + + tableView.reloadData() + tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) } - func displayMore(photos: FeedModels.Photos.ViewModel) {} + func displayMore(photos: FeedModels.Photos.ViewModel) { + let currentPhotoCount = dataSource.photoCount + + dataSource.add(photos: photos.photos) + + tableView.insertSections(IndexSet(integersIn: currentPhotoCount.. Int { fatalError("tableView(_:numberOfRowsInSection:) has not been implemented") } + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + 1 + } + + public func numberOfSections(in tableView: UITableView) -> Int { + photos.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = dequeueFeedCell(from: tableView, at: indexPath) + + let photo = photos[indexPath.item] - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { fatalError("tableView(_:cellForRowAt:) has not been implemented") } + cellConfigurator.configure(cell: cell, with: photo) + + return cell + } } // MARK: - FeedDataSourcing extension FeedViewDataSource: FeedDataSourcing { + var photoCount: Int { + photos.count + } + + func register(for tableView: UITableView) { + tableView.dataSource = self + tableView.register(FeedViewCell.self, forCellReuseIdentifier: FeedViewCell.identifier) + } + func add(photos: [Photo]) { + self.photos.append(contentsOf: photos) + } + + func set(photos: [Photo]) { + self.photos = photos + } } +// MARK: - FeedViewCell dequeuing + +extension FeedViewDataSource { + func dequeueFeedCell(from tableView: UITableView, at indexPath: IndexPath) -> FeedViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: FeedViewCell.identifier, for: indexPath) + + guard let feedCell = cell as? FeedViewCell else { + fatalError("Unexpected cell type while dequeuing in \(FeedViewDataSource.self): got \(cell)") + } + + return feedCell + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/Entities/FlickrPhotos.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/Entities/FlickrPhotos.swift index cb77c18..a2e54a5 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/Entities/FlickrPhotos.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/Entities/FlickrPhotos.swift @@ -18,4 +18,8 @@ struct FlickrPhoto: Decodable { let secret: String let server: String let farm: Int + let datetaken: String? + let ownername: String? + let views: String? + let tags: String? } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift index 5635b7d..d8e9eaf 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift @@ -29,6 +29,7 @@ final class FlickrPhotosService: FlickrPhotosFetching { queryParameters: [ .page: String(page), .perPage: String(photosPerPage), + .extras: getQueryParameters(for: metadata).map { $0.rawValue }.joined(separator: ",") ] ) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift index 51b0dd5..4e5bdc1 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift @@ -3,6 +3,8 @@ // // Copyright (c) 2020 rencevio. All rights reserved. +import Foundation.NSDateFormatter + final class FlickrCollectionFetcher: PhotoCollectionFetching { private let flickrPhotosService: FlickrPhotosFetching @@ -48,11 +50,30 @@ final class FlickrCollectionFetcher: PhotoCollectionFetching { { result in switch result { case let .success(photos): - // todo: metadata - completion(.success(photos.map { Photo(id: $0.id, imageURL: FlickrPhotoURLResolver.resolveUrl(for: $0, withSize: size), metadata: nil) })) + completion(.success(photos.map { photo in + Photo(id: photo.id, + imageURL: FlickrPhotoURLResolver.resolveUrl( + for: photo, + withSize: size + ), + metadata: extractMetadata(from: photo) + ) + })) case let .failure(error): completion(.failure(error)) } } } } + +private func extractMetadata(from photo: FlickrPhoto) -> Photo.Metadata { + let dateTakenFormatter = DateFormatter() + dateTakenFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss" + + return Photo.Metadata( + views: photo.views?.toInt(), + tags: photo.tags?.components(separatedBy: " "), + ownerName: photo.ownername, + dateTaken: photo.datetaken?.toDate(formatter: dateTakenFormatter) + ) +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift index 4165efc..9e80dc6 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift @@ -10,7 +10,7 @@ enum PhotoParameters { case large } - enum Metadata { + enum Metadata: CaseIterable { case views case tags case ownerName From d1baf29217e5d1fbbde0b93d7c9a245697a16e22 Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Fri, 27 Nov 2020 01:32:21 +0100 Subject: [PATCH 07/16] Removed the browser component; added worker to fetch image data for a collection of photos --- .../project.pbxproj | 116 +--------- .../SimpleFlickrBrowser/Models/Photo.swift | 12 +- .../RootDependencies.swift | 12 +- .../Scenes/Browser/BrowserFactory.swift | 27 --- .../Scenes/Browser/BrowserInteractor.swift | 55 ----- .../Scenes/Browser/BrowserModels.swift | 24 -- .../Scenes/Browser/BrowserPresenter.swift | 22 -- .../Browser/BrowserViewController.swift | 219 ------------------ .../Browser/BrowserViewDataSource.swift | 79 ------- .../Browser/Cell/BrowserCellFactory.swift | 20 -- .../Browser/Cell/BrowserCellInteractor.swift | 45 ---- .../Browser/Cell/BrowserCellModels.swift | 23 -- .../Browser/Cell/BrowserCellPresenter.swift | 32 --- .../Scenes/Browser/Cell/BrowserCellView.swift | 112 --------- .../BrowserCellConfigurator.swift | 25 -- .../ConfigurableBrowserCell.swift | 8 - .../Configurator/ConfigurableFeedCell.swift | 8 - .../Configurator/FeedCellConfigurator.swift | 25 -- .../Scenes/Feed/Cell/FeedCellFactory.swift | 20 -- .../Scenes/Feed/Cell/FeedCellInteractor.swift | 45 ---- .../Scenes/Feed/Cell/FeedCellModels.swift | 23 -- .../Scenes/Feed/Cell/FeedCellPresenter.swift | 32 --- .../Scenes/Feed/Cell/FeedCellView.swift | 76 ++---- .../Scenes/Feed/FeedFactory.swift | 12 +- .../Scenes/Feed/FeedInteractor.swift | 10 +- .../Scenes/Feed/FeedModels.swift | 2 + .../Scenes/Feed/FeedViewController.swift | 3 +- .../Scenes/Feed/FeedViewDataSource.swift | 13 +- .../Photos/Entities/FlickrPhotos.swift | 8 +- .../Flickr/FlickrCollectionDataFetcher.swift | 46 ++++ .../Flickr/FlickrCollectionDataFetching.swift | 10 + .../Flickr/FlickrCollectionFetcher.swift | 104 +++++---- .../FlickrCollectionFetcherFactory.swift | 8 +- .../PhotoCollectionFetcherFactory.swift | 2 +- .../PhotoCollectionFetching.swift | 2 - 35 files changed, 177 insertions(+), 1103 deletions(-) delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserFactory.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserInteractor.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserModels.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserPresenter.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewDataSource.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellFactory.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellInteractor.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellModels.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellPresenter.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellView.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/Configurator/BrowserCellConfigurator.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/Configurator/ConfigurableBrowserCell.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/ConfigurableFeedCell.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/FeedCellConfigurator.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellInteractor.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellModels.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellPresenter.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcher.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetching.swift diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj index c76242e..4781bfd 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj @@ -7,14 +7,9 @@ objects = { /* Begin PBXBuildFile section */ - 62DE9072B9CEDE9C92CBF7AF /* BrowserPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE98FFE252C3D837867816 /* BrowserPresenter.swift */; }; - 62DE90A60057F36A04993DE3 /* BrowserCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE92C6C8EA6BC8A24E8EBC /* BrowserCellFactory.swift */; }; - 62DE90B3B714B21C13925831 /* BrowserCellConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9B272F1AC1988F71AF6E /* BrowserCellConfigurator.swift */; }; 62DE90E73B9BFCA4A54B76C2 /* MockPhotoDataRetriever.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE96B5EBB14213DC9E7804 /* MockPhotoDataRetriever.swift */; }; 62DE90F40E919CCFF876D1A6 /* MockFlickrPhotoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9225FFEBB4968F60A987 /* MockFlickrPhotoService.swift */; }; 62DE911E1EB4D78BCA9085CB /* FlickrCollectionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9A2EBADCA5EE9059C3DD /* FlickrCollectionFetcherTests.swift */; }; - 62DE9150E7D8AE5820E891E1 /* FeedCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9779CE157EB936EF5A99 /* FeedCellFactory.swift */; }; - 62DE91E5B84C14466618EAC1 /* ConfigurableBrowserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE91396D5EDB77544792D3 /* ConfigurableBrowserCell.swift */; }; 62DE91F2D0080AF13460E47C /* FeedInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9DB2850E842E22C4CC84 /* FeedInteractor.swift */; }; 62DE91F96D06BDB6AE8D40A1 /* XCTestCase+QueueAwait.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE924CEDF37B8D5BC86D48 /* XCTestCase+QueueAwait.swift */; }; 62DE927526937FB6BEDCE8BC /* MockBrowserCellPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9B3E9AAA8D0661DEEFFE /* MockBrowserCellPresenter.swift */; }; @@ -22,14 +17,12 @@ 62DE92DECC87D7DACFB69CC9 /* PhotoDataRetrieving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99F1F322B9F470256B1E /* PhotoDataRetrieving.swift */; }; 62DE93261A41C5DDC9BACDB0 /* MockPhotoDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE91C1A5EBF975C64A37B3 /* MockPhotoDataCache.swift */; }; 62DE9387D309D2DD3F2AC102 /* BrowserPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE915A672C1C41DCCF9C8D /* BrowserPresenterTests.swift */; }; - 62DE93997644B299A146E0FF /* FeedCellPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE92C92B36ED5061F378E7 /* FeedCellPresenter.swift */; }; 62DE93E9065F0528B8577C0E /* BrowserCellInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE93A95BC1DB3D74EC66D8 /* BrowserCellInteractorTests.swift */; }; - 62DE9427597853C3B0A5E661 /* BrowserCellModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE969E8F3F30D004171974 /* BrowserCellModels.swift */; }; 62DE944EF838C8C313469CCB /* MockPhotoDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE92EE74C81312D3DB90B0 /* MockPhotoDataProvider.swift */; }; - 62DE9452E6FC1D01DD8976DA /* BrowserFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9CC652A43A976EDE4FF6 /* BrowserFactory.swift */; }; 62DE946CFE4B0C1C5B9E1324 /* MockBrowserDisplaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9810EDB0220E74893A89 /* MockBrowserDisplaying.swift */; }; - 62DE94F49D115431A2630DAB /* FeedCellInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99D07CD51D362F953646 /* FeedCellInteractor.swift */; }; + 62DE947BBB3CC8AC189E0F4F /* FlickrCollectionDataFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE94AB83D36878355DAA8C /* FlickrCollectionDataFetching.swift */; }; 62DE9521164585DF38F0C460 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99C6333390A5CB4F91B0 /* String.swift */; }; + 62DE959A03CFDDCC0107EB65 /* FlickrCollectionDataFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE92B5279132DA6C6FA657 /* FlickrCollectionDataFetcher.swift */; }; 62DE959CA829F11A51767E30 /* FlickrCollectionFetcherFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9B8BC72A4AB8A5A34B65 /* FlickrCollectionFetcherFactory.swift */; }; 62DE95BAA18D1A52C35EF040 /* FlickrPhotos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE96CE6B44327361D73D24 /* FlickrPhotos.swift */; }; 62DE96046383936ED2BDE433 /* FeedModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9D31EAF1CFCCDE85B8DE /* FeedModels.swift */; }; @@ -41,32 +34,22 @@ 62DE97615F6230CBFF9F92A4 /* FeedFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE96216E4D360CAB68AF75 /* FeedFactory.swift */; }; 62DE97B6EC739349BC9A6D2F /* FlickrPhotoServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE97953D19EE0C13151316 /* FlickrPhotoServiceTests.swift */; }; 62DE97CDCFCD2718C1BBC1E8 /* PhotoCollectionFetcherFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9A0F5F5E76B6AFE7159B /* PhotoCollectionFetcherFactory.swift */; }; - 62DE9825AD3853FDB25206DB /* FeedCellModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9E7BAAEC8599D46BFE80 /* FeedCellModels.swift */; }; - 62DE983EF92FD18305C80488 /* BrowserCellInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE997B53C61FE26593A99A /* BrowserCellInteractor.swift */; }; 62DE9866170F4BCE2B93E2E0 /* MockBrowserPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE944604E007DD383ABDE1 /* MockBrowserPresenter.swift */; }; - 62DE98D6E300CD17DD7D23A3 /* BrowserCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9648DCA09C364860E8BB /* BrowserCellView.swift */; }; 62DE9921C1443189F640771E /* PhotoDataProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C85502219A9858DCF22 /* PhotoDataProviderTests.swift */; }; 62DE9927B234F05FAB311B8D /* RootDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE94A9AA78F83D54078368 /* RootDependencies.swift */; }; 62DE9934B5A84CB2A3736360 /* MockPhotoCollectionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9001FE62BE27C3D938B3 /* MockPhotoCollectionFetcher.swift */; }; 62DE9A474D891C506B86BF8A /* ViewsLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE97EF27BCB2A06EB35FDF /* ViewsLabel.swift */; }; 62DE9A8C69E9E0A200181E5C /* FlickrApiURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9D790ECF920DF2C9802B /* FlickrApiURLBuilder.swift */; }; 62DE9A9250BD0AC541234841 /* PhotoDataProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99EC2B14DB9F0C554529 /* PhotoDataProviderFactory.swift */; }; - 62DE9B36C2A72575C7802A24 /* BrowserInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9E2159757D6020248C32 /* BrowserInteractor.swift */; }; 62DE9B3C2CAC899A108EFA70 /* DateLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE963F12E663C923326C82 /* DateLabel.swift */; }; - 62DE9B6C8EDB44A25EEE77A0 /* BrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE900EADCC52560B246392 /* BrowserViewController.swift */; }; 62DE9C2CEA5B44E24BB2D6F5 /* PhotoCollectionFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE90B754DC827D73A31B9E /* PhotoCollectionFetching.swift */; }; - 62DE9C8EC1EBD9B055EF5388 /* BrowserViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9817F61216E36044EA42 /* BrowserViewDataSource.swift */; }; 62DE9CACADCC0438B0091A5C /* FeedCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE98ED3E76A320E0BF36D3 /* FeedCellView.swift */; }; - 62DE9CBE1FB6013A700EB635 /* BrowserCellPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9591CAA01E3F3C91A977 /* BrowserCellPresenter.swift */; }; 62DE9CD65849E14B67A5AF42 /* PhotoParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9031E5EEBEA8EBA4D644 /* PhotoParameters.swift */; }; 62DE9CE6366FD6ECE740C55C /* FlickrCollectionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C90249CCE2FAF835CF5 /* FlickrCollectionFetcher.swift */; }; - 62DE9CF01CC66EDE19407A30 /* ConfigurableFeedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE948E9483F63E1CCCCCF5 /* ConfigurableFeedCell.swift */; }; 62DE9D29B400373915DB581A /* FeedViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE925A929D79068D7BE7CB /* FeedViewDataSource.swift */; }; - 62DE9DE0C4D4BD2598CA2C7F /* FeedCellConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C5C53014BB9513FCD95 /* FeedCellConfigurator.swift */; }; 62DE9E63C0B530594A5B150B /* Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C8D791DA5418ED32FCF /* Style.swift */; }; 62DE9EBE33EDC3D97D929FF2 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE970A3FD09743A03DAF33 /* Photo.swift */; }; 62DE9EC2FE48A5A204531EEA /* HttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE93C78893DAA0E46DBD6A /* HttpClient.swift */; }; - 62DE9F2E46CDDC60B4A5DEAE /* BrowserModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9E652D625ECC302D66F9 /* BrowserModels.swift */; }; 62DE9F3410EB4A39040BBC55 /* TagsLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE940D06ADFF0BDAC17ADC /* TagsLabel.swift */; }; 62DE9F5B37C5FE4BED753FB2 /* PhotoDataNSCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9805D6D4DBDDBAD078E2 /* PhotoDataNSCache.swift */; }; 62DE9F69E85302202E6884B3 /* PhotoDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9FF6B55060E5204AC907 /* PhotoDataProvider.swift */; }; @@ -91,69 +74,52 @@ /* Begin PBXFileReference section */ 62DE9001FE62BE27C3D938B3 /* MockPhotoCollectionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoCollectionFetcher.swift; sourceTree = ""; }; - 62DE900EADCC52560B246392 /* BrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = ""; }; 62DE9031E5EEBEA8EBA4D644 /* PhotoParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoParameters.swift; sourceTree = ""; }; 62DE90B754DC827D73A31B9E /* PhotoCollectionFetching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionFetching.swift; sourceTree = ""; }; - 62DE91396D5EDB77544792D3 /* ConfigurableBrowserCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurableBrowserCell.swift; sourceTree = ""; }; 62DE915A672C1C41DCCF9C8D /* BrowserPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserPresenterTests.swift; sourceTree = ""; }; 62DE91C1A5EBF975C64A37B3 /* MockPhotoDataCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataCache.swift; sourceTree = ""; }; 62DE9225FFEBB4968F60A987 /* MockFlickrPhotoService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFlickrPhotoService.swift; sourceTree = ""; }; 62DE924CEDF37B8D5BC86D48 /* XCTestCase+QueueAwait.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCTestCase+QueueAwait.swift"; sourceTree = ""; }; 62DE925A929D79068D7BE7CB /* FeedViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedViewDataSource.swift; sourceTree = ""; }; - 62DE92C6C8EA6BC8A24E8EBC /* BrowserCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellFactory.swift; sourceTree = ""; }; - 62DE92C92B36ED5061F378E7 /* FeedCellPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellPresenter.swift; sourceTree = ""; }; + 62DE92B5279132DA6C6FA657 /* FlickrCollectionDataFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionDataFetcher.swift; sourceTree = ""; }; 62DE92EE74C81312D3DB90B0 /* MockPhotoDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataProvider.swift; sourceTree = ""; }; 62DE93A95BC1DB3D74EC66D8 /* BrowserCellInteractorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellInteractorTests.swift; sourceTree = ""; }; 62DE93C78893DAA0E46DBD6A /* HttpClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpClient.swift; sourceTree = ""; }; 62DE940D06ADFF0BDAC17ADC /* TagsLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TagsLabel.swift; sourceTree = ""; }; 62DE944604E007DD383ABDE1 /* MockBrowserPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBrowserPresenter.swift; sourceTree = ""; }; - 62DE948E9483F63E1CCCCCF5 /* ConfigurableFeedCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurableFeedCell.swift; sourceTree = ""; }; 62DE94A9AA78F83D54078368 /* RootDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootDependencies.swift; sourceTree = ""; }; + 62DE94AB83D36878355DAA8C /* FlickrCollectionDataFetching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionDataFetching.swift; sourceTree = ""; }; 62DE954BE0BFF3D2EE91DE12 /* FlickrPhotosService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotosService.swift; sourceTree = ""; }; - 62DE9591CAA01E3F3C91A977 /* BrowserCellPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellPresenter.swift; sourceTree = ""; }; 62DE96216E4D360CAB68AF75 /* FeedFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedFactory.swift; sourceTree = ""; }; 62DE963F12E663C923326C82 /* DateLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateLabel.swift; sourceTree = ""; }; - 62DE9648DCA09C364860E8BB /* BrowserCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellView.swift; sourceTree = ""; }; 62DE9673FC376E2BC90087D6 /* FeedPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedPresenter.swift; sourceTree = ""; }; - 62DE969E8F3F30D004171974 /* BrowserCellModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellModels.swift; sourceTree = ""; }; 62DE96B5EBB14213DC9E7804 /* MockPhotoDataRetriever.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataRetriever.swift; sourceTree = ""; }; 62DE96CE6B44327361D73D24 /* FlickrPhotos.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotos.swift; sourceTree = ""; }; 62DE970A3FD09743A03DAF33 /* Photo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = ""; }; - 62DE9779CE157EB936EF5A99 /* FeedCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellFactory.swift; sourceTree = ""; }; 62DE97953D19EE0C13151316 /* FlickrPhotoServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotoServiceTests.swift; sourceTree = ""; }; 62DE97EF27BCB2A06EB35FDF /* ViewsLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewsLabel.swift; sourceTree = ""; }; 62DE9805D6D4DBDDBAD078E2 /* PhotoDataNSCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataNSCache.swift; sourceTree = ""; }; 62DE9810EDB0220E74893A89 /* MockBrowserDisplaying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBrowserDisplaying.swift; sourceTree = ""; }; - 62DE9817F61216E36044EA42 /* BrowserViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserViewDataSource.swift; sourceTree = ""; }; 62DE986497C5FB223DA10256 /* FlickrPhotoURLResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotoURLResolver.swift; sourceTree = ""; }; 62DE9868FFC49BF89E9F9192 /* NetworkPhotoDataRetriever.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkPhotoDataRetriever.swift; sourceTree = ""; }; 62DE987D3B885CA1A25FB693 /* FlickrApiValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrApiValues.swift; sourceTree = ""; }; 62DE98ED3E76A320E0BF36D3 /* FeedCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellView.swift; sourceTree = ""; }; - 62DE98FFE252C3D837867816 /* BrowserPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserPresenter.swift; sourceTree = ""; }; - 62DE997B53C61FE26593A99A /* BrowserCellInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellInteractor.swift; sourceTree = ""; }; 62DE99C6333390A5CB4F91B0 /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; - 62DE99D07CD51D362F953646 /* FeedCellInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellInteractor.swift; sourceTree = ""; }; 62DE99EC2B14DB9F0C554529 /* PhotoDataProviderFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProviderFactory.swift; sourceTree = ""; }; 62DE99F1F322B9F470256B1E /* PhotoDataRetrieving.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataRetrieving.swift; sourceTree = ""; }; 62DE9A0F5F5E76B6AFE7159B /* PhotoCollectionFetcherFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionFetcherFactory.swift; sourceTree = ""; }; 62DE9A2EBADCA5EE9059C3DD /* FlickrCollectionFetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcherTests.swift; sourceTree = ""; }; - 62DE9B272F1AC1988F71AF6E /* BrowserCellConfigurator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellConfigurator.swift; sourceTree = ""; }; 62DE9B3E9AAA8D0661DEEFFE /* MockBrowserCellPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBrowserCellPresenter.swift; sourceTree = ""; }; 62DE9B49CABD228CCFBCE936 /* PhotoDataCaching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataCaching.swift; sourceTree = ""; }; 62DE9B8BC72A4AB8A5A34B65 /* FlickrCollectionFetcherFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcherFactory.swift; sourceTree = ""; }; 62DE9C09B259CEA839DAE2B9 /* FeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = ""; }; - 62DE9C5C53014BB9513FCD95 /* FeedCellConfigurator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellConfigurator.swift; sourceTree = ""; }; 62DE9C85502219A9858DCF22 /* PhotoDataProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProviderTests.swift; sourceTree = ""; }; 62DE9C8D791DA5418ED32FCF /* Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Style.swift; sourceTree = ""; }; 62DE9C90249CCE2FAF835CF5 /* FlickrCollectionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcher.swift; sourceTree = ""; }; - 62DE9CC652A43A976EDE4FF6 /* BrowserFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserFactory.swift; sourceTree = ""; }; 62DE9D31EAF1CFCCDE85B8DE /* FeedModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedModels.swift; sourceTree = ""; }; 62DE9D790ECF920DF2C9802B /* FlickrApiURLBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrApiURLBuilder.swift; sourceTree = ""; }; 62DE9DB2850E842E22C4CC84 /* FeedInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedInteractor.swift; sourceTree = ""; }; 62DE9E19BC1900DC8648FD5E /* MockHttpClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockHttpClient.swift; sourceTree = ""; }; - 62DE9E2159757D6020248C32 /* BrowserInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserInteractor.swift; sourceTree = ""; }; - 62DE9E652D625ECC302D66F9 /* BrowserModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserModels.swift; sourceTree = ""; }; - 62DE9E7BAAEC8599D46BFE80 /* FeedCellModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellModels.swift; sourceTree = ""; }; 62DE9EA9D6ACC68E32F8A681 /* BrowserInteractorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserInteractorTests.swift; sourceTree = ""; }; 62DE9FF6B55060E5204AC907 /* PhotoDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProvider.swift; sourceTree = ""; }; 944AC01A23CE0E9C009FD611 /* SimpleFlickrBrowser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimpleFlickrBrowser.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -319,11 +285,6 @@ 62DE98914295E0F2A4403FBE /* Cell */ = { isa = PBXGroup; children = ( - 62DE9B5801A7627FCBDA8DF6 /* Configurator */, - 62DE9779CE157EB936EF5A99 /* FeedCellFactory.swift */, - 62DE99D07CD51D362F953646 /* FeedCellInteractor.swift */, - 62DE9E7BAAEC8599D46BFE80 /* FeedCellModels.swift */, - 62DE92C92B36ED5061F378E7 /* FeedCellPresenter.swift */, 62DE98ED3E76A320E0BF36D3 /* FeedCellView.swift */, 62DE9EA0E878692CF74D79CB /* Metadata */, ); @@ -357,15 +318,6 @@ path = Photos; sourceTree = ""; }; - 62DE99D691735A5674751646 /* Configurator */ = { - isa = PBXGroup; - children = ( - 62DE91396D5EDB77544792D3 /* ConfigurableBrowserCell.swift */, - 62DE9B272F1AC1988F71AF6E /* BrowserCellConfigurator.swift */, - ); - path = Configurator; - sourceTree = ""; - }; 62DE9AABA2EDCDF4B92072F7 /* Extensions */ = { isa = PBXGroup; children = ( @@ -374,15 +326,6 @@ path = Extensions; sourceTree = ""; }; - 62DE9B5801A7627FCBDA8DF6 /* Configurator */ = { - isa = PBXGroup; - children = ( - 62DE9C5C53014BB9513FCD95 /* FeedCellConfigurator.swift */, - 62DE948E9483F63E1CCCCCF5 /* ConfigurableFeedCell.swift */, - ); - path = Configurator; - sourceTree = ""; - }; 62DE9C11F52BBF38DEB44438 /* Network */ = { isa = PBXGroup; children = ( @@ -449,24 +392,13 @@ path = Metadata; sourceTree = ""; }; - 62DE9ED1568E25802A697C02 /* Cell */ = { - isa = PBXGroup; - children = ( - 62DE969E8F3F30D004171974 /* BrowserCellModels.swift */, - 62DE9648DCA09C364860E8BB /* BrowserCellView.swift */, - 62DE997B53C61FE26593A99A /* BrowserCellInteractor.swift */, - 62DE9591CAA01E3F3C91A977 /* BrowserCellPresenter.swift */, - 62DE99D691735A5674751646 /* Configurator */, - 62DE92C6C8EA6BC8A24E8EBC /* BrowserCellFactory.swift */, - ); - path = Cell; - sourceTree = ""; - }; 62DE9F3D2015C40F75DEED96 /* Flickr */ = { isa = PBXGroup; children = ( 62DE9C90249CCE2FAF835CF5 /* FlickrCollectionFetcher.swift */, 62DE9B8BC72A4AB8A5A34B65 /* FlickrCollectionFetcherFactory.swift */, + 62DE92B5279132DA6C6FA657 /* FlickrCollectionDataFetcher.swift */, + 62DE94AB83D36878355DAA8C /* FlickrCollectionDataFetching.swift */, ); path = Flickr; sourceTree = ""; @@ -539,27 +471,12 @@ 944AC04123CFA5A1009FD611 /* Scenes */ = { isa = PBXGroup; children = ( - 944AC04523CFA643009FD611 /* Browser */, 62DE9C8D791DA5418ED32FCF /* Style.swift */, 62DE923C7B6825D8B8E3001A /* Feed */, ); path = Scenes; sourceTree = ""; }; - 944AC04523CFA643009FD611 /* Browser */ = { - isa = PBXGroup; - children = ( - 62DE900EADCC52560B246392 /* BrowserViewController.swift */, - 62DE9E2159757D6020248C32 /* BrowserInteractor.swift */, - 62DE98FFE252C3D837867816 /* BrowserPresenter.swift */, - 62DE9E652D625ECC302D66F9 /* BrowserModels.swift */, - 62DE9817F61216E36044EA42 /* BrowserViewDataSource.swift */, - 62DE9CC652A43A976EDE4FF6 /* BrowserFactory.swift */, - 62DE9ED1568E25802A697C02 /* Cell */, - ); - path = Browser; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -662,13 +579,7 @@ files = ( 944AC04423CFA5CC009FD611 /* FlickrApiKey.swift in Sources */, 944AC01E23CE0E9C009FD611 /* AppDelegate.swift in Sources */, - 62DE9B6C8EDB44A25EEE77A0 /* BrowserViewController.swift in Sources */, - 62DE9B36C2A72575C7802A24 /* BrowserInteractor.swift in Sources */, - 62DE9072B9CEDE9C92CBF7AF /* BrowserPresenter.swift in Sources */, - 62DE9F2E46CDDC60B4A5DEAE /* BrowserModels.swift in Sources */, - 62DE9C8EC1EBD9B055EF5388 /* BrowserViewDataSource.swift in Sources */, 62DE9927B234F05FAB311B8D /* RootDependencies.swift in Sources */, - 62DE9452E6FC1D01DD8976DA /* BrowserFactory.swift in Sources */, 62DE9E63C0B530594A5B150B /* Style.swift in Sources */, 62DE9F69E85302202E6884B3 /* PhotoDataProvider.swift in Sources */, 62DE9FDF59C5BAB0F8D43494 /* PhotoDataCaching.swift in Sources */, @@ -676,13 +587,6 @@ 62DE92DECC87D7DACFB69CC9 /* PhotoDataRetrieving.swift in Sources */, 62DE92A1A0D21DCC9FC97F7E /* NetworkPhotoDataRetriever.swift in Sources */, 62DE9F5B37C5FE4BED753FB2 /* PhotoDataNSCache.swift in Sources */, - 62DE98D6E300CD17DD7D23A3 /* BrowserCellView.swift in Sources */, - 62DE9427597853C3B0A5E661 /* BrowserCellModels.swift in Sources */, - 62DE983EF92FD18305C80488 /* BrowserCellInteractor.swift in Sources */, - 62DE9CBE1FB6013A700EB635 /* BrowserCellPresenter.swift in Sources */, - 62DE91E5B84C14466618EAC1 /* ConfigurableBrowserCell.swift in Sources */, - 62DE90B3B714B21C13925831 /* BrowserCellConfigurator.swift in Sources */, - 62DE90A60057F36A04993DE3 /* BrowserCellFactory.swift in Sources */, 62DE9EC2FE48A5A204531EEA /* HttpClient.swift in Sources */, 62DE9C2CEA5B44E24BB2D6F5 /* PhotoCollectionFetching.swift in Sources */, 62DE97CDCFCD2718C1BBC1E8 /* PhotoCollectionFetcherFactory.swift in Sources */, @@ -701,17 +605,13 @@ 62DE9CD65849E14B67A5AF42 /* PhotoParameters.swift in Sources */, 62DE97615F6230CBFF9F92A4 /* FeedFactory.swift in Sources */, 62DE9D29B400373915DB581A /* FeedViewDataSource.swift in Sources */, - 62DE9DE0C4D4BD2598CA2C7F /* FeedCellConfigurator.swift in Sources */, - 62DE9CF01CC66EDE19407A30 /* ConfigurableFeedCell.swift in Sources */, - 62DE9150E7D8AE5820E891E1 /* FeedCellFactory.swift in Sources */, - 62DE94F49D115431A2630DAB /* FeedCellInteractor.swift in Sources */, - 62DE9825AD3853FDB25206DB /* FeedCellModels.swift in Sources */, - 62DE93997644B299A146E0FF /* FeedCellPresenter.swift in Sources */, 62DE9CACADCC0438B0091A5C /* FeedCellView.swift in Sources */, 62DE9B3C2CAC899A108EFA70 /* DateLabel.swift in Sources */, 62DE9A474D891C506B86BF8A /* ViewsLabel.swift in Sources */, 62DE9F3410EB4A39040BBC55 /* TagsLabel.swift in Sources */, 62DE9521164585DF38F0C460 /* String.swift in Sources */, + 62DE959A03CFDDCC0107EB65 /* FlickrCollectionDataFetcher.swift in Sources */, + 62DE947BBB3CC8AC189E0F4F /* FlickrCollectionDataFetching.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift index cb0b877..185992b 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift @@ -5,18 +5,20 @@ import struct Foundation.URL import struct Foundation.Date +import struct Foundation.Data struct Photo { struct Metadata { - let views: Int? - let tags: [String]? - let ownerName: String? - let dateTaken: Date? + let views: Int + let tags: [String] + let ownerName: String + let dateTaken: Date } typealias ID = String let id: ID - let imageURL: URL + let imageData: Data + let fullSizeImageURL: URL let metadata: Metadata } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift index abde65b..0431b31 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift @@ -12,23 +12,13 @@ final class RootDependencies { photoDataProvider = photoDataProviderFactory.createPhotoProvider() let photoCollectionFetcherFactory = FlickrCollectionFetcherFactory() - photoCollectionFetcher = photoCollectionFetcherFactory.createCollectionFetcher() - } - - func createBrowserViewController() -> BrowserViewController { - let browserFactory = BrowserFactory() - - return browserFactory.createViewController( - photoDataProvider: photoDataProvider, - photoCollectionFetcher: photoCollectionFetcher - ) + photoCollectionFetcher = photoCollectionFetcherFactory.createCollectionFetcher(photoDataProvider: photoDataProvider) } func createFeedViewController() -> FeedViewController { let feedFactory = FeedFactory() return feedFactory.createViewController( - photoDataProvider: photoDataProvider, photoCollectionFetcher: photoCollectionFetcher ) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserFactory.swift deleted file mode 100644 index c06e54e..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserFactory.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Created by Maxim Berezhnoy on 16/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -final class BrowserFactory { - func createViewController(photoDataProvider: PhotoDataProviding, - photoCollectionFetcher: PhotoCollectionFetching) -> BrowserViewController { - let presenter = BrowserPresenter() - let interactor = BrowserInteractor(presenter: presenter, photoCollectionFetcher: photoCollectionFetcher) - - let dataSource = createDataSource(photoDataProvider: photoDataProvider) - - let viewController = BrowserViewController(interactor: interactor, dataSource: dataSource) - - presenter.view = viewController - - return viewController - } - - private func createDataSource(photoDataProvider: PhotoDataProviding) -> BrowserDataSourcing { - let cellFactory = BrowserCellFactory(photoDataProvider: photoDataProvider) - let cellConfigurator = cellFactory.createConfigurator() - - return BrowserViewDataSource(cellConfigurator: cellConfigurator) - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserInteractor.swift deleted file mode 100644 index abce38c..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserInteractor.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// Created by Maxim Berezhnoy on 16/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -import Foundation - -protocol BrowserInteracting { - func fetch(photos request: BrowserModels.Photos.Request) -} - -final class BrowserInteractor: BrowserInteracting { - private let presenter: BrowserPresenting - private let photoCollectionFetcher: PhotoCollectionFetching - - private var currentRequest: BrowserModels.Photos.Request? - - init(presenter: BrowserPresenting, photoCollectionFetcher: PhotoCollectionFetching) { - self.presenter = presenter - self.photoCollectionFetcher = photoCollectionFetcher - } - - func fetch(photos request: BrowserModels.Photos.Request) { - if currentRequest == request { - return - } - - currentRequest = request - - photoCollectionFetcher.fetchPhotos( - startingFrom: request.startFromPosition, - fetchAtMost: request.fetchAtMost, - matching: request.searchCriteria, - withSize: request.size, - includeMetadata: [] - ) { result in - switch result { - case let .success(photos): - DispatchQueue.main.async { [weak self, request] in - guard let self = self else { return } - - if self.currentRequest == request { - self.currentRequest = nil - } - - let response = BrowserModels.Photos.Response(startingPosition: request.startFromPosition, photos: photos) - - self.presenter.present(photos: response) - } - case let .failure(error): - print("Error while retrieving photos (request: \(request)): \(error)") - } - } - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserModels.swift deleted file mode 100644 index 66a3de5..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserModels.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Created by Maxim Berezhnoy on 16/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -struct BrowserModels { - struct Photos { - struct Request: Equatable { - let startFromPosition: Int - let fetchAtMost: Int - let searchCriteria: String - let size: PhotoParameters.Size - } - - struct Response { - let startingPosition: Int - let photos: [Photo] - } - - struct ViewModel { - let photos: [Photo] - } - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserPresenter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserPresenter.swift deleted file mode 100644 index ca9066a..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserPresenter.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Created by Maxim Berezhnoy on 16/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -protocol BrowserPresenting: AnyObject { - func present(photos: BrowserModels.Photos.Response) -} - -final class BrowserPresenter: BrowserPresenting { - weak var view: BrowserDisplaying? - - func present(photos: BrowserModels.Photos.Response) { - let viewModel = BrowserModels.Photos.ViewModel(photos: photos.photos) - - if photos.startingPosition == 0 { - view?.displayNew(photos: viewModel) - } else { - view?.displayMore(photos: viewModel) - } - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift deleted file mode 100644 index a7878d1..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewController.swift +++ /dev/null @@ -1,219 +0,0 @@ -// -// Created by Maxim Berezhnoy on 15/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -import UIKit - -protocol BrowserDisplaying: AnyObject { - func displayNew(photos: BrowserModels.Photos.ViewModel) - func displayMore(photos: BrowserModels.Photos.ViewModel) -} - -private struct LayoutConstants { - static let itemsPerRow = 3 - - static let padding: CGFloat = 10.0 - static let interitemSpacing: CGFloat = 10 - static let lineSpacing: CGFloat = 10 - - static let heightRatio: CGFloat = 1.0 - - static let photoSize = PhotoParameters.Size.thumbSquare -} - -final class BrowserViewController: UIViewController { - private let photosPerFetchRequest = LayoutConstants.itemsPerRow * 15 - - private let interactor: BrowserInteracting - private let dataSource: BrowserDataSourcing - - private lazy var searchController: UISearchController = { - let controller = UISearchController() - - controller.obscuresBackgroundDuringPresentation = false - controller.definesPresentationContext = true - - return controller - }() - - private lazy var collectionFlowLayout: UICollectionViewFlowLayout = { - let layout = UICollectionViewFlowLayout() - - layout.sectionInset = UIEdgeInsets( - top: LayoutConstants.padding, - left: LayoutConstants.padding, - bottom: LayoutConstants.padding, - right: LayoutConstants.padding - ) - - layout.minimumInteritemSpacing = LayoutConstants.interitemSpacing - layout.minimumLineSpacing = LayoutConstants.lineSpacing - - return layout - }() - - private lazy var collectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: collectionFlowLayout) - - view.alwaysBounceVertical = true - view.backgroundColor = Style.ScreenBackground.color - - return view - }() - - private lazy var refreshControl: UIRefreshControl = { - let control = UIRefreshControl(frame: .zero) - - return control - }() - - init(interactor: BrowserInteracting, dataSource: BrowserDataSourcing) { - self.interactor = interactor - self.dataSource = dataSource - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - setupCollectionView() - setupSearchController() - setupRefreshControl() - - requestNewPhotos() - } - - // MARK: - View Setup - - private func setupCollectionView() { - view.addSubview(collectionView) - - collectionView.translatesAutoresizingMaskIntoConstraints = false - collectionView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true - collectionView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true - collectionView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true - collectionView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true - - collectionView.delegate = self - - dataSource.register(for: collectionView) - } - - private func setupSearchController() { - searchController.searchBar.delegate = self - searchController.searchBar.enablesReturnKeyAutomatically = false - - navigationItem.searchController = searchController - navigationItem.hidesSearchBarWhenScrolling = false - } - - private func setupRefreshControl() { - collectionView.refreshControl = refreshControl - - refreshControl.addTarget(self, action: #selector(handleRefreshControl), for: .valueChanged) - } - - // MARK: - Data requesting - - func requestMorePhotos() { - interactor.fetch( - photos: BrowserModels.Photos.Request( - startFromPosition: dataSource.photoCount, - fetchAtMost: photosPerFetchRequest, - searchCriteria: searchController.searchBar.text ?? "", - size: LayoutConstants.photoSize - ) - ) - } - - func requestNewPhotos() { - interactor.fetch( - photos: BrowserModels.Photos.Request( - startFromPosition: 0, - fetchAtMost: photosPerFetchRequest, - searchCriteria: searchController.searchBar.text ?? "", - size: LayoutConstants.photoSize - ) - ) - } -} - -// MARK: - BrowserDisplaying - -extension BrowserViewController: BrowserDisplaying { - func displayNew(photos: BrowserModels.Photos.ViewModel) { - dataSource.set(photos: photos.photos) - - refreshControl.endRefreshing() - - collectionView.reloadData() - collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: false) - } - - func displayMore(photos: BrowserModels.Photos.ViewModel) { - let currentPhotoCount = dataSource.photoCount - - dataSource.add(photos: photos.photos) - - collectionView.insertItems(at: (0.. CGSize { - let viewWidth = self.collectionView.bounds.width - LayoutConstants.padding * 2 - - let totalInteritemSpacing = CGFloat(LayoutConstants.itemsPerRow - 1) * LayoutConstants.interitemSpacing - - let itemWidth = ((viewWidth - totalInteritemSpacing) / CGFloat(LayoutConstants.itemsPerRow)).rounded(.down) - - return CGSize(width: itemWidth, height: itemWidth * LayoutConstants.heightRatio) - } -} - -// MARK: - UISearchBarDelegate - -extension BrowserViewController: UISearchBarDelegate { - public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { - requestNewPhotos() - } - - public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - searchBar.text = "" - requestNewPhotos() - } -} - -// MARK: - Refresh control - -extension BrowserViewController { - @objc func handleRefreshControl() { - requestNewPhotos() - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewDataSource.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewDataSource.swift deleted file mode 100644 index 9462453..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/BrowserViewDataSource.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// Created by Maxim Berezhnoy on 16/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -import UIKit - -protocol BrowserDataSourcing: UICollectionViewDataSource { - var photoCount: Int { get } - - func register(for collectionView: UICollectionView) - func add(photos: [Photo]) - func set(photos: [Photo]) -} - -final class BrowserViewDataSource: NSObject { - private let cellConfigurator: BrowserCellConfiguring - - private var photos = [Photo]() - - init(cellConfigurator: BrowserCellConfiguring) { - self.cellConfigurator = cellConfigurator - - super.init() - } -} - -// MARK: - UICollectionViewDataSource - -extension BrowserViewDataSource: UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - photos.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = dequeueBrowserCell(from: collectionView, at: indexPath) - - let photo = photos[indexPath.item] - - cellConfigurator.configure(cell: cell, with: photo) - - return cell - } -} - -// MARK: - BrowserDataSourcing - -extension BrowserViewDataSource: BrowserDataSourcing { - var photoCount: Int { - photos.count - } - - func register(for collectionView: UICollectionView) { - collectionView.dataSource = self - collectionView.register(BrowserViewCell.self, forCellWithReuseIdentifier: BrowserViewCell.identifier) - } - - func add(photos: [Photo]) { - self.photos.append(contentsOf: photos) - } - - func set(photos: [Photo]) { - self.photos = photos - } -} - -// MARK: - BrowserViewCell dequeuing - -extension BrowserViewDataSource { - func dequeueBrowserCell(from collectionView: UICollectionView, at indexPath: IndexPath) -> BrowserViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BrowserViewCell.identifier, for: indexPath) - - guard let browserCell = cell as? BrowserViewCell else { - fatalError("Unexpected cell type while dequeuing in \(BrowserViewDataSource.self): got \(cell)") - } - - return browserCell - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellFactory.swift deleted file mode 100644 index 1e81600..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellFactory.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Created by Maxim Berezhnoy on 17/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -protocol BrowserCellCreating { - func createConfigurator() -> BrowserCellConfiguring -} - -final class BrowserCellFactory: BrowserCellCreating { - private let photoDataProvider: PhotoDataProviding - - init(photoDataProvider: PhotoDataProviding) { - self.photoDataProvider = photoDataProvider - } - - func createConfigurator() -> BrowserCellConfiguring { - BrowserCellConfigurator(photoDataProvider: photoDataProvider) - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellInteractor.swift deleted file mode 100644 index d45210b..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellInteractor.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// Created by Maxim Berezhnoy on 16/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -import Foundation - -protocol BrowserCellInteracting { - func fetch(image: BrowserCellModels.PhotoImage.Request) -} - -final class BrowserCellInteractor: BrowserCellInteracting { - private let presenter: BrowserCellPresenting - private let photoDataProvider: PhotoDataProviding - - private var currentPhotoId: Photo.ID? - - init(presenter: BrowserCellPresenting, photoDataProvider: PhotoDataProviding) { - self.presenter = presenter - self.photoDataProvider = photoDataProvider - } - - func fetch(image: BrowserCellModels.PhotoImage.Request) { - presenter.presentLoading() - - currentPhotoId = image.photoID - - photoDataProvider.getPhotoData(from: image.url) { result in - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - // Do not send a response if the ID has changed meanwhile. - // Another option would be to make requests to provider cancelable - if let currentPhotoId = self.currentPhotoId, currentPhotoId == image.photoID { - switch result { - case let .success(data): - self.presenter.present(image: BrowserCellModels.PhotoImage.Response(data: data)) - case .failure: - self.presenter.presentError() - } - } - } - } - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellModels.swift deleted file mode 100644 index ac0c57d..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellModels.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Created by Maxim Berezhnoy on 16/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -import UIKit - -struct BrowserCellModels { - struct PhotoImage { - struct Request { - let photoID: Photo.ID - let url: URL - } - - struct Response { - let data: Data - } - - struct ViewModel { - let image: UIImage - } - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellPresenter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellPresenter.swift deleted file mode 100644 index 53e0508..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellPresenter.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Created by Maxim Berezhnoy on 16/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -import UIKit - -protocol BrowserCellPresenting: AnyObject { - func present(image: BrowserCellModels.PhotoImage.Response) - func presentLoading() - func presentError() -} - -final class BrowserCellPresenter: BrowserCellPresenting { - weak var view: BrowserCellDisplaying? - - func present(image: BrowserCellModels.PhotoImage.Response) { - guard let view = view, - let image = UIImage(data: image.data) - else { return } - - view.display(image: BrowserCellModels.PhotoImage.ViewModel(image: image)) - } - - func presentLoading() { - view?.displayLoading() - } - - func presentError() { - view?.displayLoading() - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellView.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellView.swift deleted file mode 100644 index 8087ab4..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/BrowserCellView.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// Created by Maxim Berezhnoy on 16/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -import UIKit - -protocol BrowserCellDisplaying: AnyObject { - func display(image: BrowserCellModels.PhotoImage.ViewModel) - func displayLoading() -} - -final class BrowserViewCell: UICollectionViewCell { - private enum ViewState { - case loading - case image(UIImage) - } - - static let identifier = "\(BrowserViewCell.self)" - - private var interactor: BrowserCellInteracting? - - lazy var imageView: UIImageView = { - let view = UIImageView() - - view.contentMode = .scaleAspectFit - - view.layer.masksToBounds = true - view.layer.cornerRadius = 7 - - return view - }() - - lazy var placeholderView: UIView = { - let view = UIView() - - view.backgroundColor = .white - - return view - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - setupImageView() - setupPlaceholderView() - - set(state: .loading) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - View Setup - - private func setupImageView() { - addSubview(imageView) - - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true - imageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true - imageView.widthAnchor.constraint(equalTo: widthAnchor).isActive = true - imageView.heightAnchor.constraint(equalTo: heightAnchor).isActive = true - } - - private func setupPlaceholderView() { - addSubview(placeholderView) - - placeholderView.translatesAutoresizingMaskIntoConstraints = false - placeholderView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true - placeholderView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true - placeholderView.widthAnchor.constraint(equalTo: widthAnchor).isActive = true - placeholderView.heightAnchor.constraint(equalTo: heightAnchor).isActive = true - } - - private func set(state: ViewState) { - switch state { - case .loading: - placeholderView.isHidden = false - imageView.isHidden = true - - case let .image(image): - imageView.isHidden = false - placeholderView.isHidden = true - - imageView.image = image - } - } -} - -// MARK: - BrowserCellDisplaying - -extension BrowserViewCell: BrowserCellDisplaying { - func display(image: BrowserCellModels.PhotoImage.ViewModel) { - set(state: .image(image.image)) - } - - func displayLoading() { - set(state: .loading) - } -} - -// MARK: - ConfigurableBrowserCell - -extension BrowserViewCell: ConfigurableBrowserCell { - func configure(interactor: BrowserCellInteracting, photo: Photo) { - self.interactor = interactor - - interactor.fetch(image: BrowserCellModels.PhotoImage.Request(photoID: photo.id, url: photo.imageURL)) - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/Configurator/BrowserCellConfigurator.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/Configurator/BrowserCellConfigurator.swift deleted file mode 100644 index 407c7d0..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/Configurator/BrowserCellConfigurator.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Created by Maxim Berezhnoy on 17/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -protocol BrowserCellConfiguring { - func configure(cell: ConfigurableBrowserCell, with photo: Photo) -} - -final class BrowserCellConfigurator: BrowserCellConfiguring { - private let photoDataProvider: PhotoDataProviding - - init(photoDataProvider: PhotoDataProviding) { - self.photoDataProvider = photoDataProvider - } - - func configure(cell: ConfigurableBrowserCell, with photo: Photo) { - let presenter = BrowserCellPresenter() - let interactor = BrowserCellInteractor(presenter: presenter, photoDataProvider: photoDataProvider) - - presenter.view = cell - - cell.configure(interactor: interactor, photo: photo) - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/Configurator/ConfigurableBrowserCell.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/Configurator/ConfigurableBrowserCell.swift deleted file mode 100644 index b2a3058..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Browser/Cell/Configurator/ConfigurableBrowserCell.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// Created by Maxim Berezhnoy on 17/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -protocol ConfigurableBrowserCell: BrowserCellDisplaying { - func configure(interactor: BrowserCellInteracting, photo: Photo) -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/ConfigurableFeedCell.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/ConfigurableFeedCell.swift deleted file mode 100644 index e5ccfdf..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/ConfigurableFeedCell.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// Created by Maxim Berezhnoy on 24/11/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -protocol ConfigurableFeedCell: FeedCellDisplaying { - func configure(interactor: FeedCellInteracting, photo: Photo) -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/FeedCellConfigurator.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/FeedCellConfigurator.swift deleted file mode 100644 index 630a88b..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Configurator/FeedCellConfigurator.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Created by Maxim Berezhnoy on 24/11/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -protocol FeedCellConfiguring { - func configure(cell: ConfigurableFeedCell, with photo: Photo) -} - -final class FeedCellConfigurator: FeedCellConfiguring { - private let photoDataProvider: PhotoDataProviding - - init(photoDataProvider: PhotoDataProviding) { - self.photoDataProvider = photoDataProvider - } - - func configure(cell: ConfigurableFeedCell, with photo: Photo) { - let presenter = FeedCellPresenter() - let interactor = FeedCellInteractor(presenter: presenter, photoDataProvider: photoDataProvider) - - presenter.view = cell - - cell.configure(interactor: interactor, photo: photo) - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift deleted file mode 100644 index 2164cbd..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Created by Maxim Berezhnoy on 25/11/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -protocol FeedCellCreating { - func createConfigurator() -> FeedCellConfiguring -} - -final class FeedCellFactory: FeedCellCreating { - private let photoDataProvider: PhotoDataProviding - - init(photoDataProvider: PhotoDataProviding) { - self.photoDataProvider = photoDataProvider - } - - func createConfigurator() -> FeedCellConfiguring { - FeedCellConfigurator(photoDataProvider: photoDataProvider) - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellInteractor.swift deleted file mode 100644 index 627284b..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellInteractor.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// Created by Maxim Berezhnoy on 25/11/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -import Foundation - -protocol FeedCellInteracting { - func fetch(image: FeedCellModels.PhotoImage.Request) -} - -final class FeedCellInteractor: FeedCellInteracting { - private let presenter: FeedCellPresenting - private let photoDataProvider: PhotoDataProviding - - private var currentPhotoId: Photo.ID? - - init(presenter: FeedCellPresenting, photoDataProvider: PhotoDataProviding) { - self.presenter = presenter - self.photoDataProvider = photoDataProvider - } - - func fetch(image: FeedCellModels.PhotoImage.Request) { - presenter.presentLoading() - - currentPhotoId = image.photoID - - photoDataProvider.getPhotoData(from: image.url) { result in - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - // Do not send a response if the ID has changed in the meantime. - // Another option would be to make requests to provider cancelable - if let currentPhotoId = self.currentPhotoId, currentPhotoId == image.photoID { - switch result { - case let .success(data): - self.presenter.present(image: FeedCellModels.PhotoImage.Response(data: data)) - case .failure: - self.presenter.presentError() - } - } - } - } - } -} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellModels.swift deleted file mode 100644 index 28c3cbc..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellModels.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Created by Maxim Berezhnoy on 25/11/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -import UIKit - -struct FeedCellModels { - struct PhotoImage { - struct Request { - let photoID: Photo.ID - let url: URL - } - - struct Response { - let data: Data - } - - struct ViewModel { - let image: UIImage - } - } -} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellPresenter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellPresenter.swift deleted file mode 100644 index cc23d12..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellPresenter.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Created by Maxim Berezhnoy on 25/11/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -import UIKit - -protocol FeedCellPresenting: AnyObject { - func present(image: FeedCellModels.PhotoImage.Response) - func presentLoading() - func presentError() -} - -final class FeedCellPresenter: FeedCellPresenting { - weak var view: FeedCellDisplaying? - - func present(image: FeedCellModels.PhotoImage.Response) { - guard let view = view, - let image = UIImage(data: image.data) - else { return } - - view.display(image: FeedCellModels.PhotoImage.ViewModel(image: image)) - } - - func presentLoading() { - view?.displayLoading() - } - - func presentError() { - view?.displayLoading() - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift index 6cd8125..5b5835f 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift @@ -5,11 +5,6 @@ import UIKit -protocol FeedCellDisplaying: AnyObject { - func display(image: FeedCellModels.PhotoImage.ViewModel) - func displayLoading() -} - private struct LayoutConstants { static let ownerNameFontSize: CGFloat = 12 static let tagsFontSize: CGFloat = 11 @@ -23,6 +18,10 @@ private struct LayoutConstants { static let dateTakenFormat = "MMM dd, yyyy" } +protocol FeedViewCellPhotoDisplaying { + func display(photo: Photo) +} + final class FeedViewCell: UITableViewCell { private enum ImageViewState { case loading @@ -31,8 +30,6 @@ final class FeedViewCell: UITableViewCell { static let identifier = "\(FeedViewCell.self)" - private var interactor: FeedCellInteracting? - private var imageHeightConstraint: NSLayoutConstraint? // MARK: - Subviews @@ -81,13 +78,13 @@ final class FeedViewCell: UITableViewCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) + contentView.backgroundColor = Style.ScreenBackground.color + setupImageView() setupOwnerNameView() setupViewsView() setupTagsView() setupDateTakenView() - - set(imageState: .loading) } required init?(coder: NSCoder) { @@ -151,23 +148,11 @@ final class FeedViewCell: UITableViewCell { dateTakenView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: frame.width * LayoutConstants.sideMarginRatio).isActive = true } - private func set(imageState: ImageViewState) { - switch imageState { - case .loading: - cellImageView.isHidden = true - - case let .image(image): - cellImageView.isHidden = false - cellImageView.image = image - layoutImage() - } - } - private func layoutImage() { guard let image = cellImageView.image else { return } - + imageHeightConstraint?.isActive = false imageHeightConstraint = cellImageView.heightAnchor.constraint(equalTo: cellImageView.widthAnchor, multiplier: image.size.height / image.size.width) imageHeightConstraint?.priority = .defaultHigh @@ -175,47 +160,20 @@ final class FeedViewCell: UITableViewCell { } } - -// MARK: - FeedCellDisplaying - -extension FeedViewCell: FeedCellDisplaying { - func display(image: FeedCellModels.PhotoImage.ViewModel) { - set(imageState: .image(image.image)) - } - - func displayLoading() { - set(imageState: .loading) - } -} - -// MARK: - ConfigurableFeedCell - -extension FeedViewCell: ConfigurableFeedCell { - func configure(interactor: FeedCellInteracting, photo: Photo) { - self.interactor = interactor - - interactor.fetch(image: FeedCellModels.PhotoImage.Request(photoID: photo.id, url: photo.imageURL)) - +// MARK: - FeedViewCellPhotoDisplaying +extension FeedViewCell: FeedViewCellPhotoDisplaying { + func display(photo: Photo) { let metadata = photo.metadata - ownerNameView.text = metadata.ownerName ?? "" + ownerNameView.text = metadata.ownerName - if let dateTaken = metadata.dateTaken { - dateTakenView.set(date: dateTaken) - } else { - dateTakenView.text = "" - } + dateTakenView.set(date: metadata.dateTaken) - if let views = metadata.views { - viewsView.set(views: views) - } else { - viewsView.text = "" - } + viewsView.set(views: metadata.views) + + tagsView.set(tags: metadata.tags) - if let tags = metadata.tags { - tagsView.set(tags: tags) - } else { - tagsView.text = "" - } + cellImageView.image = UIImage(data: photo.imageData) + layoutImage() } } \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift index 51d9f09..6173a74 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift @@ -4,12 +4,11 @@ // Copyright (c) 2020 rencevio. All rights reserved. final class FeedFactory { - func createViewController(photoDataProvider: PhotoDataProviding, - photoCollectionFetcher: PhotoCollectionFetching) -> FeedViewController { + func createViewController(photoCollectionFetcher: PhotoCollectionFetching) -> FeedViewController { let presenter = FeedPresenter() let interactor = FeedInteractor(presenter: presenter, photoCollectionFetcher: photoCollectionFetcher) - let dataSource = createDataSource(photoDataProvider: photoDataProvider) + let dataSource = createDataSource() let viewController = FeedViewController(interactor: interactor, dataSource: dataSource) @@ -18,10 +17,7 @@ final class FeedFactory { return viewController } - private func createDataSource(photoDataProvider: PhotoDataProviding) -> FeedDataSourcing { - let cellFactory = FeedCellFactory(photoDataProvider: photoDataProvider) - let cellConfigurator = cellFactory.createConfigurator() - - return FeedViewDataSource(cellConfigurator: cellConfigurator) + private func createDataSource() -> FeedDataSourcing { + FeedViewDataSource() } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift index 1ec254c..6ccf617 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift @@ -30,9 +30,7 @@ final class FeedInteractor: FeedInteracting { photoCollectionFetcher.fetchPhotos( startingFrom: request.startFromPosition, fetchAtMost: request.fetchAtMost, - matching: "", - withSize: request.size, - includeMetadata: request.metadata + withSize: request.size ) { result in switch result { case let .success(photos): @@ -43,10 +41,14 @@ final class FeedInteractor: FeedInteracting { self.currentRequest = nil } - let response = FeedModels.Photos.Response(startingPosition: request.startFromPosition, photos: photos) + let response = FeedModels.Photos.Response( + startingPosition: request.startFromPosition, + photos: photos + ) self.presenter.present(photos: response) } + case let .failure(error): print("Error while retrieving photos (request: \(request)): \(error)") } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift index eec01fe..d783d85 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift @@ -3,6 +3,8 @@ // // Copyright (c) 2020 rencevio. All rights reserved. +import UIKit.UIImage + struct FeedModels { struct Photos { struct Request: Equatable { diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift index b19bc72..43fbfd6 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift @@ -36,6 +36,7 @@ final class FeedViewController: UIViewController { view.tableFooterView = UIView() view.separatorStyle = .none view.sectionHeaderHeight = LayoutConstants.itemSpacing + view.estimatedRowHeight = 500 view.allowsSelection = false @@ -95,7 +96,7 @@ final class FeedViewController: UIViewController { private func requestNewPhotos() { interactor.fetch( photos: FeedModels.Photos.Request( - startFromPosition: dataSource.photoCount, + startFromPosition: 0, fetchAtMost: photosPerFetchRequest, size: LayoutConstants.photoSize, metadata: metadataToFetch diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift index a91d9ec..66c51df 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift @@ -14,15 +14,8 @@ protocol FeedDataSourcing: UITableViewDataSource { } final class FeedViewDataSource: NSObject { - private let cellConfigurator: FeedCellConfiguring - private var photos = [Photo]() - - init(cellConfigurator: FeedCellConfiguring) { - self.cellConfigurator = cellConfigurator - - super.init() - } + private var images = [Photo.ID: UIImage]() } // MARK: - UITableViewDataSource @@ -39,9 +32,9 @@ extension FeedViewDataSource: UITableViewDataSource { public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = dequeueFeedCell(from: tableView, at: indexPath) - let photo = photos[indexPath.item] + let photo = photos[indexPath.section] - cellConfigurator.configure(cell: cell, with: photo) + cell.display(photo: photo) return cell } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/Entities/FlickrPhotos.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/Entities/FlickrPhotos.swift index a2e54a5..36d27c1 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/Entities/FlickrPhotos.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/Entities/FlickrPhotos.swift @@ -18,8 +18,8 @@ struct FlickrPhoto: Decodable { let secret: String let server: String let farm: Int - let datetaken: String? - let ownername: String? - let views: String? - let tags: String? + let datetaken: String + let ownername: String + let views: String + let tags: String } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcher.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcher.swift new file mode 100644 index 0000000..047594a --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcher.swift @@ -0,0 +1,46 @@ +// +// Created by Maxim Berezhnoy on 27/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import Foundation.NSData + +final class FlickrCollectionDataFetcher: FlickrCollectionDataFetching { + let dataProvider: PhotoDataProviding; + + private let operatingQueue = DispatchQueue(label: "\(FlickrCollectionDataFetcher.self)OperatingQueue") + + init(dataProvider: PhotoDataProviding) { + self.dataProvider = dataProvider + } + + // Completes with dictionary containing only successfully retrieved data for photos + func fetchData( + photos: [FlickrPhoto], + withSize size: PhotoParameters.Size, + _ completion: @escaping ([Photo.ID: Foundation.Data]) -> ()) { + var photosToFetch = Set(photos.map { $0.id }) + var photosImageData = [Photo.ID: Data]() + + photos.forEach { photo in + let imageURL = FlickrPhotoURLResolver.resolveUrl(for: photo, withSize: size) + + dataProvider.getPhotoData(from: imageURL) { [operatingQueue] result in + operatingQueue.async { + switch result { + case let .success(data): + photosImageData[photo.id] = data + case let .failure(error): + print("Failed to retrieve data for photo \(imageURL): \(error)") + } + + photosToFetch.remove(photo.id) + + if photosToFetch.isEmpty { + completion(photosImageData) + } + } + } + } + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetching.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetching.swift new file mode 100644 index 0000000..54a3bee --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetching.swift @@ -0,0 +1,10 @@ +// +// Created by Maxim Berezhnoy on 27/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import Foundation.NSData + +protocol FlickrCollectionDataFetching { + func fetchData(photos: [FlickrPhoto], withSize size: PhotoParameters.Size, _ completion: @escaping ([Photo.ID: Data]) -> Void) +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift index 4e5bdc1..f207aff 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift @@ -7,73 +7,89 @@ import Foundation.NSDateFormatter final class FlickrCollectionFetcher: PhotoCollectionFetching { private let flickrPhotosService: FlickrPhotosFetching + private let collectionDataFetcher: FlickrCollectionDataFetching - init(flickrPhotosService: FlickrPhotosFetching) { + init(flickrPhotosService: FlickrPhotosFetching, collectionDataFetcher: FlickrCollectionDataFetching) { self.flickrPhotosService = flickrPhotosService + self.collectionDataFetcher = collectionDataFetcher } func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, withSize size: PhotoParameters.Size, - includeMetadata metadata: [PhotoParameters.Metadata], - completion: @escaping Completion) { - fetchPhotos(startingFrom: position, fetchAtMost: maxFetchCount, matching: nil, withSize: size, includeMetadata: metadata, completion: completion) - } - - func fetchPhotos(startingFrom position: Int, - fetchAtMost maxFetchCount: Int, - matching searchCriteria: String?, - withSize size: PhotoParameters.Size, - includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion) { let page = position / maxFetchCount + 1 + let metadata = PhotoParameters.Metadata.allCases - if let searchCriteria = searchCriteria, !searchCriteria.isEmpty { - flickrPhotosService.search( - matching: searchCriteria, - page: page, - photosPerPage: maxFetchCount, - includeMetadata: metadata, - completion: transformFetchResult(withSize: size, completion) - ) - } else { - flickrPhotosService.getRecent( - page: page, - photosPerPage: maxFetchCount, - includeMetadata: metadata, - completion: transformFetchResult(withSize: size, completion) - ) - } + flickrPhotosService.getRecent( + page: page, + photosPerPage: maxFetchCount, + includeMetadata: metadata, + completion: transformFetchResult(withSize: size, completion) + ) } private func transformFetchResult(withSize size: PhotoParameters.Size, _ completion: @escaping Completion) -> FlickrPhotosService.Completion { - { result in + { [weak self] result in + guard let self = self else { + return + } + switch result { case let .success(photos): - completion(.success(photos.map { photo in - Photo(id: photo.id, - imageURL: FlickrPhotoURLResolver.resolveUrl( - for: photo, - withSize: size - ), - metadata: extractMetadata(from: photo) - ) - })) + self.transformValidPhotos(photos, withSize: size) { validPhotos in + completion(.success(validPhotos)) + } case let .failure(error): completion(.failure(error)) } } } + + private func transformValidPhotos(_ photos: [FlickrPhoto], + withSize size: PhotoParameters.Size, + _ completion: @escaping ([Photo]) -> Void) { + collectionDataFetcher.fetchData(photos: photos, withSize: size) { photosImageData in + let transformedPhotos = photos.map { photo -> Photo? in + guard let metadata = extractMetadata(from: photo) else { + return nil + } + + guard let imageData = photosImageData[photo.id] else { + return nil + } + + let fullSizeImageURL = FlickrPhotoURLResolver.resolveUrl(for: photo, withSize: .large) + + return Photo( + id: photo.id, + imageData: imageData, + fullSizeImageURL: fullSizeImageURL, + metadata: metadata + ) + } + + completion(transformedPhotos.compactMap { $0 }) + } + } } -private func extractMetadata(from photo: FlickrPhoto) -> Photo.Metadata { +private func extractMetadata(from photo: FlickrPhoto) -> Photo.Metadata? { let dateTakenFormatter = DateFormatter() dateTakenFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss" - + + guard let views = photo.views.toInt() else { + return nil + } + + guard let dateTaken = photo.datetaken.toDate(formatter: dateTakenFormatter) else { + return nil + } + return Photo.Metadata( - views: photo.views?.toInt(), - tags: photo.tags?.components(separatedBy: " "), - ownerName: photo.ownername, - dateTaken: photo.datetaken?.toDate(formatter: dateTakenFormatter) + views: views, + tags: photo.tags.components(separatedBy: " "), + ownerName: photo.ownername, + dateTaken: dateTaken ) -} +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherFactory.swift index 86ed6cb..3721979 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherFactory.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherFactory.swift @@ -4,11 +4,15 @@ // Copyright (c) 2020 rencevio. All rights reserved. final class FlickrCollectionFetcherFactory: PhotoCollectionFetcherCreating { - func createCollectionFetcher() -> PhotoCollectionFetching { + func createCollectionFetcher(photoDataProvider: PhotoDataProviding) -> PhotoCollectionFetching { let httpClient = Http.Client() let flickrPhotosService = FlickrPhotosService(apiKey: flickrApiKey, httpClient: httpClient) + let collectionDataFetcher = FlickrCollectionDataFetcher(dataProvider: photoDataProvider) - let fetcher = FlickrCollectionFetcher(flickrPhotosService: flickrPhotosService) + let fetcher = FlickrCollectionFetcher( + flickrPhotosService: flickrPhotosService, + collectionDataFetcher: collectionDataFetcher + ) return fetcher } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetcherFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetcherFactory.swift index ad7bba2..ddfb8e4 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetcherFactory.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetcherFactory.swift @@ -4,5 +4,5 @@ // Copyright (c) 2020 rencevio. All rights reserved. protocol PhotoCollectionFetcherCreating { - func createCollectionFetcher() -> PhotoCollectionFetching + func createCollectionFetcher(photoDataProvider: PhotoDataProviding) -> PhotoCollectionFetching } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift index 02c4b03..67e38ed 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift @@ -8,9 +8,7 @@ protocol PhotoCollectionFetching { func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, - matching searchCriteria: String?, withSize size: PhotoParameters.Size, - includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion ) } From 215ecf5a6e7cf5c6bdfd0c449f335d398409fdc1 Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Fri, 27 Nov 2020 01:34:55 +0100 Subject: [PATCH 08/16] Properly request new photos when user has scrolled down to the bottom --- .../SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift index 43fbfd6..abe423e 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift @@ -130,7 +130,7 @@ extension FeedViewController: UITableViewDelegate { let loadedPhotosCount = dataSource.photoCount - if itemToDisplay == loadedPhotosCount { + if itemToDisplay == loadedPhotosCount - 1 { requestMorePhotos() } } From 866399568cef29865e20ce09a3e2a0b187102ea4 Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Sun, 29 Nov 2020 16:29:57 +0100 Subject: [PATCH 09/16] created router to display full image VC; added full image VC stub --- .../project.pbxproj | 40 +++++++++++++++++++ .../RootDependencies.swift | 3 +- .../Feed/Cell/FeedCellConfigurator.swift | 20 ++++++++++ .../Scenes/Feed/Cell/FeedCellFactory.swift | 14 +++++++ .../Scenes/Feed/Cell/FeedCellView.swift | 7 +--- .../Scenes/Feed/FeedFactory.swift | 34 +++++++++++++--- .../Scenes/Feed/FeedRouter.swift | 30 ++++++++++++++ .../Scenes/Feed/FeedViewController.swift | 5 ++- .../Scenes/Feed/FeedViewDataSource.swift | 7 +++- .../Feed/FullImage/FullImageFactory.swift | 14 +++++++ .../FullImage/FullImageViewController.swift | 10 +++++ .../SimpleFlickrBrowser/Utils/Routing.swift | 10 +++++ 12 files changed, 180 insertions(+), 14 deletions(-) create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellConfigurator.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageFactory.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Utils/Routing.swift diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj index 4781bfd..eb8c796 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj @@ -12,15 +12,19 @@ 62DE911E1EB4D78BCA9085CB /* FlickrCollectionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9A2EBADCA5EE9059C3DD /* FlickrCollectionFetcherTests.swift */; }; 62DE91F2D0080AF13460E47C /* FeedInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9DB2850E842E22C4CC84 /* FeedInteractor.swift */; }; 62DE91F96D06BDB6AE8D40A1 /* XCTestCase+QueueAwait.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE924CEDF37B8D5BC86D48 /* XCTestCase+QueueAwait.swift */; }; + 62DE924525362E9212DE0671 /* FullImageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9EB1763D78889BAD9753 /* FullImageFactory.swift */; }; 62DE927526937FB6BEDCE8BC /* MockBrowserCellPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9B3E9AAA8D0661DEEFFE /* MockBrowserCellPresenter.swift */; }; 62DE92A1A0D21DCC9FC97F7E /* NetworkPhotoDataRetriever.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9868FFC49BF89E9F9192 /* NetworkPhotoDataRetriever.swift */; }; + 62DE92CC1AB4CFA092F2DB10 /* FullImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE95E529D91B15DE6E0C58 /* FullImageViewController.swift */; }; 62DE92DECC87D7DACFB69CC9 /* PhotoDataRetrieving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99F1F322B9F470256B1E /* PhotoDataRetrieving.swift */; }; 62DE93261A41C5DDC9BACDB0 /* MockPhotoDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE91C1A5EBF975C64A37B3 /* MockPhotoDataCache.swift */; }; 62DE9387D309D2DD3F2AC102 /* BrowserPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE915A672C1C41DCCF9C8D /* BrowserPresenterTests.swift */; }; 62DE93E9065F0528B8577C0E /* BrowserCellInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE93A95BC1DB3D74EC66D8 /* BrowserCellInteractorTests.swift */; }; + 62DE940CB3E25C0A9AEFE252 /* FeedCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9927EE51CE772D3DD171 /* FeedCellFactory.swift */; }; 62DE944EF838C8C313469CCB /* MockPhotoDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE92EE74C81312D3DB90B0 /* MockPhotoDataProvider.swift */; }; 62DE946CFE4B0C1C5B9E1324 /* MockBrowserDisplaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9810EDB0220E74893A89 /* MockBrowserDisplaying.swift */; }; 62DE947BBB3CC8AC189E0F4F /* FlickrCollectionDataFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE94AB83D36878355DAA8C /* FlickrCollectionDataFetching.swift */; }; + 62DE94F27746C528BFA5C787 /* FeedRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9BF2A2C0391D9FFA480B /* FeedRouter.swift */; }; 62DE9521164585DF38F0C460 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99C6333390A5CB4F91B0 /* String.swift */; }; 62DE959A03CFDDCC0107EB65 /* FlickrCollectionDataFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE92B5279132DA6C6FA657 /* FlickrCollectionDataFetcher.swift */; }; 62DE959CA829F11A51767E30 /* FlickrCollectionFetcherFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9B8BC72A4AB8A5A34B65 /* FlickrCollectionFetcherFactory.swift */; }; @@ -43,10 +47,12 @@ 62DE9A9250BD0AC541234841 /* PhotoDataProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99EC2B14DB9F0C554529 /* PhotoDataProviderFactory.swift */; }; 62DE9B3C2CAC899A108EFA70 /* DateLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE963F12E663C923326C82 /* DateLabel.swift */; }; 62DE9C2CEA5B44E24BB2D6F5 /* PhotoCollectionFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE90B754DC827D73A31B9E /* PhotoCollectionFetching.swift */; }; + 62DE9C82855405B5A0BA6B57 /* FeedCellConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE970B2B651FC6FDE03FC1 /* FeedCellConfigurator.swift */; }; 62DE9CACADCC0438B0091A5C /* FeedCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE98ED3E76A320E0BF36D3 /* FeedCellView.swift */; }; 62DE9CD65849E14B67A5AF42 /* PhotoParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9031E5EEBEA8EBA4D644 /* PhotoParameters.swift */; }; 62DE9CE6366FD6ECE740C55C /* FlickrCollectionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C90249CCE2FAF835CF5 /* FlickrCollectionFetcher.swift */; }; 62DE9D29B400373915DB581A /* FeedViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE925A929D79068D7BE7CB /* FeedViewDataSource.swift */; }; + 62DE9D8224882F3BAB4798A5 /* Routing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9506DAADD74BB93C3549 /* Routing.swift */; }; 62DE9E63C0B530594A5B150B /* Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C8D791DA5418ED32FCF /* Style.swift */; }; 62DE9EBE33EDC3D97D929FF2 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE970A3FD09743A03DAF33 /* Photo.swift */; }; 62DE9EC2FE48A5A204531EEA /* HttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE93C78893DAA0E46DBD6A /* HttpClient.swift */; }; @@ -89,13 +95,16 @@ 62DE944604E007DD383ABDE1 /* MockBrowserPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBrowserPresenter.swift; sourceTree = ""; }; 62DE94A9AA78F83D54078368 /* RootDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootDependencies.swift; sourceTree = ""; }; 62DE94AB83D36878355DAA8C /* FlickrCollectionDataFetching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionDataFetching.swift; sourceTree = ""; }; + 62DE9506DAADD74BB93C3549 /* Routing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Routing.swift; sourceTree = ""; }; 62DE954BE0BFF3D2EE91DE12 /* FlickrPhotosService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotosService.swift; sourceTree = ""; }; + 62DE95E529D91B15DE6E0C58 /* FullImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullImageViewController.swift; sourceTree = ""; }; 62DE96216E4D360CAB68AF75 /* FeedFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedFactory.swift; sourceTree = ""; }; 62DE963F12E663C923326C82 /* DateLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateLabel.swift; sourceTree = ""; }; 62DE9673FC376E2BC90087D6 /* FeedPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedPresenter.swift; sourceTree = ""; }; 62DE96B5EBB14213DC9E7804 /* MockPhotoDataRetriever.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataRetriever.swift; sourceTree = ""; }; 62DE96CE6B44327361D73D24 /* FlickrPhotos.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotos.swift; sourceTree = ""; }; 62DE970A3FD09743A03DAF33 /* Photo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = ""; }; + 62DE970B2B651FC6FDE03FC1 /* FeedCellConfigurator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellConfigurator.swift; sourceTree = ""; }; 62DE97953D19EE0C13151316 /* FlickrPhotoServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotoServiceTests.swift; sourceTree = ""; }; 62DE97EF27BCB2A06EB35FDF /* ViewsLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewsLabel.swift; sourceTree = ""; }; 62DE9805D6D4DBDDBAD078E2 /* PhotoDataNSCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataNSCache.swift; sourceTree = ""; }; @@ -104,6 +113,7 @@ 62DE9868FFC49BF89E9F9192 /* NetworkPhotoDataRetriever.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkPhotoDataRetriever.swift; sourceTree = ""; }; 62DE987D3B885CA1A25FB693 /* FlickrApiValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrApiValues.swift; sourceTree = ""; }; 62DE98ED3E76A320E0BF36D3 /* FeedCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellView.swift; sourceTree = ""; }; + 62DE9927EE51CE772D3DD171 /* FeedCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellFactory.swift; sourceTree = ""; }; 62DE99C6333390A5CB4F91B0 /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 62DE99EC2B14DB9F0C554529 /* PhotoDataProviderFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProviderFactory.swift; sourceTree = ""; }; 62DE99F1F322B9F470256B1E /* PhotoDataRetrieving.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataRetrieving.swift; sourceTree = ""; }; @@ -112,6 +122,7 @@ 62DE9B3E9AAA8D0661DEEFFE /* MockBrowserCellPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBrowserCellPresenter.swift; sourceTree = ""; }; 62DE9B49CABD228CCFBCE936 /* PhotoDataCaching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataCaching.swift; sourceTree = ""; }; 62DE9B8BC72A4AB8A5A34B65 /* FlickrCollectionFetcherFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcherFactory.swift; sourceTree = ""; }; + 62DE9BF2A2C0391D9FFA480B /* FeedRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedRouter.swift; sourceTree = ""; }; 62DE9C09B259CEA839DAE2B9 /* FeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = ""; }; 62DE9C85502219A9858DCF22 /* PhotoDataProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProviderTests.swift; sourceTree = ""; }; 62DE9C8D791DA5418ED32FCF /* Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Style.swift; sourceTree = ""; }; @@ -121,6 +132,7 @@ 62DE9DB2850E842E22C4CC84 /* FeedInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedInteractor.swift; sourceTree = ""; }; 62DE9E19BC1900DC8648FD5E /* MockHttpClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockHttpClient.swift; sourceTree = ""; }; 62DE9EA9D6ACC68E32F8A681 /* BrowserInteractorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserInteractorTests.swift; sourceTree = ""; }; + 62DE9EB1763D78889BAD9753 /* FullImageFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullImageFactory.swift; sourceTree = ""; }; 62DE9FF6B55060E5204AC907 /* PhotoDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProvider.swift; sourceTree = ""; }; 944AC01A23CE0E9C009FD611 /* SimpleFlickrBrowser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimpleFlickrBrowser.app; sourceTree = BUILT_PRODUCTS_DIR; }; 944AC01D23CE0E9C009FD611 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -185,6 +197,8 @@ 62DE96216E4D360CAB68AF75 /* FeedFactory.swift */, 62DE925A929D79068D7BE7CB /* FeedViewDataSource.swift */, 62DE98914295E0F2A4403FBE /* Cell */, + 62DE9BF2A2C0391D9FFA480B /* FeedRouter.swift */, + 62DE964E422134BBFB88C94F /* FullImage */, ); path = Feed; sourceTree = ""; @@ -216,6 +230,15 @@ path = PhotoDataCache; sourceTree = ""; }; + 62DE964E422134BBFB88C94F /* FullImage */ = { + isa = PBXGroup; + children = ( + 62DE95E529D91B15DE6E0C58 /* FullImageViewController.swift */, + 62DE9EB1763D78889BAD9753 /* FullImageFactory.swift */, + ); + path = FullImage; + sourceTree = ""; + }; 62DE964EA55B5AFD5FFAF651 /* Photos */ = { isa = PBXGroup; children = ( @@ -287,6 +310,8 @@ children = ( 62DE98ED3E76A320E0BF36D3 /* FeedCellView.swift */, 62DE9EA0E878692CF74D79CB /* Metadata */, + 62DE970B2B651FC6FDE03FC1 /* FeedCellConfigurator.swift */, + 62DE9927EE51CE772D3DD171 /* FeedCellFactory.swift */, ); path = Cell; sourceTree = ""; @@ -326,6 +351,14 @@ path = Extensions; sourceTree = ""; }; + 62DE9B38420B42751F63A44F /* Utils */ = { + isa = PBXGroup; + children = ( + 62DE9506DAADD74BB93C3549 /* Routing.swift */, + ); + path = Utils; + sourceTree = ""; + }; 62DE9C11F52BBF38DEB44438 /* Network */ = { isa = PBXGroup; children = ( @@ -441,6 +474,7 @@ 62DE9E4975BBE450619E428D /* Workers */, 62DE9C11F52BBF38DEB44438 /* Network */, 62DE9086A97F986E80E89C32 /* Models */, + 62DE9B38420B42751F63A44F /* Utils */, ); path = SimpleFlickrBrowser; sourceTree = ""; @@ -612,6 +646,12 @@ 62DE9521164585DF38F0C460 /* String.swift in Sources */, 62DE959A03CFDDCC0107EB65 /* FlickrCollectionDataFetcher.swift in Sources */, 62DE947BBB3CC8AC189E0F4F /* FlickrCollectionDataFetching.swift in Sources */, + 62DE9C82855405B5A0BA6B57 /* FeedCellConfigurator.swift in Sources */, + 62DE94F27746C528BFA5C787 /* FeedRouter.swift in Sources */, + 62DE9D8224882F3BAB4798A5 /* Routing.swift in Sources */, + 62DE92CC1AB4CFA092F2DB10 /* FullImageViewController.swift in Sources */, + 62DE924525362E9212DE0671 /* FullImageFactory.swift in Sources */, + 62DE940CB3E25C0A9AEFE252 /* FeedCellFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift index 0431b31..51ca21d 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift @@ -16,7 +16,8 @@ final class RootDependencies { } func createFeedViewController() -> FeedViewController { - let feedFactory = FeedFactory() + let feedCellFactory = FeedCellFactory() + let feedFactory = FeedFactory(feedCellFactory: feedCellFactory) return feedFactory.createViewController( photoCollectionFetcher: photoCollectionFetcher diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellConfigurator.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellConfigurator.swift new file mode 100644 index 0000000..1a3e590 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellConfigurator.swift @@ -0,0 +1,20 @@ +// +// Created by Maxim Berezhnoy on 27/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +protocol FeedCellConfiguring { + func configure(_ cell: FeedViewCell) +} + +final class FeedCellConfigurator: FeedCellConfiguring { + private let router: FeedRouting + + init(router: FeedRouting) { + self.router = router + } + + func configure(_ cell: FeedViewCell) { + cell.router = router + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift new file mode 100644 index 0000000..817788e --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift @@ -0,0 +1,14 @@ +// +// Created by Maxim Berezhnoy on 29/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +protocol FeedCellCreating { + func createFeedCellConfigurator(feedRouter: FeedRouting) -> FeedCellConfigurator +} + +final class FeedCellFactory: FeedCellCreating { + func createFeedCellConfigurator(feedRouter: FeedRouting) -> FeedCellConfigurator { + FeedCellConfigurator(router: feedRouter) + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift index 5b5835f..ee48209 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift @@ -23,12 +23,9 @@ protocol FeedViewCellPhotoDisplaying { } final class FeedViewCell: UITableViewCell { - private enum ImageViewState { - case loading - case image(UIImage) - } - static let identifier = "\(FeedViewCell.self)" + + var router: FeedRouting? private var imageHeightConstraint: NSLayoutConstraint? diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift index 6173a74..29d3f86 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift @@ -3,21 +3,43 @@ // // Copyright (c) 2020 rencevio. All rights reserved. -final class FeedFactory { +protocol FeedCreating { + func createViewController(photoCollectionFetcher: PhotoCollectionFetching) -> FeedViewController +} + +final class FeedFactory: FeedCreating { + let feedCellFactory: FeedCellCreating + + init(feedCellFactory: FeedCellCreating) { + self.feedCellFactory = feedCellFactory + } + func createViewController(photoCollectionFetcher: PhotoCollectionFetching) -> FeedViewController { let presenter = FeedPresenter() let interactor = FeedInteractor(presenter: presenter, photoCollectionFetcher: photoCollectionFetcher) + let router = createRouter() + let dataSource = createDataSource(router: router) - let dataSource = createDataSource() - - let viewController = FeedViewController(interactor: interactor, dataSource: dataSource) + let viewController = FeedViewController( + interactor: interactor, + dataSource: dataSource, + router: router + ) presenter.view = viewController return viewController } - private func createDataSource() -> FeedDataSourcing { - FeedViewDataSource() + private func createDataSource(router: FeedRouting) -> FeedDataSourcing { + let configurator = feedCellFactory.createFeedCellConfigurator(feedRouter: router) + + return FeedViewDataSource(feedCellConfigurator: configurator) + } + + private func createRouter() -> FeedRouting { + let fullImageFactory = FullImageFactory() + + return FeedRouter(fullImageFactory: fullImageFactory) } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift new file mode 100644 index 0000000..d201fb3 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift @@ -0,0 +1,30 @@ +// +// Created by Maxim Berezhnoy on 27/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import UIKit + +protocol FeedRouting: Routing { + func displayFullImage(withInfo info: Photo) +} + +final class FeedRouter: FeedRouting { + var sourceVC: UIViewController? + + private let fullImageFactory: FullImageCreating + + init(fullImageFactory: FullImageCreating) { + self.fullImageFactory = fullImageFactory + } + + func displayFullImage(withInfo info: Photo) { + guard let sourceVC = sourceVC else { + return + } + + let fullImageViewController = fullImageFactory.createViewController() + + sourceVC.present(fullImageViewController, animated: true) + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift index abe423e..cb9db3b 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift @@ -26,12 +26,14 @@ final class FeedViewController: UIViewController { private let interactor: FeedInteracting private let dataSource: FeedDataSourcing + private let router: FeedRouting private lazy var tableView: UITableView = { let view = UITableView(frame: .zero) view.alwaysBounceVertical = true view.backgroundColor = Style.ScreenBackground.color + view.showsVerticalScrollIndicator = false view.tableFooterView = UIView() view.separatorStyle = .none @@ -49,9 +51,10 @@ final class FeedViewController: UIViewController { return control }() - init(interactor: FeedInteracting, dataSource: FeedDataSourcing) { + init(interactor: FeedInteracting, dataSource: FeedDataSourcing, router: FeedRouting) { self.interactor = interactor self.dataSource = dataSource + self.router = router super.init(nibName: nil, bundle: nil) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift index 66c51df..7cfc19a 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift @@ -14,8 +14,12 @@ protocol FeedDataSourcing: UITableViewDataSource { } final class FeedViewDataSource: NSObject { + private let feedCellConfigurator: FeedCellConfiguring private var photos = [Photo]() - private var images = [Photo.ID: UIImage]() + + init(feedCellConfigurator: FeedCellConfiguring) { + self.feedCellConfigurator = feedCellConfigurator + } } // MARK: - UITableViewDataSource @@ -34,6 +38,7 @@ extension FeedViewDataSource: UITableViewDataSource { let photo = photos[indexPath.section] + feedCellConfigurator.configure(cell) cell.display(photo: photo) return cell diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageFactory.swift new file mode 100644 index 0000000..0abcbf7 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageFactory.swift @@ -0,0 +1,14 @@ +// +// Created by Maxim Berezhnoy on 29/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +protocol FullImageCreating { + func createViewController() -> FullImageViewController +} + +final class FullImageFactory: FullImageCreating { + func createViewController() -> FullImageViewController { + FullImageViewController() + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift new file mode 100644 index 0000000..974fde3 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift @@ -0,0 +1,10 @@ +// +// Created by Maxim Berezhnoy on 27/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import UIKit + +final class FullImageViewController: UIViewController { + +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Utils/Routing.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Utils/Routing.swift new file mode 100644 index 0000000..897bf42 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Utils/Routing.swift @@ -0,0 +1,10 @@ +// +// Created by Maxim Berezhnoy on 27/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import UIKit + +protocol Routing { + var sourceVC: UIViewController? { get set } +} \ No newline at end of file From bb22b540999759ba9ddde3833165df3284e744f2 Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Sun, 29 Nov 2020 18:18:10 +0100 Subject: [PATCH 10/16] Improved UT coverage for collection fetching and feed VIP --- .../project.pbxproj | 62 +++++++------- .../Photos/FlickrPhotosService.swift | 17 ---- .../Photos/FlickrPhotoServiceTests.swift | 16 +++- .../Photos/MockFlickrPhotoService.swift | 15 +--- .../Browser/BrowserPresenterTests.swift | 41 ---------- .../Cell/BrowserCellInteractorTests.swift | 81 ------------------- .../Cell/MockBrowserCellPresenter.swift | 26 ------ .../Browser/MockBrowserDisplaying.swift | 19 ----- .../FeedInteractorTests.swift} | 14 ++-- .../Scenes/Feed/FeedPresenterTests.swift | 51 ++++++++++++ .../Scenes/Feed/MockFeedDisplaying.swift | 19 +++++ .../MockFeedPresenter.swift} | 6 +- .../FlickrCollectionDataFetcherTests.swift | 54 +++++++++++++ .../Flickr/FlickrCollectionFetcherTests.swift | 50 ++++++------ .../MockFlickrCollectionDataFetcher.swift | 23 ++++++ .../MockPhotoCollectionFetcher.swift | 6 +- .../MockPhotoDataProvider.swift | 6 +- 17 files changed, 230 insertions(+), 276 deletions(-) delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/BrowserPresenterTests.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/Cell/BrowserCellInteractorTests.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/Cell/MockBrowserCellPresenter.swift delete mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/MockBrowserDisplaying.swift rename SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/{Browser/BrowserInteractorTests.swift => Feed/FeedInteractorTests.swift} (70%) create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/FeedPresenterTests.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/MockFeedDisplaying.swift rename SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/{Browser/MockBrowserPresenter.swift => Feed/MockFeedPresenter.swift} (54%) create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcherTests.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/MockFlickrCollectionDataFetcher.swift diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj index eb8c796..e2ff891 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj @@ -13,16 +13,14 @@ 62DE91F2D0080AF13460E47C /* FeedInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9DB2850E842E22C4CC84 /* FeedInteractor.swift */; }; 62DE91F96D06BDB6AE8D40A1 /* XCTestCase+QueueAwait.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE924CEDF37B8D5BC86D48 /* XCTestCase+QueueAwait.swift */; }; 62DE924525362E9212DE0671 /* FullImageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9EB1763D78889BAD9753 /* FullImageFactory.swift */; }; - 62DE927526937FB6BEDCE8BC /* MockBrowserCellPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9B3E9AAA8D0661DEEFFE /* MockBrowserCellPresenter.swift */; }; 62DE92A1A0D21DCC9FC97F7E /* NetworkPhotoDataRetriever.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9868FFC49BF89E9F9192 /* NetworkPhotoDataRetriever.swift */; }; 62DE92CC1AB4CFA092F2DB10 /* FullImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE95E529D91B15DE6E0C58 /* FullImageViewController.swift */; }; 62DE92DECC87D7DACFB69CC9 /* PhotoDataRetrieving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99F1F322B9F470256B1E /* PhotoDataRetrieving.swift */; }; 62DE93261A41C5DDC9BACDB0 /* MockPhotoDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE91C1A5EBF975C64A37B3 /* MockPhotoDataCache.swift */; }; - 62DE9387D309D2DD3F2AC102 /* BrowserPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE915A672C1C41DCCF9C8D /* BrowserPresenterTests.swift */; }; - 62DE93E9065F0528B8577C0E /* BrowserCellInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE93A95BC1DB3D74EC66D8 /* BrowserCellInteractorTests.swift */; }; + 62DE9387D309D2DD3F2AC102 /* FeedPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE915A672C1C41DCCF9C8D /* FeedPresenterTests.swift */; }; 62DE940CB3E25C0A9AEFE252 /* FeedCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9927EE51CE772D3DD171 /* FeedCellFactory.swift */; }; 62DE944EF838C8C313469CCB /* MockPhotoDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE92EE74C81312D3DB90B0 /* MockPhotoDataProvider.swift */; }; - 62DE946CFE4B0C1C5B9E1324 /* MockBrowserDisplaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9810EDB0220E74893A89 /* MockBrowserDisplaying.swift */; }; + 62DE946CFE4B0C1C5B9E1324 /* MockFeedDisplaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9810EDB0220E74893A89 /* MockFeedDisplaying.swift */; }; 62DE947BBB3CC8AC189E0F4F /* FlickrCollectionDataFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE94AB83D36878355DAA8C /* FlickrCollectionDataFetching.swift */; }; 62DE94F27746C528BFA5C787 /* FeedRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9BF2A2C0391D9FFA480B /* FeedRouter.swift */; }; 62DE9521164585DF38F0C460 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99C6333390A5CB4F91B0 /* String.swift */; }; @@ -38,13 +36,14 @@ 62DE97615F6230CBFF9F92A4 /* FeedFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE96216E4D360CAB68AF75 /* FeedFactory.swift */; }; 62DE97B6EC739349BC9A6D2F /* FlickrPhotoServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE97953D19EE0C13151316 /* FlickrPhotoServiceTests.swift */; }; 62DE97CDCFCD2718C1BBC1E8 /* PhotoCollectionFetcherFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9A0F5F5E76B6AFE7159B /* PhotoCollectionFetcherFactory.swift */; }; - 62DE9866170F4BCE2B93E2E0 /* MockBrowserPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE944604E007DD383ABDE1 /* MockBrowserPresenter.swift */; }; + 62DE9866170F4BCE2B93E2E0 /* MockFeedPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE944604E007DD383ABDE1 /* MockFeedPresenter.swift */; }; 62DE9921C1443189F640771E /* PhotoDataProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C85502219A9858DCF22 /* PhotoDataProviderTests.swift */; }; 62DE9927B234F05FAB311B8D /* RootDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE94A9AA78F83D54078368 /* RootDependencies.swift */; }; 62DE9934B5A84CB2A3736360 /* MockPhotoCollectionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9001FE62BE27C3D938B3 /* MockPhotoCollectionFetcher.swift */; }; 62DE9A474D891C506B86BF8A /* ViewsLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE97EF27BCB2A06EB35FDF /* ViewsLabel.swift */; }; 62DE9A8C69E9E0A200181E5C /* FlickrApiURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9D790ECF920DF2C9802B /* FlickrApiURLBuilder.swift */; }; 62DE9A9250BD0AC541234841 /* PhotoDataProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99EC2B14DB9F0C554529 /* PhotoDataProviderFactory.swift */; }; + 62DE9ADB8EF31EA3CD79A2B6 /* FlickrCollectionDataFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99120A20109EF4E3A62B /* FlickrCollectionDataFetcherTests.swift */; }; 62DE9B3C2CAC899A108EFA70 /* DateLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE963F12E663C923326C82 /* DateLabel.swift */; }; 62DE9C2CEA5B44E24BB2D6F5 /* PhotoCollectionFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE90B754DC827D73A31B9E /* PhotoCollectionFetching.swift */; }; 62DE9C82855405B5A0BA6B57 /* FeedCellConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE970B2B651FC6FDE03FC1 /* FeedCellConfigurator.swift */; }; @@ -59,7 +58,8 @@ 62DE9F3410EB4A39040BBC55 /* TagsLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE940D06ADFF0BDAC17ADC /* TagsLabel.swift */; }; 62DE9F5B37C5FE4BED753FB2 /* PhotoDataNSCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9805D6D4DBDDBAD078E2 /* PhotoDataNSCache.swift */; }; 62DE9F69E85302202E6884B3 /* PhotoDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9FF6B55060E5204AC907 /* PhotoDataProvider.swift */; }; - 62DE9FA335C6F1E04D690818 /* BrowserInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9EA9D6ACC68E32F8A681 /* BrowserInteractorTests.swift */; }; + 62DE9F6D54B5EA2DA23BED9C /* MockFlickrCollectionDataFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE97BAB29B5E48A31A7687 /* MockFlickrCollectionDataFetcher.swift */; }; + 62DE9FA335C6F1E04D690818 /* FeedInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9EA9D6ACC68E32F8A681 /* FeedInteractorTests.swift */; }; 62DE9FAF86BD3911FC84BF72 /* FeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C09B259CEA839DAE2B9 /* FeedViewController.swift */; }; 62DE9FDF59C5BAB0F8D43494 /* PhotoDataCaching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9B49CABD228CCFBCE936 /* PhotoDataCaching.swift */; }; 944AC01E23CE0E9C009FD611 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944AC01D23CE0E9C009FD611 /* AppDelegate.swift */; }; @@ -82,17 +82,16 @@ 62DE9001FE62BE27C3D938B3 /* MockPhotoCollectionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoCollectionFetcher.swift; sourceTree = ""; }; 62DE9031E5EEBEA8EBA4D644 /* PhotoParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoParameters.swift; sourceTree = ""; }; 62DE90B754DC827D73A31B9E /* PhotoCollectionFetching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionFetching.swift; sourceTree = ""; }; - 62DE915A672C1C41DCCF9C8D /* BrowserPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserPresenterTests.swift; sourceTree = ""; }; + 62DE915A672C1C41DCCF9C8D /* FeedPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedPresenterTests.swift; sourceTree = ""; }; 62DE91C1A5EBF975C64A37B3 /* MockPhotoDataCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataCache.swift; sourceTree = ""; }; 62DE9225FFEBB4968F60A987 /* MockFlickrPhotoService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFlickrPhotoService.swift; sourceTree = ""; }; 62DE924CEDF37B8D5BC86D48 /* XCTestCase+QueueAwait.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCTestCase+QueueAwait.swift"; sourceTree = ""; }; 62DE925A929D79068D7BE7CB /* FeedViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedViewDataSource.swift; sourceTree = ""; }; 62DE92B5279132DA6C6FA657 /* FlickrCollectionDataFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionDataFetcher.swift; sourceTree = ""; }; 62DE92EE74C81312D3DB90B0 /* MockPhotoDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataProvider.swift; sourceTree = ""; }; - 62DE93A95BC1DB3D74EC66D8 /* BrowserCellInteractorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserCellInteractorTests.swift; sourceTree = ""; }; 62DE93C78893DAA0E46DBD6A /* HttpClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpClient.swift; sourceTree = ""; }; 62DE940D06ADFF0BDAC17ADC /* TagsLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TagsLabel.swift; sourceTree = ""; }; - 62DE944604E007DD383ABDE1 /* MockBrowserPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBrowserPresenter.swift; sourceTree = ""; }; + 62DE944604E007DD383ABDE1 /* MockFeedPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFeedPresenter.swift; sourceTree = ""; }; 62DE94A9AA78F83D54078368 /* RootDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootDependencies.swift; sourceTree = ""; }; 62DE94AB83D36878355DAA8C /* FlickrCollectionDataFetching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionDataFetching.swift; sourceTree = ""; }; 62DE9506DAADD74BB93C3549 /* Routing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Routing.swift; sourceTree = ""; }; @@ -106,20 +105,21 @@ 62DE970A3FD09743A03DAF33 /* Photo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = ""; }; 62DE970B2B651FC6FDE03FC1 /* FeedCellConfigurator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellConfigurator.swift; sourceTree = ""; }; 62DE97953D19EE0C13151316 /* FlickrPhotoServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotoServiceTests.swift; sourceTree = ""; }; + 62DE97BAB29B5E48A31A7687 /* MockFlickrCollectionDataFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFlickrCollectionDataFetcher.swift; sourceTree = ""; }; 62DE97EF27BCB2A06EB35FDF /* ViewsLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewsLabel.swift; sourceTree = ""; }; 62DE9805D6D4DBDDBAD078E2 /* PhotoDataNSCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataNSCache.swift; sourceTree = ""; }; - 62DE9810EDB0220E74893A89 /* MockBrowserDisplaying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBrowserDisplaying.swift; sourceTree = ""; }; + 62DE9810EDB0220E74893A89 /* MockFeedDisplaying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFeedDisplaying.swift; sourceTree = ""; }; 62DE986497C5FB223DA10256 /* FlickrPhotoURLResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotoURLResolver.swift; sourceTree = ""; }; 62DE9868FFC49BF89E9F9192 /* NetworkPhotoDataRetriever.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkPhotoDataRetriever.swift; sourceTree = ""; }; 62DE987D3B885CA1A25FB693 /* FlickrApiValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrApiValues.swift; sourceTree = ""; }; 62DE98ED3E76A320E0BF36D3 /* FeedCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellView.swift; sourceTree = ""; }; + 62DE99120A20109EF4E3A62B /* FlickrCollectionDataFetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionDataFetcherTests.swift; sourceTree = ""; }; 62DE9927EE51CE772D3DD171 /* FeedCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedCellFactory.swift; sourceTree = ""; }; 62DE99C6333390A5CB4F91B0 /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 62DE99EC2B14DB9F0C554529 /* PhotoDataProviderFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProviderFactory.swift; sourceTree = ""; }; 62DE99F1F322B9F470256B1E /* PhotoDataRetrieving.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataRetrieving.swift; sourceTree = ""; }; 62DE9A0F5F5E76B6AFE7159B /* PhotoCollectionFetcherFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionFetcherFactory.swift; sourceTree = ""; }; 62DE9A2EBADCA5EE9059C3DD /* FlickrCollectionFetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcherTests.swift; sourceTree = ""; }; - 62DE9B3E9AAA8D0661DEEFFE /* MockBrowserCellPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockBrowserCellPresenter.swift; sourceTree = ""; }; 62DE9B49CABD228CCFBCE936 /* PhotoDataCaching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataCaching.swift; sourceTree = ""; }; 62DE9B8BC72A4AB8A5A34B65 /* FlickrCollectionFetcherFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcherFactory.swift; sourceTree = ""; }; 62DE9BF2A2C0391D9FFA480B /* FeedRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedRouter.swift; sourceTree = ""; }; @@ -131,7 +131,7 @@ 62DE9D790ECF920DF2C9802B /* FlickrApiURLBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrApiURLBuilder.swift; sourceTree = ""; }; 62DE9DB2850E842E22C4CC84 /* FeedInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedInteractor.swift; sourceTree = ""; }; 62DE9E19BC1900DC8648FD5E /* MockHttpClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockHttpClient.swift; sourceTree = ""; }; - 62DE9EA9D6ACC68E32F8A681 /* BrowserInteractorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserInteractorTests.swift; sourceTree = ""; }; + 62DE9EA9D6ACC68E32F8A681 /* FeedInteractorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedInteractorTests.swift; sourceTree = ""; }; 62DE9EB1763D78889BAD9753 /* FullImageFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullImageFactory.swift; sourceTree = ""; }; 62DE9FF6B55060E5204AC907 /* PhotoDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProvider.swift; sourceTree = ""; }; 944AC01A23CE0E9C009FD611 /* SimpleFlickrBrowser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimpleFlickrBrowser.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -182,7 +182,7 @@ 62DE92353EFEFAB905C719AC /* Scenes */ = { isa = PBXGroup; children = ( - 62DE983292E5D66368CF497E /* Browser */, + 62DE983292E5D66368CF497E /* Feed */, ); path = Scenes; sourceTree = ""; @@ -261,6 +261,8 @@ isa = PBXGroup; children = ( 62DE9A2EBADCA5EE9059C3DD /* FlickrCollectionFetcherTests.swift */, + 62DE97BAB29B5E48A31A7687 /* MockFlickrCollectionDataFetcher.swift */, + 62DE99120A20109EF4E3A62B /* FlickrCollectionDataFetcherTests.swift */, ); path = Flickr; sourceTree = ""; @@ -285,16 +287,15 @@ path = PhotoDataProvider; sourceTree = ""; }; - 62DE983292E5D66368CF497E /* Browser */ = { + 62DE983292E5D66368CF497E /* Feed */ = { isa = PBXGroup; children = ( - 62DE9F9AB6B2EBDB591C446A /* Cell */, - 62DE9EA9D6ACC68E32F8A681 /* BrowserInteractorTests.swift */, - 62DE944604E007DD383ABDE1 /* MockBrowserPresenter.swift */, - 62DE915A672C1C41DCCF9C8D /* BrowserPresenterTests.swift */, - 62DE9810EDB0220E74893A89 /* MockBrowserDisplaying.swift */, + 62DE9EA9D6ACC68E32F8A681 /* FeedInteractorTests.swift */, + 62DE944604E007DD383ABDE1 /* MockFeedPresenter.swift */, + 62DE915A672C1C41DCCF9C8D /* FeedPresenterTests.swift */, + 62DE9810EDB0220E74893A89 /* MockFeedDisplaying.swift */, ); - path = Browser; + path = Feed; sourceTree = ""; }; 62DE986DB8FB2B4857D0B727 /* PhotoDataRetriever */ = { @@ -436,15 +437,6 @@ path = Flickr; sourceTree = ""; }; - 62DE9F9AB6B2EBDB591C446A /* Cell */ = { - isa = PBXGroup; - children = ( - 62DE9B3E9AAA8D0661DEEFFE /* MockBrowserCellPresenter.swift */, - 62DE93A95BC1DB3D74EC66D8 /* BrowserCellInteractorTests.swift */, - ); - path = Cell; - sourceTree = ""; - }; 944AC01123CE0E9C009FD611 = { isa = PBXGroup; children = ( @@ -662,19 +654,19 @@ 62DE93261A41C5DDC9BACDB0 /* MockPhotoDataCache.swift in Sources */, 62DE90E73B9BFCA4A54B76C2 /* MockPhotoDataRetriever.swift in Sources */, 62DE9921C1443189F640771E /* PhotoDataProviderTests.swift in Sources */, - 62DE927526937FB6BEDCE8BC /* MockBrowserCellPresenter.swift in Sources */, 62DE944EF838C8C313469CCB /* MockPhotoDataProvider.swift in Sources */, - 62DE93E9065F0528B8577C0E /* BrowserCellInteractorTests.swift in Sources */, 62DE91F96D06BDB6AE8D40A1 /* XCTestCase+QueueAwait.swift in Sources */, 62DE97140AE23BE5CD384EF0 /* MockHttpClient.swift in Sources */, 62DE911E1EB4D78BCA9085CB /* FlickrCollectionFetcherTests.swift in Sources */, 62DE90F40E919CCFF876D1A6 /* MockFlickrPhotoService.swift in Sources */, 62DE97B6EC739349BC9A6D2F /* FlickrPhotoServiceTests.swift in Sources */, - 62DE9FA335C6F1E04D690818 /* BrowserInteractorTests.swift in Sources */, + 62DE9FA335C6F1E04D690818 /* FeedInteractorTests.swift in Sources */, 62DE9934B5A84CB2A3736360 /* MockPhotoCollectionFetcher.swift in Sources */, - 62DE9866170F4BCE2B93E2E0 /* MockBrowserPresenter.swift in Sources */, - 62DE9387D309D2DD3F2AC102 /* BrowserPresenterTests.swift in Sources */, - 62DE946CFE4B0C1C5B9E1324 /* MockBrowserDisplaying.swift in Sources */, + 62DE9866170F4BCE2B93E2E0 /* MockFeedPresenter.swift in Sources */, + 62DE9387D309D2DD3F2AC102 /* FeedPresenterTests.swift in Sources */, + 62DE946CFE4B0C1C5B9E1324 /* MockFeedDisplaying.swift in Sources */, + 62DE9ADB8EF31EA3CD79A2B6 /* FlickrCollectionDataFetcherTests.swift in Sources */, + 62DE9F6D54B5EA2DA23BED9C /* MockFlickrCollectionDataFetcher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift index d8e9eaf..1a81f3a 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift @@ -9,8 +9,6 @@ protocol FlickrPhotosFetching { typealias Completion = (Result<[FlickrPhoto], Error>) -> Void func getRecent(page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion) - - func search(matching text: String, page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion) } final class FlickrPhotosService: FlickrPhotosFetching { @@ -36,21 +34,6 @@ final class FlickrPhotosService: FlickrPhotosFetching { fetchFrom(url: url, completion: completion) } - func search(matching text: String, page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion) { - let url = FlickrApiURLResolver.build( - method: .photosSearch, - apiKey: apiKey, - queryParameters: [ - .text: text, - .page: String(page), - .perPage: String(photosPerPage), - .extras: getQueryParameters(for: metadata).map { $0.rawValue }.joined(separator: ",") - ] - ) - - fetchFrom(url: url, completion: completion) - } - private func fetchFrom(url: URL, completion: @escaping Completion) { httpClient.get(url: url) { [weak self] result in guard let self = self else { return } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/FlickrPhotoServiceTests.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/FlickrPhotoServiceTests.swift index ab963bb..56510a3 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/FlickrPhotoServiceTests.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/FlickrPhotoServiceTests.swift @@ -20,7 +20,13 @@ let validPhotosData = """ "title": "Ford 6.4 Powerstroke EGR Delete Kit", "ispublic": 1, "isfriend": 0, - "isfamily": 0 + "isfamily": 0, + "datetaken":"2020-11-27 17:30:40", + "datetakengranularity":"0", + "datetakenunknown":"0", + "ownername":"owner1", + "views":"1023", + "tags":"tag1 tag2 tag3" }, { "id": "49400436126", @@ -31,7 +37,13 @@ let validPhotosData = """ "title": "BWO20V", "ispublic": 1, "isfriend": 0, - "isfamily": 0 + "isfamily": 0, + "datetaken":"2020-12-24 12:25:40", + "datetakengranularity":"0", + "datetakenunknown":"0", + "ownername":"owner2", + "views":"20", + "tags":"tag1 tag2 tag3" } ] }} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/MockFlickrPhotoService.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/MockFlickrPhotoService.swift index a3cb8f3..918032c 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/MockFlickrPhotoService.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/MockFlickrPhotoService.swift @@ -9,10 +9,7 @@ class MockFlickrPhotosService: FlickrPhotosFetching { var getRecentCalls = [(Int, Int)]() var getRecentResult: Result<[FlickrPhoto], Error>? - var searchCalls = [(String, Int, Int)]() - var searchResult: Result<[FlickrPhoto], Error>? - - func getRecent(page: Int, photosPerPage: Int, completion: @escaping Completion) { + func getRecent(page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion) { guard let result = getRecentResult else { fatalError("\(#function) expectation was not set") } @@ -21,14 +18,4 @@ class MockFlickrPhotosService: FlickrPhotosFetching { completion(result) } - - func search(matching text: String, page: Int, photosPerPage: Int, completion: @escaping Completion) { - guard let result = searchResult else { - fatalError("\(#function) expectation was not set") - } - - searchCalls.append((text, page, photosPerPage)) - - completion(result) - } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/BrowserPresenterTests.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/BrowserPresenterTests.swift deleted file mode 100644 index 71fa81f..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/BrowserPresenterTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// Created by Maxim Berezhnoy on 18/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -@testable import SimpleFlickrBrowser - -import XCTest - -class BrowserPresenterTests: XCTestCase { - var displayingMock: MockBrowserDisplaying! - - var sut: BrowserPresenter! - - override func setUp() { - super.setUp() - - displayingMock = MockBrowserDisplaying() - - sut = BrowserPresenter() - sut.view = displayingMock - } - - func test_presentPhotos_startingFromZero_dataIsReset() { - let photo = Photo(id: "id", imageURL: URL(string: "www.not-a-website.com")!) - - sut.present(photos: BrowserModels.Photos.Response(startingPosition: 0, photos: [photo])) - - XCTAssertEqual(displayingMock.displayMorePhotosCalls.count, 0) - XCTAssertEqual(displayingMock.displayNewPhotosCalls.count, 1) - } - - func test_presentPhotosTwice_startingFromNonZero_dataIsUpdated() { - let photo = Photo(id: "id", imageURL: URL(string: "www.not-a-website.com")!) - - sut.present(photos: BrowserModels.Photos.Response(startingPosition: 1, photos: [photo])) - - XCTAssertEqual(displayingMock.displayMorePhotosCalls.count, 1) - XCTAssertEqual(displayingMock.displayNewPhotosCalls.count, 0) - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/Cell/BrowserCellInteractorTests.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/Cell/BrowserCellInteractorTests.swift deleted file mode 100644 index 04f2bca..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/Cell/BrowserCellInteractorTests.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// Created by Maxim Berezhnoy on 16/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -@testable import SimpleFlickrBrowser - -import Foundation -import XCTest - -class BrowserCellInteractorTests: XCTestCase { - var presenter: MockBrowserCellPresenter! - var photoDataProvider: MockPhotoDataProvider! - var sut: BrowserCellInteractor! - - override func setUp() { - super.setUp() - - presenter = MockBrowserCellPresenter() - photoDataProvider = MockPhotoDataProvider() - - sut = BrowserCellInteractor(presenter: presenter, photoDataProvider: photoDataProvider) - } - - func test_fetchImage_retrievesFromProvider_success_sendsDataToPresenter() { - let url = URL(string: "www.not-a-website.com")! - let photoData = Data() - - photoDataProvider.getPhotoDataResult = .success(photoData) - - sut.fetch(image: BrowserCellModels.PhotoImage.Request(photoID: "id", url: url)) - - awaitMainQueueResolution() - - XCTAssertEqual(photoDataProvider.getPhotoDataCalls, [url]) - - XCTAssertEqual(presenter.presentLoadingCalls, 1) - - XCTAssertEqual(presenter.presentImageCalls.map { $0.data }, [photoData]) - XCTAssertEqual(presenter.presentErrorCalls, 0) - } - - func test_fetchImage_retrievesFromProvider_failure_sendsErrorToPresenter() { - let url = URL(string: "www.not-a-website.com")! - - enum RetrievalError: Error { - case undefined - } - - photoDataProvider.getPhotoDataResult = .failure(RetrievalError.undefined) - - sut.fetch(image: BrowserCellModels.PhotoImage.Request(photoID: "id", url: url)) - - awaitMainQueueResolution() - - XCTAssertEqual(photoDataProvider.getPhotoDataCalls, [url]) - - XCTAssertEqual(presenter.presentLoadingCalls, 1) - - XCTAssertEqual(presenter.presentImageCalls.count, 0) - XCTAssertEqual(presenter.presentErrorCalls, 1) - } - - func test_fetchImage_fetchAgainWhileFirstIsLoading_onlyPresentsSecondImage() { - let url = URL(string: "www.not-a-website.com")! - - let firstImageData = "1".data(using: .utf8)! - photoDataProvider.getPhotoDataResult = .success(firstImageData) - sut.fetch(image: BrowserCellModels.PhotoImage.Request(photoID: "id1", url: url)) - - let secondImageData = "2".data(using: .utf8)! - photoDataProvider.getPhotoDataResult = .success(secondImageData) - sut.fetch(image: BrowserCellModels.PhotoImage.Request(photoID: "id2", url: url)) - - awaitMainQueueResolution() - - XCTAssertEqual(photoDataProvider.getPhotoDataCalls.count, 2) - - XCTAssertEqual(presenter.presentImageCalls.map { $0.data }, [secondImageData]) - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/Cell/MockBrowserCellPresenter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/Cell/MockBrowserCellPresenter.swift deleted file mode 100644 index ebba37d..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/Cell/MockBrowserCellPresenter.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Created by Maxim Berezhnoy on 16/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -@testable import SimpleFlickrBrowser - -import Foundation - -final class MockBrowserCellPresenter: BrowserCellPresenting { - var presentImageCalls = [BrowserCellModels.PhotoImage.Response]() - var presentErrorCalls = 0 - var presentLoadingCalls = 0 - - func present(image: BrowserCellModels.PhotoImage.Response) { - presentImageCalls.append(image) - } - - func presentLoading() { - presentLoadingCalls += 1 - } - - func presentError() { - presentErrorCalls += 1 - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/MockBrowserDisplaying.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/MockBrowserDisplaying.swift deleted file mode 100644 index 831d91e..0000000 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/MockBrowserDisplaying.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Created by Maxim Berezhnoy on 18/01/2020. -// -// Copyright (c) 2020 rencevio. All rights reserved. - -@testable import SimpleFlickrBrowser - -class MockBrowserDisplaying: BrowserDisplaying { - var displayNewPhotosCalls = [BrowserModels.Photos.ViewModel]() - var displayMorePhotosCalls = [BrowserModels.Photos.ViewModel]() - - func displayNew(photos: BrowserModels.Photos.ViewModel) { - displayNewPhotosCalls.append(photos) - } - - func displayMore(photos: BrowserModels.Photos.ViewModel) { - displayMorePhotosCalls.append(photos) - } -} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/BrowserInteractorTests.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/FeedInteractorTests.swift similarity index 70% rename from SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/BrowserInteractorTests.swift rename to SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/FeedInteractorTests.swift index 9793725..64c9ba4 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/BrowserInteractorTests.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/FeedInteractorTests.swift @@ -7,23 +7,23 @@ import XCTest -class BrowserInteractorTests: XCTestCase { - var presenterMock: MockBrowserPresenter! +class FeedInteractorTests: XCTestCase { + var presenterMock: MockFeedPresenter! var collectionFetcherMock: MockPhotoCollectionFetcher! - var sut: BrowserInteractor! + var sut: FeedInteractor! override func setUp() { super.setUp() - presenterMock = MockBrowserPresenter() + presenterMock = MockFeedPresenter() collectionFetcherMock = MockPhotoCollectionFetcher() - sut = BrowserInteractor(presenter: presenterMock, photoCollectionFetcher: collectionFetcherMock) + sut = FeedInteractor(presenter: presenterMock, photoCollectionFetcher: collectionFetcherMock) } func test_fetch_fetchAgainImmediatelyWithSameParams_fetchesAndPresentsOnlyOnce() { - let fetchRequest = BrowserModels.Photos.Request(startFromPosition: 0, fetchAtMost: 10, searchCriteria: "42") + let fetchRequest = FeedModels.Photos.Request(startFromPosition: 0, fetchAtMost: 10, size: .large, metadata: []) collectionFetcherMock.fetchPhotoResult = .success([]) @@ -37,7 +37,7 @@ class BrowserInteractorTests: XCTestCase { } func test_fetch_fetchAgainAfterFirstRequestFinished_fetchesAndPresentsTwice() { - let fetchRequest = BrowserModels.Photos.Request(startFromPosition: 0, fetchAtMost: 10, searchCriteria: "42") + let fetchRequest = FeedModels.Photos.Request(startFromPosition: 0, fetchAtMost: 10, size: .large, metadata: []) collectionFetcherMock.fetchPhotoResult = .success([]) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/FeedPresenterTests.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/FeedPresenterTests.swift new file mode 100644 index 0000000..48b2b4a --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/FeedPresenterTests.swift @@ -0,0 +1,51 @@ +// +// Created by Maxim Berezhnoy on 18/01/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +@testable import SimpleFlickrBrowser + +import XCTest + +class FeedPresenterTests: XCTestCase { + var displayingMock: MockFeedDisplaying! + + var sut: FeedPresenter! + + override func setUp() { + super.setUp() + + displayingMock = MockFeedDisplaying() + + sut = FeedPresenter() + sut.view = displayingMock + } + + func test_presentPhotos_startingFromZero_dataIsReset() { + let photo = Photo( + id: "id", + imageData: Data(), + fullSizeImageURL: URL(string: "www.not-a-website.com")!, + metadata: Photo.Metadata(views: 0, tags: [], ownerName: "", dateTaken: Date()) + ) + + sut.present(photos: FeedModels.Photos.Response(startingPosition: 0, photos: [photo])) + + XCTAssertEqual(displayingMock.displayMorePhotosCalls.count, 0) + XCTAssertEqual(displayingMock.displayNewPhotosCalls.count, 1) + } + + func test_presentPhotosTwice_startingFromNonZero_dataIsUpdated() { + let photo = Photo( + id: "id", + imageData: Data(), + fullSizeImageURL: URL(string: "www.not-a-website.com")!, + metadata: Photo.Metadata(views: 0, tags: [], ownerName: "", dateTaken: Date()) + ) + + sut.present(photos: FeedModels.Photos.Response(startingPosition: 1, photos: [photo])) + + XCTAssertEqual(displayingMock.displayMorePhotosCalls.count, 1) + XCTAssertEqual(displayingMock.displayNewPhotosCalls.count, 0) + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/MockFeedDisplaying.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/MockFeedDisplaying.swift new file mode 100644 index 0000000..1497c43 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/MockFeedDisplaying.swift @@ -0,0 +1,19 @@ +// +// Created by Maxim Berezhnoy on 18/01/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +@testable import SimpleFlickrBrowser + +class MockFeedDisplaying: FeedDisplaying { + var displayNewPhotosCalls = [FeedModels.Photos.ViewModel]() + var displayMorePhotosCalls = [FeedModels.Photos.ViewModel]() + + func displayNew(photos: FeedModels.Photos.ViewModel) { + displayNewPhotosCalls.append(photos) + } + + func displayMore(photos: FeedModels.Photos.ViewModel) { + displayMorePhotosCalls.append(photos) + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/MockBrowserPresenter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/MockFeedPresenter.swift similarity index 54% rename from SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/MockBrowserPresenter.swift rename to SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/MockFeedPresenter.swift index 44b2e42..9ea4716 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Browser/MockBrowserPresenter.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/MockFeedPresenter.swift @@ -7,10 +7,10 @@ import Foundation -final class MockBrowserPresenter: BrowserPresenting { - var presentPhotosCalls = [BrowserModels.Photos.Response]() +final class MockFeedPresenter: FeedPresenting { + var presentPhotosCalls = [FeedModels.Photos.Response]() - func present(photos: BrowserModels.Photos.Response) { + func present(photos: FeedModels.Photos.Response) { presentPhotosCalls.append(photos) } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcherTests.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcherTests.swift new file mode 100644 index 0000000..50fbd8d --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcherTests.swift @@ -0,0 +1,54 @@ +// +// Created by Maxim Berezhnoy on 29/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +@testable import SimpleFlickrBrowser + +import XCTest + +class FlickrCollectionDataFetcherTests: XCTestCase { + let expectationTimeout = 1.0 + + var dataProviderMock: MockPhotoDataProvider! + var sut: FlickrCollectionDataFetcher! + + override func setUp() { + super.setUp() + + dataProviderMock = MockPhotoDataProvider() + sut = FlickrCollectionDataFetcher(dataProvider: dataProviderMock) + } + + func test_fetchData_somePhotosHaveNoImageData_returnsOnlyPhotosWithData() { + let photosWithData = [ + FlickrPhoto(id: "1", secret: "1", server: "1", farm: 1, datetaken: "1", ownername: "1", views: "1", tags: "1"), + FlickrPhoto(id: "2", secret: "2", server: "2", farm: 2, datetaken: "2", ownername: "2", views: "2", tags: "2") + ] + let photosWithoutData = [ + FlickrPhoto(id: "3", secret: "3", server: "3", farm: 3, datetaken: "3", ownername: "3", views: "3", tags: "3"), + ] + + let photoSize = PhotoParameters.Size.large + + photosWithoutData.forEach { _ in + dataProviderMock.getPhotoDataResults.append(.failure(Http.RequestError.noData)) + } + photosWithData.forEach { _ in + dataProviderMock.getPhotoDataResults.append(.success(Data())) + } + + let fetchExpectation = XCTestExpectation(description: "Fetching photos data") + var fetchResult: [Photo.ID: Data]! + + sut.fetchData(photos: photosWithData + photosWithoutData, withSize: photoSize) { result in + fetchResult = result + fetchExpectation.fulfill() + } + + wait(for: [fetchExpectation], timeout: expectationTimeout) + + XCTAssertEqual(fetchResult.count, photosWithData.count) + XCTAssertEqual(fetchResult, Dictionary(uniqueKeysWithValues: photosWithData.map { ($0.id, Data()) })) + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherTests.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherTests.swift index ff7ed50..7419bb8 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherTests.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherTests.swift @@ -11,30 +11,36 @@ class FlickrCollectionFetcherTests: XCTestCase { let expectationTimeout = 1.0 var photosServiceMock: MockFlickrPhotosService! + var collectionDataFetcherMock: MockFlickrCollectionDataFetcher! var sut: FlickrCollectionFetcher! override func setUp() { super.setUp() photosServiceMock = MockFlickrPhotosService() - sut = FlickrCollectionFetcher(flickrPhotosService: photosServiceMock) + collectionDataFetcherMock = MockFlickrCollectionDataFetcher() + sut = FlickrCollectionFetcher(flickrPhotosService: photosServiceMock, collectionDataFetcher: collectionDataFetcherMock) } - func test_fetchPhotos_withoutSearchCriteria_fetchesRecentPhotos() { + func test_fetchPhotos_fetchesRecentPhotos() { let serviceFetchResult = [ - FlickrPhoto(id: "1", secret: "1", server: "1", farm: 1), - FlickrPhoto(id: "2", secret: "2", server: "2", farm: 2), + FlickrPhoto(id: "1", secret: "1", server: "1", farm: 1, datetaken: "2020-11-22 21:21:29", ownername: "1", views: "1", tags: "1"), + FlickrPhoto(id: "2", secret: "2", server: "2", farm: 2, datetaken: "2012-10-20 04:02:01", ownername: "2", views: "2", tags: "2"), ] + let collectionDataFetchResult = Dictionary(uniqueKeysWithValues: serviceFetchResult.map { ($0.id, Data()) }) + let startingPosition = 0 let maxFetchCount = 50 + let photoSize = PhotoParameters.Size.large let fetchExpectation = XCTestExpectation(description: "Fetching photos") var fetchResult: [Photo]! photosServiceMock.getRecentResult = .success(serviceFetchResult) + collectionDataFetcherMock.fetchDataResult = collectionDataFetchResult - sut.fetchPhotos(startingFrom: startingPosition, fetchAtMost: maxFetchCount) { result in + sut.fetchPhotos(startingFrom: startingPosition, fetchAtMost: maxFetchCount, withSize: photoSize) { result in switch result { case let .success(photos): fetchResult = photos @@ -50,30 +56,25 @@ class FlickrCollectionFetcherTests: XCTestCase { XCTAssertEqual(photosServiceMock.getRecentCalls.count, 1) XCTAssertTrue(photosServiceMock.getRecentCalls[0] == (startingPosition + 1, maxFetchCount)) - - XCTAssertEqual(photosServiceMock.searchCalls.count, 0) + + XCTAssertEqual(collectionDataFetcherMock.fetchDataCalls.count, 1) } - func test_fetchPhotos_withSearchCriteria_searchesForPhotos() { - let serviceFetchResult = [ - FlickrPhoto(id: "1", secret: "1", server: "1", farm: 1), - FlickrPhoto(id: "2", secret: "2", server: "2", farm: 2), - ] + func test_fetchPhotos_correctlyConvertsMetadata() { + let flickrPhoto = FlickrPhoto(id: "1", secret: "1", server: "1", farm: 1, datetaken: "2020-11-22 21:21:29", ownername: "1", views: "1", tags: "1") + + let collectionDataFetchResult = [flickrPhoto.id: Data()] - let searchCriteria = "cars" let startingPosition = 0 let maxFetchCount = 50 let fetchExpectation = XCTestExpectation(description: "Fetching photos") var fetchResult: [Photo]! - photosServiceMock.searchResult = .success(serviceFetchResult) + photosServiceMock.getRecentResult = .success([flickrPhoto]) + collectionDataFetcherMock.fetchDataResult = collectionDataFetchResult - sut.fetchPhotos( - startingFrom: startingPosition, - fetchAtMost: maxFetchCount, - matching: searchCriteria - ) { result in + sut.fetchPhotos(startingFrom: startingPosition, fetchAtMost: maxFetchCount, withSize: .large) { result in switch result { case let .success(photos): fetchResult = photos @@ -85,11 +86,10 @@ class FlickrCollectionFetcherTests: XCTestCase { wait(for: [fetchExpectation], timeout: expectationTimeout) - XCTAssertEqual(serviceFetchResult.map { $0.id }, fetchResult.map { $0.id }) - - XCTAssertEqual(photosServiceMock.searchCalls.count, 1) - XCTAssertTrue(photosServiceMock.searchCalls[0] == (searchCriteria, startingPosition + 1, maxFetchCount)) - - XCTAssertEqual(photosServiceMock.getRecentCalls.count, 0) + XCTAssertEqual(fetchResult.count, 1) + + let metadata = fetchResult[0].metadata + XCTAssertNotNil(metadata.dateTaken) + XCTAssertEqual(metadata.views, flickrPhoto.views.toInt()!) } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/MockFlickrCollectionDataFetcher.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/MockFlickrCollectionDataFetcher.swift new file mode 100644 index 0000000..14d7414 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/MockFlickrCollectionDataFetcher.swift @@ -0,0 +1,23 @@ +// +// Created by Maxim Berezhnoy on 29/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +@testable import SimpleFlickrBrowser + +import Foundation.NSData + +class MockFlickrCollectionDataFetcher: FlickrCollectionDataFetching { + var fetchDataCalls = [([FlickrPhoto], PhotoParameters.Size)]() + var fetchDataResult: [Photo.ID: Data]! + + func fetchData(photos: [FlickrPhoto], withSize size: PhotoParameters.Size, _ completion: @escaping ([Photo.ID: Data]) -> Void) { + guard let result = fetchDataResult else { + fatalError("\(#function) expectation was not set") + } + + fetchDataCalls.append((photos, size)) + + completion(result) + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/MockPhotoCollectionFetcher.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/MockPhotoCollectionFetcher.swift index 2a614ca..8d0de42 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/MockPhotoCollectionFetcher.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/MockPhotoCollectionFetcher.swift @@ -8,18 +8,18 @@ import XCTest class MockPhotoCollectionFetcher: PhotoCollectionFetching { - var fetchPhotosCalls = [(Int, Int, String?)]() + var fetchPhotosCalls = [(Int, Int, PhotoParameters.Size)]() var fetchPhotoResult: Result<[Photo], Error>! func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, - matching searchCriteria: String?, + withSize size: PhotoParameters.Size, completion: @escaping Completion) { guard let result = fetchPhotoResult else { fatalError("\(#function) expectation was not set") } - fetchPhotosCalls.append((position, maxFetchCount, searchCriteria)) + fetchPhotosCalls.append((position, maxFetchCount, size)) completion(result) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/MockPhotoDataProvider.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/MockPhotoDataProvider.swift index 2c4340d..81bd37b 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/MockPhotoDataProvider.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/MockPhotoDataProvider.swift @@ -9,15 +9,15 @@ import Foundation final class MockPhotoDataProvider: PhotoDataProviding { var getPhotoDataCalls = [URL]() - var getPhotoDataResult: Result? + var getPhotoDataResults = [Result]() func getPhotoData(from url: URL, completion: @escaping Completion) { - guard let result = getPhotoDataResult else { + guard !getPhotoDataResults.isEmpty else { fatalError("\(#function) expectation was not set") } getPhotoDataCalls.append(url) - completion(result) + completion(getPhotoDataResults.popLast()!) } } From 67da3346b8fbbfad2d9e981e00b8e8d6990ca3e4 Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Sun, 29 Nov 2020 20:20:26 +0100 Subject: [PATCH 11/16] Display full size image when an image in the feed is tapped --- .../project.pbxproj | 28 +++++ .../RootDependencies.swift | 3 +- .../Scenes/Feed/Cell/FeedCellView.swift | 25 ++++- .../Scenes/Feed/FeedFactory.swift | 11 +- .../Scenes/Feed/FeedModels.swift | 2 - .../Scenes/Feed/FeedRouter.swift | 6 +- .../Feed/FullImage/FullImageFactory.swift | 19 +++- .../Feed/FullImage/FullImageInteractor.swift | 37 +++++++ .../Feed/FullImage/FullImageModels.swift | 22 ++++ .../Feed/FullImage/FullImagePresenter.swift | 21 ++++ .../FullImage/FullImageViewController.swift | 104 ++++++++++++++++++ .../SimpleFlickrBrowser/Scenes/Style.swift | 10 ++ .../FullImage/FullImageInteractorTests.swift | 59 ++++++++++ .../FullImage/MockFullImagePresenter.swift | 21 ++++ 14 files changed, 351 insertions(+), 17 deletions(-) create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageInteractor.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageModels.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImagePresenter.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/FullImage/FullImageInteractorTests.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/FullImage/MockFullImagePresenter.swift diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj index e2ff891..bb4c2eb 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 62DE90247F9DE029191EAEB0 /* MockFullImagePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9EA282C378A8A45AC310 /* MockFullImagePresenter.swift */; }; 62DE90E73B9BFCA4A54B76C2 /* MockPhotoDataRetriever.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE96B5EBB14213DC9E7804 /* MockPhotoDataRetriever.swift */; }; 62DE90F40E919CCFF876D1A6 /* MockFlickrPhotoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9225FFEBB4968F60A987 /* MockFlickrPhotoService.swift */; }; 62DE911E1EB4D78BCA9085CB /* FlickrCollectionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9A2EBADCA5EE9059C3DD /* FlickrCollectionFetcherTests.swift */; }; @@ -45,16 +46,20 @@ 62DE9A9250BD0AC541234841 /* PhotoDataProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99EC2B14DB9F0C554529 /* PhotoDataProviderFactory.swift */; }; 62DE9ADB8EF31EA3CD79A2B6 /* FlickrCollectionDataFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99120A20109EF4E3A62B /* FlickrCollectionDataFetcherTests.swift */; }; 62DE9B3C2CAC899A108EFA70 /* DateLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE963F12E663C923326C82 /* DateLabel.swift */; }; + 62DE9BC9A5B77235063C5BCE /* FullImageModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9F51F8C74F47616FAA4A /* FullImageModels.swift */; }; 62DE9C2CEA5B44E24BB2D6F5 /* PhotoCollectionFetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE90B754DC827D73A31B9E /* PhotoCollectionFetching.swift */; }; 62DE9C82855405B5A0BA6B57 /* FeedCellConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE970B2B651FC6FDE03FC1 /* FeedCellConfigurator.swift */; }; + 62DE9C8CF78EA38A820A8862 /* FullImagePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE94B072B2C7ABF725A761 /* FullImagePresenter.swift */; }; 62DE9CACADCC0438B0091A5C /* FeedCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE98ED3E76A320E0BF36D3 /* FeedCellView.swift */; }; 62DE9CD65849E14B67A5AF42 /* PhotoParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9031E5EEBEA8EBA4D644 /* PhotoParameters.swift */; }; 62DE9CE6366FD6ECE740C55C /* FlickrCollectionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C90249CCE2FAF835CF5 /* FlickrCollectionFetcher.swift */; }; 62DE9D29B400373915DB581A /* FeedViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE925A929D79068D7BE7CB /* FeedViewDataSource.swift */; }; + 62DE9D3B49411AE049F480B6 /* FullImageInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE97F7CB8E1F85FFC0EEEC /* FullImageInteractorTests.swift */; }; 62DE9D8224882F3BAB4798A5 /* Routing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9506DAADD74BB93C3549 /* Routing.swift */; }; 62DE9E63C0B530594A5B150B /* Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9C8D791DA5418ED32FCF /* Style.swift */; }; 62DE9EBE33EDC3D97D929FF2 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE970A3FD09743A03DAF33 /* Photo.swift */; }; 62DE9EC2FE48A5A204531EEA /* HttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE93C78893DAA0E46DBD6A /* HttpClient.swift */; }; + 62DE9EF2BE05068AD50ACC44 /* FullImageInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9A6628BCBFFE088E4420 /* FullImageInteractor.swift */; }; 62DE9F3410EB4A39040BBC55 /* TagsLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE940D06ADFF0BDAC17ADC /* TagsLabel.swift */; }; 62DE9F5B37C5FE4BED753FB2 /* PhotoDataNSCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9805D6D4DBDDBAD078E2 /* PhotoDataNSCache.swift */; }; 62DE9F69E85302202E6884B3 /* PhotoDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9FF6B55060E5204AC907 /* PhotoDataProvider.swift */; }; @@ -94,6 +99,7 @@ 62DE944604E007DD383ABDE1 /* MockFeedPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFeedPresenter.swift; sourceTree = ""; }; 62DE94A9AA78F83D54078368 /* RootDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootDependencies.swift; sourceTree = ""; }; 62DE94AB83D36878355DAA8C /* FlickrCollectionDataFetching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionDataFetching.swift; sourceTree = ""; }; + 62DE94B072B2C7ABF725A761 /* FullImagePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullImagePresenter.swift; sourceTree = ""; }; 62DE9506DAADD74BB93C3549 /* Routing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Routing.swift; sourceTree = ""; }; 62DE954BE0BFF3D2EE91DE12 /* FlickrPhotosService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotosService.swift; sourceTree = ""; }; 62DE95E529D91B15DE6E0C58 /* FullImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullImageViewController.swift; sourceTree = ""; }; @@ -107,6 +113,7 @@ 62DE97953D19EE0C13151316 /* FlickrPhotoServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotoServiceTests.swift; sourceTree = ""; }; 62DE97BAB29B5E48A31A7687 /* MockFlickrCollectionDataFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFlickrCollectionDataFetcher.swift; sourceTree = ""; }; 62DE97EF27BCB2A06EB35FDF /* ViewsLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewsLabel.swift; sourceTree = ""; }; + 62DE97F7CB8E1F85FFC0EEEC /* FullImageInteractorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullImageInteractorTests.swift; sourceTree = ""; }; 62DE9805D6D4DBDDBAD078E2 /* PhotoDataNSCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataNSCache.swift; sourceTree = ""; }; 62DE9810EDB0220E74893A89 /* MockFeedDisplaying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFeedDisplaying.swift; sourceTree = ""; }; 62DE986497C5FB223DA10256 /* FlickrPhotoURLResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotoURLResolver.swift; sourceTree = ""; }; @@ -120,6 +127,7 @@ 62DE99F1F322B9F470256B1E /* PhotoDataRetrieving.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataRetrieving.swift; sourceTree = ""; }; 62DE9A0F5F5E76B6AFE7159B /* PhotoCollectionFetcherFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionFetcherFactory.swift; sourceTree = ""; }; 62DE9A2EBADCA5EE9059C3DD /* FlickrCollectionFetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcherTests.swift; sourceTree = ""; }; + 62DE9A6628BCBFFE088E4420 /* FullImageInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullImageInteractor.swift; sourceTree = ""; }; 62DE9B49CABD228CCFBCE936 /* PhotoDataCaching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataCaching.swift; sourceTree = ""; }; 62DE9B8BC72A4AB8A5A34B65 /* FlickrCollectionFetcherFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcherFactory.swift; sourceTree = ""; }; 62DE9BF2A2C0391D9FFA480B /* FeedRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedRouter.swift; sourceTree = ""; }; @@ -131,8 +139,10 @@ 62DE9D790ECF920DF2C9802B /* FlickrApiURLBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrApiURLBuilder.swift; sourceTree = ""; }; 62DE9DB2850E842E22C4CC84 /* FeedInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedInteractor.swift; sourceTree = ""; }; 62DE9E19BC1900DC8648FD5E /* MockHttpClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockHttpClient.swift; sourceTree = ""; }; + 62DE9EA282C378A8A45AC310 /* MockFullImagePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockFullImagePresenter.swift; sourceTree = ""; }; 62DE9EA9D6ACC68E32F8A681 /* FeedInteractorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedInteractorTests.swift; sourceTree = ""; }; 62DE9EB1763D78889BAD9753 /* FullImageFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullImageFactory.swift; sourceTree = ""; }; + 62DE9F51F8C74F47616FAA4A /* FullImageModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullImageModels.swift; sourceTree = ""; }; 62DE9FF6B55060E5204AC907 /* PhotoDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataProvider.swift; sourceTree = ""; }; 944AC01A23CE0E9C009FD611 /* SimpleFlickrBrowser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimpleFlickrBrowser.app; sourceTree = BUILT_PRODUCTS_DIR; }; 944AC01D23CE0E9C009FD611 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -183,6 +193,7 @@ isa = PBXGroup; children = ( 62DE983292E5D66368CF497E /* Feed */, + 62DE9A45435D656998BEDE7A /* FullImage */, ); path = Scenes; sourceTree = ""; @@ -235,6 +246,9 @@ children = ( 62DE95E529D91B15DE6E0C58 /* FullImageViewController.swift */, 62DE9EB1763D78889BAD9753 /* FullImageFactory.swift */, + 62DE9A6628BCBFFE088E4420 /* FullImageInteractor.swift */, + 62DE94B072B2C7ABF725A761 /* FullImagePresenter.swift */, + 62DE9F51F8C74F47616FAA4A /* FullImageModels.swift */, ); path = FullImage; sourceTree = ""; @@ -344,6 +358,15 @@ path = Photos; sourceTree = ""; }; + 62DE9A45435D656998BEDE7A /* FullImage */ = { + isa = PBXGroup; + children = ( + 62DE97F7CB8E1F85FFC0EEEC /* FullImageInteractorTests.swift */, + 62DE9EA282C378A8A45AC310 /* MockFullImagePresenter.swift */, + ); + path = FullImage; + sourceTree = ""; + }; 62DE9AABA2EDCDF4B92072F7 /* Extensions */ = { isa = PBXGroup; children = ( @@ -644,6 +667,9 @@ 62DE92CC1AB4CFA092F2DB10 /* FullImageViewController.swift in Sources */, 62DE924525362E9212DE0671 /* FullImageFactory.swift in Sources */, 62DE940CB3E25C0A9AEFE252 /* FeedCellFactory.swift in Sources */, + 62DE9EF2BE05068AD50ACC44 /* FullImageInteractor.swift in Sources */, + 62DE9C8CF78EA38A820A8862 /* FullImagePresenter.swift in Sources */, + 62DE9BC9A5B77235063C5BCE /* FullImageModels.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -667,6 +693,8 @@ 62DE946CFE4B0C1C5B9E1324 /* MockFeedDisplaying.swift in Sources */, 62DE9ADB8EF31EA3CD79A2B6 /* FlickrCollectionDataFetcherTests.swift in Sources */, 62DE9F6D54B5EA2DA23BED9C /* MockFlickrCollectionDataFetcher.swift in Sources */, + 62DE9D3B49411AE049F480B6 /* FullImageInteractorTests.swift in Sources */, + 62DE90247F9DE029191EAEB0 /* MockFullImagePresenter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift index 51ca21d..8f6638c 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift @@ -18,8 +18,9 @@ final class RootDependencies { func createFeedViewController() -> FeedViewController { let feedCellFactory = FeedCellFactory() let feedFactory = FeedFactory(feedCellFactory: feedCellFactory) - + return feedFactory.createViewController( + photoDataProvider: photoDataProvider, photoCollectionFetcher: photoCollectionFetcher ) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift index ee48209..614a028 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift @@ -26,13 +26,17 @@ final class FeedViewCell: UITableViewCell { static let identifier = "\(FeedViewCell.self)" var router: FeedRouting? + var photo: Photo? private var imageHeightConstraint: NSLayoutConstraint? // MARK: - Subviews lazy var cellImageView: UIImageView = { let view = UIImageView() + view.contentMode = .scaleAspectFit + view.isUserInteractionEnabled = true + view.addGestureRecognizer(imageGestureRecognizer) return view }() @@ -71,6 +75,14 @@ final class FeedViewCell: UITableViewCell { return view }() + lazy var imageGestureRecognizer: UITapGestureRecognizer = { + let gesture = UITapGestureRecognizer(target: self, action: #selector(onImageTapped)) + + gesture.numberOfTapsRequired = 1 + + return gesture + }() + // MARK: - Init override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -155,19 +167,26 @@ final class FeedViewCell: UITableViewCell { imageHeightConstraint?.priority = .defaultHigh imageHeightConstraint?.isActive = true } + + @objc private func onImageTapped() { + guard let router = router, let photo = photo else { + return + } + + router.displayFullImage(withInfoFrom: photo) + } } // MARK: - FeedViewCellPhotoDisplaying extension FeedViewCell: FeedViewCellPhotoDisplaying { func display(photo: Photo) { + self.photo = photo + let metadata = photo.metadata ownerNameView.text = metadata.ownerName - dateTakenView.set(date: metadata.dateTaken) - viewsView.set(views: metadata.views) - tagsView.set(tags: metadata.tags) cellImageView.image = UIImage(data: photo.imageData) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift index 29d3f86..516cd40 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift @@ -4,7 +4,7 @@ // Copyright (c) 2020 rencevio. All rights reserved. protocol FeedCreating { - func createViewController(photoCollectionFetcher: PhotoCollectionFetching) -> FeedViewController + func createViewController(photoDataProvider: PhotoDataProviding, photoCollectionFetcher: PhotoCollectionFetching) -> FeedViewController } final class FeedFactory: FeedCreating { @@ -14,10 +14,10 @@ final class FeedFactory: FeedCreating { self.feedCellFactory = feedCellFactory } - func createViewController(photoCollectionFetcher: PhotoCollectionFetching) -> FeedViewController { + func createViewController(photoDataProvider: PhotoDataProviding, photoCollectionFetcher: PhotoCollectionFetching) -> FeedViewController { let presenter = FeedPresenter() let interactor = FeedInteractor(presenter: presenter, photoCollectionFetcher: photoCollectionFetcher) - let router = createRouter() + var router = createRouter(photoDataProvider: photoDataProvider) let dataSource = createDataSource(router: router) let viewController = FeedViewController( @@ -27,6 +27,7 @@ final class FeedFactory: FeedCreating { ) presenter.view = viewController + router.sourceVC = viewController return viewController } @@ -37,8 +38,8 @@ final class FeedFactory: FeedCreating { return FeedViewDataSource(feedCellConfigurator: configurator) } - private func createRouter() -> FeedRouting { - let fullImageFactory = FullImageFactory() + private func createRouter(photoDataProvider: PhotoDataProviding) -> FeedRouting { + let fullImageFactory = FullImageFactory(photoDataProvider: photoDataProvider) return FeedRouter(fullImageFactory: fullImageFactory) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift index d783d85..eec01fe 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift @@ -3,8 +3,6 @@ // // Copyright (c) 2020 rencevio. All rights reserved. -import UIKit.UIImage - struct FeedModels { struct Photos { struct Request: Equatable { diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift index d201fb3..4fe0dc5 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift @@ -6,7 +6,7 @@ import UIKit protocol FeedRouting: Routing { - func displayFullImage(withInfo info: Photo) + func displayFullImage(withInfoFrom photo: Photo) } final class FeedRouter: FeedRouting { @@ -18,12 +18,12 @@ final class FeedRouter: FeedRouting { self.fullImageFactory = fullImageFactory } - func displayFullImage(withInfo info: Photo) { + func displayFullImage(withInfoFrom photo: Photo) { guard let sourceVC = sourceVC else { return } - let fullImageViewController = fullImageFactory.createViewController() + let fullImageViewController = fullImageFactory.createViewController(withInfoFrom: photo) sourceVC.present(fullImageViewController, animated: true) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageFactory.swift index 0abcbf7..6f1f902 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageFactory.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageFactory.swift @@ -4,11 +4,24 @@ // Copyright (c) 2020 rencevio. All rights reserved. protocol FullImageCreating { - func createViewController() -> FullImageViewController + func createViewController(withInfoFrom photo: Photo) -> FullImageViewController } final class FullImageFactory: FullImageCreating { - func createViewController() -> FullImageViewController { - FullImageViewController() + private let photoDataProvider: PhotoDataProviding + + init(photoDataProvider: PhotoDataProviding) { + self.photoDataProvider = photoDataProvider + } + + func createViewController(withInfoFrom photo: Photo) -> FullImageViewController { + let presenter = FullImagePresenter() + let interactor = FullImageInteractor(presenter: presenter, photoDataProvider: photoDataProvider) + + let viewController = FullImageViewController(photo: photo, interactor: interactor) + + presenter.view = viewController + + return viewController } } \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageInteractor.swift new file mode 100644 index 0000000..001f288 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageInteractor.swift @@ -0,0 +1,37 @@ +// +// Created by Maxim Berezhnoy on 29/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import Foundation + +protocol FullImageInteracting { + func fetch(image: FullImageModels.Image.Request) +} + +final class FullImageInteractor: FullImageInteracting { + private let presenter: FullImagePresenting + private let photoDataProvider: PhotoDataProviding + + init(presenter: FullImagePresenting, photoDataProvider: PhotoDataProviding) { + self.presenter = presenter + self.photoDataProvider = photoDataProvider + } + + func fetch(image: FullImageModels.Image.Request) { + photoDataProvider.getPhotoData(from: image.url) { result in + DispatchQueue.main.async { [weak self] in + guard let self = self else { + return + } + + switch result { + case let .success(data): + self.presenter.present(image: FullImageModels.Image.Response(image: data)) + case .failure(_): + self.presenter.presentError() + } + } + } + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageModels.swift new file mode 100644 index 0000000..a4011ab --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageModels.swift @@ -0,0 +1,22 @@ +// +// Created by Maxim Berezhnoy on 29/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import Foundation.NSData + +struct FullImageModels { + struct Image { + struct Request { + let url: URL + } + + struct Response { + let image: Data + } + + struct ViewModel { + let image: Data + } + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImagePresenter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImagePresenter.swift new file mode 100644 index 0000000..d30ea18 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImagePresenter.swift @@ -0,0 +1,21 @@ +// +// Created by Maxim Berezhnoy on 29/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +protocol FullImagePresenting { + func present(image: FullImageModels.Image.Response) + func presentError() +} + +final class FullImagePresenter: FullImagePresenting { + weak var view: FullImageDisplaying? + + func present(image: FullImageModels.Image.Response) { + view?.display(image: FullImageModels.Image.ViewModel(image: image.image)) + } + + func presentError() { + view?.displayError() + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift index 974fde3..3288911 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift @@ -5,6 +5,110 @@ import UIKit +protocol FullImageDisplaying: AnyObject { + func display(image: FullImageModels.Image.ViewModel) + func displayError() +} + final class FullImageViewController: UIViewController { + private let interactor: FullImageInteracting + private let photo: Photo + + private enum State { + case loading + case image(image: UIImage) + case error + } + + private lazy var imageView: UIImageView = { + let view = UIImageView() + + view.clipsToBounds = true + view.contentMode = .scaleAspectFit + + return view + }() + + private lazy var loadingIndicator: UIActivityIndicatorView = { + let indicator = UIActivityIndicatorView() + + indicator.hidesWhenStopped = true + indicator.color = Style.FullImage.LoadingIndicator.color + + return indicator + }() + + init(photo: Photo, interactor: FullImageInteracting) { + self.photo = photo + self.interactor = interactor + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = Style.FullImage.Background.color + + setupImageView() + setupLoadingIndicator() + + set(state: .loading) + interactor.fetch(image: FullImageModels.Image.Request(url: photo.fullSizeImageURL)) + } + + // MARK: - UI Setup + private func setupImageView() { + view.addSubview(imageView) + + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + imageView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true + imageView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true + } + + private func setupLoadingIndicator() { + view.addSubview(loadingIndicator) + + loadingIndicator.translatesAutoresizingMaskIntoConstraints = false + loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + } + + private func set(state: State) { + switch state { + case .loading: + loadingIndicator.startAnimating() + imageView.isHidden = true + case let .image(image): + loadingIndicator.stopAnimating() + imageView.isHidden = false + imageView.image = image + case .error: + loadingIndicator.stopAnimating() + imageView.isHidden = true + } + } +} + +// MARK: - FullImageDisplaying + +extension FullImageViewController: FullImageDisplaying { + func display(image: FullImageModels.Image.ViewModel) { + if let image = UIImage(data: image.image) { + set(state: .image(image: image)) + } else { + set(state: .error) + } + } + + func displayError() { + set(state: .error) + } } \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Style.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Style.swift index 3eb473b..dcff488 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Style.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Style.swift @@ -9,4 +9,14 @@ struct Style { struct ScreenBackground { static let color = UIColor(red: 0.93, green: 0.94, blue: 0.95, alpha: 1.0) } + + struct FullImage { + struct LoadingIndicator { + static let color = UIColor(red: 0.74, green: 0.76, blue: 0.78, alpha: 1.0) + } + + struct Background { + static let color = UIColor(red: 0.93, green: 0.94, blue: 0.95, alpha: 1.0) + } + } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/FullImage/FullImageInteractorTests.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/FullImage/FullImageInteractorTests.swift new file mode 100644 index 0000000..ccf01c6 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/FullImage/FullImageInteractorTests.swift @@ -0,0 +1,59 @@ +// +// Created by Maxim Berezhnoy on 29/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +@testable import SimpleFlickrBrowser + +import Foundation +import XCTest + +class BrowserCellInteractorTests: XCTestCase { + var presenter: MockFullImagePresenter! + var photoDataProvider: MockPhotoDataProvider! + var sut: FullImageInteractor! + + override func setUp() { + super.setUp() + + presenter = MockFullImagePresenter() + photoDataProvider = MockPhotoDataProvider() + + sut = FullImageInteractor(presenter: presenter, photoDataProvider: photoDataProvider) + } + + func test_fetchImage_retrievesFromProvider_success_sendsDataToPresenter() { + let url = URL(string: "www.not-a-website.com")! + let photoData = Data() + + photoDataProvider.getPhotoDataResults = [.success(photoData)] + + sut.fetch(image: FullImageModels.Image.Request(url: url)) + + awaitMainQueueResolution() + + XCTAssertEqual(photoDataProvider.getPhotoDataCalls, [url]) + + XCTAssertEqual(presenter.presentImageCalls.map { $0.image }, [photoData]) + XCTAssertEqual(presenter.presentErrorCalls, 0) + } + + func test_fetchImage_retrievesFromProvider_failure_sendsErrorToPresenter() { + let url = URL(string: "www.not-a-website.com")! + + enum RetrievalError: Error { + case undefined + } + + photoDataProvider.getPhotoDataResults = [.failure(RetrievalError.undefined)] + + sut.fetch(image: FullImageModels.Image.Request(url: url)) + + awaitMainQueueResolution() + + XCTAssertEqual(photoDataProvider.getPhotoDataCalls, [url]) + + XCTAssertEqual(presenter.presentImageCalls.count, 0) + XCTAssertEqual(presenter.presentErrorCalls, 1) + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/FullImage/MockFullImagePresenter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/FullImage/MockFullImagePresenter.swift new file mode 100644 index 0000000..b541558 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/FullImage/MockFullImagePresenter.swift @@ -0,0 +1,21 @@ +// +// Created by Maxim Berezhnoy on 29/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +@testable import SimpleFlickrBrowser + +import Foundation + +final class MockFullImagePresenter: FullImagePresenting { + var presentImageCalls = [FullImageModels.Image.Response]() + var presentErrorCalls = 0 + + func present(image: FullImageModels.Image.Response) { + presentImageCalls.append(image) + } + + func presentError() { + presentErrorCalls += 1 + } +} From a53d19cb8ccc2658a9d7f2474a2cc2f16b76e370 Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Sun, 29 Nov 2020 23:03:28 +0100 Subject: [PATCH 12/16] Added BackgroundURLSession for downloading images in background; full size image view now utilizes the background session --- .../project.pbxproj | 8 ++ .../SimpleFlickrBrowser/AppDelegate.swift | 5 +- .../Network/BackgroundURLSession.swift | 117 ++++++++++++++++++ .../Network/HttpClient.swift | 11 +- .../Network/NetworkSession.swift | 20 +++ .../Feed/FullImage/FullImageInteractor.swift | 2 +- .../Photos/FlickrPhotosService.swift | 2 +- .../Flickr/FlickrCollectionDataFetcher.swift | 2 +- .../PhotoDataProvider/PhotoDataProvider.swift | 10 +- .../NetworkPhotoDataRetriever.swift | 4 +- .../PhotoDataRetrieving.swift | 2 +- .../Network/MockHttpClient.swift | 2 +- .../MockPhotoDataProvider.swift | 2 +- .../PhotoDataProviderTests.swift | 4 +- .../MockPhotoDataRetriever.swift | 2 +- 15 files changed, 171 insertions(+), 22 deletions(-) create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Network/BackgroundURLSession.swift create mode 100644 SimpleFlickrBrowser/SimpleFlickrBrowser/Network/NetworkSession.swift diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj index bb4c2eb..7ed3b8e 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser.xcodeproj/project.pbxproj @@ -9,11 +9,13 @@ /* Begin PBXBuildFile section */ 62DE90247F9DE029191EAEB0 /* MockFullImagePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9EA282C378A8A45AC310 /* MockFullImagePresenter.swift */; }; 62DE90E73B9BFCA4A54B76C2 /* MockPhotoDataRetriever.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE96B5EBB14213DC9E7804 /* MockPhotoDataRetriever.swift */; }; + 62DE90F215A6D5285CDB3D6F /* BackgroundURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9B27E27EA0257A2A7F3D /* BackgroundURLSession.swift */; }; 62DE90F40E919CCFF876D1A6 /* MockFlickrPhotoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9225FFEBB4968F60A987 /* MockFlickrPhotoService.swift */; }; 62DE911E1EB4D78BCA9085CB /* FlickrCollectionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9A2EBADCA5EE9059C3DD /* FlickrCollectionFetcherTests.swift */; }; 62DE91F2D0080AF13460E47C /* FeedInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9DB2850E842E22C4CC84 /* FeedInteractor.swift */; }; 62DE91F96D06BDB6AE8D40A1 /* XCTestCase+QueueAwait.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE924CEDF37B8D5BC86D48 /* XCTestCase+QueueAwait.swift */; }; 62DE924525362E9212DE0671 /* FullImageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9EB1763D78889BAD9753 /* FullImageFactory.swift */; }; + 62DE925F04793F3F2CCF355A /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE964E7B317C90B5B5DE7B /* NetworkSession.swift */; }; 62DE92A1A0D21DCC9FC97F7E /* NetworkPhotoDataRetriever.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE9868FFC49BF89E9F9192 /* NetworkPhotoDataRetriever.swift */; }; 62DE92CC1AB4CFA092F2DB10 /* FullImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE95E529D91B15DE6E0C58 /* FullImageViewController.swift */; }; 62DE92DECC87D7DACFB69CC9 /* PhotoDataRetrieving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DE99F1F322B9F470256B1E /* PhotoDataRetrieving.swift */; }; @@ -105,6 +107,7 @@ 62DE95E529D91B15DE6E0C58 /* FullImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullImageViewController.swift; sourceTree = ""; }; 62DE96216E4D360CAB68AF75 /* FeedFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedFactory.swift; sourceTree = ""; }; 62DE963F12E663C923326C82 /* DateLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateLabel.swift; sourceTree = ""; }; + 62DE964E7B317C90B5B5DE7B /* NetworkSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkSession.swift; sourceTree = ""; }; 62DE9673FC376E2BC90087D6 /* FeedPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedPresenter.swift; sourceTree = ""; }; 62DE96B5EBB14213DC9E7804 /* MockPhotoDataRetriever.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPhotoDataRetriever.swift; sourceTree = ""; }; 62DE96CE6B44327361D73D24 /* FlickrPhotos.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrPhotos.swift; sourceTree = ""; }; @@ -128,6 +131,7 @@ 62DE9A0F5F5E76B6AFE7159B /* PhotoCollectionFetcherFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionFetcherFactory.swift; sourceTree = ""; }; 62DE9A2EBADCA5EE9059C3DD /* FlickrCollectionFetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcherTests.swift; sourceTree = ""; }; 62DE9A6628BCBFFE088E4420 /* FullImageInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullImageInteractor.swift; sourceTree = ""; }; + 62DE9B27E27EA0257A2A7F3D /* BackgroundURLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundURLSession.swift; sourceTree = ""; }; 62DE9B49CABD228CCFBCE936 /* PhotoDataCaching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDataCaching.swift; sourceTree = ""; }; 62DE9B8BC72A4AB8A5A34B65 /* FlickrCollectionFetcherFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlickrCollectionFetcherFactory.swift; sourceTree = ""; }; 62DE9BF2A2C0391D9FFA480B /* FeedRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedRouter.swift; sourceTree = ""; }; @@ -387,6 +391,8 @@ isa = PBXGroup; children = ( 62DE93C78893DAA0E46DBD6A /* HttpClient.swift */, + 62DE9B27E27EA0257A2A7F3D /* BackgroundURLSession.swift */, + 62DE964E7B317C90B5B5DE7B /* NetworkSession.swift */, ); path = Network; sourceTree = ""; @@ -670,6 +676,8 @@ 62DE9EF2BE05068AD50ACC44 /* FullImageInteractor.swift in Sources */, 62DE9C8CF78EA38A820A8862 /* FullImagePresenter.swift in Sources */, 62DE9BC9A5B77235063C5BCE /* FullImageModels.swift in Sources */, + 62DE90F215A6D5285CDB3D6F /* BackgroundURLSession.swift in Sources */, + 62DE925F04793F3F2CCF355A /* NetworkSession.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/AppDelegate.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/AppDelegate.swift index 81d1065..f65660f 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/AppDelegate.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/AppDelegate.swift @@ -11,7 +11,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let dependencies = RootDependencies() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { -// let viewController = dependencies.createBrowserViewController() let viewController = dependencies.createFeedViewController() let navigationController = UINavigationController(rootViewController: viewController) @@ -22,4 +21,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + + func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> ()) { + BackgroundURLSession.shared.set(onBackgroundEventsHandledCallback: completionHandler) + } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/BackgroundURLSession.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/BackgroundURLSession.swift new file mode 100644 index 0000000..85831d2 --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/BackgroundURLSession.swift @@ -0,0 +1,117 @@ +// +// Created by Maxim Berezhnoy on 29/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import Foundation + +extension URLSession { + static let sharedBackground = BackgroundURLSession.shared +} + +final class BackgroundURLSession { + static let shared = BackgroundURLSession() + + private var onBackgroundEventsHandled: (() -> Void)? + + private let operatingQueue = DispatchQueue(label: "\(BackgroundURLSession.self)OperatingQueue") + + private let identifier = "\(BackgroundURLSession.self)Identifier" + + private var runningTasks = [URLSessionTask: Completion]() + + private lazy var session: URLSession = { + let configuration = URLSessionConfiguration.background(withIdentifier: identifier) + + return URLSession(configuration: configuration, delegate: sessionDelegate, delegateQueue: nil) + }() + + private lazy var sessionDelegate = { + BackgroundURLSessionDelegate(onTaskFinished: onTaskFinished(), onAllTasksFinished: onAllTasksFinished()) + }() + + private init() {} + + func set(onBackgroundEventsHandledCallback: @escaping () -> Void) { + DispatchQueue.main.async { [weak self] in + self?.onBackgroundEventsHandled = onBackgroundEventsHandledCallback + } + } + + private func onTaskFinished() -> (URLSessionTask, Data?, Error?) -> Void { + { [weak self] task, data, error in + self?.operatingQueue.async { [weak self] in + guard let self = self, let taskCallback = self.runningTasks[task] else { + return + } + + self.runningTasks.removeValue(forKey: task) + + taskCallback(data, error) + } + } + } + + private func onAllTasksFinished() -> () -> Void { + { + DispatchQueue.main.async { [weak self] in + self?.onBackgroundEventsHandled?() + self?.onBackgroundEventsHandled = nil + } + } + } +} + +// MARK: - NetworkSession + +extension BackgroundURLSession: NetworkSession { + func getData(from url: URL, _ completion: @escaping Completion) { + let task = session.downloadTask(with: url) + + operatingQueue.async { [weak self] in + self?.runningTasks[task] = completion + } + + task.resume() + } +} + +private final class BackgroundURLSessionDelegate: NSObject { + private let onTaskFinished: (URLSessionTask, Data?, Error?) -> Void + private let onAllTasksFinished: () -> Void + + init(onTaskFinished: @escaping (URLSessionTask, Data?, Error?) -> Void, onAllTasksFinished: @escaping () -> Void) { + self.onTaskFinished = onTaskFinished + self.onAllTasksFinished = onAllTasksFinished + } +} + +// MARK: - URLSessionDelegate + +extension BackgroundURLSessionDelegate: URLSessionDelegate { + func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { + onAllTasksFinished() + } +} + +// MARK: - URLSessionTaskDelegate + +extension BackgroundURLSessionDelegate: URLSessionTaskDelegate { + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + onTaskFinished(task, nil, error) + } +} + +// MARK: - URLSessionDownloadDelegate + +extension BackgroundURLSessionDelegate: URLSessionDownloadDelegate { + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + do { + let data = try Data(contentsOf: location) + + onTaskFinished(downloadTask, data, nil) + } catch { + onTaskFinished(downloadTask, nil, error) + } + } +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/HttpClient.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/HttpClient.swift index 86ea1d1..3b97038 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/HttpClient.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/HttpClient.swift @@ -7,7 +7,7 @@ import Foundation protocol HttpCommunicator { typealias Completion = (Result) -> Void - func get(url: URL, completion: @escaping Completion) + func get(url: URL, background: Bool, completion: @escaping Completion) } enum Http { @@ -18,9 +18,12 @@ enum Http { final class Client: HttpCommunicator { private let urlSession = URLSession.shared + private let backgroundUrlSession = URLSession.sharedBackground - func get(url: URL, completion: @escaping Completion) { - let task = urlSession.dataTask(with: url) { data, _, error in + func get(url: URL, background: Bool, completion: @escaping Completion) { + let session: NetworkSession = background ? backgroundUrlSession : urlSession + + session.getData(from: url) { data, error in guard let data = data else { if let error = error { completion(.failure(.httpError(error))) @@ -33,8 +36,6 @@ enum Http { completion(.success(data)) } - - task.resume() } } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/NetworkSession.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/NetworkSession.swift new file mode 100644 index 0000000..845a99c --- /dev/null +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/NetworkSession.swift @@ -0,0 +1,20 @@ +// +// Created by Maxim Berezhnoy on 29/11/2020. +// +// Copyright (c) 2020 rencevio. All rights reserved. + +import Foundation + +protocol NetworkSession { + typealias Completion = (Data?, Error?) -> Void + + func getData(from url: URL, _ completion: @escaping Completion) +} + +extension URLSession: NetworkSession { + func getData(from url: URL, _ completion: @escaping Completion) { + dataTask(with: url) { data, _, error in + completion(data, error) + }.resume() + } +} \ No newline at end of file diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageInteractor.swift index 001f288..261de91 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageInteractor.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageInteractor.swift @@ -19,7 +19,7 @@ final class FullImageInteractor: FullImageInteracting { } func fetch(image: FullImageModels.Image.Request) { - photoDataProvider.getPhotoData(from: image.url) { result in + photoDataProvider.getPhotoData(from: image.url, backgroundDownload: true) { result in DispatchQueue.main.async { [weak self] in guard let self = self else { return diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift index 1a81f3a..4c81ae5 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift @@ -35,7 +35,7 @@ final class FlickrPhotosService: FlickrPhotosFetching { } private func fetchFrom(url: URL, completion: @escaping Completion) { - httpClient.get(url: url) { [weak self] result in + httpClient.get(url: url, background: false) { [weak self] result in guard let self = self else { return } let photosResponse = self.parse(response: result) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcher.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcher.swift index 047594a..a9dc9fc 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcher.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcher.swift @@ -25,7 +25,7 @@ final class FlickrCollectionDataFetcher: FlickrCollectionDataFetching { photos.forEach { photo in let imageURL = FlickrPhotoURLResolver.resolveUrl(for: photo, withSize: size) - dataProvider.getPhotoData(from: imageURL) { [operatingQueue] result in + dataProvider.getPhotoData(from: imageURL, backgroundDownload: false) { [operatingQueue] result in operatingQueue.async { switch result { case let .success(data): diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoDataProvider/PhotoDataProvider.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoDataProvider/PhotoDataProvider.swift index 5e25712..c0d6995 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoDataProvider/PhotoDataProvider.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoDataProvider/PhotoDataProvider.swift @@ -8,7 +8,7 @@ import Foundation protocol PhotoDataProviding { typealias Completion = (Result) -> Void - func getPhotoData(from url: URL, completion: @escaping Completion) + func getPhotoData(from url: URL, backgroundDownload: Bool, _ completion: @escaping Completion) } final class PhotoDataProvider: PhotoDataProviding { @@ -22,7 +22,7 @@ final class PhotoDataProvider: PhotoDataProviding { self.retriever = retriever } - func getPhotoData(from url: URL, completion: @escaping Completion) { + func getPhotoData(from url: URL, backgroundDownload: Bool, _ completion: @escaping Completion) { operatingQueue.async { [weak self] in guard let self = self else { return } @@ -32,13 +32,13 @@ final class PhotoDataProvider: PhotoDataProviding { case let .success(data): completion(.success(data)) case .failure: - self.retrievePhotoData(from: url, completion: completion) + self.retrievePhotoData(from: url, backgroundDownload: backgroundDownload, completion: completion) } } } - private func retrievePhotoData(from url: URL, completion: @escaping Completion) { - retriever.retrieve(from: url) { [weak cache] result in + private func retrievePhotoData(from url: URL, backgroundDownload: Bool, completion: @escaping Completion) { + retriever.retrieve(from: url, backgroundDownload: backgroundDownload) { [weak cache] result in switch result { case let .success(data): cache?.store(data: data, for: url) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoDataProvider/PhotoDataRetriever/NetworkPhotoDataRetriever.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoDataProvider/PhotoDataRetriever/NetworkPhotoDataRetriever.swift index 97d8ac2..c243d09 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoDataProvider/PhotoDataRetriever/NetworkPhotoDataRetriever.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoDataProvider/PhotoDataRetriever/NetworkPhotoDataRetriever.swift @@ -12,8 +12,8 @@ final class NetworkPhotoDataRetriever: PhotoDataRetrieving { self.httpClient = httpClient } - func retrieve(from url: URL, completion: @escaping Completion) { - httpClient.get(url: url) { result in + func retrieve(from url: URL, backgroundDownload: Bool, completion: @escaping Completion) { + httpClient.get(url: url, background: backgroundDownload) { result in switch result { case let .success(data): completion(.success(data)) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoDataProvider/PhotoDataRetriever/PhotoDataRetrieving.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoDataProvider/PhotoDataRetriever/PhotoDataRetrieving.swift index 9650248..1265ba4 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoDataProvider/PhotoDataRetriever/PhotoDataRetrieving.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoDataProvider/PhotoDataRetriever/PhotoDataRetrieving.swift @@ -8,5 +8,5 @@ import Foundation protocol PhotoDataRetrieving { typealias Completion = (Result) -> Void - func retrieve(from url: URL, completion: @escaping Completion) + func retrieve(from url: URL, backgroundDownload: Bool, completion: @escaping Completion) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Network/MockHttpClient.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Network/MockHttpClient.swift index 6ad5d45..b47ffbe 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Network/MockHttpClient.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Network/MockHttpClient.swift @@ -11,7 +11,7 @@ class MockHttpClient: HttpCommunicator { var getCalls = [URL]() var getResult: Result! - func get(url: URL, completion: @escaping Completion) { + func get(url: URL, background: Bool, completion: @escaping Completion) { guard let result = getResult else { fatalError("\(#function) expectation was not set") } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/MockPhotoDataProvider.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/MockPhotoDataProvider.swift index 81bd37b..5ccb3e1 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/MockPhotoDataProvider.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/MockPhotoDataProvider.swift @@ -11,7 +11,7 @@ final class MockPhotoDataProvider: PhotoDataProviding { var getPhotoDataCalls = [URL]() var getPhotoDataResults = [Result]() - func getPhotoData(from url: URL, completion: @escaping Completion) { + func getPhotoData(from url: URL, backgroundDownload: Bool, _ completion: @escaping Completion) { guard !getPhotoDataResults.isEmpty else { fatalError("\(#function) expectation was not set") } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/PhotoDataProviderTests.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/PhotoDataProviderTests.swift index c7322f2..544ca45 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/PhotoDataProviderTests.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/PhotoDataProviderTests.swift @@ -30,7 +30,7 @@ class PhotoDataProviderTests: XCTestCase { let fetchExpectation = XCTestExpectation(description: "Fetching photo") - sut.getPhotoData(from: url) { result in + sut.getPhotoData(from: url, backgroundDownload: false) { result in switch result { case let .success(data): XCTAssertEqual(cachedData, data) @@ -59,7 +59,7 @@ class PhotoDataProviderTests: XCTestCase { let fetchExpectation = XCTestExpectation(description: "Fetching photo") - sut.getPhotoData(from: url) { result in + sut.getPhotoData(from: url, backgroundDownload: false) { result in switch result { case let .success(data): XCTAssertEqual(retrievedData, data) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/PhotoDataRetriever/MockPhotoDataRetriever.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/PhotoDataRetriever/MockPhotoDataRetriever.swift index 30bf37a..48996de 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/PhotoDataRetriever/MockPhotoDataRetriever.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/PhotoDataRetriever/MockPhotoDataRetriever.swift @@ -11,7 +11,7 @@ class MockDataPhotoRetriever: PhotoDataRetrieving { var retrieveCalls = [URL]() var retrieveResult: Result? - func retrieve(from url: URL, completion: @escaping Completion) { + func retrieve(from url: URL, backgroundDownload: Bool, completion: @escaping Completion) { guard let result = retrieveResult else { fatalError("\(#function) expectation was not set") } From f8b11bc6aeb6cb8d4bed2228f2340daedb463539 Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Sun, 29 Nov 2020 23:15:01 +0100 Subject: [PATCH 13/16] Fixed formatting --- SimpleFlickrBrowser/Extensions/String.swift | 4 +- .../SimpleFlickrBrowser/AppDelegate.swift | 6 +- .../SimpleFlickrBrowser/Models/Photo.swift | 6 +- .../Network/BackgroundURLSession.swift | 10 ++-- .../Network/NetworkSession.swift | 4 +- .../RootDependencies.swift | 4 +- .../Feed/Cell/FeedCellConfigurator.swift | 4 +- .../Scenes/Feed/Cell/FeedCellFactory.swift | 4 +- .../Scenes/Feed/Cell/FeedCellView.swift | 25 ++++---- .../Scenes/Feed/Cell/Metadata/DateLabel.swift | 8 +-- .../Scenes/Feed/Cell/Metadata/TagsLabel.swift | 2 +- .../Feed/Cell/Metadata/ViewsLabel.swift | 2 +- .../Scenes/Feed/FeedFactory.swift | 12 ++-- .../Scenes/Feed/FeedInteractor.swift | 12 ++-- .../Scenes/Feed/FeedModels.swift | 4 +- .../Scenes/Feed/FeedRouter.swift | 8 +-- .../Scenes/Feed/FeedViewController.swift | 57 ++++++++++--------- .../Scenes/Feed/FeedViewDataSource.swift | 10 ++-- .../Feed/FullImage/FullImageFactory.swift | 2 +- .../Feed/FullImage/FullImageInteractor.swift | 4 +- .../Feed/FullImage/FullImageModels.swift | 4 +- .../Feed/FullImage/FullImagePresenter.swift | 6 +- .../FullImage/FullImageViewController.swift | 20 +++---- .../SimpleFlickrBrowser/Scenes/Style.swift | 10 ++-- .../SimpleFlickrBrowser/Utils/Routing.swift | 2 +- .../FlickrApiURLBuilder.swift | 3 +- .../FlickrApiServices/FlickrApiValues.swift | 2 +- .../Photos/FlickrPhotoURLResolver.swift | 6 +- .../Photos/FlickrPhotosService.swift | 6 +- .../Flickr/FlickrCollectionDataFetcher.swift | 15 ++--- .../Flickr/FlickrCollectionDataFetching.swift | 2 +- .../Flickr/FlickrCollectionFetcher.swift | 36 ++++++------ .../FlickrCollectionFetcherFactory.swift | 4 +- .../PhotoCollectionFetching.swift | 3 +- .../PhotoParameters.swift | 2 +- .../Photos/MockFlickrPhotoService.swift | 2 +- .../Network/MockHttpClient.swift | 2 +- .../Scenes/Feed/FeedPresenterTests.swift | 16 +++--- .../FullImage/MockFullImagePresenter.swift | 2 +- .../FlickrCollectionDataFetcherTests.swift | 4 +- .../Flickr/FlickrCollectionFetcherTests.swift | 4 +- .../MockFlickrCollectionDataFetcher.swift | 8 +-- .../MockPhotoCollectionFetcher.swift | 3 +- .../MockPhotoDataProvider.swift | 2 +- .../MockPhotoDataRetriever.swift | 2 +- 45 files changed, 182 insertions(+), 172 deletions(-) diff --git a/SimpleFlickrBrowser/Extensions/String.swift b/SimpleFlickrBrowser/Extensions/String.swift index 2406782..67f3e6f 100644 --- a/SimpleFlickrBrowser/Extensions/String.swift +++ b/SimpleFlickrBrowser/Extensions/String.swift @@ -9,8 +9,8 @@ extension String { func toInt() -> Int? { Int(self) } - + func toDate(formatter: DateFormatter) -> Date? { formatter.date(from: self) } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/AppDelegate.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/AppDelegate.swift index f65660f..5470eaf 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/AppDelegate.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/AppDelegate.swift @@ -10,9 +10,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? let dependencies = RootDependencies() - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let viewController = dependencies.createFeedViewController() - + let navigationController = UINavigationController(rootViewController: viewController) window = UIWindow(frame: UIScreen.main.bounds) @@ -22,7 +22,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } - func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> ()) { + func application(_: UIApplication, handleEventsForBackgroundURLSession _: String, completionHandler: @escaping () -> Void) { BackgroundURLSession.shared.set(onBackgroundEventsHandledCallback: completionHandler) } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift index 185992b..d23d4e6 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Models/Photo.swift @@ -3,9 +3,9 @@ // // Copyright (c) 2020 rencevio. All rights reserved. -import struct Foundation.URL -import struct Foundation.Date import struct Foundation.Data +import struct Foundation.Date +import struct Foundation.URL struct Photo { struct Metadata { @@ -14,7 +14,7 @@ struct Photo { let ownerName: String let dateTaken: Date } - + typealias ID = String let id: ID diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/BackgroundURLSession.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/BackgroundURLSession.swift index 85831d2..6875990 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/BackgroundURLSession.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/BackgroundURLSession.swift @@ -31,7 +31,7 @@ final class BackgroundURLSession { }() private init() {} - + func set(onBackgroundEventsHandledCallback: @escaping () -> Void) { DispatchQueue.main.async { [weak self] in self?.onBackgroundEventsHandled = onBackgroundEventsHandledCallback @@ -44,7 +44,7 @@ final class BackgroundURLSession { guard let self = self, let taskCallback = self.runningTasks[task] else { return } - + self.runningTasks.removeValue(forKey: task) taskCallback(data, error) @@ -89,7 +89,7 @@ private final class BackgroundURLSessionDelegate: NSObject { // MARK: - URLSessionDelegate extension BackgroundURLSessionDelegate: URLSessionDelegate { - func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { + func urlSessionDidFinishEvents(forBackgroundURLSession _: URLSession) { onAllTasksFinished() } } @@ -97,7 +97,7 @@ extension BackgroundURLSessionDelegate: URLSessionDelegate { // MARK: - URLSessionTaskDelegate extension BackgroundURLSessionDelegate: URLSessionTaskDelegate { - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { onTaskFinished(task, nil, error) } } @@ -105,7 +105,7 @@ extension BackgroundURLSessionDelegate: URLSessionTaskDelegate { // MARK: - URLSessionDownloadDelegate extension BackgroundURLSessionDelegate: URLSessionDownloadDelegate { - func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + func urlSession(_: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { do { let data = try Data(contentsOf: location) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/NetworkSession.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/NetworkSession.swift index 845a99c..05c697c 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/NetworkSession.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Network/NetworkSession.swift @@ -7,7 +7,7 @@ import Foundation protocol NetworkSession { typealias Completion = (Data?, Error?) -> Void - + func getData(from url: URL, _ completion: @escaping Completion) } @@ -17,4 +17,4 @@ extension URLSession: NetworkSession { completion(data, error) }.resume() } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift index 8f6638c..6cdaff2 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/RootDependencies.swift @@ -20,8 +20,8 @@ final class RootDependencies { let feedFactory = FeedFactory(feedCellFactory: feedCellFactory) return feedFactory.createViewController( - photoDataProvider: photoDataProvider, - photoCollectionFetcher: photoCollectionFetcher + photoDataProvider: photoDataProvider, + photoCollectionFetcher: photoCollectionFetcher ) } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellConfigurator.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellConfigurator.swift index 1a3e590..9a5a19f 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellConfigurator.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellConfigurator.swift @@ -10,8 +10,8 @@ protocol FeedCellConfiguring { final class FeedCellConfigurator: FeedCellConfiguring { private let router: FeedRouting - init(router: FeedRouting) { - self.router = router + init(router: FeedRouting) { + self.router = router } func configure(_ cell: FeedViewCell) { diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift index 817788e..91dc053 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellFactory.swift @@ -8,7 +8,7 @@ protocol FeedCellCreating { } final class FeedCellFactory: FeedCellCreating { - func createFeedCellConfigurator(feedRouter: FeedRouting) -> FeedCellConfigurator { + func createFeedCellConfigurator(feedRouter: FeedRouting) -> FeedCellConfigurator { FeedCellConfigurator(router: feedRouter) } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift index 614a028..bf2e4ac 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift @@ -5,7 +5,7 @@ import UIKit -private struct LayoutConstants { +private enum LayoutConstants { static let ownerNameFontSize: CGFloat = 12 static let tagsFontSize: CGFloat = 11 static let viewsFontSize: CGFloat = 12 @@ -24,16 +24,17 @@ protocol FeedViewCellPhotoDisplaying { final class FeedViewCell: UITableViewCell { static let identifier = "\(FeedViewCell.self)" - + var router: FeedRouting? var photo: Photo? private var imageHeightConstraint: NSLayoutConstraint? // MARK: - Subviews + lazy var cellImageView: UIImageView = { let view = UIImageView() - + view.contentMode = .scaleAspectFit view.isUserInteractionEnabled = true view.addGestureRecognizer(imageGestureRecognizer) @@ -84,6 +85,7 @@ final class FeedViewCell: UITableViewCell { }() // MARK: - Init + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -107,13 +109,15 @@ final class FeedViewCell: UITableViewCell { }() override func systemLayoutSizeFitting(_ targetSize: CGSize, - withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, - verticalFittingPriority: UILayoutPriority) -> CGSize { + withHorizontalFittingPriority _: UILayoutPriority, + verticalFittingPriority _: UILayoutPriority) -> CGSize + { viewWidthConstraint.constant = bounds.size.width return contentView.systemLayoutSizeFitting(CGSize(width: targetSize.width, height: 1)) } // MARK: - View Setup + private func setupImageView() { contentView.addSubview(cellImageView) @@ -167,29 +171,30 @@ final class FeedViewCell: UITableViewCell { imageHeightConstraint?.priority = .defaultHigh imageHeightConstraint?.isActive = true } - + @objc private func onImageTapped() { guard let router = router, let photo = photo else { return } - + router.displayFullImage(withInfoFrom: photo) } } // MARK: - FeedViewCellPhotoDisplaying + extension FeedViewCell: FeedViewCellPhotoDisplaying { func display(photo: Photo) { self.photo = photo - + let metadata = photo.metadata ownerNameView.text = metadata.ownerName dateTakenView.set(date: metadata.dateTaken) viewsView.set(views: metadata.views) tagsView.set(tags: metadata.tags) - + cellImageView.image = UIImage(data: photo.imageData) layoutImage() } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/DateLabel.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/DateLabel.swift index adda6eb..727dfdf 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/DateLabel.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/DateLabel.swift @@ -9,17 +9,17 @@ final class DateLabel: UILabel { private let dateFormatter: DateFormatter init(dateFormat: String, frame: CGRect = .zero) { - self.dateFormatter = DateFormatter() + dateFormatter = DateFormatter() dateFormatter.dateFormat = dateFormat - + super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + func set(date: Date) { text = dateFormatter.string(from: date) } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/TagsLabel.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/TagsLabel.swift index e561e02..807aaa3 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/TagsLabel.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/TagsLabel.swift @@ -9,4 +9,4 @@ class TagsLabel: UILabel { func set(tags: [String]) { super.text = tags.joined(separator: ", ") } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/ViewsLabel.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/ViewsLabel.swift index b351fe9..1d4fb23 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/ViewsLabel.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/Metadata/ViewsLabel.swift @@ -10,4 +10,4 @@ final class ViewsLabel: UILabel { let description = views == 1 ? "view" : "views" super.text = "\(views) \(description)" } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift index 516cd40..33b0d85 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedFactory.swift @@ -10,8 +10,8 @@ protocol FeedCreating { final class FeedFactory: FeedCreating { let feedCellFactory: FeedCellCreating - init(feedCellFactory: FeedCellCreating) { - self.feedCellFactory = feedCellFactory + init(feedCellFactory: FeedCellCreating) { + self.feedCellFactory = feedCellFactory } func createViewController(photoDataProvider: PhotoDataProviding, photoCollectionFetcher: PhotoCollectionFetching) -> FeedViewController { @@ -21,9 +21,9 @@ final class FeedFactory: FeedCreating { let dataSource = createDataSource(router: router) let viewController = FeedViewController( - interactor: interactor, - dataSource: dataSource, - router: router + interactor: interactor, + dataSource: dataSource, + router: router ) presenter.view = viewController @@ -34,7 +34,7 @@ final class FeedFactory: FeedCreating { private func createDataSource(router: FeedRouting) -> FeedDataSourcing { let configurator = feedCellFactory.createFeedCellConfigurator(feedRouter: router) - + return FeedViewDataSource(feedCellConfigurator: configurator) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift index 6ccf617..c5588ca 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedInteractor.swift @@ -28,9 +28,9 @@ final class FeedInteractor: FeedInteracting { currentRequest = request photoCollectionFetcher.fetchPhotos( - startingFrom: request.startFromPosition, - fetchAtMost: request.fetchAtMost, - withSize: request.size + startingFrom: request.startFromPosition, + fetchAtMost: request.fetchAtMost, + withSize: request.size ) { result in switch result { case let .success(photos): @@ -42,13 +42,13 @@ final class FeedInteractor: FeedInteracting { } let response = FeedModels.Photos.Response( - startingPosition: request.startFromPosition, - photos: photos + startingPosition: request.startFromPosition, + photos: photos ) self.presenter.present(photos: response) } - + case let .failure(error): print("Error while retrieving photos (request: \(request)): \(error)") } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift index eec01fe..272a49b 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedModels.swift @@ -3,8 +3,8 @@ // // Copyright (c) 2020 rencevio. All rights reserved. -struct FeedModels { - struct Photos { +enum FeedModels { + enum Photos { struct Request: Equatable { let startFromPosition: Int let fetchAtMost: Int diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift index 4fe0dc5..8928a85 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift @@ -11,7 +11,7 @@ protocol FeedRouting: Routing { final class FeedRouter: FeedRouting { var sourceVC: UIViewController? - + private let fullImageFactory: FullImageCreating init(fullImageFactory: FullImageCreating) { @@ -22,9 +22,9 @@ final class FeedRouter: FeedRouting { guard let sourceVC = sourceVC else { return } - + let fullImageViewController = fullImageFactory.createViewController(withInfoFrom: photo) - + sourceVC.present(fullImageViewController, animated: true) } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift index cb9db3b..0118ab0 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift @@ -10,7 +10,7 @@ protocol FeedDisplaying: AnyObject { func displayMore(photos: FeedModels.Photos.ViewModel) } -private struct LayoutConstants { +private enum LayoutConstants { static let photoSize = PhotoParameters.Size.medium static let itemSpacing: CGFloat = 30 } @@ -21,27 +21,27 @@ final class FeedViewController: UIViewController { .views, .dateTaken, .tags, - .ownerName + .ownerName, ] private let interactor: FeedInteracting private let dataSource: FeedDataSourcing private let router: FeedRouting - + private lazy var tableView: UITableView = { let view = UITableView(frame: .zero) - + view.alwaysBounceVertical = true view.backgroundColor = Style.ScreenBackground.color view.showsVerticalScrollIndicator = false - + view.tableFooterView = UIView() view.separatorStyle = .none view.sectionHeaderHeight = LayoutConstants.itemSpacing view.estimatedRowHeight = 500 - + view.allowsSelection = false - + return view }() @@ -87,34 +87,34 @@ final class FeedViewController: UIViewController { dataSource.register(for: tableView) } - + private func setupRefreshControl() { tableView.refreshControl = refreshControl refreshControl.addTarget(self, action: #selector(handleRefreshControl), for: .valueChanged) } - + // MARK: - Data Requesting - + private func requestNewPhotos() { interactor.fetch( - photos: FeedModels.Photos.Request( - startFromPosition: 0, - fetchAtMost: photosPerFetchRequest, - size: LayoutConstants.photoSize, - metadata: metadataToFetch - ) - ) + photos: FeedModels.Photos.Request( + startFromPosition: 0, + fetchAtMost: photosPerFetchRequest, + size: LayoutConstants.photoSize, + metadata: metadataToFetch + ) + ) } func requestMorePhotos() { interactor.fetch( - photos: FeedModels.Photos.Request( - startFromPosition: dataSource.photoCount, - fetchAtMost: photosPerFetchRequest, - size: LayoutConstants.photoSize, - metadata: metadataToFetch - ) + photos: FeedModels.Photos.Request( + startFromPosition: dataSource.photoCount, + fetchAtMost: photosPerFetchRequest, + size: LayoutConstants.photoSize, + metadata: metadataToFetch + ) ) } } @@ -122,13 +122,14 @@ final class FeedViewController: UIViewController { // MARK: - UITableViewDelegate extension FeedViewController: UITableViewDelegate { - public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + public func tableView(_: UITableView, viewForHeaderInSection _: Int) -> UIView? { UIView() } - public func tableView(_ tableView: UITableView, - willDisplay cell: UITableViewCell, - forRowAt indexPath: IndexPath) { + public func tableView(_: UITableView, + willDisplay _: UITableViewCell, + forRowAt indexPath: IndexPath) + { let itemToDisplay = indexPath.section let loadedPhotosCount = dataSource.photoCount @@ -156,7 +157,7 @@ extension FeedViewController: FeedDisplaying { dataSource.add(photos: photos.photos) - tableView.insertSections(IndexSet(integersIn: currentPhotoCount.. Int { + public func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { 1 } - public func numberOfSections(in tableView: UITableView) -> Int { + public func numberOfSections(in _: UITableView) -> Int { photos.count } - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = dequeueFeedCell(from: tableView, at: indexPath) let photo = photos[indexPath.section] - feedCellConfigurator.configure(cell) + feedCellConfigurator.configure(cell) cell.display(photo: photo) - + return cell } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageFactory.swift index 6f1f902..1e6013f 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageFactory.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageFactory.swift @@ -24,4 +24,4 @@ final class FullImageFactory: FullImageCreating { return viewController } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageInteractor.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageInteractor.swift index 261de91..a4b87e6 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageInteractor.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageInteractor.swift @@ -28,10 +28,10 @@ final class FullImageInteractor: FullImageInteracting { switch result { case let .success(data): self.presenter.present(image: FullImageModels.Image.Response(image: data)) - case .failure(_): + case .failure: self.presenter.presentError() } } } } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageModels.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageModels.swift index a4011ab..21aa5f6 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageModels.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageModels.swift @@ -6,7 +6,7 @@ import Foundation.NSData struct FullImageModels { - struct Image { + enum Image { struct Request { let url: URL } @@ -19,4 +19,4 @@ struct FullImageModels { let image: Data } } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImagePresenter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImagePresenter.swift index d30ea18..10beb22 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImagePresenter.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImagePresenter.swift @@ -10,12 +10,12 @@ protocol FullImagePresenting { final class FullImagePresenter: FullImagePresenting { weak var view: FullImageDisplaying? - + func present(image: FullImageModels.Image.Response) { view?.display(image: FullImageModels.Image.ViewModel(image: image.image)) } - + func presentError() { view?.displayError() } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift index 3288911..cd89634 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift @@ -13,7 +13,7 @@ protocol FullImageDisplaying: AnyObject { final class FullImageViewController: UIViewController { private let interactor: FullImageInteracting private let photo: Photo - + private enum State { case loading case image(image: UIImage) @@ -22,7 +22,7 @@ final class FullImageViewController: UIViewController { private lazy var imageView: UIImageView = { let view = UIImageView() - + view.clipsToBounds = true view.contentMode = .scaleAspectFit @@ -41,7 +41,7 @@ final class FullImageViewController: UIViewController { init(photo: Photo, interactor: FullImageInteracting) { self.photo = photo self.interactor = interactor - + super.init(nibName: nil, bundle: nil) } @@ -49,23 +49,23 @@ final class FullImageViewController: UIViewController { fatalError("init(coder:) has not been implemented") } - override func viewDidLoad() { + override func viewDidLoad() { super.viewDidLoad() - + view.backgroundColor = Style.FullImage.Background.color - + setupImageView() setupLoadingIndicator() - + set(state: .loading) interactor.fetch(image: FullImageModels.Image.Request(url: photo.fullSizeImageURL)) } // MARK: - UI Setup - + private func setupImageView() { view.addSubview(imageView) - + imageView.translatesAutoresizingMaskIntoConstraints = false imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true @@ -111,4 +111,4 @@ extension FullImageViewController: FullImageDisplaying { func displayError() { set(state: .error) } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Style.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Style.swift index dcff488..ccf3868 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Style.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Style.swift @@ -6,16 +6,16 @@ import UIKit struct Style { - struct ScreenBackground { + enum ScreenBackground { static let color = UIColor(red: 0.93, green: 0.94, blue: 0.95, alpha: 1.0) } - struct FullImage { - struct LoadingIndicator { + enum FullImage { + enum LoadingIndicator { static let color = UIColor(red: 0.74, green: 0.76, blue: 0.78, alpha: 1.0) } - - struct Background { + + enum Background { static let color = UIColor(red: 0.93, green: 0.94, blue: 0.95, alpha: 1.0) } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Utils/Routing.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Utils/Routing.swift index 897bf42..6ffce38 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Utils/Routing.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Utils/Routing.swift @@ -7,4 +7,4 @@ import UIKit protocol Routing { var sourceVC: UIViewController? { get set } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/FlickrApiURLBuilder.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/FlickrApiURLBuilder.swift index 65ed2ea..42d0aaf 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/FlickrApiURLBuilder.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/FlickrApiURLBuilder.swift @@ -9,7 +9,8 @@ final class FlickrApiURLResolver { static func build(method: FlickrApiValues.Method, apiKey: String, queryParameters: [FlickrApiValues.QueryParameter: String], - format: String = "json") -> URL { + format: String = "json") -> URL + { let defaultQueryParameters: [FlickrApiValues.QueryParameter: String] = [ .method: method.rawValue, .apiKey: apiKey, diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/FlickrApiValues.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/FlickrApiValues.swift index 3f3d7ab..ea0ec46 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/FlickrApiValues.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/FlickrApiValues.swift @@ -28,7 +28,7 @@ enum FlickrApiValues { case medium = "c" case large = "b" } - + enum PhotoMetadata: String { case views case tags diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotoURLResolver.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotoURLResolver.swift index e16a347..b60745c 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotoURLResolver.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotoURLResolver.swift @@ -5,12 +5,12 @@ import struct Foundation.URL -final class FlickrPhotoURLResolver { +enum FlickrPhotoURLResolver { static func resolveUrl(for photo: FlickrPhoto, withSize size: PhotoParameters.Size) -> URL { let sizeSuffix = getSizeSuffix(for: size) guard let url = URL(string: "https://farm\(photo.farm).staticflickr.com/\(photo.server)/\(photo.id)_\(photo.secret)_\(sizeSuffix.rawValue).jpg") - else { + else { fatalError("Failed to resolve flickr photo url (input photo: \(photo))") } @@ -18,7 +18,7 @@ final class FlickrPhotoURLResolver { } private static func getSizeSuffix(for size: PhotoParameters.Size) -> FlickrApiValues.PhotoSizeSuffix { - switch (size) { + switch size { case .thumbSquare: return .thumbSquare case .medium: diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift index 4c81ae5..bc337b7 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/FlickrApiServices/Photos/FlickrPhotosService.swift @@ -27,7 +27,7 @@ final class FlickrPhotosService: FlickrPhotosFetching { queryParameters: [ .page: String(page), .perPage: String(photosPerPage), - .extras: getQueryParameters(for: metadata).map { $0.rawValue }.joined(separator: ",") + .extras: getQueryParameters(for: metadata).map { $0.rawValue }.joined(separator: ","), ] ) @@ -64,10 +64,10 @@ final class FlickrPhotosService: FlickrPhotosFetching { return .failure(error) } } - + private func getQueryParameters(for metadata: [PhotoParameters.Metadata]) -> [FlickrApiValues.PhotoMetadata] { metadata.map { - switch ($0) { + switch $0 { case .views: return .views case .tags: diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcher.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcher.swift index a9dc9fc..d109c11 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcher.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcher.swift @@ -6,7 +6,7 @@ import Foundation.NSData final class FlickrCollectionDataFetcher: FlickrCollectionDataFetching { - let dataProvider: PhotoDataProviding; + let dataProvider: PhotoDataProviding private let operatingQueue = DispatchQueue(label: "\(FlickrCollectionDataFetcher.self)OperatingQueue") @@ -14,11 +14,12 @@ final class FlickrCollectionDataFetcher: FlickrCollectionDataFetching { self.dataProvider = dataProvider } - // Completes with dictionary containing only successfully retrieved data for photos + // Completes with dictionary containing only successfully retrieved data for photos func fetchData( - photos: [FlickrPhoto], - withSize size: PhotoParameters.Size, - _ completion: @escaping ([Photo.ID: Foundation.Data]) -> ()) { + photos: [FlickrPhoto], + withSize size: PhotoParameters.Size, + _ completion: @escaping ([Photo.ID: Foundation.Data]) -> Void + ) { var photosToFetch = Set(photos.map { $0.id }) var photosImageData = [Photo.ID: Data]() @@ -35,7 +36,7 @@ final class FlickrCollectionDataFetcher: FlickrCollectionDataFetching { } photosToFetch.remove(photo.id) - + if photosToFetch.isEmpty { completion(photosImageData) } @@ -43,4 +44,4 @@ final class FlickrCollectionDataFetcher: FlickrCollectionDataFetching { } } } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetching.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetching.swift index 54a3bee..4d51a91 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetching.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetching.swift @@ -7,4 +7,4 @@ import Foundation.NSData protocol FlickrCollectionDataFetching { func fetchData(photos: [FlickrPhoto], withSize size: PhotoParameters.Size, _ completion: @escaping ([Photo.ID: Data]) -> Void) -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift index f207aff..26202da 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcher.swift @@ -17,15 +17,16 @@ final class FlickrCollectionFetcher: PhotoCollectionFetching { func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, withSize size: PhotoParameters.Size, - completion: @escaping Completion) { + completion: @escaping Completion) + { let page = position / maxFetchCount + 1 let metadata = PhotoParameters.Metadata.allCases flickrPhotosService.getRecent( - page: page, - photosPerPage: maxFetchCount, - includeMetadata: metadata, - completion: transformFetchResult(withSize: size, completion) + page: page, + photosPerPage: maxFetchCount, + includeMetadata: metadata, + completion: transformFetchResult(withSize: size, completion) ) } @@ -34,7 +35,7 @@ final class FlickrCollectionFetcher: PhotoCollectionFetching { guard let self = self else { return } - + switch result { case let .success(photos): self.transformValidPhotos(photos, withSize: size) { validPhotos in @@ -48,7 +49,8 @@ final class FlickrCollectionFetcher: PhotoCollectionFetching { private func transformValidPhotos(_ photos: [FlickrPhoto], withSize size: PhotoParameters.Size, - _ completion: @escaping ([Photo]) -> Void) { + _ completion: @escaping ([Photo]) -> Void) + { collectionDataFetcher.fetchData(photos: photos, withSize: size) { photosImageData in let transformedPhotos = photos.map { photo -> Photo? in guard let metadata = extractMetadata(from: photo) else { @@ -62,14 +64,14 @@ final class FlickrCollectionFetcher: PhotoCollectionFetching { let fullSizeImageURL = FlickrPhotoURLResolver.resolveUrl(for: photo, withSize: .large) return Photo( - id: photo.id, - imageData: imageData, - fullSizeImageURL: fullSizeImageURL, - metadata: metadata + id: photo.id, + imageData: imageData, + fullSizeImageURL: fullSizeImageURL, + metadata: metadata ) } - completion(transformedPhotos.compactMap { $0 }) + completion(transformedPhotos.compactMap { $0 }) } } } @@ -87,9 +89,9 @@ private func extractMetadata(from photo: FlickrPhoto) -> Photo.Metadata? { } return Photo.Metadata( - views: views, - tags: photo.tags.components(separatedBy: " "), - ownerName: photo.ownername, - dateTaken: dateTaken + views: views, + tags: photo.tags.components(separatedBy: " "), + ownerName: photo.ownername, + dateTaken: dateTaken ) -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherFactory.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherFactory.swift index 3721979..d72daaa 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherFactory.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherFactory.swift @@ -10,8 +10,8 @@ final class FlickrCollectionFetcherFactory: PhotoCollectionFetcherCreating { let collectionDataFetcher = FlickrCollectionDataFetcher(dataProvider: photoDataProvider) let fetcher = FlickrCollectionFetcher( - flickrPhotosService: flickrPhotosService, - collectionDataFetcher: collectionDataFetcher + flickrPhotosService: flickrPhotosService, + collectionDataFetcher: collectionDataFetcher ) return fetcher diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift index 67e38ed..c56d0d4 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoCollectionFetching.swift @@ -9,6 +9,5 @@ protocol PhotoCollectionFetching { func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, withSize size: PhotoParameters.Size, - completion: @escaping Completion - ) + completion: @escaping Completion) } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift index 9e80dc6..7a7d9f3 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Workers/PhotoCollectionFetcher/PhotoParameters.swift @@ -16,4 +16,4 @@ enum PhotoParameters { case ownerName case dateTaken } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/MockFlickrPhotoService.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/MockFlickrPhotoService.swift index 918032c..942f449 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/MockFlickrPhotoService.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/FlickrApiServices/Photos/MockFlickrPhotoService.swift @@ -9,7 +9,7 @@ class MockFlickrPhotosService: FlickrPhotosFetching { var getRecentCalls = [(Int, Int)]() var getRecentResult: Result<[FlickrPhoto], Error>? - func getRecent(page: Int, photosPerPage: Int, includeMetadata metadata: [PhotoParameters.Metadata], completion: @escaping Completion) { + func getRecent(page: Int, photosPerPage: Int, includeMetadata _: [PhotoParameters.Metadata], completion: @escaping Completion) { guard let result = getRecentResult else { fatalError("\(#function) expectation was not set") } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Network/MockHttpClient.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Network/MockHttpClient.swift index b47ffbe..817a504 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Network/MockHttpClient.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Network/MockHttpClient.swift @@ -11,7 +11,7 @@ class MockHttpClient: HttpCommunicator { var getCalls = [URL]() var getResult: Result! - func get(url: URL, background: Bool, completion: @escaping Completion) { + func get(url: URL, background _: Bool, completion: @escaping Completion) { guard let result = getResult else { fatalError("\(#function) expectation was not set") } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/FeedPresenterTests.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/FeedPresenterTests.swift index 48b2b4a..8661518 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/FeedPresenterTests.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/Feed/FeedPresenterTests.swift @@ -23,10 +23,10 @@ class FeedPresenterTests: XCTestCase { func test_presentPhotos_startingFromZero_dataIsReset() { let photo = Photo( - id: "id", - imageData: Data(), - fullSizeImageURL: URL(string: "www.not-a-website.com")!, - metadata: Photo.Metadata(views: 0, tags: [], ownerName: "", dateTaken: Date()) + id: "id", + imageData: Data(), + fullSizeImageURL: URL(string: "www.not-a-website.com")!, + metadata: Photo.Metadata(views: 0, tags: [], ownerName: "", dateTaken: Date()) ) sut.present(photos: FeedModels.Photos.Response(startingPosition: 0, photos: [photo])) @@ -37,10 +37,10 @@ class FeedPresenterTests: XCTestCase { func test_presentPhotosTwice_startingFromNonZero_dataIsUpdated() { let photo = Photo( - id: "id", - imageData: Data(), - fullSizeImageURL: URL(string: "www.not-a-website.com")!, - metadata: Photo.Metadata(views: 0, tags: [], ownerName: "", dateTaken: Date()) + id: "id", + imageData: Data(), + fullSizeImageURL: URL(string: "www.not-a-website.com")!, + metadata: Photo.Metadata(views: 0, tags: [], ownerName: "", dateTaken: Date()) ) sut.present(photos: FeedModels.Photos.Response(startingPosition: 1, photos: [photo])) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/FullImage/MockFullImagePresenter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/FullImage/MockFullImagePresenter.swift index b541558..423d74e 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/FullImage/MockFullImagePresenter.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Scenes/FullImage/MockFullImagePresenter.swift @@ -14,7 +14,7 @@ final class MockFullImagePresenter: FullImagePresenting { func present(image: FullImageModels.Image.Response) { presentImageCalls.append(image) } - + func presentError() { presentErrorCalls += 1 } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcherTests.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcherTests.swift index 50fbd8d..7351b17 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcherTests.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionDataFetcherTests.swift @@ -23,7 +23,7 @@ class FlickrCollectionDataFetcherTests: XCTestCase { func test_fetchData_somePhotosHaveNoImageData_returnsOnlyPhotosWithData() { let photosWithData = [ FlickrPhoto(id: "1", secret: "1", server: "1", farm: 1, datetaken: "1", ownername: "1", views: "1", tags: "1"), - FlickrPhoto(id: "2", secret: "2", server: "2", farm: 2, datetaken: "2", ownername: "2", views: "2", tags: "2") + FlickrPhoto(id: "2", secret: "2", server: "2", farm: 2, datetaken: "2", ownername: "2", views: "2", tags: "2"), ] let photosWithoutData = [ FlickrPhoto(id: "3", secret: "3", server: "3", farm: 3, datetaken: "3", ownername: "3", views: "3", tags: "3"), @@ -51,4 +51,4 @@ class FlickrCollectionDataFetcherTests: XCTestCase { XCTAssertEqual(fetchResult.count, photosWithData.count) XCTAssertEqual(fetchResult, Dictionary(uniqueKeysWithValues: photosWithData.map { ($0.id, Data()) })) } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherTests.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherTests.swift index 7419bb8..6408b78 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherTests.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/FlickrCollectionFetcherTests.swift @@ -56,7 +56,7 @@ class FlickrCollectionFetcherTests: XCTestCase { XCTAssertEqual(photosServiceMock.getRecentCalls.count, 1) XCTAssertTrue(photosServiceMock.getRecentCalls[0] == (startingPosition + 1, maxFetchCount)) - + XCTAssertEqual(collectionDataFetcherMock.fetchDataCalls.count, 1) } @@ -87,7 +87,7 @@ class FlickrCollectionFetcherTests: XCTestCase { wait(for: [fetchExpectation], timeout: expectationTimeout) XCTAssertEqual(fetchResult.count, 1) - + let metadata = fetchResult[0].metadata XCTAssertNotNil(metadata.dateTaken) XCTAssertEqual(metadata.views, flickrPhoto.views.toInt()!) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/MockFlickrCollectionDataFetcher.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/MockFlickrCollectionDataFetcher.swift index 14d7414..0d7e90a 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/MockFlickrCollectionDataFetcher.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/Flickr/MockFlickrCollectionDataFetcher.swift @@ -10,14 +10,14 @@ import Foundation.NSData class MockFlickrCollectionDataFetcher: FlickrCollectionDataFetching { var fetchDataCalls = [([FlickrPhoto], PhotoParameters.Size)]() var fetchDataResult: [Photo.ID: Data]! - + func fetchData(photos: [FlickrPhoto], withSize size: PhotoParameters.Size, _ completion: @escaping ([Photo.ID: Data]) -> Void) { guard let result = fetchDataResult else { fatalError("\(#function) expectation was not set") } - + fetchDataCalls.append((photos, size)) - + completion(result) } -} \ No newline at end of file +} diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/MockPhotoCollectionFetcher.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/MockPhotoCollectionFetcher.swift index 8d0de42..f3e3020 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/MockPhotoCollectionFetcher.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoCollectionFetcher/MockPhotoCollectionFetcher.swift @@ -14,7 +14,8 @@ class MockPhotoCollectionFetcher: PhotoCollectionFetching { func fetchPhotos(startingFrom position: Int, fetchAtMost maxFetchCount: Int, withSize size: PhotoParameters.Size, - completion: @escaping Completion) { + completion: @escaping Completion) + { guard let result = fetchPhotoResult else { fatalError("\(#function) expectation was not set") } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/MockPhotoDataProvider.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/MockPhotoDataProvider.swift index 5ccb3e1..2ddb8d5 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/MockPhotoDataProvider.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/MockPhotoDataProvider.swift @@ -11,7 +11,7 @@ final class MockPhotoDataProvider: PhotoDataProviding { var getPhotoDataCalls = [URL]() var getPhotoDataResults = [Result]() - func getPhotoData(from url: URL, backgroundDownload: Bool, _ completion: @escaping Completion) { + func getPhotoData(from url: URL, backgroundDownload _: Bool, _ completion: @escaping Completion) { guard !getPhotoDataResults.isEmpty else { fatalError("\(#function) expectation was not set") } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/PhotoDataRetriever/MockPhotoDataRetriever.swift b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/PhotoDataRetriever/MockPhotoDataRetriever.swift index 48996de..a9c90ef 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/PhotoDataRetriever/MockPhotoDataRetriever.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowserTests/Workers/PhotoDataProvider/PhotoDataRetriever/MockPhotoDataRetriever.swift @@ -11,7 +11,7 @@ class MockDataPhotoRetriever: PhotoDataRetrieving { var retrieveCalls = [URL]() var retrieveResult: Result? - func retrieve(from url: URL, backgroundDownload: Bool, completion: @escaping Completion) { + func retrieve(from url: URL, backgroundDownload _: Bool, completion: @escaping Completion) { guard let result = retrieveResult else { fatalError("\(#function) expectation was not set") } From 8e5c880cb8cbde2e7ad7e9c0c3105e54bf7213cb Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Sun, 29 Nov 2020 23:34:42 +0100 Subject: [PATCH 14/16] Better-looking fullscreen modal --- .../SimpleFlickrBrowser/Resources/Info.plist | 2 -- .../Scenes/Feed/FeedRouter.swift | 3 ++- .../Feed/FullImage/FullImageViewController.swift | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Resources/Info.plist b/SimpleFlickrBrowser/SimpleFlickrBrowser/Resources/Info.plist index 5a63475..75404a2 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Resources/Info.plist +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Resources/Info.plist @@ -29,8 +29,6 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift index 8928a85..b11b28d 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift @@ -24,7 +24,8 @@ final class FeedRouter: FeedRouting { } let fullImageViewController = fullImageFactory.createViewController(withInfoFrom: photo) - + + fullImageViewController.modalPresentationStyle = .fullScreen sourceVC.present(fullImageViewController, animated: true) } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift index cd89634..22ebbff 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FullImage/FullImageViewController.swift @@ -25,6 +25,9 @@ final class FullImageViewController: UIViewController { view.clipsToBounds = true view.contentMode = .scaleAspectFit + + view.isUserInteractionEnabled = true + view.addGestureRecognizer(imageGestureRecognizer) return view }() @@ -38,6 +41,14 @@ final class FullImageViewController: UIViewController { return indicator }() + lazy var imageGestureRecognizer: UITapGestureRecognizer = { + let gesture = UITapGestureRecognizer(target: self, action: #selector(onImageTapped)) + + gesture.numberOfTapsRequired = 1 + + return gesture + }() + init(photo: Photo, interactor: FullImageInteracting) { self.photo = photo self.interactor = interactor @@ -95,6 +106,10 @@ final class FullImageViewController: UIViewController { imageView.isHidden = true } } + + @objc private func onImageTapped() { + self.dismiss(animated: true) + } } // MARK: - FullImageDisplaying From 1d0785408ea129d8ee4c7d317d38bfb06a79319d Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Mon, 30 Nov 2020 17:55:01 +0100 Subject: [PATCH 15/16] Layout table cells using rows instead of sections; enfroce interitem padding using autolayout. --- .../Scenes/Feed/Cell/FeedCellView.swift | 4 +++- .../Scenes/Feed/FeedViewController.swift | 10 ++-------- .../Scenes/Feed/FeedViewDataSource.swift | 6 +----- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift index bf2e4ac..e0f92f7 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/Cell/FeedCellView.swift @@ -14,6 +14,8 @@ private enum LayoutConstants { static let sideMarginRatio: CGFloat = 0.02 static let tagsSideMarginRatio: CGFloat = 0.04 static let topBottomMarginRatio: CGFloat = 0.2 + + static let cellBottomPadding: CGFloat = 30 static let dateTakenFormat = "MMM dd, yyyy" } @@ -157,7 +159,7 @@ final class FeedViewCell: UITableViewCell { dateTakenView.translatesAutoresizingMaskIntoConstraints = false dateTakenView.topAnchor.constraint(equalTo: tagsView.bottomAnchor, constant: frame.height * LayoutConstants.topBottomMarginRatio).isActive = true - dateTakenView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true + dateTakenView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -1 * LayoutConstants.cellBottomPadding).isActive = true dateTakenView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: frame.width * LayoutConstants.sideMarginRatio).isActive = true } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift index 0118ab0..4f5c115 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewController.swift @@ -12,7 +12,6 @@ protocol FeedDisplaying: AnyObject { private enum LayoutConstants { static let photoSize = PhotoParameters.Size.medium - static let itemSpacing: CGFloat = 30 } final class FeedViewController: UIViewController { @@ -37,7 +36,6 @@ final class FeedViewController: UIViewController { view.tableFooterView = UIView() view.separatorStyle = .none - view.sectionHeaderHeight = LayoutConstants.itemSpacing view.estimatedRowHeight = 500 view.allowsSelection = false @@ -122,15 +120,11 @@ final class FeedViewController: UIViewController { // MARK: - UITableViewDelegate extension FeedViewController: UITableViewDelegate { - public func tableView(_: UITableView, viewForHeaderInSection _: Int) -> UIView? { - UIView() - } - public func tableView(_: UITableView, willDisplay _: UITableViewCell, forRowAt indexPath: IndexPath) { - let itemToDisplay = indexPath.section + let itemToDisplay = indexPath.row let loadedPhotosCount = dataSource.photoCount @@ -157,7 +151,7 @@ extension FeedViewController: FeedDisplaying { dataSource.add(photos: photos.photos) - tableView.insertSections(IndexSet(integersIn: currentPhotoCount ..< currentPhotoCount + photos.photos.count), with: .none) + tableView.insertRows(at: (0 ..< photos.photos.count).map { IndexPath(row: currentPhotoCount + $0, section: 0) }, with: .fade) } } diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift index 47a5deb..48d7d97 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedViewDataSource.swift @@ -26,17 +26,13 @@ final class FeedViewDataSource: NSObject { extension FeedViewDataSource: UITableViewDataSource { public func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { - 1 - } - - public func numberOfSections(in _: UITableView) -> Int { photos.count } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = dequeueFeedCell(from: tableView, at: indexPath) - let photo = photos[indexPath.section] + let photo = photos[indexPath.row] feedCellConfigurator.configure(cell) cell.display(photo: photo) From c3137495e035961cf570996f0621f23c648258d2 Mon Sep 17 00:00:00 2001 From: Maxim Berezhnoy Date: Mon, 30 Nov 2020 17:56:40 +0100 Subject: [PATCH 16/16] Changed modal transition style to one that better corresponds with the dismissal behavior --- .../SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift index b11b28d..f043764 100644 --- a/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift +++ b/SimpleFlickrBrowser/SimpleFlickrBrowser/Scenes/Feed/FeedRouter.swift @@ -26,6 +26,7 @@ final class FeedRouter: FeedRouting { let fullImageViewController = fullImageFactory.createViewController(withInfoFrom: photo) fullImageViewController.modalPresentationStyle = .fullScreen + fullImageViewController.modalTransitionStyle = .crossDissolve sourceVC.present(fullImageViewController, animated: true) } }