diff --git a/TakeHomeApp.xcodeproj/project.pbxproj b/TakeHomeApp.xcodeproj/project.pbxproj index 7474ccf..c269d42 100644 --- a/TakeHomeApp.xcodeproj/project.pbxproj +++ b/TakeHomeApp.xcodeproj/project.pbxproj @@ -3,12 +3,25 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ + 3D8BD83F28744BEC004FCCF8 /* PlacesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8BD83E28744BEC004FCCF8 /* PlacesViewModel.swift */; }; + 3D8BD84128744CC3004FCCF8 /* Place.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8BD84028744CC3004FCCF8 /* Place.swift */; }; + 3D8BD84328776029004FCCF8 /* PlaceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8BD84228776029004FCCF8 /* PlaceTableViewCell.swift */; }; + 3D8BD84528776049004FCCF8 /* Reusable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8BD84428776049004FCCF8 /* Reusable.swift */; }; + 3D8BD848287760C3004FCCF8 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3D8BD847287760C3004FCCF8 /* SnapKit */; }; + 3D8BD84B287760D1004FCCF8 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 3D8BD84A287760D1004FCCF8 /* Kingfisher */; }; + 3D8BD84F287762DE004FCCF8 /* PlaceTableViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8BD84E287762DE004FCCF8 /* PlaceTableViewViewModel.swift */; }; + 3D8BD85128789E29004FCCF8 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8BD85028789E29004FCCF8 /* Endpoint.swift */; }; + 3D8BD85428789FCC004FCCF8 /* PhotosViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8BD85328789FCC004FCCF8 /* PhotosViewController.swift */; }; + 3D8BD8562878A00B004FCCF8 /* PhotosViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8BD8552878A00B004FCCF8 /* PhotosViewModel.swift */; }; + 3D8BD8582878A02B004FCCF8 /* PhotoCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8BD8572878A02B004FCCF8 /* PhotoCollectionViewCell.swift */; }; + 3D8BD85B2878AC2F004FCCF8 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8BD85A2878AC2F004FCCF8 /* Photo.swift */; }; + 3D8BD85D2878ACD7004FCCF8 /* PhotoCollectionViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8BD85C2878ACD7004FCCF8 /* PhotoCollectionViewCellViewModel.swift */; }; AF8DA7D02639E4AB0011F652 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF8DA7CF2639E4AB0011F652 /* AppDelegate.swift */; }; - AF8DA7D42639E4AB0011F652 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF8DA7D32639E4AB0011F652 /* ViewController.swift */; }; + AF8DA7D42639E4AB0011F652 /* PlacesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF8DA7D32639E4AB0011F652 /* PlacesViewController.swift */; }; AF8DA7D92639E4AE0011F652 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AF8DA7D82639E4AE0011F652 /* Assets.xcassets */; }; AF8DA7DC2639E4AE0011F652 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AF8DA7DA2639E4AE0011F652 /* LaunchScreen.storyboard */; }; AF8DA7E72639E4AE0011F652 /* TakeHomeAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF8DA7E62639E4AE0011F652 /* TakeHomeAppTests.swift */; }; @@ -34,9 +47,20 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 3D8BD83E28744BEC004FCCF8 /* PlacesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacesViewModel.swift; sourceTree = ""; }; + 3D8BD84028744CC3004FCCF8 /* Place.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Place.swift; sourceTree = ""; }; + 3D8BD84228776029004FCCF8 /* PlaceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceTableViewCell.swift; sourceTree = ""; }; + 3D8BD84428776049004FCCF8 /* Reusable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reusable.swift; sourceTree = ""; }; + 3D8BD84E287762DE004FCCF8 /* PlaceTableViewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceTableViewViewModel.swift; sourceTree = ""; }; + 3D8BD85028789E29004FCCF8 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; + 3D8BD85328789FCC004FCCF8 /* PhotosViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosViewController.swift; sourceTree = ""; }; + 3D8BD8552878A00B004FCCF8 /* PhotosViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosViewModel.swift; sourceTree = ""; }; + 3D8BD8572878A02B004FCCF8 /* PhotoCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCollectionViewCell.swift; sourceTree = ""; }; + 3D8BD85A2878AC2F004FCCF8 /* Photo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = ""; }; + 3D8BD85C2878ACD7004FCCF8 /* PhotoCollectionViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCollectionViewCellViewModel.swift; sourceTree = ""; }; AF8DA7CC2639E4AB0011F652 /* TakeHomeApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TakeHomeApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; AF8DA7CF2639E4AB0011F652 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - AF8DA7D32639E4AB0011F652 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + AF8DA7D32639E4AB0011F652 /* PlacesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacesViewController.swift; sourceTree = ""; }; AF8DA7D82639E4AE0011F652 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; AF8DA7DB2639E4AE0011F652 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; AF8DA7DD2639E4AE0011F652 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -54,6 +78,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3D8BD84B287760D1004FCCF8 /* Kingfisher in Frameworks */, + 3D8BD848287760C3004FCCF8 /* SnapKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -74,6 +100,66 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3D8BD83C28744BA5004FCCF8 /* Sources */ = { + isa = PBXGroup; + children = ( + 3D8BD85228789F66004FCCF8 /* PhotosList */, + 3D8BD84D287762C3004FCCF8 /* PlacesList */, + 3D8BD83D28744BBE004FCCF8 /* Model */, + AF8DA7CF2639E4AB0011F652 /* AppDelegate.swift */, + 3D8BD84428776049004FCCF8 /* Reusable.swift */, + 3D8BD85028789E29004FCCF8 /* Endpoint.swift */, + ); + path = Sources; + sourceTree = ""; + }; + 3D8BD83D28744BBE004FCCF8 /* Model */ = { + isa = PBXGroup; + children = ( + 3D8BD84028744CC3004FCCF8 /* Place.swift */, + 3D8BD85A2878AC2F004FCCF8 /* Photo.swift */, + ); + path = Model; + sourceTree = ""; + }; + 3D8BD84C287762B3004FCCF8 /* PlaceCell */ = { + isa = PBXGroup; + children = ( + 3D8BD84228776029004FCCF8 /* PlaceTableViewCell.swift */, + 3D8BD84E287762DE004FCCF8 /* PlaceTableViewViewModel.swift */, + ); + path = PlaceCell; + sourceTree = ""; + }; + 3D8BD84D287762C3004FCCF8 /* PlacesList */ = { + isa = PBXGroup; + children = ( + 3D8BD84C287762B3004FCCF8 /* PlaceCell */, + 3D8BD83E28744BEC004FCCF8 /* PlacesViewModel.swift */, + AF8DA7D32639E4AB0011F652 /* PlacesViewController.swift */, + ); + path = PlacesList; + sourceTree = ""; + }; + 3D8BD85228789F66004FCCF8 /* PhotosList */ = { + isa = PBXGroup; + children = ( + 3D8BD8592878A04E004FCCF8 /* PhotoCell */, + 3D8BD85328789FCC004FCCF8 /* PhotosViewController.swift */, + 3D8BD8552878A00B004FCCF8 /* PhotosViewModel.swift */, + ); + path = PhotosList; + sourceTree = ""; + }; + 3D8BD8592878A04E004FCCF8 /* PhotoCell */ = { + isa = PBXGroup; + children = ( + 3D8BD8572878A02B004FCCF8 /* PhotoCollectionViewCell.swift */, + 3D8BD85C2878ACD7004FCCF8 /* PhotoCollectionViewCellViewModel.swift */, + ); + path = PhotoCell; + sourceTree = ""; + }; AF8DA7C32639E4AB0011F652 = { isa = PBXGroup; children = ( @@ -98,8 +184,7 @@ AF8DA7CE2639E4AB0011F652 /* TakeHomeApp */ = { isa = PBXGroup; children = ( - AF8DA7CF2639E4AB0011F652 /* AppDelegate.swift */, - AF8DA7D32639E4AB0011F652 /* ViewController.swift */, + 3D8BD83C28744BA5004FCCF8 /* Sources */, AF8DA7D82639E4AE0011F652 /* Assets.xcassets */, AF8DA7DA2639E4AE0011F652 /* LaunchScreen.storyboard */, AF8DA7DD2639E4AE0011F652 /* Info.plist */, @@ -141,6 +226,10 @@ dependencies = ( ); name = TakeHomeApp; + packageProductDependencies = ( + 3D8BD847287760C3004FCCF8 /* SnapKit */, + 3D8BD84A287760D1004FCCF8 /* Kingfisher */, + ); productName = TakeHomeApp; productReference = AF8DA7CC2639E4AB0011F652 /* TakeHomeApp.app */; productType = "com.apple.product-type.application"; @@ -212,6 +301,10 @@ Base, ); mainGroup = AF8DA7C32639E4AB0011F652; + packageReferences = ( + 3D8BD846287760C3004FCCF8 /* XCRemoteSwiftPackageReference "SnapKit" */, + 3D8BD849287760D1004FCCF8 /* XCRemoteSwiftPackageReference "Kingfisher" */, + ); productRefGroup = AF8DA7CD2639E4AB0011F652 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -255,8 +348,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - AF8DA7D42639E4AB0011F652 /* ViewController.swift in Sources */, + 3D8BD85B2878AC2F004FCCF8 /* Photo.swift in Sources */, + 3D8BD85428789FCC004FCCF8 /* PhotosViewController.swift in Sources */, + 3D8BD84528776049004FCCF8 /* Reusable.swift in Sources */, + 3D8BD84F287762DE004FCCF8 /* PlaceTableViewViewModel.swift in Sources */, + 3D8BD85128789E29004FCCF8 /* Endpoint.swift in Sources */, + 3D8BD83F28744BEC004FCCF8 /* PlacesViewModel.swift in Sources */, + AF8DA7D42639E4AB0011F652 /* PlacesViewController.swift in Sources */, + 3D8BD85D2878ACD7004FCCF8 /* PhotoCollectionViewCellViewModel.swift in Sources */, AF8DA7D02639E4AB0011F652 /* AppDelegate.swift in Sources */, + 3D8BD8562878A00B004FCCF8 /* PhotosViewModel.swift in Sources */, + 3D8BD84128744CC3004FCCF8 /* Place.swift in Sources */, + 3D8BD8582878A02B004FCCF8 /* PhotoCollectionViewCell.swift in Sources */, + 3D8BD84328776029004FCCF8 /* PlaceTableViewCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -575,6 +679,38 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 3D8BD846287760C3004FCCF8 /* XCRemoteSwiftPackageReference "SnapKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SnapKit/SnapKit.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.0.0; + }; + }; + 3D8BD849287760D1004FCCF8 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 3D8BD847287760C3004FCCF8 /* SnapKit */ = { + isa = XCSwiftPackageProductDependency; + package = 3D8BD846287760C3004FCCF8 /* XCRemoteSwiftPackageReference "SnapKit" */; + productName = SnapKit; + }; + 3D8BD84A287760D1004FCCF8 /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 3D8BD849287760D1004FCCF8 /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = AF8DA7C42639E4AB0011F652 /* Project object */; } diff --git a/TakeHomeApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TakeHomeApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..add5150 --- /dev/null +++ b/TakeHomeApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher.git", + "state" : { + "revision" : "2235a22ca249199cb736237f4bb9cc12b6ed416a", + "version" : "7.3.0" + } + }, + { + "identity" : "snapkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SnapKit/SnapKit.git", + "state" : { + "revision" : "f222cbdf325885926566172f6f5f06af95473158", + "version" : "5.6.0" + } + } + ], + "version" : 2 +} diff --git a/TakeHomeApp/Assets.xcassets/placeholder.imageset/Contents.json b/TakeHomeApp/Assets.xcassets/placeholder.imageset/Contents.json new file mode 100644 index 0000000..bd5ed7d --- /dev/null +++ b/TakeHomeApp/Assets.xcassets/placeholder.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "placeholder-images-image_large.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TakeHomeApp/Assets.xcassets/placeholder.imageset/placeholder-images-image_large.png b/TakeHomeApp/Assets.xcassets/placeholder.imageset/placeholder-images-image_large.png new file mode 100644 index 0000000..41d321d Binary files /dev/null and b/TakeHomeApp/Assets.xcassets/placeholder.imageset/placeholder-images-image_large.png differ diff --git a/TakeHomeApp/AppDelegate.swift b/TakeHomeApp/Sources/AppDelegate.swift similarity index 72% rename from TakeHomeApp/AppDelegate.swift rename to TakeHomeApp/Sources/AppDelegate.swift index fa8ef7c..faf8aad 100644 --- a/TakeHomeApp/AppDelegate.swift +++ b/TakeHomeApp/Sources/AppDelegate.swift @@ -7,7 +7,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let window = UIWindow(frame: UIScreen.main.bounds) - window.rootViewController = ViewController() + let navigationController = UINavigationController(rootViewController: PlacesViewController()) + window.rootViewController = navigationController window.makeKeyAndVisible() self.window = window diff --git a/TakeHomeApp/Sources/Endpoint.swift b/TakeHomeApp/Sources/Endpoint.swift new file mode 100644 index 0000000..9741a26 --- /dev/null +++ b/TakeHomeApp/Sources/Endpoint.swift @@ -0,0 +1,14 @@ +// +// Endpoint.swift +// TakeHomeApp +// +// Created by Silvia Kuzmova on 08.07.22. +// + +import Foundation + +enum Endpoint { + + static let mainURLString = "https://608948878c8043001757e68c.mockapi.io/api/v1" + +} diff --git a/TakeHomeApp/Sources/Model/Photo.swift b/TakeHomeApp/Sources/Model/Photo.swift new file mode 100644 index 0000000..a8ab132 --- /dev/null +++ b/TakeHomeApp/Sources/Model/Photo.swift @@ -0,0 +1,37 @@ +// +// Photo.swift +// TakeHomeApp +// +// Created by Silvia Kuzmova on 08.07.22. +// + +import Foundation + +struct Photo { + let id: String + let placeId: String + let createdAt: Date + let imageURL: URL? +} + +extension Photo: Decodable { + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + placeId = try container.decode(String.self, forKey: .placeId) + let createdAtString = try container.decode(String.self, forKey: .createdAt) + let dateFormatter = ISO8601DateFormatter() // todo + createdAt = dateFormatter.date(from: createdAtString) ?? Date() + let imageURLString = try container.decode(String.self, forKey: .imageURL) + imageURL = URL(string: imageURLString) + } + + private enum CodingKeys: String, CodingKey { + case id + case placeId + case createdAt + case imageURL = "image" + } +} + diff --git a/TakeHomeApp/Sources/Model/Place.swift b/TakeHomeApp/Sources/Model/Place.swift new file mode 100644 index 0000000..d602f88 --- /dev/null +++ b/TakeHomeApp/Sources/Model/Place.swift @@ -0,0 +1,36 @@ +// +// Place.swift +// TakeHomeApp +// +// Created by Silvia Kuzmova on 05.07.22. +// + +import Foundation + +struct Place { + let id: String + let createdAt: Date + let name: String + let thumbnailURL: URL? +} + +extension Place: Decodable { + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + let createdAtString = try container.decode(String.self, forKey: .createdAt) + let dateFormatter = ISO8601DateFormatter() // todo + createdAt = dateFormatter.date(from: createdAtString) ?? Date() + let thumbnailURLString = try container.decode(String.self, forKey: .thumbnailURL) + thumbnailURL = URL(string: thumbnailURLString) + } + + private enum CodingKeys: String, CodingKey { + case id + case createdAt + case name + case thumbnailURL = "thumbnail" + } +} diff --git a/TakeHomeApp/Sources/PhotosList/PhotoCell/PhotoCollectionViewCell.swift b/TakeHomeApp/Sources/PhotosList/PhotoCell/PhotoCollectionViewCell.swift new file mode 100644 index 0000000..287a64b --- /dev/null +++ b/TakeHomeApp/Sources/PhotosList/PhotoCell/PhotoCollectionViewCell.swift @@ -0,0 +1,53 @@ +// +// PhotoCollectionViewCell.swift +// TakeHomeApp +// +// Created by Silvia Kuzmova on 08.07.22. +// + +import UIKit +import Kingfisher + +class PhotoCollectionViewCell: UICollectionViewCell { + + // MARK: - Views + + private let photoImageView: UIImageView = { + let view = UIImageView() + view.contentMode = .scaleAspectFill + return view + }() + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + setupConstraints() + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func set(photoURL: URL?) { + photoImageView.kf.setImage(with: photoURL, + placeholder: UIImage(named: "placeholder")) + } +} + +// MARK: - Private + +private extension PhotoCollectionViewCell { + + func setupViews() { + backgroundColor = .white + contentView.addSubview(photoImageView) + } + + func setupConstraints() { + photoImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } +} diff --git a/TakeHomeApp/Sources/PhotosList/PhotoCell/PhotoCollectionViewCellViewModel.swift b/TakeHomeApp/Sources/PhotosList/PhotoCell/PhotoCollectionViewCellViewModel.swift new file mode 100644 index 0000000..271e63e --- /dev/null +++ b/TakeHomeApp/Sources/PhotosList/PhotoCell/PhotoCollectionViewCellViewModel.swift @@ -0,0 +1,17 @@ +// +// PhotoCollectionViewCellViewModel.swift +// TakeHomeApp +// +// Created by Silvia Kuzmova on 08.07.22. +// + +import Foundation + +struct PhotoCollectionViewCellViewModel { + + let photo: Photo + + var imageURL: URL? { + return photo.imageURL + } +} diff --git a/TakeHomeApp/Sources/PhotosList/PhotosViewController.swift b/TakeHomeApp/Sources/PhotosList/PhotosViewController.swift new file mode 100644 index 0000000..8b4d260 --- /dev/null +++ b/TakeHomeApp/Sources/PhotosList/PhotosViewController.swift @@ -0,0 +1,113 @@ +// +// PhotosViewController.swift +// TakeHomeApp +// +// Created by Silvia Kuzmova on 08.07.22. +// + +import UIKit + +class PhotosViewController: UIViewController { + + private lazy var photosCollectionView: UICollectionView = { + let layout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumInteritemSpacing = 0 + layout.minimumLineSpacing = 0 + return layout + }() + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.register(PhotoCollectionViewCell.self) + collectionView.dataSource = self + collectionView.delegate = self + collectionView.isPagingEnabled = true + collectionView.contentInsetAdjustmentBehavior = .never + collectionView.showsVerticalScrollIndicator = false + collectionView.showsHorizontalScrollIndicator = false + return collectionView + }() + + var photoURLs: [URL] = [] { + didSet { + photosCollectionView.reloadData() + } + } + + let viewModel: PhotosViewModel + + // MARK: - Init + + init(viewModel: PhotosViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupViews() + setupConstraints() + + viewModel.fetchPhotos { [weak self] photoURLs, error in + guard let self = self else { return } + guard error == nil else { + // todo: show error + return + } + if let photoURLs = photoURLs { + DispatchQueue.main.async { + self.photoURLs = photoURLs + } + } + } + } +} + +// MARK: - UICollectionViewDataSource + +extension PhotosViewController: UICollectionViewDataSource { + + func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int { + return photoURLs.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoCollectionViewCell.reuseIdentifier, for: indexPath) + if let photoCell = cell as? PhotoCollectionViewCell { + photoCell.set(photoURL: photoURLs[indexPath.row]) + } + return cell + } +} + +// MARK: - UICollectionViewDelegate + +extension PhotosViewController: UICollectionViewDelegate {} + +// MARK: - UICollectionViewDelegateFlowLayout + +extension PhotosViewController: UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: photosCollectionView.frame.width, height: photosCollectionView.frame.height) + } +} + +// MARK: - Private + +private extension PhotosViewController { + + func setupViews() { + view.addSubview(photosCollectionView) + } + + func setupConstraints() { + photosCollectionView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } +} diff --git a/TakeHomeApp/Sources/PhotosList/PhotosViewModel.swift b/TakeHomeApp/Sources/PhotosList/PhotosViewModel.swift new file mode 100644 index 0000000..96ba682 --- /dev/null +++ b/TakeHomeApp/Sources/PhotosList/PhotosViewModel.swift @@ -0,0 +1,40 @@ +// +// PhotosViewModel.swift +// TakeHomeApp +// +// Created by Silvia Kuzmova on 08.07.22. +// + +import Foundation + +class PhotosViewModel { + + let placeId: String + + init(placeId: String) { + self.placeId = placeId + } + + func fetchPhotos(onCompleted: @escaping ([URL]?, Error?) -> Void) { + let urlString = Endpoint.mainURLString + "/places/" + placeId + "/photos" + let url = URL(string: urlString)! + + let task = URLSession.shared.dataTask(with: url) { (data, response, error) in + guard let data = data else { return } + + do { + let decoder = JSONDecoder() + let byDateSortedPhotos = try decoder.decode([Photo].self, from: data) + .sorted { $0.createdAt < $1.createdAt } + .compactMap {$0.imageURL} + + onCompleted(byDateSortedPhotos, nil) + } catch { + print(error) + onCompleted(nil, error) + } + } + task.resume() + } + +} diff --git a/TakeHomeApp/Sources/PlacesList/PlaceCell/PlaceTableViewCell.swift b/TakeHomeApp/Sources/PlacesList/PlaceCell/PlaceTableViewCell.swift new file mode 100644 index 0000000..5c4efe3 --- /dev/null +++ b/TakeHomeApp/Sources/PlacesList/PlaceCell/PlaceTableViewCell.swift @@ -0,0 +1,124 @@ +// +// PlaceTableViewCell.swift +// TakeHomeApp +// +// Created by Silvia Kuzmova on 07.07.22. +// + +import UIKit +import Kingfisher + +class PlaceTableViewCell: UITableViewCell { + + // MARK: - Constants + + enum Constants { + static let iconHeight: CGFloat = 80 + } + + private let iconView: UIImageView = { + let view = UIImageView() + view.contentMode = .scaleAspectFill + view.backgroundColor = .gray + view.clipsToBounds = true + return view + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 20, weight: .bold) + label.contentMode = .left + return label + }() + + private let dateLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 17) + label.contentMode = .right + return label + }() + + private let containerStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.alignment = .center + stackView.distribution = .fill + stackView.spacing = 8 + return stackView + }() + + private let nameStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.alignment = .leading + stackView.distribution = .fill + stackView.spacing = 8 + return stackView + }() + + var isDarkMode: Bool = false + + // MARK: - Properties + + + // MARK: - Init + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + setupConstraints() + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + iconView.layer.cornerRadius = Constants.iconHeight * 0.5 + } + + override func prepareForReuse() { + super.prepareForReuse() + iconView.image = nil + } + + // MARK: - Public + + func bind(_ viewModel: PlaceTableViewViewModel) { + titleLabel.text = viewModel.title + dateLabel.text = viewModel.createdAt + + let url = viewModel.iconURL + iconView.kf.setImage(with: url) + } +} + +// MARK: - Private + +private extension PlaceTableViewCell { + + func setupViews() { + contentView.addSubview(containerStackView) + + containerStackView.addArrangedSubview(iconView) + containerStackView.addArrangedSubview(nameStackView) + + nameStackView.addArrangedSubview(titleLabel) + nameStackView.addArrangedSubview(dateLabel) + } + + func setupConstraints() { + containerStackView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + .inset(20) + make.top.bottom.equalToSuperview() + .inset(12) + } + + iconView.snp.makeConstraints { make in + make.height.equalTo(80) + make.width.equalTo(iconView.snp.height) + } + } +} diff --git a/TakeHomeApp/Sources/PlacesList/PlaceCell/PlaceTableViewViewModel.swift b/TakeHomeApp/Sources/PlacesList/PlaceCell/PlaceTableViewViewModel.swift new file mode 100644 index 0000000..ca8afac --- /dev/null +++ b/TakeHomeApp/Sources/PlacesList/PlaceCell/PlaceTableViewViewModel.swift @@ -0,0 +1,31 @@ +// +// PlaceTableViewViewModel.swift +// TakeHomeApp +// +// Created by Silvia Kuzmova on 07.07.22. +// + +import Foundation + +struct PlaceTableViewViewModel { + + let place: Place + + let formatter : DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "MMM dd, yyyy" + return formatter + }() + + var title: String { + return place.name + } + + var iconURL: URL? { + return place.thumbnailURL + } + + var createdAt: String { + return formatter.string(from: place.createdAt) + } +} diff --git a/TakeHomeApp/Sources/PlacesList/PlacesViewController.swift b/TakeHomeApp/Sources/PlacesList/PlacesViewController.swift new file mode 100644 index 0000000..1ff2bd0 --- /dev/null +++ b/TakeHomeApp/Sources/PlacesList/PlacesViewController.swift @@ -0,0 +1,96 @@ +import UIKit +import SnapKit + +class PlacesViewController: UIViewController { + + // MARK: UI Elements + + private let tableView: UITableView = { + let tableView = UITableView() + tableView.backgroundColor = .clear + tableView.separatorStyle = .none + tableView.estimatedRowHeight = 50 + tableView.showsHorizontalScrollIndicator = false + tableView.rowHeight = UITableView.automaticDimension + tableView.register(PlaceTableViewCell.self) + return tableView + }() + + private let viewModel = PlacesViewModel() + + private var places: [Place] = [] { + didSet { + tableView.reloadData() + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupViews() + setupConstraints() + + viewModel.fetchPlaces { [weak self] places, error in + guard let self = self else { return } + guard error == nil else { + // todo: show error + return + } + if let places = places { + DispatchQueue.main.async { + self.places = places + } + } + } + } +} + +// MARK: - UITableView + +extension PlacesViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + print(indexPath.row) + + let viewModel = PhotosViewModel(placeId: places[indexPath.row].id) + let photosViewController = PhotosViewController(viewModel: viewModel) + navigationController?.pushViewController(photosViewController, animated: true) + + tableView.deselectRow(at: indexPath, animated: true) + } +} + +extension PlacesViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return places.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: PlaceTableViewCell.reuseIdentifier, for: indexPath) + guard let placeCell = cell as? PlaceTableViewCell else { return cell } + let viewModel = PlaceTableViewViewModel(place: places[indexPath.row]) + placeCell.bind(viewModel) + return placeCell + } +} + +// MARK: - Private + +private extension PlacesViewController { + + func setupViews() { + view.backgroundColor = UIColor.systemBackground + + title = "Places" + navigationController?.navigationBar.prefersLargeTitles = true + + tableView.delegate = self + tableView.dataSource = self + view.addSubview(tableView) + } + + func setupConstraints() { + tableView.snp.makeConstraints { make in + make.edges.equalTo(view.safeAreaLayoutGuide) + } + } +} diff --git a/TakeHomeApp/Sources/PlacesList/PlacesViewModel.swift b/TakeHomeApp/Sources/PlacesList/PlacesViewModel.swift new file mode 100644 index 0000000..b83a360 --- /dev/null +++ b/TakeHomeApp/Sources/PlacesList/PlacesViewModel.swift @@ -0,0 +1,31 @@ +// +// PlacesViewModel.swift +// TakeHomeApp +// +// Created by Silvia Kuzmova on 05.07.22. +// + +import Foundation + +class PlacesViewModel { + + func fetchPlaces(onCompleted: @escaping ([Place]?, Error?) -> Void) { + let urlString = Endpoint.mainURLString + "/places" + let url = URL(string: urlString)! + + let task = URLSession.shared.dataTask(with: url) { (data, response, error) in + guard let data = data else { return } + + do { + let decoder = JSONDecoder() + let places = try decoder.decode([Place].self, from: data) + let alphabeticallySortedPlaces = places.sorted { $0.name < $1.name } + onCompleted(alphabeticallySortedPlaces, nil) + } catch { + print(error) + onCompleted(nil, error) + } + } + task.resume() + } +} diff --git a/TakeHomeApp/Sources/Reusable.swift b/TakeHomeApp/Sources/Reusable.swift new file mode 100644 index 0000000..19c3828 --- /dev/null +++ b/TakeHomeApp/Sources/Reusable.swift @@ -0,0 +1,58 @@ +// +// Reusable.swift +// TakeHomeApp +// +// Created by Silvia Kuzmova on 07.07.22. +// + +import UIKit + +public protocol Reuseable: AnyObject { + static var reuseIdentifier: String { get } +} + +extension Reuseable { + public static var reuseIdentifier: String { + return String(describing: self) + } +} + +extension UITableViewCell: Reuseable {} +extension UITableViewHeaderFooterView: Reuseable {} +extension UICollectionReusableView: Reuseable {} + +extension UITableView { + public func register(_ cell: T.Type) { + register(cell, forCellReuseIdentifier: cell.reuseIdentifier) + } + + public func register(headerFooter view: T.Type) { + register(view, forHeaderFooterViewReuseIdentifier: view.reuseIdentifier) + } + + public func dequeueReusableCell(for indexPath: IndexPath) -> T { + dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T + } + + public func dequeueReusableHeaderFooterView(for _: Int) -> T { + dequeueReusableHeaderFooterView(withIdentifier: T.reuseIdentifier) as! T + } +} + +extension UICollectionView { + public func register(_ cell: T.Type) { + register(cell, forCellWithReuseIdentifier: cell.reuseIdentifier) + } + + public func register(header view: T.Type) { + register(view, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: view.reuseIdentifier) + } + + public func register(footer view: T.Type) { + register(view, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: view.reuseIdentifier) + } + + public func dequeueReusableCell(for indexPath: IndexPath) -> T { + return dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath) as! T + } +} diff --git a/TakeHomeApp/ViewController.swift b/TakeHomeApp/ViewController.swift deleted file mode 100644 index 9bd93cd..0000000 --- a/TakeHomeApp/ViewController.swift +++ /dev/null @@ -1,5 +0,0 @@ -import UIKit - -class ViewController: UIViewController { - -}