diff --git a/MarvelApp.xcodeproj/project.pbxproj b/MarvelApp.xcodeproj/project.pbxproj index da2bbcf..f7fb4e4 100644 --- a/MarvelApp.xcodeproj/project.pbxproj +++ b/MarvelApp.xcodeproj/project.pbxproj @@ -8,20 +8,22 @@ /* Begin PBXBuildFile section */ 6E0AA1ECEBDC56A5A73119D6 /* Pods_MarvelApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4D525B3FE63D0AD96070A87 /* Pods_MarvelApp.framework */; }; + AD27E41A29FE801900FFAF28 /* DBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD27E41929FE801900FFAF28 /* DBManager.swift */; }; + AD27E41C29FE805400FFAF28 /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD27E41B29FE805400FFAF28 /* Repository.swift */; }; + AD27E41E2A01C78F00FFAF28 /* DescriptionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD27E41D2A01C78F00FFAF28 /* DescriptionViewModel.swift */; }; AD37541929A39A940081B177 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD37541829A39A940081B177 /* AppDelegate.swift */; }; AD37541B29A39A940081B177 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD37541A29A39A940081B177 /* SceneDelegate.swift */; }; - AD37541D29A39A940081B177 /* HeroListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD37541C29A39A940081B177 /* HeroListViewController.swift */; }; + AD37541D29A39A940081B177 /* CharactersCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD37541C29A39A940081B177 /* CharactersCollectionViewController.swift */; }; AD37542229A39A960081B177 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AD37542129A39A960081B177 /* Assets.xcassets */; }; AD37542529A39A960081B177 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD37542329A39A960081B177 /* LaunchScreen.storyboard */; }; AD37543029A39A970081B177 /* MarvelAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD37542F29A39A970081B177 /* MarvelAppTests.swift */; }; AD37543A29A39A970081B177 /* MarvelAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD37543929A39A970081B177 /* MarvelAppUITests.swift */; }; AD37543C29A39A970081B177 /* MarvelAppUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD37543B29A39A970081B177 /* MarvelAppUITestsLaunchTests.swift */; }; - AD5EA00829A560AC0033A503 /* HeroCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD5EA00729A560AC0033A503 /* HeroCollectionViewCell.swift */; }; - AD5EA00A29AC8C950033A503 /* HeroData.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD5EA00929AC8C950033A503 /* HeroData.swift */; }; + AD5EA00829A560AC0033A503 /* CharacterCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD5EA00729A560AC0033A503 /* CharacterCollectionViewCell.swift */; }; AD5EA00E29AD42150033A503 /* TriangleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD5EA00D29AD42150033A503 /* TriangleView.swift */; }; AD5EA01029BA193C0033A503 /* DescriptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD5EA00F29BA193C0033A503 /* DescriptionViewController.swift */; }; AD6FCEC229C048A000B41E1F /* BrightnessUIColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6FCEC129C048A000B41E1F /* BrightnessUIColorExtension.swift */; }; - AD6FCEC529C1A00800B41E1F /* HeroListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6FCEC429C1A00800B41E1F /* HeroListViewModel.swift */; }; + AD6FCEC529C1A00800B41E1F /* CharactersCollectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6FCEC429C1A00800B41E1F /* CharactersCollectionViewModel.swift */; }; AD6FCEC729C6F75000B41E1F /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6FCEC629C6F75000B41E1F /* ActivityIndicatorView.swift */; }; ADD2B05F29E70D9000C747F1 /* AverageUIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD2B05E29E70D9000C747F1 /* AverageUIImageExtension.swift */; }; ADD2B06129E70E1300C747F1 /* FetchUIImageViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD2B06029E70E1300C747F1 /* FetchUIImageViewExtension.swift */; }; @@ -49,10 +51,13 @@ /* Begin PBXFileReference section */ 74E58A23873C21B8D52F7C30 /* Pods-MarvelApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MarvelApp.release.xcconfig"; path = "Target Support Files/Pods-MarvelApp/Pods-MarvelApp.release.xcconfig"; sourceTree = ""; }; 9391ECB7D79CA63443BF4CB9 /* Pods-MarvelApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MarvelApp.debug.xcconfig"; path = "Target Support Files/Pods-MarvelApp/Pods-MarvelApp.debug.xcconfig"; sourceTree = ""; }; + AD27E41929FE801900FFAF28 /* DBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBManager.swift; sourceTree = ""; }; + AD27E41B29FE805400FFAF28 /* Repository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = ""; }; + AD27E41D2A01C78F00FFAF28 /* DescriptionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionViewModel.swift; sourceTree = ""; }; AD37541529A39A940081B177 /* MarvelApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MarvelApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; AD37541829A39A940081B177 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; AD37541A29A39A940081B177 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - AD37541C29A39A940081B177 /* HeroListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeroListViewController.swift; sourceTree = ""; }; + AD37541C29A39A940081B177 /* CharactersCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharactersCollectionViewController.swift; sourceTree = ""; }; AD37542129A39A960081B177 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; AD37542429A39A960081B177 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; AD37542629A39A960081B177 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -61,12 +66,11 @@ AD37543529A39A970081B177 /* MarvelAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MarvelAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AD37543929A39A970081B177 /* MarvelAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarvelAppUITests.swift; sourceTree = ""; }; AD37543B29A39A970081B177 /* MarvelAppUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarvelAppUITestsLaunchTests.swift; sourceTree = ""; }; - AD5EA00729A560AC0033A503 /* HeroCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeroCollectionViewCell.swift; sourceTree = ""; }; - AD5EA00929AC8C950033A503 /* HeroData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeroData.swift; sourceTree = ""; }; + AD5EA00729A560AC0033A503 /* CharacterCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCollectionViewCell.swift; sourceTree = ""; }; AD5EA00D29AD42150033A503 /* TriangleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriangleView.swift; sourceTree = ""; }; AD5EA00F29BA193C0033A503 /* DescriptionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionViewController.swift; sourceTree = ""; }; AD6FCEC129C048A000B41E1F /* BrightnessUIColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrightnessUIColorExtension.swift; sourceTree = ""; }; - AD6FCEC429C1A00800B41E1F /* HeroListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeroListViewModel.swift; sourceTree = ""; }; + AD6FCEC429C1A00800B41E1F /* CharactersCollectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharactersCollectionViewModel.swift; sourceTree = ""; }; AD6FCEC629C6F75000B41E1F /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; }; ADD2B05E29E70D9000C747F1 /* AverageUIImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AverageUIImageExtension.swift; sourceTree = ""; }; ADD2B06029E70E1300C747F1 /* FetchUIImageViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchUIImageViewExtension.swift; sourceTree = ""; }; @@ -143,16 +147,17 @@ AD37541729A39A940081B177 /* MarvelApp */ = { isa = PBXGroup; children = ( - ADD2873829F402280090344C /* Models */, - ADD2873729F401E00090344C /* Cells */, + ADD2873829F402280090344C /* CharactersCollectionViewScreen */, ADD2873629F401A10090344C /* Views */, ADD2B05D29E70D5200C747F1 /* Extensions */, - AD6FCEC329C19FF300B41E1F /* ViewControllers */, + AD6FCEC329C19FF300B41E1F /* DescriptionViewScreen */, AD37541829A39A940081B177 /* AppDelegate.swift */, AD37541A29A39A940081B177 /* SceneDelegate.swift */, AD37542129A39A960081B177 /* Assets.xcassets */, AD37542329A39A960081B177 /* LaunchScreen.storyboard */, AD37542629A39A960081B177 /* Info.plist */, + AD27E41929FE801900FFAF28 /* DBManager.swift */, + AD27E41B29FE805400FFAF28 /* Repository.swift */, ); path = MarvelApp; sourceTree = ""; @@ -174,13 +179,13 @@ path = MarvelAppUITests; sourceTree = ""; }; - AD6FCEC329C19FF300B41E1F /* ViewControllers */ = { + AD6FCEC329C19FF300B41E1F /* DescriptionViewScreen */ = { isa = PBXGroup; children = ( AD5EA00F29BA193C0033A503 /* DescriptionViewController.swift */, - AD37541C29A39A940081B177 /* HeroListViewController.swift */, + AD27E41D2A01C78F00FFAF28 /* DescriptionViewModel.swift */, ); - path = ViewControllers; + path = DescriptionViewScreen; sourceTree = ""; }; ADD2873629F401A10090344C /* Views */ = { @@ -197,18 +202,19 @@ isa = PBXGroup; children = ( ADD2B06429EED96D00C747F1 /* LoadingActivityCollectionViewCell.swift */, - AD5EA00729A560AC0033A503 /* HeroCollectionViewCell.swift */, + AD5EA00729A560AC0033A503 /* CharacterCollectionViewCell.swift */, ); path = Cells; sourceTree = ""; }; - ADD2873829F402280090344C /* Models */ = { + ADD2873829F402280090344C /* CharactersCollectionViewScreen */ = { isa = PBXGroup; children = ( - AD6FCEC429C1A00800B41E1F /* HeroListViewModel.swift */, - AD5EA00929AC8C950033A503 /* HeroData.swift */, + ADD2873729F401E00090344C /* Cells */, + AD37541C29A39A940081B177 /* CharactersCollectionViewController.swift */, + AD6FCEC429C1A00800B41E1F /* CharactersCollectionViewModel.swift */, ); - path = Models; + path = CharactersCollectionViewScreen; sourceTree = ""; }; ADD2B05D29E70D5200C747F1 /* Extensions */ = { @@ -395,20 +401,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - AD37541D29A39A940081B177 /* HeroListViewController.swift in Sources */, + AD37541D29A39A940081B177 /* CharactersCollectionViewController.swift in Sources */, AD5EA01029BA193C0033A503 /* DescriptionViewController.swift in Sources */, + AD27E41C29FE805400FFAF28 /* Repository.swift in Sources */, AD6FCEC229C048A000B41E1F /* BrightnessUIColorExtension.swift in Sources */, AD6FCEC729C6F75000B41E1F /* ActivityIndicatorView.swift in Sources */, + AD27E41E2A01C78F00FFAF28 /* DescriptionViewModel.swift in Sources */, ADD2B06529EED96D00C747F1 /* LoadingActivityCollectionViewCell.swift in Sources */, - AD5EA00829A560AC0033A503 /* HeroCollectionViewCell.swift in Sources */, - AD5EA00A29AC8C950033A503 /* HeroData.swift in Sources */, + AD5EA00829A560AC0033A503 /* CharacterCollectionViewCell.swift in Sources */, AD5EA00E29AD42150033A503 /* TriangleView.swift in Sources */, AD37541929A39A940081B177 /* AppDelegate.swift in Sources */, - AD6FCEC529C1A00800B41E1F /* HeroListViewModel.swift in Sources */, + AD6FCEC529C1A00800B41E1F /* CharactersCollectionViewModel.swift in Sources */, AD37541B29A39A940081B177 /* SceneDelegate.swift in Sources */, ADD2B06329E7256200C747F1 /* GradientView.swift in Sources */, ADD2B06129E70E1300C747F1 /* FetchUIImageViewExtension.swift in Sources */, ADD2B05F29E70D9000C747F1 /* AverageUIImageExtension.swift in Sources */, + AD27E41A29FE801900FFAF28 /* DBManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MarvelApp/Cells/HeroCollectionViewCell.swift b/MarvelApp/CharactersCollectionViewScreen/Cells/CharacterCollectionViewCell.swift similarity index 95% rename from MarvelApp/Cells/HeroCollectionViewCell.swift rename to MarvelApp/CharactersCollectionViewScreen/Cells/CharacterCollectionViewCell.swift index 4aca006..f8550d3 100644 --- a/MarvelApp/Cells/HeroCollectionViewCell.swift +++ b/MarvelApp/CharactersCollectionViewScreen/Cells/CharacterCollectionViewCell.swift @@ -1,14 +1,14 @@ import UIKit import CollectionViewPagingLayout -final class HeroCollectionViewCell: UICollectionViewCell { +final class CharacterCollectionViewCell: UICollectionViewCell { struct Model { var name: String var url: URL? } - func setupCell(model: Model) { + func setup(with model: Model) { heroImageView.fetch(from: model.url) imageAverageColor = heroImageView.image?.averageColor?.lighter(by: 30) label.text = model.name @@ -91,7 +91,7 @@ final class HeroCollectionViewCell: UICollectionViewCell { } -extension HeroCollectionViewCell: ScaleTransformView { +extension CharacterCollectionViewCell: ScaleTransformView { var scaleOptions: ScaleTransformViewOptions { .layout(.linear) } diff --git a/MarvelApp/Cells/LoadingActivityCollectionViewCell.swift b/MarvelApp/CharactersCollectionViewScreen/Cells/LoadingActivityCollectionViewCell.swift similarity index 98% rename from MarvelApp/Cells/LoadingActivityCollectionViewCell.swift rename to MarvelApp/CharactersCollectionViewScreen/Cells/LoadingActivityCollectionViewCell.swift index 9dc161e..dbd4b53 100644 --- a/MarvelApp/Cells/LoadingActivityCollectionViewCell.swift +++ b/MarvelApp/CharactersCollectionViewScreen/Cells/LoadingActivityCollectionViewCell.swift @@ -5,7 +5,6 @@ final class LoadingActivityCollectionViewCell: UICollectionViewCell { override init(frame: CGRect) { super.init(frame: .zero) - } required init?(coder: NSCoder) { diff --git a/MarvelApp/ViewControllers/HeroListViewController.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift similarity index 52% rename from MarvelApp/ViewControllers/HeroListViewController.swift rename to MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift index 4a75268..dcf048a 100644 --- a/MarvelApp/ViewControllers/HeroListViewController.swift +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift @@ -1,15 +1,24 @@ import UIKit import CollectionViewPagingLayout +import Kingfisher -final class HeroListViewController: UIViewController { - - private var lastCenterIndexPath: IndexPath? = nil - - private let viewModel = HeroListViewModel() - - private var heroesData = [HeroData]() +final class CharactersCollectionViewController: UIViewController { - private var isLoadingMore = false + enum State { + case loading + case loaded + case connectionError + case showDescriptionScreen(Model) + case changeTriangleColor(Model) + } + + struct Model { + let id: Int + let name: String + let imageURL: String + } + + private var viewModel: CharactersCollectionViewModel private let activityIndicatorView: ActivityIndicatorView = { let activityIndicatorView = ActivityIndicatorView() @@ -19,12 +28,23 @@ final class HeroListViewController: UIViewController { return activityIndicatorView }() + private let connectionErrorLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "No connection, try again later" + label.textColor = .white + label.textAlignment = .center + label.alpha = 0 + return label + }() + private let triangleView: TriangleView = { let pathView = TriangleView() pathView.translatesAutoresizingMaskIntoConstraints = false pathView.backgroundColor = .clear return pathView }() + private let logoImageView: UIImageView = { let imageView = UIImageView() @@ -38,6 +58,7 @@ final class HeroListViewController: UIViewController { private let collectionViewLayout: CollectionViewPagingLayout = { let layout = CollectionViewPagingLayout() + layout.finalizeCollectionViewUpdates() return layout }() @@ -46,7 +67,7 @@ final class HeroListViewController: UIViewController { collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.isPagingEnabled = true collectionView.showsHorizontalScrollIndicator = false - collectionView.register(HeroCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: HeroCollectionViewCell.self)) + collectionView.register(CharacterCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: CharacterCollectionViewCell.self)) collectionView.backgroundColor = .clear collectionView.dataSource = self collectionView.delegate = self @@ -63,24 +84,49 @@ final class HeroListViewController: UIViewController { label.backgroundColor = .clear return label }() + + + init(viewModel: CharactersCollectionViewModel) { + 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() view.backgroundColor = UIColor(named: "BackgroundColor") - activityIndicatorView.start() - isLoadingMore = true - viewModel.fetchHeroes( - compleation: { [weak self] newHeroesData, offset in - self?.isLoadingMore = false - self?.heroesData.append(contentsOf: newHeroesData) + viewModel.onChangeViewState = {[weak self] state in + switch state { + case .loading: + UIView.animate(withDuration: 0.5) { [weak self] in + self?.connectionErrorLabel.alpha = 0 + } + self?.activityIndicatorView.start() + + case .loaded: self?.collectionView.reloadData() - self?.collectionViewLayout.setCurrentPage(offset) self?.activityIndicatorView.stop() - }, - failed: { - print("failed to fetch data") + + case .connectionError: + UIView.animate(withDuration: 0.5) { [weak self] in + self?.connectionErrorLabel.alpha = 1 + } + + case .changeTriangleColor(let character): + self?.changeTriangleColor(character: character) + + case .showDescriptionScreen(let character): + guard let descriptionViewController = self?.descriptionViewController else { + return + } + descriptionViewController.viewModel.start(with: character.id) + self?.navigationController?.pushViewController(descriptionViewController, animated: true) } - ) + } + viewModel.start() setupViewLayout() } @@ -89,15 +135,26 @@ final class HeroListViewController: UIViewController { view.addSubview(logoImageView) view.addSubview(label) view.addSubview(collectionView) + view.addSubview(connectionErrorLabel) view.addSubview(activityIndicatorView) - setupTriangle() - setupMarvelLogo() - setupLabel() - setupHeroesCollection() - setupLoadingView() + setupConstraintsTriangle() + setupConstraintsMarvelLogo() + setupConstraintsLabel() + setupConstraintsHeroesCollection() + setupConstraintsLoadingView() + setupConstraintsConnectionLabel() } - private func setupTriangle() { + private func setupConstraintsConnectionLabel() { + NSLayoutConstraint.activate([ + connectionErrorLabel.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), + connectionErrorLabel.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), + connectionErrorLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + connectionErrorLabel.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor) + ]) + } + + private func setupConstraintsTriangle() { NSLayoutConstraint.activate([ triangleView.topAnchor.constraint(equalTo: view.topAnchor), triangleView.bottomAnchor.constraint(equalTo: view.bottomAnchor), @@ -106,7 +163,7 @@ final class HeroListViewController: UIViewController { ]) } - private func setupMarvelLogo() { + private func setupConstraintsMarvelLogo() { NSLayoutConstraint.activate([ logoImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), logoImageView.heightAnchor.constraint(equalToConstant: 25), @@ -115,7 +172,7 @@ final class HeroListViewController: UIViewController { ]) } - private func setupLabel() { + private func setupConstraintsLabel() { NSLayoutConstraint.activate([ label.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 20), label.heightAnchor.constraint(equalToConstant: 75), @@ -124,7 +181,7 @@ final class HeroListViewController: UIViewController { ]) } - private func setupHeroesCollection() { + private func setupConstraintsHeroesCollection() { NSLayoutConstraint.activate([ collectionView.topAnchor.constraint(equalTo: label.bottomAnchor), collectionView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor), @@ -132,7 +189,7 @@ final class HeroListViewController: UIViewController { ]) } - private func setupLoadingView() { + private func setupConstraintsLoadingView() { NSLayoutConstraint.activate([ activityIndicatorView.topAnchor.constraint(equalTo: view.topAnchor), activityIndicatorView.bottomAnchor.constraint(equalTo: view.bottomAnchor), @@ -142,63 +199,62 @@ final class HeroListViewController: UIViewController { } } -extension HeroListViewController: UICollectionViewDataSource, UICollectionViewDelegate { +extension CharactersCollectionViewController: UICollectionViewDataSource, UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - heroesData.count + return viewModel.charactersCount() } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: HeroCollectionViewCell.self), for: indexPath) as? HeroCollectionViewCell else { - return HeroCollectionViewCell() + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CharacterCollectionViewCell.self), for: indexPath) + guard let cell = cell as? CharacterCollectionViewCell else { + return cell } - let hero = heroesData[indexPath.item] - cell.setupCell(model: HeroCollectionViewCell.Model(name: hero.name, url: URL(string: hero.imageURL))) + let character = viewModel.onCellDeque(at: indexPath.item) + cell.setup(with: CharacterCollectionViewCell.Model(name: character.name, url: URL(string: character.imageURL))) + return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let hero = heroesData[indexPath.item] - let model = DescriptionViewController.Model(url: URL(string: hero.imageURL), name: hero.name, description: hero.description) - descriptionViewController.setup(model) - navigationController?.pushViewController(descriptionViewController, animated: true) + viewModel.onCharacterCellTapped(at: indexPath.item) } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - guard findCenterIndexPath() == lastCenterIndexPath else { - return - } - lastCenterIndexPath = findCenterIndexPath() - guard let lastCenterIndex = lastCenterIndexPath else { - return - } - let cell = collectionView.cellForItem(at: lastCenterIndex) as? HeroCollectionViewCell - guard let cell = cell else { - return - } - triangleView.color = cell.imageAverageColor + guard scrollView == collectionView else { return } + let index = findCenterIndex() + guard let index = index else { return } + viewModel.onDeceleratingEnd(at: index.item) } func scrollViewDidScroll(_ scrollView: UIScrollView) { - if (scrollView.contentOffset.x - scrollView.contentSize.width) > CGFloat(-420) && !isLoadingMore { - isLoadingMore = true - viewModel.fetchHeroes( - compleation: { [weak self] newHeroesData, offset in - self?.heroesData.append(contentsOf: newHeroesData) - self?.collectionView.reloadData() - self?.collectionViewLayout.setCurrentPage(offset - 1) - self?.isLoadingMore = false - }, - failed: { [weak self] in - self?.isLoadingMore = false - } - ) + if (scrollView.contentOffset.x - scrollView.contentSize.width) > CGFloat(-350) { + viewModel.onPullToRefresh() } } - private func findCenterIndexPath() -> IndexPath? { + private func findCenterIndex() -> IndexPath? { let center = self.view.convert(self.collectionView.center, to: self.collectionView) let index = collectionView.indexPathForItem(at: center) return index } + + private func changeTriangleColor(character: Model) { + guard let url: URL = URL(string: character.imageURL) else { + return + } + let imageResource = ImageResource(downloadURL: url) + + KingfisherManager.shared.retrieveImage(with: imageResource, options: nil, progressBlock: nil, completionHandler: { + result in + switch result { + case .success(let result): + self.triangleView.color = result.image.averageColor + case .failure(_): + self.triangleView.color = .clear + } + }) + } } diff --git a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift new file mode 100644 index 0000000..e3dff66 --- /dev/null +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift @@ -0,0 +1,80 @@ +import Alamofire +import RealmSwift + +protocol CharactersCollectionViewModel: AnyObject { + var onChangeViewState: ((CharactersCollectionViewController.State) -> Void)? { get set } + func start() + func onPullToRefresh() + func onCellDeque(at index: Int) -> CharactersCollectionViewController.Model + func onCharacterCellTapped(at index: Int) + func onChangingTriangleColor(at index: Int) -> CharactersCollectionViewController.Model + func onDeceleratingEnd(at index: Int) + func charactersCount() -> Int +} + +final class CharactersCollectionViewModelImpl: CharactersCollectionViewModel{ + + private var characters: [CharactersCollectionViewController.Model] + private var repository: CharactersRepository + private var offset: Int + + init(repository: CharactersRepository) { + self.repository = repository + self.characters = [] + self.offset = 0 + } + + var onChangeViewState: ((CharactersCollectionViewController.State) -> Void)? + + func start() { + fetchCharacters() + } + + func onPullToRefresh() { + fetchCharacters() + } + + func onCellDeque(at index: Int) -> CharactersCollectionViewController.Model { + return characters[index] + } + + func onCharacterCellTapped(at index: Int){ + onChangeViewState?(.showDescriptionScreen(characters[index])) + } + + func onChangingTriangleColor(at index: Int) -> CharactersCollectionViewController.Model { + return characters[index] + } + func onDeceleratingEnd(at index: Int) { + onChangeViewState?(.changeTriangleColor(characters[index])) + } + func charactersCount() -> Int { + return characters.count + } + + private func fetchCharacters() { + onChangeViewState?(.loading) + repository.fetchCharacters(offset: offset) { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let moreCharacters): + self.characters += moreCharacters + self.offset += moreCharacters.count + self.onChangeViewState?(.loaded) + print(self.characters.count) + case .failure(let error as CharactersRepositoryImpl.MyCustomError): + switch error { + case .offlineCharacters(let savedCharacters): + self.characters += savedCharacters + self.onChangeViewState?(.loaded) + + case .offlineCharacter(_): + break + } + + case .failure(_): + self.onChangeViewState?(.connectionError) + } + } + } +} diff --git a/MarvelApp/DBManager.swift b/MarvelApp/DBManager.swift new file mode 100644 index 0000000..af5b09d --- /dev/null +++ b/MarvelApp/DBManager.swift @@ -0,0 +1,62 @@ +import RealmSwift + +@objcMembers +class CharacterModel: Object { + @Persisted(primaryKey: true) var id: Int + @Persisted var name: String + @Persisted var imageUrl: String + @Persisted var descriptions: String + + convenience init(id: Int, name:String, imageUrl: String, description: String) { + self.init() + self.id = id + self.name = name + self.imageUrl = imageUrl + self.descriptions = description + } +} + +protocol CharacterDataBaseManager: AnyObject{ + func saveCharacter(_ character: CharacterModel) + func getCharacters() -> [CharacterModel] + func getCharacter(by id: Int) -> CharacterModel? +} + +final class DataBaseManagerImpl { + private let realm = try? Realm() + + private func add(_ object: T) { + try? realm?.write { + realm?.add(object, update: .modified) + } + } + + private func get(by id: Int) -> T? { + guard let object = realm?.object(ofType: T.self, forPrimaryKey: id) else { + return nil + } + return object + } + + private func getAll() -> [T] { + guard let objects = realm?.objects(T.self) else { + return [] + } + return Array(objects) + } +} + +extension DataBaseManagerImpl: CharacterDataBaseManager { + + func saveCharacter(_ character: CharacterModel) { + add(character) + } + + func getCharacters() -> [CharacterModel] { + return getAll() + } + + func getCharacter(by id: Int) -> CharacterModel? { + return get(by: id) + } +} diff --git a/MarvelApp/ViewControllers/DescriptionViewController.swift b/MarvelApp/DescriptionViewScreen/DescriptionViewController.swift similarity index 74% rename from MarvelApp/ViewControllers/DescriptionViewController.swift rename to MarvelApp/DescriptionViewScreen/DescriptionViewController.swift index db7072a..3cd7863 100644 --- a/MarvelApp/ViewControllers/DescriptionViewController.swift +++ b/MarvelApp/DescriptionViewScreen/DescriptionViewController.swift @@ -2,18 +2,30 @@ import UIKit final class DescriptionViewController: UIViewController { + enum State { + case loaded(Model) + } + struct Model { let url: URL? let name: String let description: String } + let viewModel = DescriptionViewModelImpl() + private let imageView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleAspectFill + imageView.contentMode = .scaleToFill return imageView }() + private var gradientView: GradientView = { + let gradientView = GradientView(gradientStartColor: UIColor.black.withAlphaComponent(0), gradientEndColor: UIColor.black.withAlphaComponent(0.5)) + gradientView.translatesAutoresizingMaskIntoConstraints = false + return gradientView + }() + private let nameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -34,13 +46,6 @@ final class DescriptionViewController: UIViewController { return textView }() - private var gradientView: GradientView = { - let gradientView = GradientView(gradientStartColor: UIColor.black.withAlphaComponent(0), gradientEndColor: UIColor.black.withAlphaComponent(0.5)) - gradientView.translatesAutoresizingMaskIntoConstraints = false - return gradientView - }() - - override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black @@ -49,32 +54,39 @@ final class DescriptionViewController: UIViewController { view.addSubview(nameLabel) view.addSubview(descriptionTextView) - setupImageView() - setupNameLabel() - setupDescriptionLabel() - setupGradientView() + setupConstraintsImageView() + setupConstraintsNameLabel() + setupConstraintsDescriptionLabel() + setupConstraintsGradientView() + + viewModel.onChangeViewState = {[weak self] state in + switch state { + case .loaded(let character): + self?.setup(character) + } + } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) } - func setup(_ model: Model) { + private func setup(_ model: Model) { nameLabel.text = model.name descriptionTextView.text = model.description imageView.fetch(from: model.url) } - private func setupImageView() { + private func setupConstraintsImageView() { NSLayoutConstraint.activate([ - imageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - imageView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - imageView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), - imageView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor) + imageView.topAnchor.constraint(equalTo: view.topAnchor), + imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + imageView.leftAnchor.constraint(equalTo: view.leftAnchor), + imageView.rightAnchor.constraint(equalTo: view.rightAnchor) ]) } - private func setupNameLabel() { + private func setupConstraintsNameLabel() { NSLayoutConstraint.activate([ nameLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor), nameLabel.leftAnchor.constraint(equalTo: view.leftAnchor), @@ -82,7 +94,7 @@ final class DescriptionViewController: UIViewController { ]) } - private func setupDescriptionLabel() { + private func setupConstraintsDescriptionLabel() { NSLayoutConstraint.activate([ descriptionTextView.topAnchor.constraint(equalTo: nameLabel.safeAreaLayoutGuide.bottomAnchor, constant: 10), descriptionTextView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), @@ -91,7 +103,7 @@ final class DescriptionViewController: UIViewController { ]) } - private func setupGradientView() { + private func setupConstraintsGradientView() { NSLayoutConstraint.activate([ gradientView.topAnchor.constraint(equalTo: view.topAnchor), gradientView.bottomAnchor.constraint(equalTo: view.bottomAnchor), diff --git a/MarvelApp/DescriptionViewScreen/DescriptionViewModel.swift b/MarvelApp/DescriptionViewScreen/DescriptionViewModel.swift new file mode 100644 index 0000000..34657e7 --- /dev/null +++ b/MarvelApp/DescriptionViewScreen/DescriptionViewModel.swift @@ -0,0 +1,39 @@ +protocol DescriptionViewModel: AnyObject { + var onChangeViewState: ((DescriptionViewController.State) -> Void)? { get set } + func start(with id: Int) +} + +final class DescriptionViewModelImpl: DescriptionViewModel { + private let repository = CharactersRepositoryImpl() + var onChangeViewState: ((DescriptionViewController.State) -> Void)? + + func start(with id: Int) { + fetchCharacter(by: id) + } + + private func fetchCharacter(by id: Int) { + repository.fetchCharacter(by: id) { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let character): + guard let character = character else { + return + } + self.onChangeViewState?(.loaded(character)) + case .failure(let error as CharactersRepositoryImpl.MyCustomError): + switch error { + case .offlineCharacter(let savedCharacter): + guard let character = savedCharacter else { + return + } + self.onChangeViewState?(.loaded(character)) + case .offlineCharacters(_): + break + } + + case .failure(_): + break + } + } + } +} diff --git a/MarvelApp/Extensions/AverageUIImageExtension.swift b/MarvelApp/Extensions/AverageUIImageExtension.swift index 2b01b50..9f3c9a6 100644 --- a/MarvelApp/Extensions/AverageUIImageExtension.swift +++ b/MarvelApp/Extensions/AverageUIImageExtension.swift @@ -10,10 +10,11 @@ extension UIImage { guard let outputImage = filter.outputImage else { return nil } var bitmap = [UInt8](repeating: 0, count: 4) - let context = CIContext(options: [.workingColorSpace: kCFNull]) + let context = CIContext(options: [.workingColorSpace: kCFNull as Any]) context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil) return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255) } } + diff --git a/MarvelApp/Models/HeroData.swift b/MarvelApp/Models/HeroData.swift deleted file mode 100644 index 7424ec5..0000000 --- a/MarvelApp/Models/HeroData.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -struct HeroData { - let name: String - let description: String - let imageURL: String -} diff --git a/MarvelApp/Models/HeroListViewModel.swift b/MarvelApp/Models/HeroListViewModel.swift deleted file mode 100644 index 5b0e58c..0000000 --- a/MarvelApp/Models/HeroListViewModel.swift +++ /dev/null @@ -1,118 +0,0 @@ -import Alamofire -import RealmSwift - -final class HeroListViewModel { - private let base_url = "https://gateway.marvel.com" - private let heroes_endpoint = "/v1/public/characters" - private var offset = 0 - - private struct CharacterDataWrapper: Decodable { - var code: Int - var status: String - var data: CharacterDataContainer? - - enum CodingKeys: String, CodingKey { - case code = "code" - case status = "status" - case data = "data" - } - - init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - - code = try values.decode(Int.self, forKey: .code) - status = try values.decode(String.self, forKey: .status) - data = try values.decode(CharacterDataContainer.self, forKey: .data) - } - } - - private struct CharacterDataContainer: Decodable { - var offset: Int - var limit: Int - var total: Int - var count: Int - var results: [Character]? - - enum CodingKeys: String, CodingKey { - case offset = "offset" - case limit = "limit" - case total = "total" - case count = "count" - case results = "results" - } - init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - - offset = try values.decode(Int.self, forKey: .offset) - limit = try values.decode(Int.self, forKey: .limit) - total = try values.decode(Int.self, forKey: .total) - count = try values.decode(Int.self, forKey: .count) - results = try values.decode([Character].self, forKey: .results) - } - } - - private struct Character: Decodable { - var id: Int - var name: String - var description: String - var thumbnail: Thumbnail - - enum CodingKeys: String, CodingKey { - case id = "id" - case name = "name" - case description = "description" - case thumbnail = "thumbnail" - } - - init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - - id = try values.decode(Int.self, forKey: .id) - name = try values.decode(String.self, forKey: .name) - description = try values.decode(String.self, forKey: .description) - thumbnail = try values.decode(Thumbnail.self, forKey: .thumbnail) - } - } - - private struct Thumbnail: Decodable { - var path: String - var ext: String //extension - enum CodingKeys: String, CodingKey { - case path = "path" - case ext = "extension" - } - - init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - path = try values.decode(String.self, forKey: .path) - ext = try values.decode(String.self, forKey: .ext) - } - } - - func fetchHeroes(compleation: @escaping ([HeroData], Int) -> Void, failed: @escaping () -> Void ) { - let authParams = ["ts": "123", "apikey": "42597bee717ef2847e9b63553f4aff0f", "hash": "f49ba2754d66300142cf36b108860d2c", "offset": offset] as [String : Any] - var heroesData: [HeroData] = [] - AF.request(base_url + heroes_endpoint, method: .get, parameters: authParams) - .responseDecodable(of: CharacterDataWrapper.self) { response in - switch response.result { - case .success(_): { - guard - let dataWrapper = response.value, - let data = dataWrapper.data, - let results = data.results - else { - return - } - for character in results { - heroesData.append(HeroData(name: character.name, description: character.description, imageURL: character.thumbnail.path + "." + character.thumbnail.ext)) - } - compleation(heroesData, self.offset) - self.offset += heroesData.count - }() - case .failure(_): - failed() - } - } - } -} - diff --git a/MarvelApp/Repository.swift b/MarvelApp/Repository.swift new file mode 100644 index 0000000..4c48b0f --- /dev/null +++ b/MarvelApp/Repository.swift @@ -0,0 +1,208 @@ +import Alamofire + +protocol CharactersRepository { + func fetchCharacters(offset: Int, closure: @escaping (Result<([CharactersCollectionViewController.Model]), Error>) -> Void) +} + +final class CharactersRepositoryImpl: CharactersRepository { + + private let db: DataBaseManagerImpl + + init(){ + self.db = DataBaseManagerImpl() + } + private var isLoading: Bool = false + + func fetchCharacters(offset: Int, closure: @escaping (Result<[CharactersCollectionViewController.Model], Error>) -> Void) { + guard !isLoading else { + return + } + let authParams = [ + "ts": "123", + "apikey": "42597bee717ef2847e9b63553f4aff0f", + "hash": "f49ba2754d66300142cf36b108860d2c", + "offset": offset, + "limit": 10 + ] as [String : Any] + + var result: [CharactersCollectionViewController.Model] = [] + isLoading = true + AF.request("https://gateway.marvel.com/v1/public/characters", method: .get, parameters: authParams) + .responseDecodable(of: CharacterDataWrapper.self) {[weak self] response in + switch response.result { + case .success(_): + guard + let dataWrapper = response.value, + let data = dataWrapper.data, + let results = data.results + else { + return + } + for character in results { + let id = character.id + let name = character.name + let description = character.description + let imageURL = character.thumbnail.path + "." + character.thumbnail.ext + result.append(CharactersCollectionViewController.Model( + id: id, name: name, + imageURL: imageURL + )) + self?.db.saveCharacter(CharacterModel(id: id, name: name, imageUrl: imageURL, description: description)) + } + closure(.success(result)) + self?.isLoading = false + case let .failure(error): + guard let characters = self?.db.getCharacters() else { + return + } + if characters.isEmpty || offset != 0 { + closure(.failure(error)) + print("Fuck") + return + } + for character in characters { + let id = character.id + let name = character.name + let imageURL = character.imageUrl + result.append(CharactersCollectionViewController.Model(id: id, name: name, imageURL: imageURL)) + } + closure(.failure(MyCustomError.offlineCharacters(result))) + self?.isLoading = false + } + } + } + + func fetchCharacter(by id: Int, closure: @escaping (Result) -> Void) { + guard !isLoading else { + return + } + let authParams = [ + "ts": "123", + "apikey": "42597bee717ef2847e9b63553f4aff0f", + "hash": "f49ba2754d66300142cf36b108860d2c" + ] as [String : Any] + + var result: DescriptionViewController.Model? = nil + isLoading = true + AF.request("https://gateway.marvel.com/v1/public/characters/" + String(id), method: .get, parameters: authParams) + .responseDecodable(of: CharacterDataWrapper.self) {[weak self] response in + switch response.result { + case .success(_): + guard + let dataWrapper = response.value, + let data = dataWrapper.data, + let results = data.results + else { + return + } + for character in results { + let name = character.name + let description = character.description + let imageURL = character.thumbnail.path + "." + character.thumbnail.ext + result = DescriptionViewController.Model( + url: URL(string: imageURL), name: name, + description: description + ) + } + closure(.success(result)) + self?.isLoading = false + case .failure(_): + guard let character = self?.db.getCharacter(by: id) else { + return + } + result = DescriptionViewController.Model(url: URL(string: character.imageUrl), name: character.name, description: character.description) + closure(.failure(MyCustomError.offlineCharacter(result))) + self?.isLoading = false + } + } + } + + enum MyCustomError: Error { + case offlineCharacters([CharactersCollectionViewController.Model]) + case offlineCharacter(DescriptionViewController.Model?) + } + + private struct CharacterDataWrapper: Decodable { + var code: Int + var status: String + var data: CharacterDataContainer? + + enum CodingKeys: String, CodingKey { + case code = "code" + case status = "status" + case data = "data" + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + code = try values.decode(Int.self, forKey: .code) + status = try values.decode(String.self, forKey: .status) + data = try values.decode(CharacterDataContainer.self, forKey: .data) + } + } + + private struct CharacterDataContainer: Decodable { + var offset: Int + var limit: Int + var total: Int + var count: Int + var results: [Character]? + + enum CodingKeys: String, CodingKey { + case offset = "offset" + case limit = "limit" + case total = "total" + case count = "count" + case results = "results" + } + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + offset = try values.decode(Int.self, forKey: .offset) + limit = try values.decode(Int.self, forKey: .limit) + total = try values.decode(Int.self, forKey: .total) + count = try values.decode(Int.self, forKey: .count) + results = try values.decode([Character].self, forKey: .results) + } + } + + private struct Character: Decodable { + var id: Int + var name: String + var description: String + var thumbnail: Thumbnail + + enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + case description = "description" + case thumbnail = "thumbnail" + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + id = try values.decode(Int.self, forKey: .id) + name = try values.decode(String.self, forKey: .name) + description = try values.decode(String.self, forKey: .description) + thumbnail = try values.decode(Thumbnail.self, forKey: .thumbnail) + } + } + + private struct Thumbnail: Decodable { + var path: String + var ext: String //extension + enum CodingKeys: String, CodingKey { + case path = "path" + case ext = "extension" + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + path = try values.decode(String.self, forKey: .path) + ext = try values.decode(String.self, forKey: .ext) + } + } +} + diff --git a/MarvelApp/SceneDelegate.swift b/MarvelApp/SceneDelegate.swift index 1054798..928a5f7 100644 --- a/MarvelApp/SceneDelegate.swift +++ b/MarvelApp/SceneDelegate.swift @@ -9,7 +9,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window = UIWindow(frame: windowScene.coordinateSpace.bounds) window?.windowScene = windowScene - let startController = HeroListViewController() + let viewModel = CharactersCollectionViewModelImpl(repository: CharactersRepositoryImpl()) + let startController = CharactersCollectionViewController(viewModel: viewModel) let navigationController = UINavigationController(rootViewController: startController) navigationController.navigationBar.backgroundColor = .clear window?.rootViewController = navigationController diff --git a/MarvelApp/Views/ActivityIndicatorView.swift b/MarvelApp/Views/ActivityIndicatorView.swift index 1c322cb..74039ce 100644 --- a/MarvelApp/Views/ActivityIndicatorView.swift +++ b/MarvelApp/Views/ActivityIndicatorView.swift @@ -17,6 +17,7 @@ final class ActivityIndicatorView: UIView { private lazy var visualEffectView: UIVisualEffectView = { let visualEffectView = UIVisualEffectView() + visualEffectView.translatesAutoresizingMaskIntoConstraints = false visualEffectView.effect = self.blurEffect return visualEffectView }() @@ -54,14 +55,14 @@ final class ActivityIndicatorView: UIView { private func setupActivityIndiactor() { NSLayoutConstraint.activate([ - activityIndicator.topAnchor.constraint(equalTo: self.topAnchor), - activityIndicator.bottomAnchor.constraint(equalTo: self.bottomAnchor), - activityIndicator.leftAnchor.constraint(equalTo: self.leftAnchor), - activityIndicator.rightAnchor.constraint(equalTo: self.rightAnchor) + activityIndicator.centerXAnchor.constraint(equalTo: visualEffectView.centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: visualEffectView.centerYAnchor), + activityIndicator.heightAnchor.constraint(equalTo: visualEffectView.heightAnchor), + activityIndicator.widthAnchor.constraint(equalTo: visualEffectView.widthAnchor) ]) } - init() { + override init(frame: CGRect) { super.init(frame: .zero) setLayout() }