From 39d0f68e856c929b5f813ab4ada479a39174e3ca Mon Sep 17 00:00:00 2001 From: Abiteev Alihan Date: Sun, 30 Apr 2023 16:32:21 +0600 Subject: [PATCH 1/7] Change project dir strucutre, rename classes --- MarvelApp.xcodeproj/project.pbxproj | 42 +++++++++---------- .../Cells/CharacterCollectionViewCell.swift} | 4 +- .../LoadingActivityCollectionViewCell.swift | 0 .../CharactersCollectionViewController.swift} | 26 +++++++----- .../CharactersCollectionViewModel.swift} | 8 ++-- .../DescriptionViewController.swift | 23 +++++----- .../Extensions/AverageUIImageExtension.swift | 1 + MarvelApp/Models/HeroData.swift | 7 ---- MarvelApp/SceneDelegate.swift | 2 +- MarvelApp/Views/ActivityIndicatorView.swift | 11 ++--- 10 files changed, 61 insertions(+), 63 deletions(-) rename MarvelApp/{Cells/HeroCollectionViewCell.swift => CharactersCollectionViewScreen/Cells/CharacterCollectionViewCell.swift} (96%) rename MarvelApp/{ => CharactersCollectionViewScreen}/Cells/LoadingActivityCollectionViewCell.swift (100%) rename MarvelApp/{ViewControllers/HeroListViewController.swift => CharactersCollectionViewScreen/CharactersCollectionViewController.swift} (89%) rename MarvelApp/{Models/HeroListViewModel.swift => CharactersCollectionViewScreen/CharactersCollectionViewModel.swift} (89%) rename MarvelApp/{ViewControllers => DescriptionViewScreen}/DescriptionViewController.swift (89%) delete mode 100644 MarvelApp/Models/HeroData.swift diff --git a/MarvelApp.xcodeproj/project.pbxproj b/MarvelApp.xcodeproj/project.pbxproj index da2bbcf..85e2206 100644 --- a/MarvelApp.xcodeproj/project.pbxproj +++ b/MarvelApp.xcodeproj/project.pbxproj @@ -10,18 +10,17 @@ 6E0AA1ECEBDC56A5A73119D6 /* Pods_MarvelApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4D525B3FE63D0AD96070A87 /* Pods_MarvelApp.framework */; }; 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 */; }; @@ -52,7 +51,7 @@ 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 +60,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,11 +141,10 @@ 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 */, @@ -174,13 +171,12 @@ path = MarvelAppUITests; sourceTree = ""; }; - AD6FCEC329C19FF300B41E1F /* ViewControllers */ = { + AD6FCEC329C19FF300B41E1F /* DescriptionViewScreen */ = { isa = PBXGroup; children = ( AD5EA00F29BA193C0033A503 /* DescriptionViewController.swift */, - AD37541C29A39A940081B177 /* HeroListViewController.swift */, ); - path = ViewControllers; + path = DescriptionViewScreen; sourceTree = ""; }; ADD2873629F401A10090344C /* Views */ = { @@ -197,18 +193,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,16 +392,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - AD37541D29A39A940081B177 /* HeroListViewController.swift in Sources */, + AD37541D29A39A940081B177 /* CharactersCollectionViewController.swift in Sources */, AD5EA01029BA193C0033A503 /* DescriptionViewController.swift in Sources */, AD6FCEC229C048A000B41E1F /* BrightnessUIColorExtension.swift in Sources */, AD6FCEC729C6F75000B41E1F /* ActivityIndicatorView.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 */, diff --git a/MarvelApp/Cells/HeroCollectionViewCell.swift b/MarvelApp/CharactersCollectionViewScreen/Cells/CharacterCollectionViewCell.swift similarity index 96% rename from MarvelApp/Cells/HeroCollectionViewCell.swift rename to MarvelApp/CharactersCollectionViewScreen/Cells/CharacterCollectionViewCell.swift index 4aca006..0014679 100644 --- a/MarvelApp/Cells/HeroCollectionViewCell.swift +++ b/MarvelApp/CharactersCollectionViewScreen/Cells/CharacterCollectionViewCell.swift @@ -1,7 +1,7 @@ import UIKit import CollectionViewPagingLayout -final class HeroCollectionViewCell: UICollectionViewCell { +final class CharacterCollectionViewCell: UICollectionViewCell { struct Model { var name: String @@ -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 100% rename from MarvelApp/Cells/LoadingActivityCollectionViewCell.swift rename to MarvelApp/CharactersCollectionViewScreen/Cells/LoadingActivityCollectionViewCell.swift diff --git a/MarvelApp/ViewControllers/HeroListViewController.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift similarity index 89% rename from MarvelApp/ViewControllers/HeroListViewController.swift rename to MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift index 4a75268..a3f7c76 100644 --- a/MarvelApp/ViewControllers/HeroListViewController.swift +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift @@ -1,13 +1,19 @@ import UIKit import CollectionViewPagingLayout -final class HeroListViewController: UIViewController { - +final class CharactersCollectionViewController: UIViewController { + + struct Model { + let name: String + let description: String + let imageURL: String + } + private var lastCenterIndexPath: IndexPath? = nil - private let viewModel = HeroListViewModel() + private let viewModel = CharactersCollectionViewModel() - private var heroesData = [HeroData]() + private var heroesData = [Model]() private var isLoadingMore = false @@ -46,7 +52,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 @@ -142,17 +148,17 @@ final class HeroListViewController: UIViewController { } } -extension HeroListViewController: UICollectionViewDataSource, UICollectionViewDelegate { +extension CharactersCollectionViewController: UICollectionViewDataSource, UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { heroesData.count } 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() + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CharacterCollectionViewCell.self), for: indexPath) as? CharacterCollectionViewCell else { + return CharacterCollectionViewCell() } let hero = heroesData[indexPath.item] - cell.setupCell(model: HeroCollectionViewCell.Model(name: hero.name, url: URL(string: hero.imageURL))) + cell.setupCell(model: CharacterCollectionViewCell.Model(name: hero.name, url: URL(string: hero.imageURL))) return cell } @@ -172,7 +178,7 @@ extension HeroListViewController: UICollectionViewDataSource, UICollectionViewDe guard let lastCenterIndex = lastCenterIndexPath else { return } - let cell = collectionView.cellForItem(at: lastCenterIndex) as? HeroCollectionViewCell + let cell = collectionView.cellForItem(at: lastCenterIndex) as? CharacterCollectionViewCell guard let cell = cell else { return } diff --git a/MarvelApp/Models/HeroListViewModel.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift similarity index 89% rename from MarvelApp/Models/HeroListViewModel.swift rename to MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift index 5b0e58c..d968b48 100644 --- a/MarvelApp/Models/HeroListViewModel.swift +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift @@ -1,7 +1,7 @@ import Alamofire import RealmSwift -final class HeroListViewModel { +final class CharactersCollectionViewModel { private let base_url = "https://gateway.marvel.com" private let heroes_endpoint = "/v1/public/characters" private var offset = 0 @@ -89,9 +89,9 @@ final class HeroListViewModel { } } - func fetchHeroes(compleation: @escaping ([HeroData], Int) -> Void, failed: @escaping () -> Void ) { + func fetchHeroes(compleation: @escaping ([CharactersCollectionViewController.Model], Int) -> Void, failed: @escaping () -> Void ) { let authParams = ["ts": "123", "apikey": "42597bee717ef2847e9b63553f4aff0f", "hash": "f49ba2754d66300142cf36b108860d2c", "offset": offset] as [String : Any] - var heroesData: [HeroData] = [] + var heroesData: [CharactersCollectionViewController.Model] = [] AF.request(base_url + heroes_endpoint, method: .get, parameters: authParams) .responseDecodable(of: CharacterDataWrapper.self) { response in switch response.result { @@ -104,7 +104,7 @@ final class HeroListViewModel { return } for character in results { - heroesData.append(HeroData(name: character.name, description: character.description, imageURL: character.thumbnail.path + "." + character.thumbnail.ext)) + heroesData.append(CharactersCollectionViewController.Model(name: character.name, description: character.description, imageURL: character.thumbnail.path + "." + character.thumbnail.ext)) } compleation(heroesData, self.offset) self.offset += heroesData.count diff --git a/MarvelApp/ViewControllers/DescriptionViewController.swift b/MarvelApp/DescriptionViewScreen/DescriptionViewController.swift similarity index 89% rename from MarvelApp/ViewControllers/DescriptionViewController.swift rename to MarvelApp/DescriptionViewScreen/DescriptionViewController.swift index db7072a..ff37476 100644 --- a/MarvelApp/ViewControllers/DescriptionViewController.swift +++ b/MarvelApp/DescriptionViewScreen/DescriptionViewController.swift @@ -7,13 +7,20 @@ final class DescriptionViewController: UIViewController { let name: String let description: String } + 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,12 +41,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() @@ -67,10 +68,10 @@ final class DescriptionViewController: UIViewController { private func setupImageView() { 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) ]) } diff --git a/MarvelApp/Extensions/AverageUIImageExtension.swift b/MarvelApp/Extensions/AverageUIImageExtension.swift index 2b01b50..2fbba99 100644 --- a/MarvelApp/Extensions/AverageUIImageExtension.swift +++ b/MarvelApp/Extensions/AverageUIImageExtension.swift @@ -17,3 +17,4 @@ extension UIImage { } } + \ No newline at end of file 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/SceneDelegate.swift b/MarvelApp/SceneDelegate.swift index 1054798..0e156d1 100644 --- a/MarvelApp/SceneDelegate.swift +++ b/MarvelApp/SceneDelegate.swift @@ -9,7 +9,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window = UIWindow(frame: windowScene.coordinateSpace.bounds) window?.windowScene = windowScene - let startController = HeroListViewController() + let startController = CharactersCollectionViewController() 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() } From 6790d9131175c00c261588d1173fcb116136aab2 Mon Sep 17 00:00:00 2001 From: Abiteev Alihan Date: Mon, 1 May 2023 04:38:55 +0600 Subject: [PATCH 2/7] Add DBManager and Repository from lab4, remove logic from controller to view model --- MarvelApp.xcodeproj/project.pbxproj | 8 + .../LoadingActivityCollectionViewCell.swift | 1 - .../CharactersCollectionViewController.swift | 62 ++++--- .../CharactersCollectionViewModel.swift | 146 +++++----------- MarvelApp/DBManager.swift | 62 +++++++ MarvelApp/Repository.swift | 157 ++++++++++++++++++ MarvelApp/SceneDelegate.swift | 3 +- 7 files changed, 301 insertions(+), 138 deletions(-) create mode 100644 MarvelApp/DBManager.swift create mode 100644 MarvelApp/Repository.swift diff --git a/MarvelApp.xcodeproj/project.pbxproj b/MarvelApp.xcodeproj/project.pbxproj index 85e2206..7bffd8c 100644 --- a/MarvelApp.xcodeproj/project.pbxproj +++ b/MarvelApp.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* 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 */; }; AD37541929A39A940081B177 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD37541829A39A940081B177 /* AppDelegate.swift */; }; AD37541B29A39A940081B177 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD37541A29A39A940081B177 /* SceneDelegate.swift */; }; AD37541D29A39A940081B177 /* CharactersCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD37541C29A39A940081B177 /* CharactersCollectionViewController.swift */; }; @@ -48,6 +50,8 @@ /* 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 = ""; }; 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 = ""; }; @@ -150,6 +154,8 @@ AD37542129A39A960081B177 /* Assets.xcassets */, AD37542329A39A960081B177 /* LaunchScreen.storyboard */, AD37542629A39A960081B177 /* Info.plist */, + AD27E41929FE801900FFAF28 /* DBManager.swift */, + AD27E41B29FE805400FFAF28 /* Repository.swift */, ); path = MarvelApp; sourceTree = ""; @@ -394,6 +400,7 @@ files = ( 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 */, ADD2B06529EED96D00C747F1 /* LoadingActivityCollectionViewCell.swift in Sources */, @@ -405,6 +412,7 @@ 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/CharactersCollectionViewScreen/Cells/LoadingActivityCollectionViewCell.swift b/MarvelApp/CharactersCollectionViewScreen/Cells/LoadingActivityCollectionViewCell.swift index 9dc161e..dbd4b53 100644 --- a/MarvelApp/CharactersCollectionViewScreen/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/CharactersCollectionViewScreen/CharactersCollectionViewController.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift index a3f7c76..3a40183 100644 --- a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift @@ -1,8 +1,14 @@ import UIKit import CollectionViewPagingLayout + final class CharactersCollectionViewController: UIViewController { + enum State { + case loading + case loaded([Model]) + } + struct Model { let name: String let description: String @@ -11,11 +17,17 @@ final class CharactersCollectionViewController: UIViewController { private var lastCenterIndexPath: IndexPath? = nil - private let viewModel = CharactersCollectionViewModel() + private var viewModel: CharactersCollectionViewModel - private var heroesData = [Model]() + private var heroesData: [Model] = [] - private var isLoadingMore = false + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + init(viewModel: CharactersCollectionViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } private let activityIndicatorView: ActivityIndicatorView = { let activityIndicatorView = ActivityIndicatorView() @@ -73,20 +85,16 @@ final class CharactersCollectionViewController: UIViewController { 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: + self?.activityIndicatorView.start() + case .loaded(_): self?.collectionView.reloadData() - self?.collectionViewLayout.setCurrentPage(offset) self?.activityIndicatorView.stop() - }, - failed: { - print("failed to fetch data") } - ) + } + viewModel.start() setupViewLayout() } @@ -150,21 +158,22 @@ final class CharactersCollectionViewController: UIViewController { extension CharactersCollectionViewController: UICollectionViewDataSource, UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - heroesData.count + return viewModel.getCharacters().count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CharacterCollectionViewCell.self), for: indexPath) as? CharacterCollectionViewCell else { - return CharacterCollectionViewCell() + 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] + let hero = viewModel.getCharacters()[indexPath.item] cell.setupCell(model: CharacterCollectionViewCell.Model(name: hero.name, url: URL(string: hero.imageURL))) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let hero = heroesData[indexPath.item] + let hero = viewModel.getCharacters()[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) @@ -186,19 +195,8 @@ extension CharactersCollectionViewController: UICollectionViewDataSource, UIColl } 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(-420) { + viewModel.onPullToRefresh() } } diff --git a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift index d968b48..2a00a42 100644 --- a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift @@ -1,118 +1,56 @@ import Alamofire import RealmSwift -final class CharactersCollectionViewModel { - private let base_url = "https://gateway.marvel.com" - private let heroes_endpoint = "/v1/public/characters" - private var offset = 0 +protocol CharactersCollectionViewModel: AnyObject { + var onChangeViewState: ((CharactersCollectionViewController.State) -> Void)? { get set } + func start() + func getCharacters() -> [CharactersCollectionViewController.Model] + func onPullToRefresh() +} + +final class CharactersCollectionViewModelImpl: CharactersCollectionViewModel{ + var onChangeViewState: ((CharactersCollectionViewController.State) -> Void)? - 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) - } + func start() { + fetchCharacters() } - - 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) - } + + func getCharacters() -> [CharactersCollectionViewController.Model] { + self.characters } - - 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) - } + + func onPullToRefresh() { + fetchCharacters() } - - 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) - } + + private var characters: [CharactersCollectionViewController.Model] + private var repository: CharactersRepository + private var offset: Int + + init(repository: CharactersRepository) { + self.repository = repository + self.characters = [] + self.offset = 0 } - - func fetchHeroes(compleation: @escaping ([CharactersCollectionViewController.Model], Int) -> Void, failed: @escaping () -> Void ) { - let authParams = ["ts": "123", "apikey": "42597bee717ef2847e9b63553f4aff0f", "hash": "f49ba2754d66300142cf36b108860d2c", "offset": offset] as [String : Any] - var heroesData: [CharactersCollectionViewController.Model] = [] - 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 + 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(self.characters)) + print(self.characters.count) + case .failure(let error as CharactersRepositoryImpl.MyCustomError): + switch error { + case .offlineData(let savedCharacters): + self.characters += savedCharacters + self.onChangeViewState?(.loaded(self.characters)) } - for character in results { - heroesData.append(CharactersCollectionViewController.Model(name: character.name, description: character.description, imageURL: character.thumbnail.path + "." + character.thumbnail.ext)) - } - compleation(heroesData, self.offset) - self.offset += heroesData.count - }() case .failure(_): - failed() + break } } } } - 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/Repository.swift b/MarvelApp/Repository.swift new file mode 100644 index 0000000..8c45b6d --- /dev/null +++ b/MarvelApp/Repository.swift @@ -0,0 +1,157 @@ +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() + } + + func fetchCharacters(offset: Int, closure: @escaping (Result<[CharactersCollectionViewController.Model], Error>) -> Void) { + + let authParams = [ + "ts": "123", + "apikey": "42597bee717ef2847e9b63553f4aff0f", + "hash": "f49ba2754d66300142cf36b108860d2c", + "offset": offset, + "limit": 10 + ] as [String : Any] + + var result: [CharactersCollectionViewController.Model] = [] + + AF.request("https://gateway.marvel.com/v1/public/characters", 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 { + let id = character.id + let name = character.name + let description = character.description + let imageURL = character.thumbnail.path + "." + character.thumbnail.ext + result.append(CharactersCollectionViewController.Model( + name: name, description: description, + imageURL: imageURL + )) + self.db.saveCharacter(CharacterModel(id: id, name: name, imageUrl: imageURL, description: description)) + } + closure(.success(result)) + }() + case let .failure(error): + let characters = self.db.getCharacters() + if characters.isEmpty || offset != 0 { + closure(.failure(error)) + print("Fuck") + return + } + for character in characters { + let name = character.name + let description = character.descriptions + let imageURL = character.imageUrl + result.append(CharactersCollectionViewController.Model(name: name, description: description, imageURL: imageURL)) + } + closure(.failure(MyCustomError.offlineData(result))) + } + } + } + + enum MyCustomError: Error { + case offlineData([CharactersCollectionViewController.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 0e156d1..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 = CharactersCollectionViewController() + let viewModel = CharactersCollectionViewModelImpl(repository: CharactersRepositoryImpl()) + let startController = CharactersCollectionViewController(viewModel: viewModel) let navigationController = UINavigationController(rootViewController: startController) navigationController.navigationBar.backgroundColor = .clear window?.rootViewController = navigationController From 0c4259dd25ef8f60a1c6b03061b25423ee1a315d Mon Sep 17 00:00:00 2001 From: Abiteev Alihan Date: Tue, 2 May 2023 15:02:02 +0600 Subject: [PATCH 3/7] Complete refactor --- .../CharactersCollectionViewController.swift | 20 ++++++++++++++++++- MarvelApp/Repository.swift | 18 +++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift index 3a40183..338409a 100644 --- a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift @@ -1,6 +1,6 @@ import UIKit import CollectionViewPagingLayout - +import Kingfisher final class CharactersCollectionViewController: UIViewController { @@ -205,4 +205,22 @@ extension CharactersCollectionViewController: UICollectionViewDataSource, UIColl 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/Repository.swift b/MarvelApp/Repository.swift index 8c45b6d..2eba464 100644 --- a/MarvelApp/Repository.swift +++ b/MarvelApp/Repository.swift @@ -12,9 +12,11 @@ final class CharactersRepositoryImpl: CharactersRepository { 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", @@ -24,9 +26,9 @@ final class CharactersRepositoryImpl: CharactersRepository { ] 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) { response in + .responseDecodable(of: CharacterDataWrapper.self) {[weak self] response in switch response.result { case .success(_): { guard @@ -45,12 +47,15 @@ final class CharactersRepositoryImpl: CharactersRepository { name: name, description: description, imageURL: imageURL )) - self.db.saveCharacter(CharacterModel(id: id, name: name, imageUrl: imageURL, description: description)) + self?.db.saveCharacter(CharacterModel(id: id, name: name, imageUrl: imageURL, description: description)) } closure(.success(result)) + self?.isLoading = false }() case let .failure(error): - let characters = self.db.getCharacters() + guard let characters = self?.db.getCharacters() else { + return + } if characters.isEmpty || offset != 0 { closure(.failure(error)) print("Fuck") @@ -63,6 +68,7 @@ final class CharactersRepositoryImpl: CharactersRepository { result.append(CharactersCollectionViewController.Model(name: name, description: description, imageURL: imageURL)) } closure(.failure(MyCustomError.offlineData(result))) + self?.isLoading = false } } } From cecfff31a51c7bbcaade3e2bfcc0b9d993f54175 Mon Sep 17 00:00:00 2001 From: Abiteev Alihan Date: Tue, 2 May 2023 17:16:38 +0600 Subject: [PATCH 4/7] Refactor triangle view color changing, add error label on connection error --- .../CharactersCollectionViewController.swift | 58 +++++++++++++------ .../CharactersCollectionViewModel.swift | 2 +- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift index 338409a..953a717 100644 --- a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift @@ -7,6 +7,7 @@ final class CharactersCollectionViewController: UIViewController { enum State { case loading case loaded([Model]) + case connectionError } struct Model { @@ -14,9 +15,7 @@ final class CharactersCollectionViewController: UIViewController { let description: String let imageURL: String } - - private var lastCenterIndexPath: IndexPath? = nil - + private var viewModel: CharactersCollectionViewModel private var heroesData: [Model] = [] @@ -37,12 +36,23 @@ final class CharactersCollectionViewController: 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() @@ -56,6 +66,7 @@ final class CharactersCollectionViewController: UIViewController { private let collectionViewLayout: CollectionViewPagingLayout = { let layout = CollectionViewPagingLayout() + layout.setCurrentPage(0) return layout }() @@ -88,10 +99,17 @@ final class CharactersCollectionViewController: UIViewController { 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?.activityIndicatorView.stop() + case .connectionError: + UIView.animate(withDuration: 0.5) { [weak self] in + self?.connectionErrorLabel.alpha = 1 + } } } viewModel.start() @@ -103,14 +121,25 @@ final class CharactersCollectionViewController: UIViewController { view.addSubview(logoImageView) view.addSubview(label) view.addSubview(collectionView) + view.addSubview(connectionErrorLabel) view.addSubview(activityIndicatorView) setupTriangle() setupMarvelLogo() setupLabel() setupHeroesCollection() setupLoadingView() + setupConnectionLabel() } + private func setupConnectionLabel() { + 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 setupTriangle() { NSLayoutConstraint.activate([ triangleView.topAnchor.constraint(equalTo: view.topAnchor), @@ -180,37 +209,28 @@ extension CharactersCollectionViewController: UICollectionViewDataSource, UIColl } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - guard findCenterIndexPath() == lastCenterIndexPath else { - return - } - lastCenterIndexPath = findCenterIndexPath() - guard let lastCenterIndex = lastCenterIndexPath else { - return - } - let cell = collectionView.cellForItem(at: lastCenterIndex) as? CharacterCollectionViewCell - guard let cell = cell else { - return - } - triangleView.color = cell.imageAverageColor + guard scrollView == collectionView else { return } + let index = findCenterIndex() + guard let index = index else { return } + changeTriangleColor(character: viewModel.getCharacters()[index.item]) } func scrollViewDidScroll(_ scrollView: UIScrollView) { - if (scrollView.contentOffset.x - scrollView.contentSize.width) > CGFloat(-420) { + if (scrollView.contentOffset.x - scrollView.contentSize.width) > CGFloat(-350) { viewModel.onPullToRefresh() + collectionViewLayout.setCurrentPage(viewModel.getCharacters().count) } } - 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: { diff --git a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift index 2a00a42..b25129a 100644 --- a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift @@ -49,7 +49,7 @@ final class CharactersCollectionViewModelImpl: CharactersCollectionViewModel{ self.onChangeViewState?(.loaded(self.characters)) } case .failure(_): - break + self.onChangeViewState?(.connectionError) } } } From efb873df393fc9c7c60b9f8a88f0ba2606f930c6 Mon Sep 17 00:00:00 2001 From: Abiteev Alihan Date: Wed, 3 May 2023 02:14:22 +0600 Subject: [PATCH 5/7] Remove data storing from controller --- .../CharactersCollectionViewController.swift | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift index 953a717..70e6e7b 100644 --- a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift @@ -18,8 +18,6 @@ final class CharactersCollectionViewController: UIViewController { private var viewModel: CharactersCollectionViewModel - private var heroesData: [Model] = [] - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -123,15 +121,15 @@ final class CharactersCollectionViewController: UIViewController { view.addSubview(collectionView) view.addSubview(connectionErrorLabel) view.addSubview(activityIndicatorView) - setupTriangle() - setupMarvelLogo() - setupLabel() - setupHeroesCollection() - setupLoadingView() - setupConnectionLabel() + setupConstraintsTriangle() + setupConstraintsMarvelLogo() + setupConstraintsLabel() + setupConstraintsHeroesCollection() + setupConstraintsLoadingView() + setupConstraintsConnectionLabel() } - private func setupConnectionLabel() { + private func setupConstraintsConnectionLabel() { NSLayoutConstraint.activate([ connectionErrorLabel.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), connectionErrorLabel.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), @@ -140,7 +138,7 @@ final class CharactersCollectionViewController: UIViewController { ]) } - private func setupTriangle() { + private func setupConstraintsTriangle() { NSLayoutConstraint.activate([ triangleView.topAnchor.constraint(equalTo: view.topAnchor), triangleView.bottomAnchor.constraint(equalTo: view.bottomAnchor), @@ -149,7 +147,7 @@ final class CharactersCollectionViewController: UIViewController { ]) } - private func setupMarvelLogo() { + private func setupConstraintsMarvelLogo() { NSLayoutConstraint.activate([ logoImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), logoImageView.heightAnchor.constraint(equalToConstant: 25), @@ -158,7 +156,7 @@ final class CharactersCollectionViewController: UIViewController { ]) } - private func setupLabel() { + private func setupConstraintsLabel() { NSLayoutConstraint.activate([ label.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 20), label.heightAnchor.constraint(equalToConstant: 75), @@ -167,7 +165,7 @@ final class CharactersCollectionViewController: UIViewController { ]) } - private func setupHeroesCollection() { + private func setupConstraintsHeroesCollection() { NSLayoutConstraint.activate([ collectionView.topAnchor.constraint(equalTo: label.bottomAnchor), collectionView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor), @@ -175,7 +173,7 @@ final class CharactersCollectionViewController: UIViewController { ]) } - private func setupLoadingView() { + private func setupConstraintsLoadingView() { NSLayoutConstraint.activate([ activityIndicatorView.topAnchor.constraint(equalTo: view.topAnchor), activityIndicatorView.bottomAnchor.constraint(equalTo: view.bottomAnchor), From d45ce8cc9a5d5bf082565d97766576884462e529 Mon Sep 17 00:00:00 2001 From: Abiteev Alihan Date: Wed, 3 May 2023 04:59:53 +0600 Subject: [PATCH 6/7] Refactor other actions on main screen --- .../Cells/CharacterCollectionViewCell.swift | 2 +- .../CharactersCollectionViewController.swift | 57 ++++++++++++------- .../CharactersCollectionViewModel.swift | 48 +++++++++++----- .../DescriptionViewModel.swift | 8 +++ 4 files changed, 80 insertions(+), 35 deletions(-) create mode 100644 MarvelApp/DescriptionViewScreen/DescriptionViewModel.swift diff --git a/MarvelApp/CharactersCollectionViewScreen/Cells/CharacterCollectionViewCell.swift b/MarvelApp/CharactersCollectionViewScreen/Cells/CharacterCollectionViewCell.swift index 0014679..f8550d3 100644 --- a/MarvelApp/CharactersCollectionViewScreen/Cells/CharacterCollectionViewCell.swift +++ b/MarvelApp/CharactersCollectionViewScreen/Cells/CharacterCollectionViewCell.swift @@ -8,7 +8,7 @@ final class CharacterCollectionViewCell: UICollectionViewCell { 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 diff --git a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift index 70e6e7b..676c27e 100644 --- a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift @@ -6,25 +6,19 @@ final class CharactersCollectionViewController: UIViewController { enum State { case loading - case loaded([Model]) + case loaded case connectionError + case showDescriptionScreen(Model) + case changeTriangleColor(Model) } struct Model { let name: String - let description: String let imageURL: String } private var viewModel: CharactersCollectionViewModel - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - init(viewModel: CharactersCollectionViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) - } private let activityIndicatorView: ActivityIndicatorView = { let activityIndicatorView = ActivityIndicatorView() @@ -64,7 +58,7 @@ final class CharactersCollectionViewController: UIViewController { private let collectionViewLayout: CollectionViewPagingLayout = { let layout = CollectionViewPagingLayout() - layout.setCurrentPage(0) + layout.finalizeCollectionViewUpdates() return layout }() @@ -90,7 +84,17 @@ final class CharactersCollectionViewController: 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") @@ -101,13 +105,26 @@ final class CharactersCollectionViewController: UIViewController { self?.connectionErrorLabel.alpha = 0 } self?.activityIndicatorView.start() - case .loaded(_): + + case .loaded: self?.collectionView.reloadData() self?.activityIndicatorView.stop() + 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 + } + let model = DescriptionViewController.Model(url: URL(string: character.imageURL), name: character.name, description: character.description) + descriptionViewController.setup(model) + self?.navigationController?.pushViewController(descriptionViewController, animated: true) } } viewModel.start() @@ -184,8 +201,10 @@ final class CharactersCollectionViewController: UIViewController { } extension CharactersCollectionViewController: UICollectionViewDataSource, UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return viewModel.getCharacters().count + return viewModel.charactersCount() } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { @@ -193,30 +212,27 @@ extension CharactersCollectionViewController: UICollectionViewDataSource, UIColl guard let cell = cell as? CharacterCollectionViewCell else { return cell } - let hero = viewModel.getCharacters()[indexPath.item] - cell.setupCell(model: CharacterCollectionViewCell.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 = viewModel.getCharacters()[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 scrollView == collectionView else { return } let index = findCenterIndex() guard let index = index else { return } - changeTriangleColor(character: viewModel.getCharacters()[index.item]) + viewModel.onDeceleratingEnd(at: index.item) } func scrollViewDidScroll(_ scrollView: UIScrollView) { if (scrollView.contentOffset.x - scrollView.contentSize.width) > CGFloat(-350) { viewModel.onPullToRefresh() - collectionViewLayout.setCurrentPage(viewModel.getCharacters().count) } } @@ -225,6 +241,7 @@ extension CharactersCollectionViewController: UICollectionViewDataSource, UIColl let index = collectionView.indexPathForItem(at: center) return index } + private func changeTriangleColor(character: Model) { guard let url: URL = URL(string: character.imageURL) else { return diff --git a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift index b25129a..72e1c27 100644 --- a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift @@ -4,34 +4,54 @@ import RealmSwift protocol CharactersCollectionViewModel: AnyObject { var onChangeViewState: ((CharactersCollectionViewController.State) -> Void)? { get set } func start() - func getCharacters() -> [CharactersCollectionViewController.Model] 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 getCharacters() -> [CharactersCollectionViewController.Model] { - self.characters - } - func onPullToRefresh() { fetchCharacters() } - private var characters: [CharactersCollectionViewController.Model] - private var repository: CharactersRepository - private var offset: Int + func onCellDeque(at index: Int) -> CharactersCollectionViewController.Model { + return characters[index] + } - init(repository: CharactersRepository) { - self.repository = repository - self.characters = [] - self.offset = 0 + 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 @@ -40,13 +60,13 @@ final class CharactersCollectionViewModelImpl: CharactersCollectionViewModel{ case .success(let moreCharacters): self.characters += moreCharacters self.offset += moreCharacters.count - self.onChangeViewState?(.loaded(self.characters)) + self.onChangeViewState?(.loaded) print(self.characters.count) case .failure(let error as CharactersRepositoryImpl.MyCustomError): switch error { case .offlineData(let savedCharacters): self.characters += savedCharacters - self.onChangeViewState?(.loaded(self.characters)) + self.onChangeViewState?(.loaded) } case .failure(_): self.onChangeViewState?(.connectionError) diff --git a/MarvelApp/DescriptionViewScreen/DescriptionViewModel.swift b/MarvelApp/DescriptionViewScreen/DescriptionViewModel.swift new file mode 100644 index 0000000..4fb1ad0 --- /dev/null +++ b/MarvelApp/DescriptionViewScreen/DescriptionViewModel.swift @@ -0,0 +1,8 @@ +// +// DescriptionViewModel.swift +// MarvelApp +// +// Created by effective_macbook_pro on 03.05.2023. +// + +import Foundation From 7f3288509aa6d1df7512f3bdcb70dea02d03496b Mon Sep 17 00:00:00 2001 From: Abiteev Alihan Date: Wed, 3 May 2023 06:05:39 +0600 Subject: [PATCH 7/7] Add view model for description screen --- MarvelApp.xcodeproj/project.pbxproj | 4 + .../CharactersCollectionViewController.swift | 7 +- .../CharactersCollectionViewModel.swift | 6 +- .../DescriptionViewController.swift | 31 ++-- .../DescriptionViewModel.swift | 45 ++++- .../Extensions/AverageUIImageExtension.swift | 4 +- MarvelApp/Repository.swift | 161 +++++++++++------- 7 files changed, 176 insertions(+), 82 deletions(-) diff --git a/MarvelApp.xcodeproj/project.pbxproj b/MarvelApp.xcodeproj/project.pbxproj index 7bffd8c..f7fb4e4 100644 --- a/MarvelApp.xcodeproj/project.pbxproj +++ b/MarvelApp.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 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 /* CharactersCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD37541C29A39A940081B177 /* CharactersCollectionViewController.swift */; }; @@ -52,6 +53,7 @@ 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 = ""; }; @@ -181,6 +183,7 @@ isa = PBXGroup; children = ( AD5EA00F29BA193C0033A503 /* DescriptionViewController.swift */, + AD27E41D2A01C78F00FFAF28 /* DescriptionViewModel.swift */, ); path = DescriptionViewScreen; sourceTree = ""; @@ -403,6 +406,7 @@ 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 /* CharacterCollectionViewCell.swift in Sources */, AD5EA00E29AD42150033A503 /* TriangleView.swift in Sources */, diff --git a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift index 676c27e..dcf048a 100644 --- a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewController.swift @@ -11,15 +11,15 @@ final class CharactersCollectionViewController: UIViewController { 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() activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false @@ -122,8 +122,7 @@ final class CharactersCollectionViewController: UIViewController { guard let descriptionViewController = self?.descriptionViewController else { return } - let model = DescriptionViewController.Model(url: URL(string: character.imageURL), name: character.name, description: character.description) - descriptionViewController.setup(model) + descriptionViewController.viewModel.start(with: character.id) self?.navigationController?.pushViewController(descriptionViewController, animated: true) } } diff --git a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift index 72e1c27..e3dff66 100644 --- a/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift +++ b/MarvelApp/CharactersCollectionViewScreen/CharactersCollectionViewModel.swift @@ -64,10 +64,14 @@ final class CharactersCollectionViewModelImpl: CharactersCollectionViewModel{ print(self.characters.count) case .failure(let error as CharactersRepositoryImpl.MyCustomError): switch error { - case .offlineData(let savedCharacters): + case .offlineCharacters(let savedCharacters): self.characters += savedCharacters self.onChangeViewState?(.loaded) + + case .offlineCharacter(_): + break } + case .failure(_): self.onChangeViewState?(.connectionError) } diff --git a/MarvelApp/DescriptionViewScreen/DescriptionViewController.swift b/MarvelApp/DescriptionViewScreen/DescriptionViewController.swift index ff37476..3cd7863 100644 --- a/MarvelApp/DescriptionViewScreen/DescriptionViewController.swift +++ b/MarvelApp/DescriptionViewScreen/DescriptionViewController.swift @@ -2,11 +2,16 @@ 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() @@ -41,7 +46,6 @@ final class DescriptionViewController: UIViewController { return textView }() - override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black @@ -50,23 +54,30 @@ 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.topAnchor), imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor), @@ -75,7 +86,7 @@ final class DescriptionViewController: UIViewController { ]) } - private func setupNameLabel() { + private func setupConstraintsNameLabel() { NSLayoutConstraint.activate([ nameLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor), nameLabel.leftAnchor.constraint(equalTo: view.leftAnchor), @@ -83,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), @@ -92,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 index 4fb1ad0..34657e7 100644 --- a/MarvelApp/DescriptionViewScreen/DescriptionViewModel.swift +++ b/MarvelApp/DescriptionViewScreen/DescriptionViewModel.swift @@ -1,8 +1,39 @@ -// -// DescriptionViewModel.swift -// MarvelApp -// -// Created by effective_macbook_pro on 03.05.2023. -// +protocol DescriptionViewModel: AnyObject { + var onChangeViewState: ((DescriptionViewController.State) -> Void)? { get set } + func start(with id: Int) +} -import Foundation +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 2fbba99..9f3c9a6 100644 --- a/MarvelApp/Extensions/AverageUIImageExtension.swift +++ b/MarvelApp/Extensions/AverageUIImageExtension.swift @@ -10,11 +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) } } - \ No newline at end of file + diff --git a/MarvelApp/Repository.swift b/MarvelApp/Repository.swift index 2eba464..4c48b0f 100644 --- a/MarvelApp/Repository.swift +++ b/MarvelApp/Repository.swift @@ -8,73 +8,118 @@ 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( - name: name, description: description, - 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 + + 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 } - for character in characters { - let name = character.name - let description = character.descriptions - let imageURL = character.imageUrl - result.append(CharactersCollectionViewController.Model(name: name, description: description, imageURL: imageURL)) + } + } + + 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 } - closure(.failure(MyCustomError.offlineData(result))) - self?.isLoading = false } } - } - + enum MyCustomError: Error { - case offlineData([CharactersCollectionViewController.Model]) + case offlineCharacters([CharactersCollectionViewController.Model]) + case offlineCharacter(DescriptionViewController.Model?) } private struct CharacterDataWrapper: Decodable {