From aff299bf31a11d2fd37efcd4abe016b04086c573 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:57:14 +0330 Subject: [PATCH 01/12] Fix Access Control to Open --- UI/Sources/UI/Color/Theme.swift | 21 ++++++++++++------- .../UI/View/Base/BaseCollectionCell.swift | 2 +- .../UI/View/Base/BaseCollectionView.swift | 2 +- UI/Sources/UI/View/Base/BaseControl.swift | 2 +- UI/Sources/UI/View/Base/BaseLabel.swift | 2 +- UI/Sources/UI/View/Base/BaseView.swift | 2 +- .../UI/View/Base/BaseViewController.swift | 4 ++-- UI/Sources/UI/View/Label/SubTitleLabel.swift | 2 +- 8 files changed, 22 insertions(+), 15 deletions(-) diff --git a/UI/Sources/UI/Color/Theme.swift b/UI/Sources/UI/Color/Theme.swift index 9f99449..d64dc51 100644 --- a/UI/Sources/UI/Color/Theme.swift +++ b/UI/Sources/UI/Color/Theme.swift @@ -9,11 +9,18 @@ import UIKit public class Theme { - static let background = UIColor(named: "background") - static let supplementaryBackground = UIColor(named: "supplementary background") - static let blue = UIColor(named: "blue") - static let border = UIColor(named: "border") - static let divider = UIColor(named: "divider") - static let primaryText = UIColor(named: "primary text") - static let secondrayText = UIColor(named: "secondray text") + public static let background = UIColor(named: "background", in: Bundle.module + , compatibleWith: nil) + public static let supplementaryBackground = UIColor(named: "supplementary background", + in: Bundle.module, compatibleWith: nil) + public static let blue = UIColor(named: "blue", + in: Bundle.module, compatibleWith: nil) + public static let border = UIColor(named: "border", + in: Bundle.module, compatibleWith: nil) + public static let divider = UIColor(named: "divider", + in: Bundle.module, compatibleWith: nil) + public static let primaryText = UIColor(named: "primary text", + in: Bundle.module, compatibleWith: nil) + public static let secondrayText = UIColor(named: "secondray text", + in: Bundle.module, compatibleWith: nil) } diff --git a/UI/Sources/UI/View/Base/BaseCollectionCell.swift b/UI/Sources/UI/View/Base/BaseCollectionCell.swift index 789d65c..b5d9357 100644 --- a/UI/Sources/UI/View/Base/BaseCollectionCell.swift +++ b/UI/Sources/UI/View/Base/BaseCollectionCell.swift @@ -7,7 +7,7 @@ import UIKit -public class BaseCollectionCell: UICollectionViewCell, UISequenceIdentifier { +open class BaseCollectionCell: UICollectionViewCell, UISequenceIdentifier { public override init(frame: CGRect) { super.init(frame: frame) diff --git a/UI/Sources/UI/View/Base/BaseCollectionView.swift b/UI/Sources/UI/View/Base/BaseCollectionView.swift index d30caea..29bde46 100644 --- a/UI/Sources/UI/View/Base/BaseCollectionView.swift +++ b/UI/Sources/UI/View/Base/BaseCollectionView.swift @@ -7,7 +7,7 @@ import UIKit -public class BaseCollectionView: UICollectionView { +open class BaseCollectionView: UICollectionView { override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { super.init(frame: frame, collectionViewLayout: layout) diff --git a/UI/Sources/UI/View/Base/BaseControl.swift b/UI/Sources/UI/View/Base/BaseControl.swift index 3a15a2b..41de0bf 100644 --- a/UI/Sources/UI/View/Base/BaseControl.swift +++ b/UI/Sources/UI/View/Base/BaseControl.swift @@ -7,7 +7,7 @@ import UIKit -public class BaseControl: UIControl { +open class BaseControl: UIControl { public required init?(coder: NSCoder) { super.init(coder: coder) diff --git a/UI/Sources/UI/View/Base/BaseLabel.swift b/UI/Sources/UI/View/Base/BaseLabel.swift index dc9e993..57509ee 100644 --- a/UI/Sources/UI/View/Base/BaseLabel.swift +++ b/UI/Sources/UI/View/Base/BaseLabel.swift @@ -7,7 +7,7 @@ import UIKit -public class BaseLabel: UILabel { +open class BaseLabel: UILabel { public required init?(coder: NSCoder) { super.init(coder: coder) diff --git a/UI/Sources/UI/View/Base/BaseView.swift b/UI/Sources/UI/View/Base/BaseView.swift index 0fba767..9d778d2 100644 --- a/UI/Sources/UI/View/Base/BaseView.swift +++ b/UI/Sources/UI/View/Base/BaseView.swift @@ -7,7 +7,7 @@ import UIKit -public class BaseView: UIView { +open class BaseView: UIView { public required init?(coder: NSCoder) { super.init(coder: coder) diff --git a/UI/Sources/UI/View/Base/BaseViewController.swift b/UI/Sources/UI/View/Base/BaseViewController.swift index b2cc1a1..82cdbde 100644 --- a/UI/Sources/UI/View/Base/BaseViewController.swift +++ b/UI/Sources/UI/View/Base/BaseViewController.swift @@ -7,9 +7,9 @@ import UIKit -public class BaseViewController: UIViewController { +open class BaseViewController: UIViewController { - var spaceSeparatorFromEdgeInList: CGFloat { + open var spaceSeparatorFromEdgeInList: CGFloat { return 16 } diff --git a/UI/Sources/UI/View/Label/SubTitleLabel.swift b/UI/Sources/UI/View/Label/SubTitleLabel.swift index 76e8c0f..fb22023 100644 --- a/UI/Sources/UI/View/Label/SubTitleLabel.swift +++ b/UI/Sources/UI/View/Label/SubTitleLabel.swift @@ -7,7 +7,7 @@ import UIKit -class SubTitleLabel: BaseLabel { +public class SubTitleLabel: BaseLabel { override func setupViews() { self.font = Raleway.regular.customFont(basedOnTextStyle: .caption2) From 82997160ed0f9dac7e8db346421077fb23f0625d Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Mon, 6 Nov 2023 12:12:59 +0330 Subject: [PATCH 02/12] Add DiContainer Protocol, HomeViewController, and BaseCollectionViewController - Added the DiContainer protocol for creating suitable use cases. - Added the HomeViewController for displaying the Transfer list. - Added the BaseCollectionViewController in the UI module. --- TransferList.xcodeproj/project.pbxproj | 142 ++++++++++++++---- .../xcschemes/xcschememanagement.plist | 2 +- .../{ => Application}/AppDelegate.swift | 4 - TransferList/Application/DI/DIContainer.swift | 14 ++ .../Application/DI/DIContainerImpl.swift | 27 ++++ .../{ => Application}/SceneDelegate.swift | 43 +++++- TransferList/Base.lproj/Main.storyboard | 24 --- .../HomeScene/View/HomeViewController.swift | 24 +++ .../HomeScene/View/Main.storyboard | 30 ++++ .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 TransferList/{ => Resources}/Info.plist | 2 - TransferList/ViewController.swift | 19 --- .../BaseCollectionViewController.swift | 42 ++++++ UI/Sources/UI/View/Label/SubTitleLabel.swift | 2 +- 17 files changed, 293 insertions(+), 82 deletions(-) rename TransferList/{ => Application}/AppDelegate.swift (99%) create mode 100644 TransferList/Application/DI/DIContainer.swift create mode 100644 TransferList/Application/DI/DIContainerImpl.swift rename TransferList/{ => Application}/SceneDelegate.swift (62%) delete mode 100644 TransferList/Base.lproj/Main.storyboard create mode 100644 TransferList/Presentation/HomeScene/View/HomeViewController.swift create mode 100644 TransferList/Presentation/HomeScene/View/Main.storyboard rename TransferList/{ => Resources}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename TransferList/{ => Resources}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename TransferList/{ => Resources}/Assets.xcassets/Contents.json (100%) rename TransferList/{ => Resources}/Base.lproj/LaunchScreen.storyboard (100%) rename TransferList/{ => Resources}/Info.plist (90%) delete mode 100644 TransferList/ViewController.swift create mode 100644 UI/Sources/UI/View/CollectionView/BaseCollectionViewController.swift diff --git a/TransferList.xcodeproj/project.pbxproj b/TransferList.xcodeproj/project.pbxproj index 7d90c0e..777e220 100644 --- a/TransferList.xcodeproj/project.pbxproj +++ b/TransferList.xcodeproj/project.pbxproj @@ -7,10 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + DE690EA42AF842ED00E8C451 /* Data in Frameworks */ = {isa = PBXBuildFile; productRef = DE690EA32AF842ED00E8C451 /* Data */; }; + DE690EA62AF842ED00E8C451 /* Domain in Frameworks */ = {isa = PBXBuildFile; productRef = DE690EA52AF842ED00E8C451 /* Domain */; }; + DE690EA82AF842ED00E8C451 /* UI in Frameworks */ = {isa = PBXBuildFile; productRef = DE690EA72AF842ED00E8C451 /* UI */; }; + DE690EAA2AF842F800E8C451 /* DIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE690EA92AF842F800E8C451 /* DIContainer.swift */; }; + DE690EAC2AF8432500E8C451 /* DIContainerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE690EAB2AF8432500E8C451 /* DIContainerImpl.swift */; }; + DE690EB12AF8455400E8C451 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE690EB02AF8455400E8C451 /* HomeViewController.swift */; }; + DE690EB32AF84A1C00E8C451 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE690EB22AF84A1C00E8C451 /* Main.storyboard */; }; DEBE4AC32AF6C3B000A58501 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBE4AC22AF6C3B000A58501 /* AppDelegate.swift */; }; DEBE4AC52AF6C3B000A58501 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBE4AC42AF6C3B000A58501 /* SceneDelegate.swift */; }; - DEBE4AC72AF6C3B000A58501 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBE4AC62AF6C3B000A58501 /* ViewController.swift */; }; - DEBE4ACA2AF6C3B000A58501 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DEBE4AC82AF6C3B000A58501 /* Main.storyboard */; }; DEBE4ACC2AF6C3B200A58501 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DEBE4ACB2AF6C3B200A58501 /* Assets.xcassets */; }; DEBE4ACF2AF6C3B200A58501 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DEBE4ACD2AF6C3B200A58501 /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ @@ -18,11 +23,13 @@ /* Begin PBXFileReference section */ DE3EF77C2AF6F9B40071E5E4 /* Data */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Data; sourceTree = ""; }; DE690E9E2AF82D4300E8C451 /* UI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = UI; sourceTree = ""; }; + DE690EA92AF842F800E8C451 /* DIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DIContainer.swift; sourceTree = ""; }; + DE690EAB2AF8432500E8C451 /* DIContainerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DIContainerImpl.swift; sourceTree = ""; }; + DE690EB02AF8455400E8C451 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; + DE690EB22AF84A1C00E8C451 /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; DEBE4ABF2AF6C3B000A58501 /* TransferList.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TransferList.app; sourceTree = BUILT_PRODUCTS_DIR; }; DEBE4AC22AF6C3B000A58501 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; DEBE4AC42AF6C3B000A58501 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - DEBE4AC62AF6C3B000A58501 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - DEBE4AC92AF6C3B000A58501 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; DEBE4ACB2AF6C3B200A58501 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DEBE4ACE2AF6C3B200A58501 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; DEBE4AD02AF6C3B200A58501 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -34,12 +41,76 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DE690EA62AF842ED00E8C451 /* Domain in Frameworks */, + DE690EA82AF842ED00E8C451 /* UI in Frameworks */, + DE690EA42AF842ED00E8C451 /* Data in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + DE690E9F2AF8429D00E8C451 /* Resources */ = { + isa = PBXGroup; + children = ( + DEBE4ACD2AF6C3B200A58501 /* LaunchScreen.storyboard */, + DEBE4AD02AF6C3B200A58501 /* Info.plist */, + DEBE4ACB2AF6C3B200A58501 /* Assets.xcassets */, + ); + path = Resources; + sourceTree = ""; + }; + DE690EA02AF842B400E8C451 /* Application */ = { + isa = PBXGroup; + children = ( + DE690EA12AF842DB00E8C451 /* DI */, + DEBE4AC22AF6C3B000A58501 /* AppDelegate.swift */, + DEBE4AC42AF6C3B000A58501 /* SceneDelegate.swift */, + ); + path = Application; + sourceTree = ""; + }; + DE690EA12AF842DB00E8C451 /* DI */ = { + isa = PBXGroup; + children = ( + DE690EA92AF842F800E8C451 /* DIContainer.swift */, + DE690EAB2AF8432500E8C451 /* DIContainerImpl.swift */, + ); + path = DI; + sourceTree = ""; + }; + DE690EA22AF842ED00E8C451 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + DE690EAD2AF8452500E8C451 /* HomeScene */ = { + isa = PBXGroup; + children = ( + DE690EAF2AF8453800E8C451 /* ViewModel */, + DE690EAE2AF8453500E8C451 /* View */, + ); + path = HomeScene; + sourceTree = ""; + }; + DE690EAE2AF8453500E8C451 /* View */ = { + isa = PBXGroup; + children = ( + DE690EB02AF8455400E8C451 /* HomeViewController.swift */, + DE690EB22AF84A1C00E8C451 /* Main.storyboard */, + ); + path = View; + sourceTree = ""; + }; + DE690EAF2AF8453800E8C451 /* ViewModel */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModel; + sourceTree = ""; + }; DEBE4AB62AF6C3B000A58501 = { isa = PBXGroup; children = ( @@ -48,6 +119,7 @@ DEBE4AD62AF6C8F600A58501 /* Domain */, DEBE4AC12AF6C3B000A58501 /* TransferList */, DEBE4AC02AF6C3B000A58501 /* Products */, + DE690EA22AF842ED00E8C451 /* Frameworks */, ); sourceTree = ""; }; @@ -62,17 +134,21 @@ DEBE4AC12AF6C3B000A58501 /* TransferList */ = { isa = PBXGroup; children = ( - DEBE4AC22AF6C3B000A58501 /* AppDelegate.swift */, - DEBE4AC42AF6C3B000A58501 /* SceneDelegate.swift */, - DEBE4AC62AF6C3B000A58501 /* ViewController.swift */, - DEBE4AC82AF6C3B000A58501 /* Main.storyboard */, - DEBE4ACB2AF6C3B200A58501 /* Assets.xcassets */, - DEBE4ACD2AF6C3B200A58501 /* LaunchScreen.storyboard */, - DEBE4AD02AF6C3B200A58501 /* Info.plist */, + DE690EA02AF842B400E8C451 /* Application */, + DEFF1DBA2AF8DDFE00673B8C /* Presentation */, + DE690E9F2AF8429D00E8C451 /* Resources */, ); path = TransferList; sourceTree = ""; }; + DEFF1DBA2AF8DDFE00673B8C /* Presentation */ = { + isa = PBXGroup; + children = ( + DE690EAD2AF8452500E8C451 /* HomeScene */, + ); + path = Presentation; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -89,6 +165,11 @@ dependencies = ( ); name = TransferList; + packageProductDependencies = ( + DE690EA32AF842ED00E8C451 /* Data */, + DE690EA52AF842ED00E8C451 /* Domain */, + DE690EA72AF842ED00E8C451 /* UI */, + ); productName = TransferList; productReference = DEBE4ABF2AF6C3B000A58501 /* TransferList.app */; productType = "com.apple.product-type.application"; @@ -131,9 +212,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + DE690EB32AF84A1C00E8C451 /* Main.storyboard in Resources */, DEBE4ACF2AF6C3B200A58501 /* LaunchScreen.storyboard in Resources */, DEBE4ACC2AF6C3B200A58501 /* Assets.xcassets in Resources */, - DEBE4ACA2AF6C3B000A58501 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -144,23 +225,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DEBE4AC72AF6C3B000A58501 /* ViewController.swift in Sources */, + DE690EB12AF8455400E8C451 /* HomeViewController.swift in Sources */, + DE690EAA2AF842F800E8C451 /* DIContainer.swift in Sources */, DEBE4AC32AF6C3B000A58501 /* AppDelegate.swift in Sources */, DEBE4AC52AF6C3B000A58501 /* SceneDelegate.swift in Sources */, + DE690EAC2AF8432500E8C451 /* DIContainerImpl.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - DEBE4AC82AF6C3B000A58501 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - DEBE4AC92AF6C3B000A58501 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; DEBE4ACD2AF6C3B200A58501 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -294,12 +369,11 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = TransferList/Info.plist; + INFOPLIST_FILE = TransferList/Resources/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -322,12 +396,11 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = TransferList/Info.plist; + INFOPLIST_FILE = TransferList/Resources/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -364,6 +437,21 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + DE690EA32AF842ED00E8C451 /* Data */ = { + isa = XCSwiftPackageProductDependency; + productName = Data; + }; + DE690EA52AF842ED00E8C451 /* Domain */ = { + isa = XCSwiftPackageProductDependency; + productName = Domain; + }; + DE690EA72AF842ED00E8C451 /* UI */ = { + isa = XCSwiftPackageProductDependency; + productName = UI; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = DEBE4AB72AF6C3B000A58501 /* Project object */; } diff --git a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist index 52e1fe9..5eb9d26 100644 --- a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ TransferList.xcscheme_^#shared#^_ orderHint - 5 + 3 diff --git a/TransferList/AppDelegate.swift b/TransferList/Application/AppDelegate.swift similarity index 99% rename from TransferList/AppDelegate.swift rename to TransferList/Application/AppDelegate.swift index c1ea548..4ca176c 100644 --- a/TransferList/AppDelegate.swift +++ b/TransferList/Application/AppDelegate.swift @@ -10,8 +10,6 @@ import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true @@ -31,6 +29,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } - } - diff --git a/TransferList/Application/DI/DIContainer.swift b/TransferList/Application/DI/DIContainer.swift new file mode 100644 index 0000000..9106082 --- /dev/null +++ b/TransferList/Application/DI/DIContainer.swift @@ -0,0 +1,14 @@ +// +// DIContainer.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import Foundation +import Domain + +protocol DIContainer { + + func createPresonBankAccountUseCase() -> PersonBankAccountUseCase +} diff --git a/TransferList/Application/DI/DIContainerImpl.swift b/TransferList/Application/DI/DIContainerImpl.swift new file mode 100644 index 0000000..922fdc4 --- /dev/null +++ b/TransferList/Application/DI/DIContainerImpl.swift @@ -0,0 +1,27 @@ +// +// DIContainerImpl.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import Foundation +import Domain +import Data + +final class DIContainerImpl: DIContainer { + + private func createPersonBankAccountRepository() -> PersonBankAccountRepository { + let api: Api = ApiImpl() + let database = DatabaseImpl() + let local = LocalImpl(database: database) + + return PersonBankAccountRepositoryImpl(api: api, local: local) + } + + func createPresonBankAccountUseCase() -> PersonBankAccountUseCase { + let repository = createPersonBankAccountRepository() + let useCase = PersonBankAccountUseCaseImpl(repository: repository) + return useCase + } +} diff --git a/TransferList/SceneDelegate.swift b/TransferList/Application/SceneDelegate.swift similarity index 62% rename from TransferList/SceneDelegate.swift rename to TransferList/Application/SceneDelegate.swift index 64f3f6a..aaa0bdf 100644 --- a/TransferList/SceneDelegate.swift +++ b/TransferList/Application/SceneDelegate.swift @@ -6,17 +6,19 @@ // import UIKit +import UI class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + guard let window = (scene as? UIWindowScene) else { return } + startApp(windowScene: window) } func sceneDidDisconnect(_ scene: UIScene) { @@ -47,6 +49,39 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // to restore the scene back to its current state. } + private func startApp(windowScene scene: UIWindowScene) { + let window = UIWindow(windowScene: scene) + + registerFonts() + + let navController = createNavigationController() + let homeViewController = createHomeViewController() + + navController.pushViewController(homeViewController, animated: false) + + window.rootViewController = navController + self.window = window + window.makeKeyAndVisible() + } + private func createNavigationController() -> UINavigationController { + let navigationController = UINavigationController() + navigationController.isNavigationBarHidden = true + + return navigationController + } + + private func createHomeViewController() -> HomeViewController { + let homeViewController = HomeViewController() + return homeViewController + } + + private func registerFonts() { + Font.registerFonts() + } + + private func setupDIContainer() { +// let container = DIContainerImpl() +// ViewModelFactory.shared.register(DIContainer: container) + } } - diff --git a/TransferList/Base.lproj/Main.storyboard b/TransferList/Base.lproj/Main.storyboard deleted file mode 100644 index 25a7638..0000000 --- a/TransferList/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/TransferList/Presentation/HomeScene/View/HomeViewController.swift b/TransferList/Presentation/HomeScene/View/HomeViewController.swift new file mode 100644 index 0000000..4f10f35 --- /dev/null +++ b/TransferList/Presentation/HomeScene/View/HomeViewController.swift @@ -0,0 +1,24 @@ +// +// HomeViewController.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import UIKit +import UI + +class HomeViewController: BaseCollectionViewController { + + @InstantiateView(type: TitleLabel.self) private var titleLabel + + override func setupViews() { + + titleLabel.text = "Hello Blu!" + view.addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.centerXAnchor.constraint(equalTo: view.centerXSafeMargin), + titleLabel.centerYAnchor.constraint(equalTo: view.centerYSafeMargin), + ]) + } +} diff --git a/TransferList/Presentation/HomeScene/View/Main.storyboard b/TransferList/Presentation/HomeScene/View/Main.storyboard new file mode 100644 index 0000000..dd79351 --- /dev/null +++ b/TransferList/Presentation/HomeScene/View/Main.storyboard @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TransferList/Assets.xcassets/AccentColor.colorset/Contents.json b/TransferList/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from TransferList/Assets.xcassets/AccentColor.colorset/Contents.json rename to TransferList/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/TransferList/Assets.xcassets/AppIcon.appiconset/Contents.json b/TransferList/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from TransferList/Assets.xcassets/AppIcon.appiconset/Contents.json rename to TransferList/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/TransferList/Assets.xcassets/Contents.json b/TransferList/Resources/Assets.xcassets/Contents.json similarity index 100% rename from TransferList/Assets.xcassets/Contents.json rename to TransferList/Resources/Assets.xcassets/Contents.json diff --git a/TransferList/Base.lproj/LaunchScreen.storyboard b/TransferList/Resources/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from TransferList/Base.lproj/LaunchScreen.storyboard rename to TransferList/Resources/Base.lproj/LaunchScreen.storyboard diff --git a/TransferList/Info.plist b/TransferList/Resources/Info.plist similarity index 90% rename from TransferList/Info.plist rename to TransferList/Resources/Info.plist index dd3c9af..0eb786d 100644 --- a/TransferList/Info.plist +++ b/TransferList/Resources/Info.plist @@ -15,8 +15,6 @@ Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main diff --git a/TransferList/ViewController.swift b/TransferList/ViewController.swift deleted file mode 100644 index 35482af..0000000 --- a/TransferList/ViewController.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ViewController.swift -// TransferList -// -// Created by Hessam Mahdiabadi on 11/4/23. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - - -} - diff --git a/UI/Sources/UI/View/CollectionView/BaseCollectionViewController.swift b/UI/Sources/UI/View/CollectionView/BaseCollectionViewController.swift new file mode 100644 index 0000000..177346f --- /dev/null +++ b/UI/Sources/UI/View/CollectionView/BaseCollectionViewController.swift @@ -0,0 +1,42 @@ +// +// BaseCollectionViewController.swift +// +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import UIKit + +open class BaseCollectionViewController: BaseViewController, + BorderCompositionalLayoutDelegate { + + private(set) lazy var collectionView: BaseCollectionView = { + BaseCollectionView(frame: .zero, collectionViewLayout: createBorderLayout()) + }() + + open override func setupViews() { + view.addSubview(collectionView) + + configureCollectionView() + } + + private func configureCollectionView() { + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: view.topSafeMargin), + collectionView.leadingAnchor.constraint(equalTo: view.leadingSafeMargin), + collectionView.trailingAnchor.constraint(equalTo: view.trailingSafeMargin), + collectionView.bottomAnchor.constraint(equalTo: view.bottomSafeMargin) + ]) + + collectionView.contentInset.top = 0 + } + + private func createBorderLayout() -> UICollectionViewCompositionalLayout { + return BorderCompositionalLayout(delegate: self) + } + + public func customEdgeSeparatorSpacing(currentSpacing: CGFloat) -> NSDirectionalEdgeInsets { + return .init(top: 0, leading: self.spaceSeparatorFromEdgeInList, + bottom: 0, trailing: self.spaceSeparatorFromEdgeInList) + } +} diff --git a/UI/Sources/UI/View/Label/SubTitleLabel.swift b/UI/Sources/UI/View/Label/SubTitleLabel.swift index fb22023..6bfc1f9 100644 --- a/UI/Sources/UI/View/Label/SubTitleLabel.swift +++ b/UI/Sources/UI/View/Label/SubTitleLabel.swift @@ -9,7 +9,7 @@ import UIKit public class SubTitleLabel: BaseLabel { - override func setupViews() { + public override func setupViews() { self.font = Raleway.regular.customFont(basedOnTextStyle: .caption2) textColor = Theme.secondrayText From 82844645bd28cf7b536cd1440fbd8393de3330eb Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Mon, 6 Nov 2023 12:56:12 +0330 Subject: [PATCH 03/12] Add LargeHeaderCell, HomeCollectionViewDataSource, and HomeItem - Added LargeHeaderCell for displaying titles in each section. - Added HomeCollectionViewDataSource for managing the diffable data source and cell display. - Added the HomeItem component to assist with the diffable data source. --- TransferList.xcodeproj/project.pbxproj | 20 +++ .../HomeScene/View/Cell/LargeHeaderCell.swift | 23 +++ .../View/HomeCollectionViewDataSource.swift | 132 ++++++++++++++++++ .../HomeScene/View/HomeItem.swift | 28 ++++ .../HomeScene/View/HomeViewController.swift | 17 +-- .../UI/View/Base/BaseCollectionView.swift | 16 ++- .../BaseCollectionViewController.swift | 2 +- 7 files changed, 228 insertions(+), 10 deletions(-) create mode 100644 TransferList/Presentation/HomeScene/View/Cell/LargeHeaderCell.swift create mode 100644 TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift create mode 100644 TransferList/Presentation/HomeScene/View/HomeItem.swift diff --git a/TransferList.xcodeproj/project.pbxproj b/TransferList.xcodeproj/project.pbxproj index 777e220..cc7922f 100644 --- a/TransferList.xcodeproj/project.pbxproj +++ b/TransferList.xcodeproj/project.pbxproj @@ -18,6 +18,9 @@ DEBE4AC52AF6C3B000A58501 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBE4AC42AF6C3B000A58501 /* SceneDelegate.swift */; }; DEBE4ACC2AF6C3B200A58501 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DEBE4ACB2AF6C3B200A58501 /* Assets.xcassets */; }; DEBE4ACF2AF6C3B200A58501 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DEBE4ACD2AF6C3B200A58501 /* LaunchScreen.storyboard */; }; + DEFF1DBD2AF8DEE400673B8C /* LargeHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DBC2AF8DEE400673B8C /* LargeHeaderCell.swift */; }; + DEFF1DC02AF8DFA500673B8C /* HomeCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DBF2AF8DFA500673B8C /* HomeCollectionViewDataSource.swift */; }; + DEFF1DC22AF8DFE300673B8C /* HomeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DC12AF8DFE300673B8C /* HomeItem.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -34,6 +37,9 @@ DEBE4ACE2AF6C3B200A58501 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; DEBE4AD02AF6C3B200A58501 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DEBE4AD62AF6C8F600A58501 /* Domain */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Domain; sourceTree = ""; }; + DEFF1DBC2AF8DEE400673B8C /* LargeHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeHeaderCell.swift; sourceTree = ""; }; + DEFF1DBF2AF8DFA500673B8C /* HomeCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCollectionViewDataSource.swift; sourceTree = ""; }; + DEFF1DC12AF8DFE300673B8C /* HomeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeItem.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -98,8 +104,11 @@ DE690EAE2AF8453500E8C451 /* View */ = { isa = PBXGroup; children = ( + DEFF1DBB2AF8DED200673B8C /* Cell */, DE690EB02AF8455400E8C451 /* HomeViewController.swift */, DE690EB22AF84A1C00E8C451 /* Main.storyboard */, + DEFF1DBF2AF8DFA500673B8C /* HomeCollectionViewDataSource.swift */, + DEFF1DC12AF8DFE300673B8C /* HomeItem.swift */, ); path = View; sourceTree = ""; @@ -149,6 +158,14 @@ path = Presentation; sourceTree = ""; }; + DEFF1DBB2AF8DED200673B8C /* Cell */ = { + isa = PBXGroup; + children = ( + DEFF1DBC2AF8DEE400673B8C /* LargeHeaderCell.swift */, + ); + path = Cell; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -225,10 +242,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DEFF1DBD2AF8DEE400673B8C /* LargeHeaderCell.swift in Sources */, + DEFF1DC22AF8DFE300673B8C /* HomeItem.swift in Sources */, DE690EB12AF8455400E8C451 /* HomeViewController.swift in Sources */, DE690EAA2AF842F800E8C451 /* DIContainer.swift in Sources */, DEBE4AC32AF6C3B000A58501 /* AppDelegate.swift in Sources */, DEBE4AC52AF6C3B000A58501 /* SceneDelegate.swift in Sources */, + DEFF1DC02AF8DFA500673B8C /* HomeCollectionViewDataSource.swift in Sources */, DE690EAC2AF8432500E8C451 /* DIContainerImpl.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/TransferList/Presentation/HomeScene/View/Cell/LargeHeaderCell.swift b/TransferList/Presentation/HomeScene/View/Cell/LargeHeaderCell.swift new file mode 100644 index 0000000..6a9e20a --- /dev/null +++ b/TransferList/Presentation/HomeScene/View/Cell/LargeHeaderCell.swift @@ -0,0 +1,23 @@ +// +// LargeHeaderCell.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import UIKit +import UI + +class LargeHeaderCell: BaseCollectionCell { + + @InstantiateView(type: TitleLabel.self) private var title + + override func setupViews() { + self.addSubview(title) + title.pinToSuperview() + } + + public func setTitle(_ text: String) { + title.text = text + } +} diff --git a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift new file mode 100644 index 0000000..6502d8c --- /dev/null +++ b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift @@ -0,0 +1,132 @@ +// +// HomeCollectionViewDataSource.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import UIKit +import UI + +class HomeCollectionViewDataSource { + private typealias DiffableDataSource = UICollectionViewDiffableDataSource + private typealias DiffableSnapshot = NSDiffableDataSourceSnapshot + typealias UISection = HomeItem.UISection + + private var dataSource: DiffableDataSource! + private var collectionView: BaseCollectionView + + init(collectionView: BaseCollectionView) { + self.collectionView = collectionView + + configureCollectionView() + configureDiffableDataSource() + } + + private func configureCollectionView() { + collectionView.registerReusableCell(type: LargeHeaderCell.self) + } + + private func configureDiffableDataSource() { + dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, + cellProvider: { [weak self] _, indexPath, itemIdentifier in + switch itemIdentifier { + case .header(let title): + return self?.createTitleCell(for: indexPath, title: title) + } + }) + + self.dataSource.supplementaryViewProvider = collectionView.makeSeprator() + } + + private func createTitleCell(for indexPath: IndexPath, title: String) -> LargeHeaderCell { + let cell: LargeHeaderCell = collectionView.dequeueReusableCell(for: indexPath) + cell.setTitle(title) + return cell + } + + public func LoadTitle(message: String) { + var snapShot = dataSource.snapshot() + snapShot.appendSections([.FavoritesTitle]) + snapShot.appendItems([.header(title: message)], toSection: .FavoritesTitle) + dataSource.apply(snapShot, animatingDifferences: true) + } +// private func initDataSource(_ items: [UISection]) { +// var initialSnapshot = DiffableSnapshot() +// let sections = items.map { $0.section } +// initialSnapshot.appendSections(sections) +// +// for item in items { +// initialSnapshot.appendItems(item.items, toSection: item.section) +// } +// self.dataSource.apply(initialSnapshot, animatingDifferences: true) +// } + +// private func prepareToDecideShowTopicRowsBasedOn(topicCount count: Int, into snapShot: inout DiffableSnapshot) { +// let isContainTopicItems = snapShot.itemIdentifiers.contains(.topicsHeader) +// if count == 0 && isContainTopicItems { +// snapShot.deleteSections([.topicsHeader, .topics]) +// } +// } +// +// private func checkDeleteSearchOrTopicsBasedOnDeleteTopic(saveChangeIntoSnapShot snapShot: inout DiffableSnapshot) { +// if snapShot.itemIdentifiers(inSection: .topics).isEmpty { +// snapShot.deleteSections([.search, .topics, .topicsHeader]) +// } +// } + +// func loadTopicCount(count: Int) { +// var snapShot = dataSource.snapshot() +// languageItem.topicCount = count +// snapShot.reloadItems([.topicCount]) +// +// prepareToDecideShowTopicRowsBasedOn(topicCount: count, into: &snapShot) +// dataSource.apply(snapShot, animatingDifferences: true) +// } + +// func loadFirstTimeTopics(topics items: [Topic]) { +// var snapShot = dataSource.snapshot() +// if !snapShot.sectionIdentifiers.contains(.topicsHeader) { +// snapShot.insertSections([.search, .topicsHeader, .topics], beforeSection: .overviewHeader) +// snapShot.appendItems([.topicsHeader], toSection: .topicsHeader) +// snapShot.appendItems([.search], toSection: .search) +// } else { +// // remove all topics and load with new sort +// snapShot.deleteItems(snapShot.itemIdentifiers(inSection: .topics)) +// } +// +// let topicItems = items.map { DetailLanguage.topics(item: $0) } +// snapShot.appendItems(topicItems, toSection: .topics) +// +// dataSource.apply(snapShot, animatingDifferences: true) +// } + + +// func getTopic(at indexPath: IndexPath) -> Topic { +// guard let languageItem = dataSource.itemIdentifier(for: indexPath) else { +// return Topic.createEmptyTopic() +// } +// +// guard case let .topics(item) = languageItem else { +// return Topic.createEmptyTopic() +// } +// +// return item +// } + +// func deleteTopics(topics items: [Topic]) { +// var snapshot = dataSource.snapshot() +// let items = items.map { DetailLanguage.topics(item: $0) } +// snapshot.deleteItems(items) +// +// checkDeleteSearchOrTopicsBasedOnDeleteTopic(saveChangeIntoSnapShot: &snapshot) +// dataSource.apply(snapshot, animatingDifferences: true) +// } + +// func updateTopic(topic item: Topic) { +// var snapshot = dataSource.snapshot() +// let item = DetailLanguage.topics(item: item) +// snapshot.reloadItems([item]) +// dataSource.apply(snapshot, animatingDifferences: true) +// } +} diff --git a/TransferList/Presentation/HomeScene/View/HomeItem.swift b/TransferList/Presentation/HomeScene/View/HomeItem.swift new file mode 100644 index 0000000..f48786d --- /dev/null +++ b/TransferList/Presentation/HomeScene/View/HomeItem.swift @@ -0,0 +1,28 @@ +// +// HomeItem.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import Foundation + +enum HomeItem: Hashable { + typealias UISection = (section: HomeItem.Section, items: [HomeItem]) + + enum Section: CaseIterable { + case FavoritesTitle + } + + case header(title: String) + + func hash(into hasher: inout Hasher) { + switch self { + case .header(let title): hasher.combine(title) + } + } + + static func == (lhs: HomeItem, rhs: HomeItem) -> Bool { + return lhs.hashValue == rhs.hashValue + } +} diff --git a/TransferList/Presentation/HomeScene/View/HomeViewController.swift b/TransferList/Presentation/HomeScene/View/HomeViewController.swift index 4f10f35..bec73c0 100644 --- a/TransferList/Presentation/HomeScene/View/HomeViewController.swift +++ b/TransferList/Presentation/HomeScene/View/HomeViewController.swift @@ -10,15 +10,16 @@ import UI class HomeViewController: BaseCollectionViewController { - @InstantiateView(type: TitleLabel.self) private var titleLabel + private var dataSource: HomeCollectionViewDataSource! override func setupViews() { - - titleLabel.text = "Hello Blu!" - view.addSubview(titleLabel) - NSLayoutConstraint.activate([ - titleLabel.centerXAnchor.constraint(equalTo: view.centerXSafeMargin), - titleLabel.centerYAnchor.constraint(equalTo: view.centerYSafeMargin), - ]) + super.setupViews() + + configureDataSource() + dataSource.LoadTitle(message: "Favorites") + } + + private func configureDataSource() { + dataSource = .init(collectionView: collectionView) } } diff --git a/UI/Sources/UI/View/Base/BaseCollectionView.swift b/UI/Sources/UI/View/Base/BaseCollectionView.swift index 29bde46..5e07d4b 100644 --- a/UI/Sources/UI/View/Base/BaseCollectionView.swift +++ b/UI/Sources/UI/View/Base/BaseCollectionView.swift @@ -13,12 +13,14 @@ open class BaseCollectionView: UICollectionView { super.init(frame: frame, collectionViewLayout: layout) translatesAutoresizingMaskIntoConstraints = false - backgroundColor = Theme.blue + backgroundColor = Theme.background alwaysBounceVertical = true tag = 1009 // for getting collectionView inside BaseViewController keyboardDismissMode = .interactiveWithAccessory contentInset = .init(top: 32, left: 0, bottom: 20, right: 0) + + registerSupplementaryView(for: SeparatorCollectionView.uiIdentifier, type: SeparatorCollectionView.self) } public required init?(coder: NSCoder) { @@ -29,4 +31,16 @@ open class BaseCollectionView: UICollectionView { guard previousTraitCollection?.userInterfaceStyle == traitCollection.userInterfaceStyle else { return } collectionViewLayout.invalidateLayout() } + + public func makeSeprator() -> UICollectionViewDiffableDataSource.SupplementaryViewProvider { + return { (collView, _, indexPath) -> UICollectionReusableView? in + let identifier = SeparatorCollectionView.uiIdentifier + + let separatorView: SeparatorCollectionView = + collView.dequeueSupplementaryView(for: identifier, at: indexPath) + + separatorView.isHidden = indexPath.row == 0 + return separatorView + } + } } diff --git a/UI/Sources/UI/View/CollectionView/BaseCollectionViewController.swift b/UI/Sources/UI/View/CollectionView/BaseCollectionViewController.swift index 177346f..61c0b86 100644 --- a/UI/Sources/UI/View/CollectionView/BaseCollectionViewController.swift +++ b/UI/Sources/UI/View/CollectionView/BaseCollectionViewController.swift @@ -10,7 +10,7 @@ import UIKit open class BaseCollectionViewController: BaseViewController, BorderCompositionalLayoutDelegate { - private(set) lazy var collectionView: BaseCollectionView = { + public private(set) lazy var collectionView: BaseCollectionView = { BaseCollectionView(frame: .zero, collectionViewLayout: createBorderLayout()) }() From 7b5991062b8ea312c6118ba20a762127906c9aff Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Mon, 6 Nov 2023 13:48:46 +0330 Subject: [PATCH 04/12] Add VerticalAccount Cell, All Title and Account Display, ViewModels - Added the VerticalAccount Cell for displaying vertical account information. - Added display of "All Title" and "All Account" in the view. - Added the ViewModel for fetching data from the Use Case. - Implemented a ViewModelFactory for easier creation of view models. --- .../Domain/Entities/PersonBankAccount.swift | 12 +- TransferList.xcodeproj/project.pbxproj | 28 +++++ TransferList/Application/SceneDelegate.swift | 5 +- .../Extra/Core/ViewModelFactory.swift | 30 +++++ .../View/Cell/VerticalAccountCell.swift | 54 +++++++++ .../View/HomeCollectionViewDataSource.swift | 37 ++++-- .../HomeScene/View/HomeItem.swift | 7 ++ .../HomeScene/View/HomeViewController.swift | 20 ++- .../ViewModel/TransferViewModel.swift | 114 ++++++++++++++++++ 9 files changed, 290 insertions(+), 17 deletions(-) create mode 100644 TransferList/Extra/Core/ViewModelFactory.swift create mode 100644 TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift create mode 100644 TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift diff --git a/Domain/Sources/Domain/Entities/PersonBankAccount.swift b/Domain/Sources/Domain/Entities/PersonBankAccount.swift index 017dafc..b8fc415 100644 --- a/Domain/Sources/Domain/Entities/PersonBankAccount.swift +++ b/Domain/Sources/Domain/Entities/PersonBankAccount.swift @@ -7,8 +7,8 @@ import Foundation -public struct PersonBankAccount: Identifiable { - +public struct PersonBankAccount: Identifiable, Hashable { + public var id: String { card?.cardNumber ?? UUID().uuidString } @@ -36,4 +36,12 @@ public struct PersonBankAccount: Identifiable { mutating func update(indexAtList index: Int) { self.indexAtList = index } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: PersonBankAccount, rhs: PersonBankAccount) -> Bool { + lhs.id == rhs.id + } } diff --git a/TransferList.xcodeproj/project.pbxproj b/TransferList.xcodeproj/project.pbxproj index cc7922f..577ff2c 100644 --- a/TransferList.xcodeproj/project.pbxproj +++ b/TransferList.xcodeproj/project.pbxproj @@ -21,6 +21,9 @@ DEFF1DBD2AF8DEE400673B8C /* LargeHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DBC2AF8DEE400673B8C /* LargeHeaderCell.swift */; }; DEFF1DC02AF8DFA500673B8C /* HomeCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DBF2AF8DFA500673B8C /* HomeCollectionViewDataSource.swift */; }; DEFF1DC22AF8DFE300673B8C /* HomeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DC12AF8DFE300673B8C /* HomeItem.swift */; }; + DEFF1DC42AF8E8DC00673B8C /* TransferViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DC32AF8E8DC00673B8C /* TransferViewModel.swift */; }; + DEFF1DC82AF8EAD300673B8C /* ViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DC72AF8EAD300673B8C /* ViewModelFactory.swift */; }; + DEFF1DCA2AF8EC6A00673B8C /* VerticalAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DC92AF8EC6A00673B8C /* VerticalAccountCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -40,6 +43,9 @@ DEFF1DBC2AF8DEE400673B8C /* LargeHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeHeaderCell.swift; sourceTree = ""; }; DEFF1DBF2AF8DFA500673B8C /* HomeCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCollectionViewDataSource.swift; sourceTree = ""; }; DEFF1DC12AF8DFE300673B8C /* HomeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeItem.swift; sourceTree = ""; }; + DEFF1DC32AF8E8DC00673B8C /* TransferViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferViewModel.swift; sourceTree = ""; }; + DEFF1DC72AF8EAD300673B8C /* ViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelFactory.swift; sourceTree = ""; }; + DEFF1DC92AF8EC6A00673B8C /* VerticalAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalAccountCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -116,6 +122,7 @@ DE690EAF2AF8453800E8C451 /* ViewModel */ = { isa = PBXGroup; children = ( + DEFF1DC32AF8E8DC00673B8C /* TransferViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -143,6 +150,7 @@ DEBE4AC12AF6C3B000A58501 /* TransferList */ = { isa = PBXGroup; children = ( + DEFF1DC52AF8EAC700673B8C /* Extra */, DE690EA02AF842B400E8C451 /* Application */, DEFF1DBA2AF8DDFE00673B8C /* Presentation */, DE690E9F2AF8429D00E8C451 /* Resources */, @@ -162,10 +170,27 @@ isa = PBXGroup; children = ( DEFF1DBC2AF8DEE400673B8C /* LargeHeaderCell.swift */, + DEFF1DC92AF8EC6A00673B8C /* VerticalAccountCell.swift */, ); path = Cell; sourceTree = ""; }; + DEFF1DC52AF8EAC700673B8C /* Extra */ = { + isa = PBXGroup; + children = ( + DEFF1DC62AF8EACB00673B8C /* Core */, + ); + path = Extra; + sourceTree = ""; + }; + DEFF1DC62AF8EACB00673B8C /* Core */ = { + isa = PBXGroup; + children = ( + DEFF1DC72AF8EAD300673B8C /* ViewModelFactory.swift */, + ); + path = Core; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -243,7 +268,10 @@ buildActionMask = 2147483647; files = ( DEFF1DBD2AF8DEE400673B8C /* LargeHeaderCell.swift in Sources */, + DEFF1DCA2AF8EC6A00673B8C /* VerticalAccountCell.swift in Sources */, + DEFF1DC42AF8E8DC00673B8C /* TransferViewModel.swift in Sources */, DEFF1DC22AF8DFE300673B8C /* HomeItem.swift in Sources */, + DEFF1DC82AF8EAD300673B8C /* ViewModelFactory.swift in Sources */, DE690EB12AF8455400E8C451 /* HomeViewController.swift in Sources */, DE690EAA2AF842F800E8C451 /* DIContainer.swift in Sources */, DEBE4AC32AF6C3B000A58501 /* AppDelegate.swift in Sources */, diff --git a/TransferList/Application/SceneDelegate.swift b/TransferList/Application/SceneDelegate.swift index aaa0bdf..deaa961 100644 --- a/TransferList/Application/SceneDelegate.swift +++ b/TransferList/Application/SceneDelegate.swift @@ -52,6 +52,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private func startApp(windowScene scene: UIWindowScene) { let window = UIWindow(windowScene: scene) + setupDIContainer() registerFonts() let navController = createNavigationController() @@ -81,7 +82,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } private func setupDIContainer() { -// let container = DIContainerImpl() -// ViewModelFactory.shared.register(DIContainer: container) + let container = DIContainerImpl() + ViewModelFactory.shared.register(DIContainer: container) } } diff --git a/TransferList/Extra/Core/ViewModelFactory.swift b/TransferList/Extra/Core/ViewModelFactory.swift new file mode 100644 index 0000000..872a8f4 --- /dev/null +++ b/TransferList/Extra/Core/ViewModelFactory.swift @@ -0,0 +1,30 @@ +// +// ViewModelFactory.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import Foundation + +class ViewModelFactory { + + private var container: DIContainer! + + static let shared = ViewModelFactory() + + private init() {} + + func register(DIContainer container: DIContainer) { + self.container = container + } + + func createMediaViewModel() -> TransferViewModel { + guard container != nil else { + fatalError("Register DIContainer to Enable ViewModel Creation") + } + + let personBankAccountUseCase = container.createPresonBankAccountUseCase() + return .init(personBackAccountUseCase: personBankAccountUseCase) + } +} diff --git a/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift b/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift new file mode 100644 index 0000000..bf4d4b3 --- /dev/null +++ b/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift @@ -0,0 +1,54 @@ +// +// VerticalAccountCell.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import UIKit +import UI +import Domain + +class VerticalAccountCell: BaseCollectionCell { + + @InstantiateView(type: ListLabel.self) var nameLabel + @InstantiateView(type: SubTitleLabel.self) var cardTypeLabel + @InstantiateView(type: UIStackView.self) var labelsStackView + @InstantiateView(type: UIImageView.self) var arrowImage + @InstantiateView(type: UIImageView.self) var favoriteImage + @InstantiateView(type: UIStackView.self) var imagesStackView + + override func setupViews() { + super.setupViews() + + addSubview(labelsStackView) + addSubview(imagesStackView) + + NSLayoutConstraint.activate([ + labelsStackView.topAnchor.constraint(equalTo: topSafeMargin, constant: 16), + labelsStackView.bottomAnchor.constraint(equalTo: bottomSafeMargin, constant: -16), + labelsStackView.leadingAnchor.constraint(equalTo: leadingSafeMargin), + labelsStackView.trailingAnchor.constraint(equalTo: trailingSafeMargin) +// labelsStackView.trailingAnchor.constraint(lessThanOrEqualTo: clock.leadingAnchor, constant: -12), +// clock.centerYAnchor.constraint(equalTo: centerYSafeMargin), +// clock.trailingAnchor.constraint(equalTo: trailingSafeMargin, constant: -18) + ]) + + configureLabelsStackView() + } + + private func configureLabelsStackView() { + labelsStackView.spacing = 4 + labelsStackView.alignment = .leading + labelsStackView.axis = .vertical + labelsStackView.distribution = .fill + + labelsStackView.addArrangedSubview(nameLabel) + labelsStackView.addArrangedSubview(cardTypeLabel) + } + + func setAccountItem(_ personAccount: PersonBankAccount) { + nameLabel.text = personAccount.person?.name + cardTypeLabel.text = personAccount.card?.cardType + } +} diff --git a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift index 6502d8c..0a4932a 100644 --- a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift +++ b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift @@ -7,6 +7,7 @@ import UIKit import UI +import Domain class HomeCollectionViewDataSource { private typealias DiffableDataSource = UICollectionViewDiffableDataSource @@ -21,10 +22,12 @@ class HomeCollectionViewDataSource { configureCollectionView() configureDiffableDataSource() + setupDiffableSnapshot() } private func configureCollectionView() { collectionView.registerReusableCell(type: LargeHeaderCell.self) + collectionView.registerReusableCell(type: VerticalAccountCell.self) } private func configureDiffableDataSource() { @@ -33,34 +36,48 @@ class HomeCollectionViewDataSource { switch itemIdentifier { case .header(let title): return self?.createTitleCell(for: indexPath, title: title) + case .personBankAccount(let account): + return self?.createVerticalAccount(for: indexPath, account: account) } }) self.dataSource.supplementaryViewProvider = collectionView.makeSeprator() } + private func setupDiffableSnapshot() { + var initialSnapshot = DiffableSnapshot() + initialSnapshot.appendSections([.allTitle, .personBankAccounts]) + initialSnapshot.appendItems([.header(title: "All")], toSection: .allTitle) + self.dataSource.apply(initialSnapshot, animatingDifferences: false) + } + private func createTitleCell(for indexPath: IndexPath, title: String) -> LargeHeaderCell { let cell: LargeHeaderCell = collectionView.dequeueReusableCell(for: indexPath) cell.setTitle(title) return cell } + private func createVerticalAccount(for indexPath: IndexPath, account: PersonBankAccount) -> VerticalAccountCell { + let cell: VerticalAccountCell = collectionView.dequeueReusableCell(for: indexPath) + cell.setAccountItem(account) + return cell + } + public func LoadTitle(message: String) { var snapShot = dataSource.snapshot() snapShot.appendSections([.FavoritesTitle]) snapShot.appendItems([.header(title: message)], toSection: .FavoritesTitle) dataSource.apply(snapShot, animatingDifferences: true) } -// private func initDataSource(_ items: [UISection]) { -// var initialSnapshot = DiffableSnapshot() -// let sections = items.map { $0.section } -// initialSnapshot.appendSections(sections) -// -// for item in items { -// initialSnapshot.appendItems(item.items, toSection: item.section) -// } -// self.dataSource.apply(initialSnapshot, animatingDifferences: true) -// } + + public func updateAccounts(_ accounts: [PersonBankAccount]) { + var snapShot = dataSource.snapshot() + let items = accounts.map { + return HomeItem.personBankAccount(account: $0) + } + snapShot.appendItems(items, toSection: .personBankAccounts) + dataSource.apply(snapShot, animatingDifferences: true) + } // private func prepareToDecideShowTopicRowsBasedOn(topicCount count: Int, into snapShot: inout DiffableSnapshot) { // let isContainTopicItems = snapShot.itemIdentifiers.contains(.topicsHeader) diff --git a/TransferList/Presentation/HomeScene/View/HomeItem.swift b/TransferList/Presentation/HomeScene/View/HomeItem.swift index f48786d..f4c1aee 100644 --- a/TransferList/Presentation/HomeScene/View/HomeItem.swift +++ b/TransferList/Presentation/HomeScene/View/HomeItem.swift @@ -6,19 +6,26 @@ // import Foundation +import Domain enum HomeItem: Hashable { typealias UISection = (section: HomeItem.Section, items: [HomeItem]) enum Section: CaseIterable { case FavoritesTitle + case allTitle + case personBankAccounts } case header(title: String) + case personBankAccount(account: PersonBankAccount) func hash(into hasher: inout Hasher) { switch self { case .header(let title): hasher.combine(title) + case .personBankAccount(let account): + hasher.combine("accounts") + hasher.combine(account) } } diff --git a/TransferList/Presentation/HomeScene/View/HomeViewController.swift b/TransferList/Presentation/HomeScene/View/HomeViewController.swift index bec73c0..a903e09 100644 --- a/TransferList/Presentation/HomeScene/View/HomeViewController.swift +++ b/TransferList/Presentation/HomeScene/View/HomeViewController.swift @@ -7,19 +7,33 @@ import UIKit import UI +import Combine class HomeViewController: BaseCollectionViewController { private var dataSource: HomeCollectionViewDataSource! - + private let viewModel = ViewModelFactory.shared.createMediaViewModel() + private var subscriptions = Set() + override func setupViews() { super.setupViews() - + configureDataSource() - dataSource.LoadTitle(message: "Favorites") + observeDidChangeData() + viewModel.fetchTransferList() } + override var spaceSeparatorFromEdgeInList: CGFloat { return 0 } + private func configureDataSource() { dataSource = .init(collectionView: collectionView) } + + private func observeDidChangeData() { + viewModel.$bankAccounts + .sink { [weak self] accounts in + self?.dataSource?.updateAccounts(accounts) + } + .store(in: &subscriptions) + } } diff --git a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift new file mode 100644 index 0000000..35631fe --- /dev/null +++ b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift @@ -0,0 +1,114 @@ +// +// TransferViewModel.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import Foundation +import Combine +import Domain + +class TransferViewModel { + +// @Published private(set) var viewState: ViewState = .loading + @Published var bankAccounts: [PersonBankAccount] = [] +// @Published var path = NavigationPath() + private let useCase: PersonBankAccountUseCase + private var subscriptions = Set() + private var currentOffset: Int = 0 + + deinit { + cancelAllPendingTask() + } + + init(personBackAccountUseCase: PersonBankAccountUseCase) { + useCase = personBackAccountUseCase + } + + private func cancelAllPendingTask() { + subscriptions.removeAll() + } + +// private func updateViewState(newState viewState: ViewState) { +// self.viewState = viewState +// } +// +// func selected(media: Media) { +// path.append(NavigationRouter.DetailOfMedia(media: media)) +// } + + func fetchTransferList() { + // update view state + + useCase.fetchPersonAccounts(withOffest: currentOffset + 1) + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self else { return } + + switch completion { + case .finished: break + + case .failure(let error): + // show error + break + } + + } receiveValue: { [weak self] accounts in + guard let self else { return } + self.updateAccounts(appendAccounts: accounts) + } + .store(in: &subscriptions) + } + + func updateAccounts(appendAccounts accounts: [PersonBankAccount]) { + guard !accounts.isEmpty else { + // reach to end + return + } + + bankAccounts.append(contentsOf: accounts) + currentOffset += 1 + } + +// func fetchMediaList() { +// guard mediaList.isEmpty else { +// return +// } +// +// updateViewState(newState: .loading) +// +// useCase.fetchMediaList() +// .receive(on: DispatchQueue.main) +// .sink { [weak self] completion in +// guard let self else { return } +// switch completion { +// case .failure(let error): +// let alertContent = AlertContent(message: error.localizedDescription) +// self.updateViewState(newState: .error(alertContent: alertContent)) +// case .finished: break +// } +// } receiveValue: { [weak self] mediaList in +// self?.mediaList = mediaList +// self?.updateViewState(newState: .result) +// } +// .store(in: &subscriptions) +// } + +// func fetchMediaImage(WithImageUrl imageUrl: String?, previewImage: @escaping BackImage) { +// guard let imageUrl else { +// previewImage(Image("imageFailed")) +// return +// } +// +// useCase.fetchImage(withImageUrl: imageUrl) +// .retry(3) +// .replaceError(with: Image("imageFailed")) +// .receive(on: DispatchQueue.main) +// .sink(receiveValue: { image in +// previewImage(image) +// }) +// .store(in: &subscriptions) +// } +} + From 5c33b5bcd599b8d970d190a23574eb449acb98b4 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:29:37 +0330 Subject: [PATCH 05/12] Add DataTransfer, PaginationMode, and Datasource Updates - Added DataTransfer for publishing new data to the UI from the ViewModel. - Add PaginationMode to handle reaching the end of a page. - Updated the Datasource based on the DataTransfer for improved data management. --- .../Domain/Entities/PersonBankAccount.swift | 5 +- TransferList.xcodeproj/project.pbxproj | 40 ++++++++++- .../xcshareddata/swiftpm/Package.resolved | 9 +++ TransferList/Extra/Core/PaginationMode.swift | 32 +++++++++ .../HomeScene/Model/DataTransfer.swift | 29 ++++++++ .../View/Cell/VerticalAccountCell.swift | 37 ++++++++-- .../View/HomeCollectionViewDataSource.swift | 45 +++++++----- .../HomeScene/View/HomeViewController.swift | 23 +++++- .../ViewModel/TransferViewModel.swift | 70 ++++++------------- 9 files changed, 215 insertions(+), 75 deletions(-) create mode 100644 TransferList/Extra/Core/PaginationMode.swift create mode 100644 TransferList/Presentation/HomeScene/Model/DataTransfer.swift diff --git a/Domain/Sources/Domain/Entities/PersonBankAccount.swift b/Domain/Sources/Domain/Entities/PersonBankAccount.swift index b8fc415..425f8b3 100644 --- a/Domain/Sources/Domain/Entities/PersonBankAccount.swift +++ b/Domain/Sources/Domain/Entities/PersonBankAccount.swift @@ -10,7 +10,10 @@ import Foundation public struct PersonBankAccount: Identifiable, Hashable { public var id: String { - card?.cardNumber ?? UUID().uuidString + guard let name = person?.name, let cardNumber = card?.cardNumber else { + return UUID().uuidString + } + return name + cardNumber } public var person: Person? public var card: Card? diff --git a/TransferList.xcodeproj/project.pbxproj b/TransferList.xcodeproj/project.pbxproj index 577ff2c..241dee4 100644 --- a/TransferList.xcodeproj/project.pbxproj +++ b/TransferList.xcodeproj/project.pbxproj @@ -24,6 +24,9 @@ DEFF1DC42AF8E8DC00673B8C /* TransferViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DC32AF8E8DC00673B8C /* TransferViewModel.swift */; }; DEFF1DC82AF8EAD300673B8C /* ViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DC72AF8EAD300673B8C /* ViewModelFactory.swift */; }; DEFF1DCA2AF8EC6A00673B8C /* VerticalAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DC92AF8EC6A00673B8C /* VerticalAccountCell.swift */; }; + DEFF1DCD2AF8F58500673B8C /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DEFF1DCC2AF8F58500673B8C /* Kingfisher */; }; + DEFF1DD02AF8F8BB00673B8C /* PaginationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DCF2AF8F8BB00673B8C /* PaginationMode.swift */; }; + DEFF1DD32AF8FF8100673B8C /* DataTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DD22AF8FF8100673B8C /* DataTransfer.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -46,6 +49,8 @@ DEFF1DC32AF8E8DC00673B8C /* TransferViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferViewModel.swift; sourceTree = ""; }; DEFF1DC72AF8EAD300673B8C /* ViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelFactory.swift; sourceTree = ""; }; DEFF1DC92AF8EC6A00673B8C /* VerticalAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalAccountCell.swift; sourceTree = ""; }; + DEFF1DCF2AF8F8BB00673B8C /* PaginationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationMode.swift; sourceTree = ""; }; + DEFF1DD22AF8FF8100673B8C /* DataTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTransfer.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -53,6 +58,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DEFF1DCD2AF8F58500673B8C /* Kingfisher in Frameworks */, DE690EA62AF842ED00E8C451 /* Domain in Frameworks */, DE690EA82AF842ED00E8C451 /* UI in Frameworks */, DE690EA42AF842ED00E8C451 /* Data in Frameworks */, @@ -101,6 +107,7 @@ DE690EAD2AF8452500E8C451 /* HomeScene */ = { isa = PBXGroup; children = ( + DEFF1DD12AF8FF6D00673B8C /* Model */, DE690EAF2AF8453800E8C451 /* ViewModel */, DE690EAE2AF8453500E8C451 /* View */, ); @@ -150,9 +157,9 @@ DEBE4AC12AF6C3B000A58501 /* TransferList */ = { isa = PBXGroup; children = ( - DEFF1DC52AF8EAC700673B8C /* Extra */, DE690EA02AF842B400E8C451 /* Application */, DEFF1DBA2AF8DDFE00673B8C /* Presentation */, + DEFF1DC52AF8EAC700673B8C /* Extra */, DE690E9F2AF8429D00E8C451 /* Resources */, ); path = TransferList; @@ -187,10 +194,19 @@ isa = PBXGroup; children = ( DEFF1DC72AF8EAD300673B8C /* ViewModelFactory.swift */, + DEFF1DCF2AF8F8BB00673B8C /* PaginationMode.swift */, ); path = Core; sourceTree = ""; }; + DEFF1DD12AF8FF6D00673B8C /* Model */ = { + isa = PBXGroup; + children = ( + DEFF1DD22AF8FF8100673B8C /* DataTransfer.swift */, + ); + path = Model; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -211,6 +227,7 @@ DE690EA32AF842ED00E8C451 /* Data */, DE690EA52AF842ED00E8C451 /* Domain */, DE690EA72AF842ED00E8C451 /* UI */, + DEFF1DCC2AF8F58500673B8C /* Kingfisher */, ); productName = TransferList; productReference = DEBE4ABF2AF6C3B000A58501 /* TransferList.app */; @@ -240,6 +257,9 @@ Base, ); mainGroup = DEBE4AB62AF6C3B000A58501; + packageReferences = ( + DEFF1DCB2AF8F58500673B8C /* XCRemoteSwiftPackageReference "Kingfisher" */, + ); productRefGroup = DEBE4AC02AF6C3B000A58501 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -273,10 +293,12 @@ DEFF1DC22AF8DFE300673B8C /* HomeItem.swift in Sources */, DEFF1DC82AF8EAD300673B8C /* ViewModelFactory.swift in Sources */, DE690EB12AF8455400E8C451 /* HomeViewController.swift in Sources */, + DEFF1DD02AF8F8BB00673B8C /* PaginationMode.swift in Sources */, DE690EAA2AF842F800E8C451 /* DIContainer.swift in Sources */, DEBE4AC32AF6C3B000A58501 /* AppDelegate.swift in Sources */, DEBE4AC52AF6C3B000A58501 /* SceneDelegate.swift in Sources */, DEFF1DC02AF8DFA500673B8C /* HomeCollectionViewDataSource.swift in Sources */, + DEFF1DD32AF8FF8100673B8C /* DataTransfer.swift in Sources */, DE690EAC2AF8432500E8C451 /* DIContainerImpl.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -486,6 +508,17 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + DEFF1DCB2AF8F58500673B8C /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ DE690EA32AF842ED00E8C451 /* Data */ = { isa = XCSwiftPackageProductDependency; @@ -499,6 +532,11 @@ isa = XCSwiftPackageProductDependency; productName = UI; }; + DEFF1DCC2AF8F58500673B8C /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = DEFF1DCB2AF8F58500673B8C /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = DEBE4AB72AF6C3B000A58501 /* Project object */; diff --git a/TransferList.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TransferList.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index bc7ce25..bc11fed 100644 --- a/TransferList.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TransferList.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -8,6 +8,15 @@ "revision" : "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", "version" : "5.8.1" } + }, + { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher.git", + "state" : { + "revision" : "277f1ab2c6664b19b4a412e32b094b201e2d5757", + "version" : "7.10.0" + } } ], "version" : 2 diff --git a/TransferList/Extra/Core/PaginationMode.swift b/TransferList/Extra/Core/PaginationMode.swift new file mode 100644 index 0000000..b248aed --- /dev/null +++ b/TransferList/Extra/Core/PaginationMode.swift @@ -0,0 +1,32 @@ +// +// PaginationMode.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import Foundation + +struct PaginationMode { + + enum Mode { + case continues + case reachedToEnd + } + + var offset: Int = 0 + var mode: Mode = .continues + + var nextOffset: Int { + offset + 1 + } + + mutating func moveToNextOffset() { + self.offset += 1 + } + + mutating func reset() { + offset = 0 + mode = .continues + } +} diff --git a/TransferList/Presentation/HomeScene/Model/DataTransfer.swift b/TransferList/Presentation/HomeScene/Model/DataTransfer.swift new file mode 100644 index 0000000..55bcdda --- /dev/null +++ b/TransferList/Presentation/HomeScene/Model/DataTransfer.swift @@ -0,0 +1,29 @@ +// +// DataTransfer.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import Foundation +import Domain + +struct DataTransfer { + + enum Mode { + case initial + case append + } + + var list: [T] + var mode: Mode + var section: HomeItem.Section + + mutating func append(contentsOf items: [T]) { + if mode == .append { + list.append(contentsOf: items) + } else { + list = items + } + } +} diff --git a/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift b/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift index bf4d4b3..2c268ac 100644 --- a/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift +++ b/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift @@ -8,35 +8,57 @@ import UIKit import UI import Domain +import Kingfisher class VerticalAccountCell: BaseCollectionCell { + @InstantiateView(type: UIImageView.self) var personImageView @InstantiateView(type: ListLabel.self) var nameLabel @InstantiateView(type: SubTitleLabel.self) var cardTypeLabel @InstantiateView(type: UIStackView.self) var labelsStackView - @InstantiateView(type: UIImageView.self) var arrowImage - @InstantiateView(type: UIImageView.self) var favoriteImage + @InstantiateView(type: UIImageView.self) var arrowImageView + @InstantiateView(type: UIImageView.self) var favoriteImageView @InstantiateView(type: UIStackView.self) var imagesStackView + override func prepareForReuse() { + super.prepareForReuse() + + personImageView.image = nil + } + override func setupViews() { super.setupViews() + addSubview(personImageView) addSubview(labelsStackView) addSubview(imagesStackView) NSLayoutConstraint.activate([ + personImageView.centerYAnchor.constraint(equalTo: centerYSafeMargin), + personImageView.leadingAnchor.constraint(equalTo: leadingSafeMargin), + personImageView.heightAnchor.constraint(equalToConstant: 50), + personImageView.widthAnchor.constraint(equalToConstant: 50), labelsStackView.topAnchor.constraint(equalTo: topSafeMargin, constant: 16), labelsStackView.bottomAnchor.constraint(equalTo: bottomSafeMargin, constant: -16), - labelsStackView.leadingAnchor.constraint(equalTo: leadingSafeMargin), + labelsStackView.leadingAnchor.constraint(equalTo: personImageView.trailingAnchor, constant: 12), labelsStackView.trailingAnchor.constraint(equalTo: trailingSafeMargin) // labelsStackView.trailingAnchor.constraint(lessThanOrEqualTo: clock.leadingAnchor, constant: -12), // clock.centerYAnchor.constraint(equalTo: centerYSafeMargin), // clock.trailingAnchor.constraint(equalTo: trailingSafeMargin, constant: -18) ]) + configurePersonImageView() configureLabelsStackView() } + private func configurePersonImageView() { + personImageView.contentMode = .scaleAspectFit + personImageView.layer.borderColor = Theme.border?.cgColor + personImageView.layer.borderWidth = 0.5 + personImageView.setCornerRadius(radius: 25) + personImageView.layer.masksToBounds = true + } + private func configureLabelsStackView() { labelsStackView.spacing = 4 labelsStackView.alignment = .leading @@ -49,6 +71,13 @@ class VerticalAccountCell: BaseCollectionCell { func setAccountItem(_ personAccount: PersonBankAccount) { nameLabel.text = personAccount.person?.name - cardTypeLabel.text = personAccount.card?.cardType + cardTypeLabel.text = personAccount.card?.cardNumber + + let avatarUrl = URL(string: personAccount.person?.avatar ?? "") + personImageView.kf.setImage(with: avatarUrl, placeholder: UIImage()) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + personImageView.layer.borderColor = Theme.border?.cgColor } } diff --git a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift index 0a4932a..05e2379 100644 --- a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift +++ b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift @@ -22,7 +22,6 @@ class HomeCollectionViewDataSource { configureCollectionView() configureDiffableDataSource() - setupDiffableSnapshot() } private func configureCollectionView() { @@ -44,13 +43,6 @@ class HomeCollectionViewDataSource { self.dataSource.supplementaryViewProvider = collectionView.makeSeprator() } - private func setupDiffableSnapshot() { - var initialSnapshot = DiffableSnapshot() - initialSnapshot.appendSections([.allTitle, .personBankAccounts]) - initialSnapshot.appendItems([.header(title: "All")], toSection: .allTitle) - self.dataSource.apply(initialSnapshot, animatingDifferences: false) - } - private func createTitleCell(for indexPath: IndexPath, title: String) -> LargeHeaderCell { let cell: LargeHeaderCell = collectionView.dequeueReusableCell(for: indexPath) cell.setTitle(title) @@ -63,19 +55,38 @@ class HomeCollectionViewDataSource { return cell } - public func LoadTitle(message: String) { - var snapShot = dataSource.snapshot() - snapShot.appendSections([.FavoritesTitle]) - snapShot.appendItems([.header(title: message)], toSection: .FavoritesTitle) - dataSource.apply(snapShot, animatingDifferences: true) + public func updateData(_ dataTransfer: DataTransfer) { + switch dataTransfer.section { + case .personBankAccounts: updateAllSection(dataTransfer) + + default: break + } } - public func updateAccounts(_ accounts: [PersonBankAccount]) { - var snapShot = dataSource.snapshot() - let items = accounts.map { + private func updateAllSection(_ dataTransfer: DataTransfer) { + let items = dataTransfer.list.map { return HomeItem.personBankAccount(account: $0) } - snapShot.appendItems(items, toSection: .personBankAccounts) + + var snapShot = dataSource.snapshot() + if snapShot.sectionIdentifiers.contains(dataTransfer.section) { + if dataTransfer.mode == .append { + snapShot.appendItems(items, toSection: dataTransfer.section) + } else { + snapShot.deleteItems(snapShot.itemIdentifiers(inSection: dataTransfer.section)) + snapShot.appendItems(items, toSection: dataTransfer.section) + } + } else { + + if !snapShot.sectionIdentifiers.contains(.allTitle) { + snapShot.appendSections([.allTitle]) + snapShot.appendItems([.header(title: "All")], toSection: .allTitle) + } + + snapShot.appendSections([dataTransfer.section]) + snapShot.appendItems(items, toSection: dataTransfer.section) + } + dataSource.apply(snapShot, animatingDifferences: true) } diff --git a/TransferList/Presentation/HomeScene/View/HomeViewController.swift b/TransferList/Presentation/HomeScene/View/HomeViewController.swift index a903e09..823a465 100644 --- a/TransferList/Presentation/HomeScene/View/HomeViewController.swift +++ b/TransferList/Presentation/HomeScene/View/HomeViewController.swift @@ -11,6 +11,7 @@ import Combine class HomeViewController: BaseCollectionViewController { + @InstantiateView(type: UIRefreshControl.self) private var refresher private var dataSource: HomeCollectionViewDataSource! private let viewModel = ViewModelFactory.shared.createMediaViewModel() private var subscriptions = Set() @@ -18,6 +19,7 @@ class HomeViewController: BaseCollectionViewController { override func setupViews() { super.setupViews() + configureRefresher() configureDataSource() observeDidChangeData() viewModel.fetchTransferList() @@ -25,14 +27,29 @@ class HomeViewController: BaseCollectionViewController { override var spaceSeparatorFromEdgeInList: CGFloat { return 0 } + private func configureRefresher() { + collectionView.alwaysBounceVertical = true + refresher.tintColor = Theme.supplementaryBackground + refresher.addTarget(self, action: #selector(loadData), for: .valueChanged) + collectionView.addSubview(refresher) + } + + @objc func loadData() { + self.refresher.beginRefreshing() + + viewModel.refreshData() + } + private func configureDataSource() { dataSource = .init(collectionView: collectionView) } private func observeDidChangeData() { - viewModel.$bankAccounts - .sink { [weak self] accounts in - self?.dataSource?.updateAccounts(accounts) + viewModel.$dataUpdated + .compactMap { $0 } + .sink { [weak self] data in + self?.refresher.endRefreshing() + self?.dataSource.updateData(data) } .store(in: &subscriptions) } diff --git a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift index 35631fe..a536b7b 100644 --- a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift +++ b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift @@ -11,12 +11,12 @@ import Domain class TransferViewModel { -// @Published private(set) var viewState: ViewState = .loading - @Published var bankAccounts: [PersonBankAccount] = [] -// @Published var path = NavigationPath() + @Published var dataUpdated: DataTransfer! private let useCase: PersonBankAccountUseCase private var subscriptions = Set() - private var currentOffset: Int = 0 + private var paginationMode = PaginationMode() + private var dataFromServer: DataTransfer! + private var dataFromLocal: DataTransfer! deinit { cancelAllPendingTask() @@ -41,7 +41,7 @@ class TransferViewModel { func fetchTransferList() { // update view state - useCase.fetchPersonAccounts(withOffest: currentOffset + 1) + useCase.fetchPersonAccounts(withOffest: paginationMode.nextOffset) .receive(on: DispatchQueue.main) .sink { [weak self] completion in guard let self else { return } @@ -57,58 +57,30 @@ class TransferViewModel { } receiveValue: { [weak self] accounts in guard let self else { return } self.updateAccounts(appendAccounts: accounts) + self.paginationMode.moveToNextOffset() } .store(in: &subscriptions) } - func updateAccounts(appendAccounts accounts: [PersonBankAccount]) { + public func refreshData() { + // check view state must not loading + + dataFromServer?.mode = .initial + fetchTransferList() + } + + private func updateAccounts(appendAccounts accounts: [PersonBankAccount]) { guard !accounts.isEmpty else { // reach to end return } + + if dataFromServer == nil { + dataFromServer = .init(list: accounts, mode: .initial, section: .personBankAccounts) + } else { + dataFromServer.append(contentsOf: accounts) + } - bankAccounts.append(contentsOf: accounts) - currentOffset += 1 + dataUpdated = dataFromServer } - -// func fetchMediaList() { -// guard mediaList.isEmpty else { -// return -// } -// -// updateViewState(newState: .loading) -// -// useCase.fetchMediaList() -// .receive(on: DispatchQueue.main) -// .sink { [weak self] completion in -// guard let self else { return } -// switch completion { -// case .failure(let error): -// let alertContent = AlertContent(message: error.localizedDescription) -// self.updateViewState(newState: .error(alertContent: alertContent)) -// case .finished: break -// } -// } receiveValue: { [weak self] mediaList in -// self?.mediaList = mediaList -// self?.updateViewState(newState: .result) -// } -// .store(in: &subscriptions) -// } - -// func fetchMediaImage(WithImageUrl imageUrl: String?, previewImage: @escaping BackImage) { -// guard let imageUrl else { -// previewImage(Image("imageFailed")) -// return -// } -// -// useCase.fetchImage(withImageUrl: imageUrl) -// .retry(3) -// .replaceError(with: Image("imageFailed")) -// .receive(on: DispatchQueue.main) -// .sink(receiveValue: { image in -// previewImage(image) -// }) -// .store(in: &subscriptions) -// } } - From 1088042d7a1925f421cac6a137d94363e737e309 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:12:18 +0330 Subject: [PATCH 06/12] Implement Pagination Support in Collection View with Continuous Data Fetching --- Data/Sources/Data/Http/ApiImpl.swift | 2 +- TransferList.xcodeproj/project.pbxproj | 4 +++ TransferList/Extra/Core/ViewState.swift | 27 ++++++++++++++++++ .../HomeScene/Model/DataTransfer.swift | 18 ++++++++++-- .../View/HomeCollectionViewDataSource.swift | 11 ++++++++ .../HomeScene/View/HomeViewController.swift | 16 +++++++++-- .../ViewModel/TransferViewModel.swift | 28 +++++++++++++------ 7 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 TransferList/Extra/Core/ViewState.swift diff --git a/Data/Sources/Data/Http/ApiImpl.swift b/Data/Sources/Data/Http/ApiImpl.swift index e1d9d6b..4a98860 100644 --- a/Data/Sources/Data/Http/ApiImpl.swift +++ b/Data/Sources/Data/Http/ApiImpl.swift @@ -39,7 +39,7 @@ final public class ApiImpl: Api { continuation.resume(throwing: NetworkError.cannotConnectToServer) return } - + print(route.urlPath) sessionManager.request(route) .validate(statusCode: 200 ..< 300) .responseData { [weak self] responseData in diff --git a/TransferList.xcodeproj/project.pbxproj b/TransferList.xcodeproj/project.pbxproj index 241dee4..b49eff3 100644 --- a/TransferList.xcodeproj/project.pbxproj +++ b/TransferList.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ DEFF1DCD2AF8F58500673B8C /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DEFF1DCC2AF8F58500673B8C /* Kingfisher */; }; DEFF1DD02AF8F8BB00673B8C /* PaginationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DCF2AF8F8BB00673B8C /* PaginationMode.swift */; }; DEFF1DD32AF8FF8100673B8C /* DataTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DD22AF8FF8100673B8C /* DataTransfer.swift */; }; + DEFF1DD52AF9122D00673B8C /* ViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DD42AF9122D00673B8C /* ViewState.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -51,6 +52,7 @@ DEFF1DC92AF8EC6A00673B8C /* VerticalAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalAccountCell.swift; sourceTree = ""; }; DEFF1DCF2AF8F8BB00673B8C /* PaginationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationMode.swift; sourceTree = ""; }; DEFF1DD22AF8FF8100673B8C /* DataTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTransfer.swift; sourceTree = ""; }; + DEFF1DD42AF9122D00673B8C /* ViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewState.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -195,6 +197,7 @@ children = ( DEFF1DC72AF8EAD300673B8C /* ViewModelFactory.swift */, DEFF1DCF2AF8F8BB00673B8C /* PaginationMode.swift */, + DEFF1DD42AF9122D00673B8C /* ViewState.swift */, ); path = Core; sourceTree = ""; @@ -295,6 +298,7 @@ DE690EB12AF8455400E8C451 /* HomeViewController.swift in Sources */, DEFF1DD02AF8F8BB00673B8C /* PaginationMode.swift in Sources */, DE690EAA2AF842F800E8C451 /* DIContainer.swift in Sources */, + DEFF1DD52AF9122D00673B8C /* ViewState.swift in Sources */, DEBE4AC32AF6C3B000A58501 /* AppDelegate.swift in Sources */, DEBE4AC52AF6C3B000A58501 /* SceneDelegate.swift in Sources */, DEFF1DC02AF8DFA500673B8C /* HomeCollectionViewDataSource.swift in Sources */, diff --git a/TransferList/Extra/Core/ViewState.swift b/TransferList/Extra/Core/ViewState.swift new file mode 100644 index 0000000..ebd4a54 --- /dev/null +++ b/TransferList/Extra/Core/ViewState.swift @@ -0,0 +1,27 @@ +// +// ViewState.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import Foundation + +enum ViewState: Hashable, Equatable { + + case loading + case result + case error(message: String) + + func hash(into hasher: inout Hasher) { + switch self { + case .error: hasher.combine("error") + case .result: hasher.combine("result") + case .loading: hasher.combine("loading") + } + } + + static func == (lhs: ViewState, rhs: ViewState) -> Bool { + return lhs.hashValue == rhs.hashValue + } +} diff --git a/TransferList/Presentation/HomeScene/Model/DataTransfer.swift b/TransferList/Presentation/HomeScene/Model/DataTransfer.swift index 55bcdda..6a45dd2 100644 --- a/TransferList/Presentation/HomeScene/Model/DataTransfer.swift +++ b/TransferList/Presentation/HomeScene/Model/DataTransfer.swift @@ -18,12 +18,26 @@ struct DataTransfer { var list: [T] var mode: Mode var section: HomeItem.Section + private(set) var listHolder: [T] = [] + + init(list: [T], mode: Mode, section: HomeItem.Section) { + self.list = list + self.mode = mode + self.section = section + self.listHolder = list + } mutating func append(contentsOf items: [T]) { if mode == .append { - list.append(contentsOf: items) + listHolder.append(contentsOf: items) } else { - list = items + listHolder = items } + + list = items + } + + func isLastItem(row: Int) -> Bool { + return listHolder.count - 1 == row } } diff --git a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift index 05e2379..f2da1d8 100644 --- a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift +++ b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift @@ -89,6 +89,17 @@ class HomeCollectionViewDataSource { dataSource.apply(snapShot, animatingDifferences: true) } + + public func sectionIdentifier(atIndexPath indexPath: IndexPath) -> HomeItem.Section? { + if #available(iOS 15.0, *) { + return dataSource.sectionIdentifier(for: indexPath.section) + } else { + guard let item = dataSource.itemIdentifier(for: indexPath) else { + return nil + } + return dataSource.snapshot().sectionIdentifier(containingItem: item) + } + } // private func prepareToDecideShowTopicRowsBasedOn(topicCount count: Int, into snapShot: inout DiffableSnapshot) { // let isContainTopicItems = snapShot.itemIdentifiers.contains(.topicsHeader) diff --git a/TransferList/Presentation/HomeScene/View/HomeViewController.swift b/TransferList/Presentation/HomeScene/View/HomeViewController.swift index 823a465..d72e3b6 100644 --- a/TransferList/Presentation/HomeScene/View/HomeViewController.swift +++ b/TransferList/Presentation/HomeScene/View/HomeViewController.swift @@ -19,6 +19,7 @@ class HomeViewController: BaseCollectionViewController { override func setupViews() { super.setupViews() + collectionView.delegate = self configureRefresher() configureDataSource() observeDidChangeData() @@ -30,11 +31,11 @@ class HomeViewController: BaseCollectionViewController { private func configureRefresher() { collectionView.alwaysBounceVertical = true refresher.tintColor = Theme.supplementaryBackground - refresher.addTarget(self, action: #selector(loadData), for: .valueChanged) + refresher.addTarget(self, action: #selector(refreshData), for: .valueChanged) collectionView.addSubview(refresher) } - @objc func loadData() { + @objc func refreshData() { self.refresher.beginRefreshing() viewModel.refreshData() @@ -54,3 +55,14 @@ class HomeViewController: BaseCollectionViewController { .store(in: &subscriptions) } } + +extension HomeViewController: UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, + willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + guard let section = dataSource.sectionIdentifier(atIndexPath: indexPath) else { + return + } + viewModel.itemDisplay(atSection: section, row: indexPath.row) + } +} diff --git a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift index a536b7b..43051b4 100644 --- a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift +++ b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift @@ -11,6 +11,7 @@ import Domain class TransferViewModel { + @Published var viewState: ViewState! @Published var dataUpdated: DataTransfer! private let useCase: PersonBankAccountUseCase private var subscriptions = Set() @@ -32,14 +33,10 @@ class TransferViewModel { // private func updateViewState(newState viewState: ViewState) { // self.viewState = viewState -// } -// -// func selected(media: Media) { -// path.append(NavigationRouter.DetailOfMedia(media: media)) // } - func fetchTransferList() { - // update view state + public func fetchTransferList() { + viewState = .loading useCase.fetchPersonAccounts(withOffest: paginationMode.nextOffset) .receive(on: DispatchQueue.main) @@ -50,7 +47,7 @@ class TransferViewModel { case .finished: break case .failure(let error): - // show error + self.viewState = .error(message: error.localizedDescription) break } @@ -58,20 +55,33 @@ class TransferViewModel { guard let self else { return } self.updateAccounts(appendAccounts: accounts) self.paginationMode.moveToNextOffset() + self.viewState = .result } .store(in: &subscriptions) } public func refreshData() { - // check view state must not loading + guard viewState != .loading else { return } + paginationMode.reset() dataFromServer?.mode = .initial fetchTransferList() } + public func itemDisplay(atSection section: HomeItem.Section, row: Int) { + guard viewState != .loading else { return } + guard section == .personBankAccounts else { return } + guard paginationMode.mode == .continues else { return } + guard dataFromServer.isLastItem(row: row) else { return } + + dataFromServer?.mode = .append + + fetchTransferList() + } + private func updateAccounts(appendAccounts accounts: [PersonBankAccount]) { guard !accounts.isEmpty else { - // reach to end + paginationMode.mode = .reachedToEnd return } From f2a7231bfec555b6eca05188675f3b958ca4e33a Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Mon, 6 Nov 2023 18:04:54 +0330 Subject: [PATCH 07/12] Add AccountCell, Inheritance for VerticalAccountCell and FavoriteCell, and Local Data Fetching - Added the AccountCell component. - Implemented inheritance from AccountCell in VerticalAccountCell and FavoriteCell. - Added data fetching from local sources and display of favorite items. --- Data/Sources/Data/Http/ApiImpl.swift | 2 +- TransferList.xcodeproj/project.pbxproj | 8 + .../HomeScene/View/Cell/AccountCell.swift | 55 ++++++ .../View/Cell/FavoriteAccountCell.swift | 49 ++++++ .../View/Cell/VerticalAccountCell.swift | 35 +--- .../View/HomeCollectionViewDataSource.swift | 156 ++++++++---------- .../HomeScene/View/HomeItem.swift | 1 + .../HomeScene/View/HomeViewController.swift | 20 +++ .../ViewModel/TransferViewModel.swift | 23 ++- .../AccentColor.colorset/Contents.json | 11 -- .../imageFailed.imageset/Contents.json | 22 +++ .../imageFailed.imageset/imageFailed-dark.png | Bin 0 -> 6460 bytes .../imageFailed-light.png | Bin 0 -> 6290 bytes 13 files changed, 244 insertions(+), 138 deletions(-) create mode 100644 TransferList/Presentation/HomeScene/View/Cell/AccountCell.swift create mode 100644 TransferList/Presentation/HomeScene/View/Cell/FavoriteAccountCell.swift delete mode 100644 TransferList/Resources/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 TransferList/Resources/Assets.xcassets/imageFailed.imageset/Contents.json create mode 100644 TransferList/Resources/Assets.xcassets/imageFailed.imageset/imageFailed-dark.png create mode 100644 TransferList/Resources/Assets.xcassets/imageFailed.imageset/imageFailed-light.png diff --git a/Data/Sources/Data/Http/ApiImpl.swift b/Data/Sources/Data/Http/ApiImpl.swift index 4a98860..a75aa29 100644 --- a/Data/Sources/Data/Http/ApiImpl.swift +++ b/Data/Sources/Data/Http/ApiImpl.swift @@ -39,7 +39,7 @@ final public class ApiImpl: Api { continuation.resume(throwing: NetworkError.cannotConnectToServer) return } - print(route.urlPath) + sessionManager.request(route) .validate(statusCode: 200 ..< 300) .responseData { [weak self] responseData in diff --git a/TransferList.xcodeproj/project.pbxproj b/TransferList.xcodeproj/project.pbxproj index b49eff3..3d1f9cb 100644 --- a/TransferList.xcodeproj/project.pbxproj +++ b/TransferList.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ DEFF1DD02AF8F8BB00673B8C /* PaginationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DCF2AF8F8BB00673B8C /* PaginationMode.swift */; }; DEFF1DD32AF8FF8100673B8C /* DataTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DD22AF8FF8100673B8C /* DataTransfer.swift */; }; DEFF1DD52AF9122D00673B8C /* ViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DD42AF9122D00673B8C /* ViewState.swift */; }; + DEFF1DD72AF91A8A00673B8C /* FavoriteAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DD62AF91A8900673B8C /* FavoriteAccountCell.swift */; }; + DEFF1DD92AF92DB500673B8C /* AccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DD82AF92DB500673B8C /* AccountCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -53,6 +55,8 @@ DEFF1DCF2AF8F8BB00673B8C /* PaginationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationMode.swift; sourceTree = ""; }; DEFF1DD22AF8FF8100673B8C /* DataTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTransfer.swift; sourceTree = ""; }; DEFF1DD42AF9122D00673B8C /* ViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewState.swift; sourceTree = ""; }; + DEFF1DD62AF91A8900673B8C /* FavoriteAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteAccountCell.swift; sourceTree = ""; }; + DEFF1DD82AF92DB500673B8C /* AccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -180,6 +184,8 @@ children = ( DEFF1DBC2AF8DEE400673B8C /* LargeHeaderCell.swift */, DEFF1DC92AF8EC6A00673B8C /* VerticalAccountCell.swift */, + DEFF1DD62AF91A8900673B8C /* FavoriteAccountCell.swift */, + DEFF1DD82AF92DB500673B8C /* AccountCell.swift */, ); path = Cell; sourceTree = ""; @@ -299,11 +305,13 @@ DEFF1DD02AF8F8BB00673B8C /* PaginationMode.swift in Sources */, DE690EAA2AF842F800E8C451 /* DIContainer.swift in Sources */, DEFF1DD52AF9122D00673B8C /* ViewState.swift in Sources */, + DEFF1DD92AF92DB500673B8C /* AccountCell.swift in Sources */, DEBE4AC32AF6C3B000A58501 /* AppDelegate.swift in Sources */, DEBE4AC52AF6C3B000A58501 /* SceneDelegate.swift in Sources */, DEFF1DC02AF8DFA500673B8C /* HomeCollectionViewDataSource.swift in Sources */, DEFF1DD32AF8FF8100673B8C /* DataTransfer.swift in Sources */, DE690EAC2AF8432500E8C451 /* DIContainerImpl.swift in Sources */, + DEFF1DD72AF91A8A00673B8C /* FavoriteAccountCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/TransferList/Presentation/HomeScene/View/Cell/AccountCell.swift b/TransferList/Presentation/HomeScene/View/Cell/AccountCell.swift new file mode 100644 index 0000000..728ecd5 --- /dev/null +++ b/TransferList/Presentation/HomeScene/View/Cell/AccountCell.swift @@ -0,0 +1,55 @@ +// +// AccountCell.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import UIKit +import UI +import Domain +import Kingfisher + +class AccountCell: BaseCollectionCell { + + @InstantiateView(type: UIImageView.self) var personImageView + @InstantiateView(type: ListLabel.self) var nameLabel + @InstantiateView(type: SubTitleLabel.self) var cardTypeLabel + + override func prepareForReuse() { + super.prepareForReuse() + + personImageView.image = nil + } + + override func setupViews() { + super.setupViews() + + NSLayoutConstraint.activate([ + personImageView.heightAnchor.constraint(equalToConstant: 50), + personImageView.widthAnchor.constraint(equalToConstant: 50) + ]) + + configurePersonImageView() + } + + private func configurePersonImageView() { + personImageView.contentMode = .scaleAspectFit + personImageView.layer.borderColor = Theme.border?.cgColor + personImageView.layer.borderWidth = 0.5 + personImageView.setCornerRadius(radius: 25) + personImageView.layer.masksToBounds = true + } + + open func setAccountItem(_ personAccount: PersonBankAccount) { + nameLabel.text = personAccount.person?.name + cardTypeLabel.text = personAccount.card?.cardType + + let avatarUrl = URL(string: personAccount.person?.avatar ?? "") + personImageView.kf.setImage(with: avatarUrl, placeholder: UIImage(named: "imageFailed")) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + personImageView.layer.borderColor = Theme.border?.cgColor + } +} diff --git a/TransferList/Presentation/HomeScene/View/Cell/FavoriteAccountCell.swift b/TransferList/Presentation/HomeScene/View/Cell/FavoriteAccountCell.swift new file mode 100644 index 0000000..99bd3dc --- /dev/null +++ b/TransferList/Presentation/HomeScene/View/Cell/FavoriteAccountCell.swift @@ -0,0 +1,49 @@ +// +// FavoriteAccountCell.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import UIKit +import UI + +class FavoriteAccountCell: AccountCell { + + @InstantiateView(type: UIStackView.self) var stackView + + override func setupViews() { + super.setupViews() + + addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12), + stackView.trailingAnchor.constraint(equalTo: trailingSafeMargin, constant: -12), + stackView.bottomAnchor.constraint(equalTo: bottomSafeMargin), + stackView.topAnchor.constraint(equalTo: topSafeMargin), + ]) + + configureStackView() + } + + private func configureStackView() { + stackView.spacing = 4 + stackView.alignment = .center + stackView.axis = .vertical + stackView.distribution = .fill + + stackView.addArrangedSubview(personImageView) + stackView.addArrangedSubview(nameLabel) + stackView.addArrangedSubview(cardTypeLabel) + + stackView.setCustomSpacing(12, after: personImageView) + + configureLabels() + } + + private func configureLabels() { + nameLabel.textAlignment = .center + cardTypeLabel.textAlignment = .center + } +} diff --git a/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift b/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift index 2c268ac..ac67db6 100644 --- a/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift +++ b/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift @@ -8,23 +8,13 @@ import UIKit import UI import Domain -import Kingfisher -class VerticalAccountCell: BaseCollectionCell { +class VerticalAccountCell: AccountCell { - @InstantiateView(type: UIImageView.self) var personImageView - @InstantiateView(type: ListLabel.self) var nameLabel - @InstantiateView(type: SubTitleLabel.self) var cardTypeLabel @InstantiateView(type: UIStackView.self) var labelsStackView @InstantiateView(type: UIImageView.self) var arrowImageView @InstantiateView(type: UIImageView.self) var favoriteImageView @InstantiateView(type: UIStackView.self) var imagesStackView - - override func prepareForReuse() { - super.prepareForReuse() - - personImageView.image = nil - } override func setupViews() { super.setupViews() @@ -36,8 +26,6 @@ class VerticalAccountCell: BaseCollectionCell { NSLayoutConstraint.activate([ personImageView.centerYAnchor.constraint(equalTo: centerYSafeMargin), personImageView.leadingAnchor.constraint(equalTo: leadingSafeMargin), - personImageView.heightAnchor.constraint(equalToConstant: 50), - personImageView.widthAnchor.constraint(equalToConstant: 50), labelsStackView.topAnchor.constraint(equalTo: topSafeMargin, constant: 16), labelsStackView.bottomAnchor.constraint(equalTo: bottomSafeMargin, constant: -16), labelsStackView.leadingAnchor.constraint(equalTo: personImageView.trailingAnchor, constant: 12), @@ -47,18 +35,9 @@ class VerticalAccountCell: BaseCollectionCell { // clock.trailingAnchor.constraint(equalTo: trailingSafeMargin, constant: -18) ]) - configurePersonImageView() configureLabelsStackView() } - private func configurePersonImageView() { - personImageView.contentMode = .scaleAspectFit - personImageView.layer.borderColor = Theme.border?.cgColor - personImageView.layer.borderWidth = 0.5 - personImageView.setCornerRadius(radius: 25) - personImageView.layer.masksToBounds = true - } - private func configureLabelsStackView() { labelsStackView.spacing = 4 labelsStackView.alignment = .leading @@ -69,15 +48,9 @@ class VerticalAccountCell: BaseCollectionCell { labelsStackView.addArrangedSubview(cardTypeLabel) } - func setAccountItem(_ personAccount: PersonBankAccount) { - nameLabel.text = personAccount.person?.name - cardTypeLabel.text = personAccount.card?.cardNumber + override func setAccountItem(_ personAccount: PersonBankAccount) { + super.setAccountItem(personAccount) - let avatarUrl = URL(string: personAccount.person?.avatar ?? "") - personImageView.kf.setImage(with: avatarUrl, placeholder: UIImage()) - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - personImageView.layer.borderColor = Theme.border?.cgColor + // set isFavorite } } diff --git a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift index f2da1d8..ae7597c 100644 --- a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift +++ b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift @@ -16,6 +16,7 @@ class HomeCollectionViewDataSource { private var dataSource: DiffableDataSource! private var collectionView: BaseCollectionView + private let semaphore = DispatchSemaphore(value: 1) init(collectionView: BaseCollectionView) { self.collectionView = collectionView @@ -26,7 +27,8 @@ class HomeCollectionViewDataSource { private func configureCollectionView() { collectionView.registerReusableCell(type: LargeHeaderCell.self) - collectionView.registerReusableCell(type: VerticalAccountCell.self) + collectionView.registerReusableCell(type: VerticalAccountCell.self) + collectionView.registerReusableCell(type: FavoriteAccountCell.self) } private func configureDiffableDataSource() { @@ -36,7 +38,7 @@ class HomeCollectionViewDataSource { case .header(let title): return self?.createTitleCell(for: indexPath, title: title) case .personBankAccount(let account): - return self?.createVerticalAccount(for: indexPath, account: account) + return self?.createAccountCell(for: indexPath, account: account) } }) @@ -49,45 +51,89 @@ class HomeCollectionViewDataSource { return cell } - private func createVerticalAccount(for indexPath: IndexPath, account: PersonBankAccount) -> VerticalAccountCell { - let cell: VerticalAccountCell = collectionView.dequeueReusableCell(for: indexPath) - cell.setAccountItem(account) - return cell + private func createAccountCell(for indexPath: IndexPath, account: PersonBankAccount) -> AccountCell { + let section = sectionIdentifier(atSection: indexPath.section) + if section == .favoriteBankAcconts { + let cell: FavoriteAccountCell = collectionView.dequeueReusableCell(for: indexPath) + cell.setAccountItem(account) + return cell + + } else { + let cell: VerticalAccountCell = collectionView.dequeueReusableCell(for: indexPath) + cell.setAccountItem(account) + return cell + } } public func updateData(_ dataTransfer: DataTransfer) { + semaphore.wait() + + var snapshot: DiffableSnapshot! switch dataTransfer.section { - case .personBankAccounts: updateAllSection(dataTransfer) - + case .personBankAccounts: snapshot = self.updateAllSection(dataTransfer) + case .favoriteBankAcconts: snapshot = self.updateFavoriteSection(dataTransfer) default: break } + + dataSource.apply(snapshot, animatingDifferences: true) + + self.semaphore.signal() } - private func updateAllSection(_ dataTransfer: DataTransfer) { + private func updateFavoriteSection(_ dataTransfer: DataTransfer) -> DiffableSnapshot { + var snapshot = dataSource.snapshot() + let items = dataTransfer.list.map { return HomeItem.personBankAccount(account: $0) } + guard !items.isEmpty else { return snapshot } + + let newSections = [.FavoritesTitle, dataTransfer.section] + let firstSection = snapshot.sectionIdentifiers.first + if snapshot.sectionIdentifiers.isEmpty { + snapshot.appendSections(newSections) + } else { + snapshot.insertSections(newSections, beforeSection: firstSection!) + } + + snapshot.appendItems([.header(title: "Favorite")], toSection: .FavoritesTitle) + snapshot.appendItems(items, toSection: dataTransfer.section) - var snapShot = dataSource.snapshot() - if snapShot.sectionIdentifiers.contains(dataTransfer.section) { + return snapshot +// dataSource.apply(snapshot, animatingDifferences: true) + } + + private func updateAllSection(_ dataTransfer: DataTransfer) -> DiffableSnapshot { + let items = dataTransfer.list.map { + return HomeItem.personBankAccount(account: $0) + } + + var snapshot = dataSource.snapshot() + if snapshot.sectionIdentifiers.contains(dataTransfer.section) { if dataTransfer.mode == .append { - snapShot.appendItems(items, toSection: dataTransfer.section) + snapshot.appendItems(items, toSection: dataTransfer.section) } else { - snapShot.deleteItems(snapShot.itemIdentifiers(inSection: dataTransfer.section)) - snapShot.appendItems(items, toSection: dataTransfer.section) + snapshot.deleteItems(snapshot.itemIdentifiers(inSection: dataTransfer.section)) + snapshot.appendItems(items, toSection: dataTransfer.section) } } else { - if !snapShot.sectionIdentifiers.contains(.allTitle) { - snapShot.appendSections([.allTitle]) - snapShot.appendItems([.header(title: "All")], toSection: .allTitle) + if !snapshot.sectionIdentifiers.contains(.allTitle) { + snapshot.appendSections([.allTitle]) + snapshot.appendItems([.header(title: "All")], toSection: .allTitle) } - snapShot.appendSections([dataTransfer.section]) - snapShot.appendItems(items, toSection: dataTransfer.section) + snapshot.appendSections([dataTransfer.section]) + snapshot.appendItems(items, toSection: dataTransfer.section) } - - dataSource.apply(snapShot, animatingDifferences: true) + + return snapshot + } + + public func sectionIdentifier(atSection section: Int) -> HomeItem.Section? { + let sections = dataSource.snapshot().sectionIdentifiers + guard sections.indices.contains(section) else { return nil } + return dataSource.snapshot().sectionIdentifiers[section] } public func sectionIdentifier(atIndexPath indexPath: IndexPath) -> HomeItem.Section? { @@ -100,72 +146,4 @@ class HomeCollectionViewDataSource { return dataSource.snapshot().sectionIdentifier(containingItem: item) } } - -// private func prepareToDecideShowTopicRowsBasedOn(topicCount count: Int, into snapShot: inout DiffableSnapshot) { -// let isContainTopicItems = snapShot.itemIdentifiers.contains(.topicsHeader) -// if count == 0 && isContainTopicItems { -// snapShot.deleteSections([.topicsHeader, .topics]) -// } -// } -// -// private func checkDeleteSearchOrTopicsBasedOnDeleteTopic(saveChangeIntoSnapShot snapShot: inout DiffableSnapshot) { -// if snapShot.itemIdentifiers(inSection: .topics).isEmpty { -// snapShot.deleteSections([.search, .topics, .topicsHeader]) -// } -// } - -// func loadTopicCount(count: Int) { -// var snapShot = dataSource.snapshot() -// languageItem.topicCount = count -// snapShot.reloadItems([.topicCount]) -// -// prepareToDecideShowTopicRowsBasedOn(topicCount: count, into: &snapShot) -// dataSource.apply(snapShot, animatingDifferences: true) -// } - -// func loadFirstTimeTopics(topics items: [Topic]) { -// var snapShot = dataSource.snapshot() -// if !snapShot.sectionIdentifiers.contains(.topicsHeader) { -// snapShot.insertSections([.search, .topicsHeader, .topics], beforeSection: .overviewHeader) -// snapShot.appendItems([.topicsHeader], toSection: .topicsHeader) -// snapShot.appendItems([.search], toSection: .search) -// } else { -// // remove all topics and load with new sort -// snapShot.deleteItems(snapShot.itemIdentifiers(inSection: .topics)) -// } -// -// let topicItems = items.map { DetailLanguage.topics(item: $0) } -// snapShot.appendItems(topicItems, toSection: .topics) -// -// dataSource.apply(snapShot, animatingDifferences: true) -// } - - -// func getTopic(at indexPath: IndexPath) -> Topic { -// guard let languageItem = dataSource.itemIdentifier(for: indexPath) else { -// return Topic.createEmptyTopic() -// } -// -// guard case let .topics(item) = languageItem else { -// return Topic.createEmptyTopic() -// } -// -// return item -// } - -// func deleteTopics(topics items: [Topic]) { -// var snapshot = dataSource.snapshot() -// let items = items.map { DetailLanguage.topics(item: $0) } -// snapshot.deleteItems(items) -// -// checkDeleteSearchOrTopicsBasedOnDeleteTopic(saveChangeIntoSnapShot: &snapshot) -// dataSource.apply(snapshot, animatingDifferences: true) -// } - -// func updateTopic(topic item: Topic) { -// var snapshot = dataSource.snapshot() -// let item = DetailLanguage.topics(item: item) -// snapshot.reloadItems([item]) -// dataSource.apply(snapshot, animatingDifferences: true) -// } } diff --git a/TransferList/Presentation/HomeScene/View/HomeItem.swift b/TransferList/Presentation/HomeScene/View/HomeItem.swift index f4c1aee..227bc96 100644 --- a/TransferList/Presentation/HomeScene/View/HomeItem.swift +++ b/TransferList/Presentation/HomeScene/View/HomeItem.swift @@ -13,6 +13,7 @@ enum HomeItem: Hashable { enum Section: CaseIterable { case FavoritesTitle + case favoriteBankAcconts case allTitle case personBankAccounts } diff --git a/TransferList/Presentation/HomeScene/View/HomeViewController.swift b/TransferList/Presentation/HomeScene/View/HomeViewController.swift index d72e3b6..6197cd6 100644 --- a/TransferList/Presentation/HomeScene/View/HomeViewController.swift +++ b/TransferList/Presentation/HomeScene/View/HomeViewController.swift @@ -19,11 +19,13 @@ class HomeViewController: BaseCollectionViewController { override func setupViews() { super.setupViews() + collectionView.contentInset.top = 32 collectionView.delegate = self configureRefresher() configureDataSource() observeDidChangeData() viewModel.fetchTransferList() + viewModel.fetchFavoriteList() } override var spaceSeparatorFromEdgeInList: CGFloat { return 0 } @@ -54,6 +56,24 @@ class HomeViewController: BaseCollectionViewController { } .store(in: &subscriptions) } + + func createCustomSection(at section: Int) -> NSCollectionLayoutSection? { + guard dataSource.sectionIdentifier(atSection: section) == .favoriteBankAcconts else { + return nil + } + + let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(80), + heightDimension: .estimated(80)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + item.edgeSpacing = .init(leading: .fixed(18), top: nil, trailing: nil, bottom: nil) + + let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item]) + + let section = NSCollectionLayoutSection(group: group) + section.orthogonalScrollingBehavior = .continuous + + return section + } } extension HomeViewController: UICollectionViewDelegate { diff --git a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift index 43051b4..0beab04 100644 --- a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift +++ b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift @@ -31,12 +31,23 @@ class TransferViewModel { subscriptions.removeAll() } -// private func updateViewState(newState viewState: ViewState) { -// self.viewState = viewState -// } + private func updateViewState(newState viewState: ViewState) { + self.viewState = viewState + } + + public func fetchFavoriteList() { + useCase.fetchPersonAccounts(withOffest: paginationMode.nextOffset) + .replaceError(with: []) + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] accounts in + guard let self else { return } + self.dataUpdated = self.dataFromLocal + }) + .store(in: &subscriptions) + } public func fetchTransferList() { - viewState = .loading + updateViewState(newState: .loading) useCase.fetchPersonAccounts(withOffest: paginationMode.nextOffset) .receive(on: DispatchQueue.main) @@ -47,7 +58,7 @@ class TransferViewModel { case .finished: break case .failure(let error): - self.viewState = .error(message: error.localizedDescription) + updateViewState(newState: .error(message: error.localizedDescription)) break } @@ -55,7 +66,7 @@ class TransferViewModel { guard let self else { return } self.updateAccounts(appendAccounts: accounts) self.paginationMode.moveToNextOffset() - self.viewState = .result + updateViewState(newState: .result) } .store(in: &subscriptions) } diff --git a/TransferList/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/TransferList/Resources/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/TransferList/Resources/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/TransferList/Resources/Assets.xcassets/imageFailed.imageset/Contents.json b/TransferList/Resources/Assets.xcassets/imageFailed.imageset/Contents.json new file mode 100644 index 0000000..5b8b25c --- /dev/null +++ b/TransferList/Resources/Assets.xcassets/imageFailed.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "imageFailed-light.png", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "imageFailed-dark.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TransferList/Resources/Assets.xcassets/imageFailed.imageset/imageFailed-dark.png b/TransferList/Resources/Assets.xcassets/imageFailed.imageset/imageFailed-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..ede0dff2e42e6b2e09abe91527ce3487b34b52b9 GIT binary patch literal 6460 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 literal 0 HcmV?d00001 diff --git a/TransferList/Resources/Assets.xcassets/imageFailed.imageset/imageFailed-light.png b/TransferList/Resources/Assets.xcassets/imageFailed.imageset/imageFailed-light.png new file mode 100644 index 0000000000000000000000000000000000000000..5897e8c440bed6f24311695124075f5dcfb7c02f GIT binary patch literal 6290 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 XkJQt@uQ5yhg#a+rHP)%tvJ3qmwl30G literal 0 HcmV?d00001 From 19ec39ef57cbaad10f14d7f0b6f8e2592461f181 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Mon, 6 Nov 2023 19:09:47 +0330 Subject: [PATCH 08/12] Add DetailViewController, HeaderInformationCell, and TitleValueCell - Added the DetailViewController for displaying more account information. - Added the HeaderInformationCell for presenting header content. - Added the TitleValueCell for displaying title-value pairs. --- TransferList.xcodeproj/project.pbxproj | 58 +++++++++++++- TransferList/Extra/Core/Router.swift | 26 +++++++ .../DetailAccountScene/Model/DetailItem.swift | 34 +++++++++ .../View/Cell/HeaderInformationCell.swift | 23 ++++++ .../View/Cell/TitleValueCell.swift | 42 +++++++++++ .../View/DetailAccountDataSource.swift | 73 ++++++++++++++++++ .../View/DetailAccountViewConroller.swift | 75 +++++++++++++++++++ .../HomeScene/{View => Model}/HomeItem.swift | 0 .../View/HomeCollectionViewDataSource.swift | 12 +++ .../HomeScene/View/HomeViewController.swift | 36 ++++++++- .../ViewModel/TransferViewModel.swift | 31 ++++---- 11 files changed, 393 insertions(+), 17 deletions(-) create mode 100644 TransferList/Extra/Core/Router.swift create mode 100644 TransferList/Presentation/DetailAccountScene/Model/DetailItem.swift create mode 100644 TransferList/Presentation/DetailAccountScene/View/Cell/HeaderInformationCell.swift create mode 100644 TransferList/Presentation/DetailAccountScene/View/Cell/TitleValueCell.swift create mode 100644 TransferList/Presentation/DetailAccountScene/View/DetailAccountDataSource.swift create mode 100644 TransferList/Presentation/DetailAccountScene/View/DetailAccountViewConroller.swift rename TransferList/Presentation/HomeScene/{View => Model}/HomeItem.swift (100%) diff --git a/TransferList.xcodeproj/project.pbxproj b/TransferList.xcodeproj/project.pbxproj index 3d1f9cb..e809575 100644 --- a/TransferList.xcodeproj/project.pbxproj +++ b/TransferList.xcodeproj/project.pbxproj @@ -30,6 +30,12 @@ DEFF1DD52AF9122D00673B8C /* ViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DD42AF9122D00673B8C /* ViewState.swift */; }; DEFF1DD72AF91A8A00673B8C /* FavoriteAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DD62AF91A8900673B8C /* FavoriteAccountCell.swift */; }; DEFF1DD92AF92DB500673B8C /* AccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DD82AF92DB500673B8C /* AccountCell.swift */; }; + DEFF1DDF2AF9341800673B8C /* DetailAccountViewConroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DDE2AF9341800673B8C /* DetailAccountViewConroller.swift */; }; + DEFF1DE12AF9356200673B8C /* DetailAccountDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DE02AF9356200673B8C /* DetailAccountDataSource.swift */; }; + DEFF1DE42AF9358E00673B8C /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DE32AF9358E00673B8C /* DetailItem.swift */; }; + DEFF1DE72AF9366900673B8C /* TitleValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DE62AF9366900673B8C /* TitleValueCell.swift */; }; + DEFF1DEA2AF93B1200673B8C /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DE92AF93B1200673B8C /* Router.swift */; }; + DEFF1DEC2AF93E2F00673B8C /* HeaderInformationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DEB2AF93E2F00673B8C /* HeaderInformationCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -57,6 +63,12 @@ DEFF1DD42AF9122D00673B8C /* ViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewState.swift; sourceTree = ""; }; DEFF1DD62AF91A8900673B8C /* FavoriteAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteAccountCell.swift; sourceTree = ""; }; DEFF1DD82AF92DB500673B8C /* AccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCell.swift; sourceTree = ""; }; + DEFF1DDE2AF9341800673B8C /* DetailAccountViewConroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailAccountViewConroller.swift; sourceTree = ""; }; + DEFF1DE02AF9356200673B8C /* DetailAccountDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailAccountDataSource.swift; sourceTree = ""; }; + DEFF1DE32AF9358E00673B8C /* DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailItem.swift; sourceTree = ""; }; + DEFF1DE62AF9366900673B8C /* TitleValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleValueCell.swift; sourceTree = ""; }; + DEFF1DE92AF93B1200673B8C /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; + DEFF1DEB2AF93E2F00673B8C /* HeaderInformationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderInformationCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -127,7 +139,6 @@ DE690EB02AF8455400E8C451 /* HomeViewController.swift */, DE690EB22AF84A1C00E8C451 /* Main.storyboard */, DEFF1DBF2AF8DFA500673B8C /* HomeCollectionViewDataSource.swift */, - DEFF1DC12AF8DFE300673B8C /* HomeItem.swift */, ); path = View; sourceTree = ""; @@ -174,6 +185,7 @@ DEFF1DBA2AF8DDFE00673B8C /* Presentation */ = { isa = PBXGroup; children = ( + DEFF1DDA2AF933DA00673B8C /* DetailAccountScene */, DE690EAD2AF8452500E8C451 /* HomeScene */, ); path = Presentation; @@ -204,6 +216,7 @@ DEFF1DC72AF8EAD300673B8C /* ViewModelFactory.swift */, DEFF1DCF2AF8F8BB00673B8C /* PaginationMode.swift */, DEFF1DD42AF9122D00673B8C /* ViewState.swift */, + DEFF1DE92AF93B1200673B8C /* Router.swift */, ); path = Core; sourceTree = ""; @@ -211,11 +224,48 @@ DEFF1DD12AF8FF6D00673B8C /* Model */ = { isa = PBXGroup; children = ( + DEFF1DC12AF8DFE300673B8C /* HomeItem.swift */, DEFF1DD22AF8FF8100673B8C /* DataTransfer.swift */, ); path = Model; sourceTree = ""; }; + DEFF1DDA2AF933DA00673B8C /* DetailAccountScene */ = { + isa = PBXGroup; + children = ( + DEFF1DDC2AF933EA00673B8C /* Model */, + DEFF1DDB2AF933E800673B8C /* View */, + ); + path = DetailAccountScene; + sourceTree = ""; + }; + DEFF1DDB2AF933E800673B8C /* View */ = { + isa = PBXGroup; + children = ( + DEFF1DE52AF9363700673B8C /* Cell */, + DEFF1DDE2AF9341800673B8C /* DetailAccountViewConroller.swift */, + DEFF1DE02AF9356200673B8C /* DetailAccountDataSource.swift */, + ); + path = View; + sourceTree = ""; + }; + DEFF1DDC2AF933EA00673B8C /* Model */ = { + isa = PBXGroup; + children = ( + DEFF1DE32AF9358E00673B8C /* DetailItem.swift */, + ); + path = Model; + sourceTree = ""; + }; + DEFF1DE52AF9363700673B8C /* Cell */ = { + isa = PBXGroup; + children = ( + DEFF1DE62AF9366900673B8C /* TitleValueCell.swift */, + DEFF1DEB2AF93E2F00673B8C /* HeaderInformationCell.swift */, + ); + path = Cell; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -298,12 +348,17 @@ files = ( DEFF1DBD2AF8DEE400673B8C /* LargeHeaderCell.swift in Sources */, DEFF1DCA2AF8EC6A00673B8C /* VerticalAccountCell.swift in Sources */, + DEFF1DDF2AF9341800673B8C /* DetailAccountViewConroller.swift in Sources */, + DEFF1DEC2AF93E2F00673B8C /* HeaderInformationCell.swift in Sources */, DEFF1DC42AF8E8DC00673B8C /* TransferViewModel.swift in Sources */, + DEFF1DE42AF9358E00673B8C /* DetailItem.swift in Sources */, DEFF1DC22AF8DFE300673B8C /* HomeItem.swift in Sources */, DEFF1DC82AF8EAD300673B8C /* ViewModelFactory.swift in Sources */, DE690EB12AF8455400E8C451 /* HomeViewController.swift in Sources */, DEFF1DD02AF8F8BB00673B8C /* PaginationMode.swift in Sources */, DE690EAA2AF842F800E8C451 /* DIContainer.swift in Sources */, + DEFF1DEA2AF93B1200673B8C /* Router.swift in Sources */, + DEFF1DE12AF9356200673B8C /* DetailAccountDataSource.swift in Sources */, DEFF1DD52AF9122D00673B8C /* ViewState.swift in Sources */, DEFF1DD92AF92DB500673B8C /* AccountCell.swift in Sources */, DEBE4AC32AF6C3B000A58501 /* AppDelegate.swift in Sources */, @@ -311,6 +366,7 @@ DEFF1DC02AF8DFA500673B8C /* HomeCollectionViewDataSource.swift in Sources */, DEFF1DD32AF8FF8100673B8C /* DataTransfer.swift in Sources */, DE690EAC2AF8432500E8C451 /* DIContainerImpl.swift in Sources */, + DEFF1DE72AF9366900673B8C /* TitleValueCell.swift in Sources */, DEFF1DD72AF91A8A00673B8C /* FavoriteAccountCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/TransferList/Extra/Core/Router.swift b/TransferList/Extra/Core/Router.swift new file mode 100644 index 0000000..fa2dcc1 --- /dev/null +++ b/TransferList/Extra/Core/Router.swift @@ -0,0 +1,26 @@ +// +// Router.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import Foundation +import Domain + +enum Router: Hashable, Equatable { + + case home + case detail(account: PersonBankAccount) + + func hash(into hasher: inout Hasher) { + switch self { + case .home: hasher.combine("home") + case .detail(let account): hasher.combine(account) + } + } + + static func == (lhs: Router, rhs: Router) -> Bool { + return lhs.hashValue == rhs.hashValue + } +} diff --git a/TransferList/Presentation/DetailAccountScene/Model/DetailItem.swift b/TransferList/Presentation/DetailAccountScene/Model/DetailItem.swift new file mode 100644 index 0000000..e1cc644 --- /dev/null +++ b/TransferList/Presentation/DetailAccountScene/Model/DetailItem.swift @@ -0,0 +1,34 @@ +// +// DetailItem.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import Foundation +import Domain + +enum DetailItem: Hashable { + typealias UISection = (section: DetailItem.Section, items: [DetailItem]) + + enum Section: CaseIterable { + case title + case information + } + + case header(title: String) + case information(title: String, value: Int) + + func hash(into hasher: inout Hasher) { + switch self { + case .header(let title): hasher.combine(title) + case .information(let title, let value): + hasher.combine(title) + hasher.combine(value) + } + } + + static func == (lhs: DetailItem, rhs: DetailItem) -> Bool { + return lhs.hashValue == rhs.hashValue + } +} diff --git a/TransferList/Presentation/DetailAccountScene/View/Cell/HeaderInformationCell.swift b/TransferList/Presentation/DetailAccountScene/View/Cell/HeaderInformationCell.swift new file mode 100644 index 0000000..b175bf4 --- /dev/null +++ b/TransferList/Presentation/DetailAccountScene/View/Cell/HeaderInformationCell.swift @@ -0,0 +1,23 @@ +// +// HeaderInformationCell.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import UIKit +import UI + +class HeaderInformationCell: BaseCollectionCell { + + @InstantiateView(type: SubTitleLabel.self) private var title + + override func setupViews() { + self.addSubview(title) + title.pinToSuperview() + } + + public func setTitle(_ text: String) { + title.text = text + } +} diff --git a/TransferList/Presentation/DetailAccountScene/View/Cell/TitleValueCell.swift b/TransferList/Presentation/DetailAccountScene/View/Cell/TitleValueCell.swift new file mode 100644 index 0000000..71c7e13 --- /dev/null +++ b/TransferList/Presentation/DetailAccountScene/View/Cell/TitleValueCell.swift @@ -0,0 +1,42 @@ +// +// TitleValueCell.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import UIKit +import UI + +class TitleValueCell: BaseCollectionCell { + + @InstantiateView(type: ListLabel.self) var titleLabel + @InstantiateView(type: SubTitleLabel.self) var valueLabel + + override func setupViews() { + addSubview(titleLabel) + addSubview(valueLabel) + + NSLayoutConstraint.activate([ + titleLabel.leadingAnchor.constraint(equalTo: leadingSafeMargin, constant: 16), + titleLabel.topAnchor.constraint(equalTo: topSafeMargin, constant: 22), + titleLabel.bottomAnchor.constraint(equalTo: bottomSafeMargin, constant: -22), + titleLabel.widthAnchor.constraint(lessThanOrEqualTo: widthSafeMargin, multiplier: 0.55), + valueLabel.trailingAnchor.constraint(equalTo: trailingSafeMargin, constant: -16), + valueLabel.topAnchor.constraint(equalTo: topSafeMargin, constant: 22), + valueLabel.bottomAnchor.constraint(equalTo: bottomSafeMargin, constant: -22), + valueLabel.widthAnchor.constraint(lessThanOrEqualTo: widthSafeMargin, multiplier: 0.4) + ]) + + configureContentLabel() + } + + private func configureContentLabel() { + valueLabel.textAlignment = .right + } + + func set(title: String, value: Int) { + self.titleLabel.text = title + self.valueLabel.text = "\(value)" + } +} diff --git a/TransferList/Presentation/DetailAccountScene/View/DetailAccountDataSource.swift b/TransferList/Presentation/DetailAccountScene/View/DetailAccountDataSource.swift new file mode 100644 index 0000000..52d9cdd --- /dev/null +++ b/TransferList/Presentation/DetailAccountScene/View/DetailAccountDataSource.swift @@ -0,0 +1,73 @@ +// +// DetailAccountDataSource.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import UIKit +import UI +import Domain + +class DetailAccountDataSource { + private typealias DiffableDataSource = UICollectionViewDiffableDataSource + private typealias DiffableSnapshot = NSDiffableDataSourceSnapshot + typealias UISection = DetailItem.UISection + + private var dataSource: DiffableDataSource! + private var collectionView: BaseCollectionView + + init(collectionView: BaseCollectionView) { + self.collectionView = collectionView + + configureCollectionView() + configureDiffableDataSource() + } + + private func configureCollectionView() { + collectionView.registerReusableCell(type: TitleValueCell.self) + collectionView.registerReusableCell(type: HeaderInformationCell.self) + } + + private func configureDiffableDataSource() { + dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, + cellProvider: { [weak self] _, indexPath, itemIdentifier in + switch itemIdentifier { + case .information(let title, let value): + return self?.createValueCell(for: indexPath, title: title, value: value) + case .header(let title): + return self?.createHeaderCell(for: indexPath, title: title) + } + }) + + self.dataSource.supplementaryViewProvider = collectionView.makeSeprator() + } + + private func createHeaderCell(for indexPath: IndexPath, title: String) -> HeaderInformationCell { + let cell: HeaderInformationCell = collectionView.dequeueReusableCell(for: indexPath) + cell.setTitle(title) + return cell + } + + private func createValueCell(for indexPath: IndexPath, title: String, value: Int) -> TitleValueCell { + let cell: TitleValueCell = collectionView.dequeueReusableCell(for: indexPath) + cell.set(title: title, value: value) + return cell + } + + public func showInformation(_ cardInformation: CardTransferCount?) { + var initialSnapshot = dataSource.snapshot() + + initialSnapshot.appendSections([.title, .information]) + let header = DetailItem.header(title: "More information") + initialSnapshot.appendItems([header], toSection: .title) + + let total = DetailItem.information(title: "Total transfer", + value: cardInformation?.totalTransfer ?? 0) + let numberOfTrans = DetailItem.information(title: "Number of transfers", + value: cardInformation?.numberOfTransfers ?? 0) + initialSnapshot.appendItems([total, numberOfTrans], toSection: .information) + + dataSource.apply(initialSnapshot, animatingDifferences: false) + } +} diff --git a/TransferList/Presentation/DetailAccountScene/View/DetailAccountViewConroller.swift b/TransferList/Presentation/DetailAccountScene/View/DetailAccountViewConroller.swift new file mode 100644 index 0000000..f38f12e --- /dev/null +++ b/TransferList/Presentation/DetailAccountScene/View/DetailAccountViewConroller.swift @@ -0,0 +1,75 @@ +// +// DetailAccountViewConroller.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import UIKit +import UI +import Combine +import Domain + +class DetailAccountViewConroller: BaseCollectionViewController { + + struct Configuration { + var viewModel: TransferViewModel + var account: PersonBankAccount + } + + private var dataSource: DetailAccountDataSource! + private var viewModel: TransferViewModel! + private var account: PersonBankAccount! + private var subscriptions = Set() + + override func setupViews() { + super.setupViews() + + configureCollectionView() + configureDataSource() + observeDidChangeData() + showInformation() + } + + func updateConfiguration(_ configuration: Configuration) { + self.account = configuration.account + self.viewModel = configuration.viewModel + } + + override var spaceSeparatorFromEdgeInList: CGFloat { return 12 } + + private func configureCollectionView() { + collectionView.contentInset.top = 32 + } + + private func configureDataSource() { + dataSource = .init(collectionView: collectionView) + } + + private func observeDidChangeData() { +// viewModel.$dataUpdated +// .compactMap { $0 } +// .sink { [weak self] data in +// self?.refresher.endRefreshing() +// self?.dataSource.updateData(data) +// } +// .store(in: &subscriptions) + } + + private func showInformation() { + dataSource.showInformation(account.cardTransferCount) + } + + func isNeedBorder(at section: Int) -> Bool { + return section == 0 ? false : true + } + + func contentInsets(at section: Int, currentConfig: NSDirectionalEdgeInsets) -> NSDirectionalEdgeInsets { + var editConfig = currentConfig + if section == 0 { + editConfig.bottom = -12 + } + + return editConfig + } +} diff --git a/TransferList/Presentation/HomeScene/View/HomeItem.swift b/TransferList/Presentation/HomeScene/Model/HomeItem.swift similarity index 100% rename from TransferList/Presentation/HomeScene/View/HomeItem.swift rename to TransferList/Presentation/HomeScene/Model/HomeItem.swift diff --git a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift index ae7597c..af5e302 100644 --- a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift +++ b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift @@ -146,4 +146,16 @@ class HomeCollectionViewDataSource { return dataSource.snapshot().sectionIdentifier(containingItem: item) } } + + func getAccount(at indexPath: IndexPath) -> PersonBankAccount? { + guard let homeItem = dataSource.itemIdentifier(for: indexPath) else { + return nil + } + + guard case let .personBankAccount(account) = homeItem else { + return nil + } + + return account + } } diff --git a/TransferList/Presentation/HomeScene/View/HomeViewController.swift b/TransferList/Presentation/HomeScene/View/HomeViewController.swift index 6197cd6..1210654 100644 --- a/TransferList/Presentation/HomeScene/View/HomeViewController.swift +++ b/TransferList/Presentation/HomeScene/View/HomeViewController.swift @@ -8,6 +8,7 @@ import UIKit import UI import Combine +import Domain class HomeViewController: BaseCollectionViewController { @@ -18,9 +19,8 @@ class HomeViewController: BaseCollectionViewController { override func setupViews() { super.setupViews() - - collectionView.contentInset.top = 32 - collectionView.delegate = self + + configureCollectionView() configureRefresher() configureDataSource() observeDidChangeData() @@ -30,6 +30,11 @@ class HomeViewController: BaseCollectionViewController { override var spaceSeparatorFromEdgeInList: CGFloat { return 0 } + private func configureCollectionView() { + collectionView.contentInset.top = 32 + collectionView.delegate = self + } + private func configureRefresher() { collectionView.alwaysBounceVertical = true refresher.tintColor = Theme.supplementaryBackground @@ -55,6 +60,24 @@ class HomeViewController: BaseCollectionViewController { self?.dataSource.updateData(data) } .store(in: &subscriptions) + + viewModel.$changeView + .compactMap { $0 } + .sink { [weak self] route in + guard case let .detail(account) = route else { + return + } + self?.navigateToDetailViewController(withAccount: account) + } + .store(in: &subscriptions) + } + + private func navigateToDetailViewController(withAccount account: PersonBankAccount) { + let detailViewController = DetailAccountViewConroller() + detailViewController.updateConfiguration(.init(viewModel: viewModel, + account: account)) + + navigationController?.pushViewController(detailViewController, animated: true) } func createCustomSection(at section: Int) -> NSCollectionLayoutSection? { @@ -78,6 +101,13 @@ class HomeViewController: BaseCollectionViewController { extension HomeViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let account = dataSource.getAccount(at: indexPath) else { + return + } + viewModel.accountSelected(account) + } + func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { guard let section = dataSource.sectionIdentifier(atIndexPath: indexPath) else { diff --git a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift index 0beab04..356883c 100644 --- a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift +++ b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift @@ -13,6 +13,7 @@ class TransferViewModel { @Published var viewState: ViewState! @Published var dataUpdated: DataTransfer! + @Published var changeView: Router! private let useCase: PersonBankAccountUseCase private var subscriptions = Set() private var paginationMode = PaginationMode() @@ -35,6 +36,21 @@ class TransferViewModel { self.viewState = viewState } + private func updateAccounts(appendAccounts accounts: [PersonBankAccount]) { + guard !accounts.isEmpty else { + paginationMode.mode = .reachedToEnd + return + } + + if dataFromServer == nil { + dataFromServer = .init(list: accounts, mode: .initial, section: .personBankAccounts) + } else { + dataFromServer.append(contentsOf: accounts) + } + + dataUpdated = dataFromServer + } + public func fetchFavoriteList() { useCase.fetchPersonAccounts(withOffest: paginationMode.nextOffset) .replaceError(with: []) @@ -90,18 +106,7 @@ class TransferViewModel { fetchTransferList() } - private func updateAccounts(appendAccounts accounts: [PersonBankAccount]) { - guard !accounts.isEmpty else { - paginationMode.mode = .reachedToEnd - return - } - - if dataFromServer == nil { - dataFromServer = .init(list: accounts, mode: .initial, section: .personBankAccounts) - } else { - dataFromServer.append(contentsOf: accounts) - } - - dataUpdated = dataFromServer + public func accountSelected(_ account: PersonBankAccount) { + changeView = .detail(account: account) } } From 1adf068558a375a111dff625a4da73ecb403aa0c Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Mon, 6 Nov 2023 21:33:26 +0330 Subject: [PATCH 09/12] Add Save and Remove from Favorites, Data Reload in DetailViewController, ViewModel Update - Added functionality for saving and removing items from favorites. - Updated the DetailViewController to reload data as needed. - Enhanced the TransferViewModel to handle favorites addition or removal. - Added the AddRemove component for managing these operations. --- TransferList.xcodeproj/project.pbxproj | 4 ++ TransferList/Extra/Core/ViewState.swift | 10 +++- .../DetailAccountScene/Model/DetailItem.swift | 4 ++ .../View/Cell/AddRemoveFavoriteCell.swift | 41 ++++++++++++++ .../View/DetailAccountDataSource.swift | 37 ++++++++++++- .../View/DetailAccountViewConroller.swift | 55 ++++++++++++++++--- .../HomeScene/Model/DataTransfer.swift | 10 ++++ .../HomeScene/Model/HomeItem.swift | 5 ++ .../View/Cell/FavoriteAccountCell.swift | 8 ++- .../View/HomeCollectionViewDataSource.swift | 32 ++++++++--- .../HomeScene/View/HomeViewController.swift | 10 +++- .../ViewModel/TransferViewModel.swift | 42 +++++++++++++- 12 files changed, 233 insertions(+), 25 deletions(-) create mode 100644 TransferList/Presentation/DetailAccountScene/View/Cell/AddRemoveFavoriteCell.swift diff --git a/TransferList.xcodeproj/project.pbxproj b/TransferList.xcodeproj/project.pbxproj index e809575..b678995 100644 --- a/TransferList.xcodeproj/project.pbxproj +++ b/TransferList.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ DEFF1DE72AF9366900673B8C /* TitleValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DE62AF9366900673B8C /* TitleValueCell.swift */; }; DEFF1DEA2AF93B1200673B8C /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DE92AF93B1200673B8C /* Router.swift */; }; DEFF1DEC2AF93E2F00673B8C /* HeaderInformationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DEB2AF93E2F00673B8C /* HeaderInformationCell.swift */; }; + DEFF1DEE2AF949FC00673B8C /* AddRemoveFavoriteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DED2AF949FC00673B8C /* AddRemoveFavoriteCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -69,6 +70,7 @@ DEFF1DE62AF9366900673B8C /* TitleValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleValueCell.swift; sourceTree = ""; }; DEFF1DE92AF93B1200673B8C /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; DEFF1DEB2AF93E2F00673B8C /* HeaderInformationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderInformationCell.swift; sourceTree = ""; }; + DEFF1DED2AF949FC00673B8C /* AddRemoveFavoriteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRemoveFavoriteCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -262,6 +264,7 @@ children = ( DEFF1DE62AF9366900673B8C /* TitleValueCell.swift */, DEFF1DEB2AF93E2F00673B8C /* HeaderInformationCell.swift */, + DEFF1DED2AF949FC00673B8C /* AddRemoveFavoriteCell.swift */, ); path = Cell; sourceTree = ""; @@ -359,6 +362,7 @@ DE690EAA2AF842F800E8C451 /* DIContainer.swift in Sources */, DEFF1DEA2AF93B1200673B8C /* Router.swift in Sources */, DEFF1DE12AF9356200673B8C /* DetailAccountDataSource.swift in Sources */, + DEFF1DEE2AF949FC00673B8C /* AddRemoveFavoriteCell.swift in Sources */, DEFF1DD52AF9122D00673B8C /* ViewState.swift in Sources */, DEFF1DD92AF92DB500673B8C /* AccountCell.swift in Sources */, DEBE4AC32AF6C3B000A58501 /* AppDelegate.swift in Sources */, diff --git a/TransferList/Extra/Core/ViewState.swift b/TransferList/Extra/Core/ViewState.swift index ebd4a54..f19ddfc 100644 --- a/TransferList/Extra/Core/ViewState.swift +++ b/TransferList/Extra/Core/ViewState.swift @@ -7,7 +7,7 @@ import Foundation -enum ViewState: Hashable, Equatable { +enum ViewState: Hashable, Equatable, CustomStringConvertible { case loading case result @@ -21,6 +21,14 @@ enum ViewState: Hashable, Equatable { } } + var description: String { + switch self { + case .error(let message): return message + case .result: return "result" + case .loading: return "loading" + } + } + static func == (lhs: ViewState, rhs: ViewState) -> Bool { return lhs.hashValue == rhs.hashValue } diff --git a/TransferList/Presentation/DetailAccountScene/Model/DetailItem.swift b/TransferList/Presentation/DetailAccountScene/Model/DetailItem.swift index e1cc644..951b80c 100644 --- a/TransferList/Presentation/DetailAccountScene/Model/DetailItem.swift +++ b/TransferList/Presentation/DetailAccountScene/Model/DetailItem.swift @@ -18,6 +18,7 @@ enum DetailItem: Hashable { case header(title: String) case information(title: String, value: Int) + case addRemoveFavorite(isFavorite: Bool) func hash(into hasher: inout Hasher) { switch self { @@ -25,6 +26,9 @@ enum DetailItem: Hashable { case .information(let title, let value): hasher.combine(title) hasher.combine(value) + case .addRemoveFavorite(let isFavorite): + hasher.combine(isFavorite) + hasher.combine("addRmoveFavorite") } } diff --git a/TransferList/Presentation/DetailAccountScene/View/Cell/AddRemoveFavoriteCell.swift b/TransferList/Presentation/DetailAccountScene/View/Cell/AddRemoveFavoriteCell.swift new file mode 100644 index 0000000..5524801 --- /dev/null +++ b/TransferList/Presentation/DetailAccountScene/View/Cell/AddRemoveFavoriteCell.swift @@ -0,0 +1,41 @@ +// +// AddRemoveFavoriteCell.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import UIKit +import Domain +import UI + +class AddRemoveFavoriteCell: BaseCollectionCell { + + @InstantiateView(type: ListLabel.self) var titleLabel + + override func setupViews() { + addSubview(titleLabel) + + NSLayoutConstraint.activate([ + titleLabel.leadingAnchor.constraint(equalTo: leadingSafeMargin, constant: 16), + titleLabel.topAnchor.constraint(equalTo: topSafeMargin, constant: 22), + titleLabel.trailingAnchor.constraint(equalTo: trailingSafeMargin, constant: -16), + titleLabel.bottomAnchor.constraint(equalTo: bottomSafeMargin, constant: -22) + ]) + + configureTitleLabel() + } + + func configureTitleLabel() { + self.titleLabel.textAlignment = .center + } + + func setFavoriteStatus(isFavorite: Bool) { + if isFavorite { + self.titleLabel.textColor = .red + } else { + self.titleLabel.textColor = Theme.blue + } + titleLabel.text = isFavorite ? "Remove from favorite" : "Save to favorite" + } +} diff --git a/TransferList/Presentation/DetailAccountScene/View/DetailAccountDataSource.swift b/TransferList/Presentation/DetailAccountScene/View/DetailAccountDataSource.swift index 52d9cdd..07510ee 100644 --- a/TransferList/Presentation/DetailAccountScene/View/DetailAccountDataSource.swift +++ b/TransferList/Presentation/DetailAccountScene/View/DetailAccountDataSource.swift @@ -27,6 +27,7 @@ class DetailAccountDataSource { private func configureCollectionView() { collectionView.registerReusableCell(type: TitleValueCell.self) collectionView.registerReusableCell(type: HeaderInformationCell.self) + collectionView.registerReusableCell(type: AddRemoveFavoriteCell.self) } private func configureDiffableDataSource() { @@ -37,6 +38,8 @@ class DetailAccountDataSource { return self?.createValueCell(for: indexPath, title: title, value: value) case .header(let title): return self?.createHeaderCell(for: indexPath, title: title) + case .addRemoveFavorite(let isFavorite): + return self?.createAddRemoveFavorite(for: indexPath, isFavorite: isFavorite) } }) @@ -55,19 +58,49 @@ class DetailAccountDataSource { return cell } - public func showInformation(_ cardInformation: CardTransferCount?) { + private func createAddRemoveFavorite(for indexPath: IndexPath, + isFavorite: Bool) -> AddRemoveFavoriteCell { + let cell: AddRemoveFavoriteCell = collectionView.dequeueReusableCell(for: indexPath) + cell.setFavoriteStatus(isFavorite: isFavorite) + return cell + } + + public func showInformation(_ account: PersonBankAccount) { var initialSnapshot = dataSource.snapshot() initialSnapshot.appendSections([.title, .information]) let header = DetailItem.header(title: "More information") initialSnapshot.appendItems([header], toSection: .title) + let cardInformation = account.cardTransferCount let total = DetailItem.information(title: "Total transfer", value: cardInformation?.totalTransfer ?? 0) let numberOfTrans = DetailItem.information(title: "Number of transfers", value: cardInformation?.numberOfTransfers ?? 0) - initialSnapshot.appendItems([total, numberOfTrans], toSection: .information) + let addRemove = DetailItem.addRemoveFavorite(isFavorite: account.isFavorite) + + initialSnapshot.appendItems([total, numberOfTrans, addRemove], toSection: .information) dataSource.apply(initialSnapshot, animatingDifferences: false) } + + public func updateFavoriteStatus(isFavorite: Bool) { + let indexPath = IndexPath(row: 2, section: 1) + guard let oldItem = dataSource.itemIdentifier(for: indexPath) else { return } + let addRemove = DetailItem.addRemoveFavorite(isFavorite: isFavorite) + + var newSnapShot = dataSource.snapshot() + newSnapShot.deleteItems([oldItem]) + newSnapShot.appendItems([addRemove], toSection: .information) + + dataSource.apply(newSnapShot, animatingDifferences: true) + } + + public func getItem(at indexPath: IndexPath) -> DetailItem? { + guard let detailItem = dataSource.itemIdentifier(for: indexPath) else { + return nil + } + + return detailItem + } } diff --git a/TransferList/Presentation/DetailAccountScene/View/DetailAccountViewConroller.swift b/TransferList/Presentation/DetailAccountScene/View/DetailAccountViewConroller.swift index f38f12e..99ed2ff 100644 --- a/TransferList/Presentation/DetailAccountScene/View/DetailAccountViewConroller.swift +++ b/TransferList/Presentation/DetailAccountScene/View/DetailAccountViewConroller.swift @@ -31,6 +31,18 @@ class DetailAccountViewConroller: BaseCollectionViewController { showInformation() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + navigationController?.isNavigationBarHidden = false + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + navigationController?.isNavigationBarHidden = true + } + func updateConfiguration(_ configuration: Configuration) { self.account = configuration.account self.viewModel = configuration.viewModel @@ -39,6 +51,7 @@ class DetailAccountViewConroller: BaseCollectionViewController { override var spaceSeparatorFromEdgeInList: CGFloat { return 12 } private func configureCollectionView() { + collectionView.delegate = self collectionView.contentInset.top = 32 } @@ -47,17 +60,27 @@ class DetailAccountViewConroller: BaseCollectionViewController { } private func observeDidChangeData() { -// viewModel.$dataUpdated -// .compactMap { $0 } -// .sink { [weak self] data in -// self?.refresher.endRefreshing() -// self?.dataSource.updateData(data) -// } -// .store(in: &subscriptions) + viewModel.$viewState + .compactMap { $0 } + .drop(while: { + $0 == .loading || $0 == .result + }) + .compactMap { String(describing: $0) } + .sink { [weak self] errorMessage in + guard let self else { return } + print(errorMessage) + } + .store(in: &subscriptions) + + viewModel.favoriteStatusUpdated + .sink { [weak self] account in + self?.account = account + self?.dataSource.updateFavoriteStatus(isFavorite: account.isFavorite) + }.store(in: &subscriptions) } private func showInformation() { - dataSource.showInformation(account.cardTransferCount) + dataSource.showInformation(account) } func isNeedBorder(at section: Int) -> Bool { @@ -73,3 +96,19 @@ class DetailAccountViewConroller: BaseCollectionViewController { return editConfig } } + +extension DetailAccountViewConroller: UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard case let .addRemoveFavorite(isFavorite) = dataSource.getItem(at: indexPath) else { + return + } + + if isFavorite { + viewModel.removeFromFavorite(account: account) + + } else { + viewModel.saveToFavorite(account: account) + } + } +} diff --git a/TransferList/Presentation/HomeScene/Model/DataTransfer.swift b/TransferList/Presentation/HomeScene/Model/DataTransfer.swift index 6a45dd2..b7b2848 100644 --- a/TransferList/Presentation/HomeScene/Model/DataTransfer.swift +++ b/TransferList/Presentation/HomeScene/Model/DataTransfer.swift @@ -37,6 +37,16 @@ struct DataTransfer { list = items } + mutating func append(item: T) { + if mode == .append { + listHolder.append(item) + } else { + listHolder = [item] + } + + list = [item] + } + func isLastItem(row: Int) -> Bool { return listHolder.count - 1 == row } diff --git a/TransferList/Presentation/HomeScene/Model/HomeItem.swift b/TransferList/Presentation/HomeScene/Model/HomeItem.swift index 227bc96..b9980c8 100644 --- a/TransferList/Presentation/HomeScene/Model/HomeItem.swift +++ b/TransferList/Presentation/HomeScene/Model/HomeItem.swift @@ -19,14 +19,19 @@ enum HomeItem: Hashable { } case header(title: String) + case favoriteBankAccount(account: PersonBankAccount) case personBankAccount(account: PersonBankAccount) func hash(into hasher: inout Hasher) { switch self { case .header(let title): hasher.combine(title) + case .favoriteBankAccount(let account): + hasher.combine("favorite") + hasher.combine(account) case .personBankAccount(let account): hasher.combine("accounts") hasher.combine(account) + hasher.combine(account.isFavorite) } } diff --git a/TransferList/Presentation/HomeScene/View/Cell/FavoriteAccountCell.swift b/TransferList/Presentation/HomeScene/View/Cell/FavoriteAccountCell.swift index 99bd3dc..cff1ed6 100644 --- a/TransferList/Presentation/HomeScene/View/Cell/FavoriteAccountCell.swift +++ b/TransferList/Presentation/HomeScene/View/Cell/FavoriteAccountCell.swift @@ -18,10 +18,10 @@ class FavoriteAccountCell: AccountCell { addSubview(stackView) NSLayoutConstraint.activate([ - stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12), - stackView.trailingAnchor.constraint(equalTo: trailingSafeMargin, constant: -12), + stackView.leadingAnchor.constraint(equalTo: leadingSafeMargin), + stackView.trailingAnchor.constraint(equalTo: trailingSafeMargin), stackView.bottomAnchor.constraint(equalTo: bottomSafeMargin), - stackView.topAnchor.constraint(equalTo: topSafeMargin), + stackView.topAnchor.constraint(equalTo: topSafeMargin) ]) configureStackView() @@ -44,6 +44,8 @@ class FavoriteAccountCell: AccountCell { private func configureLabels() { nameLabel.textAlignment = .center + nameLabel.numberOfLines = 1 cardTypeLabel.textAlignment = .center + cardTypeLabel.numberOfLines = 1 } } diff --git a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift index af5e302..fca5f75 100644 --- a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift +++ b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift @@ -37,7 +37,8 @@ class HomeCollectionViewDataSource { switch itemIdentifier { case .header(let title): return self?.createTitleCell(for: indexPath, title: title) - case .personBankAccount(let account): + case .personBankAccount(let account), + .favoriteBankAccount(let account): return self?.createAccountCell(for: indexPath, account: account) } }) @@ -84,7 +85,7 @@ class HomeCollectionViewDataSource { var snapshot = dataSource.snapshot() let items = dataTransfer.list.map { - return HomeItem.personBankAccount(account: $0) + return HomeItem.favoriteBankAccount(account: $0) } guard !items.isEmpty else { return snapshot } @@ -100,7 +101,6 @@ class HomeCollectionViewDataSource { snapshot.appendItems(items, toSection: dataTransfer.section) return snapshot -// dataSource.apply(snapshot, animatingDifferences: true) } private func updateAllSection(_ dataTransfer: DataTransfer) -> DiffableSnapshot { @@ -147,15 +147,31 @@ class HomeCollectionViewDataSource { } } - func getAccount(at indexPath: IndexPath) -> PersonBankAccount? { + public func getAccount(at indexPath: IndexPath) -> PersonBankAccount? { guard let homeItem = dataSource.itemIdentifier(for: indexPath) else { return nil } - guard case let .personBankAccount(account) = homeItem else { - return nil + switch homeItem { + case .favoriteBankAccount(let account), + .personBankAccount(let account): + return account + + default: return nil } - - return account + } + + public func updateAccount(account: PersonBankAccount) { + var snapshot: DiffableSnapshot + + if account.isFavorite { + snapshot = updateFavoriteSection(.init(list: [account], + mode: .append, + section: .favoriteBankAcconts)) + } else { + snapshot = dataSource.snapshot() + } + + dataSource.apply(snapshot, animatingDifferences: true) } } diff --git a/TransferList/Presentation/HomeScene/View/HomeViewController.swift b/TransferList/Presentation/HomeScene/View/HomeViewController.swift index 1210654..b225146 100644 --- a/TransferList/Presentation/HomeScene/View/HomeViewController.swift +++ b/TransferList/Presentation/HomeScene/View/HomeViewController.swift @@ -70,6 +70,11 @@ class HomeViewController: BaseCollectionViewController { self?.navigateToDetailViewController(withAccount: account) } .store(in: &subscriptions) + + viewModel.favoriteStatusUpdated + .sink { [weak self] account in + self?.dataSource.updateAccount(account: account) + }.store(in: &subscriptions) } private func navigateToDetailViewController(withAccount account: PersonBankAccount) { @@ -86,9 +91,9 @@ class HomeViewController: BaseCollectionViewController { } let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(80), - heightDimension: .estimated(80)) + heightDimension: .absolute(100)) let item = NSCollectionLayoutItem(layoutSize: itemSize) - item.edgeSpacing = .init(leading: .fixed(18), top: nil, trailing: nil, bottom: nil) + item.edgeSpacing = .init(leading: .fixed(10), top: nil, trailing: nil, bottom: nil) let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item]) @@ -116,3 +121,4 @@ extension HomeViewController: UICollectionViewDelegate { viewModel.itemDisplay(atSection: section, row: indexPath.row) } } + diff --git a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift index 356883c..6b5d6dd 100644 --- a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift +++ b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift @@ -14,6 +14,7 @@ class TransferViewModel { @Published var viewState: ViewState! @Published var dataUpdated: DataTransfer! @Published var changeView: Router! + var favoriteStatusUpdated = PassthroughSubject() private let useCase: PersonBankAccountUseCase private var subscriptions = Set() private var paginationMode = PaginationMode() @@ -52,11 +53,14 @@ class TransferViewModel { } public func fetchFavoriteList() { - useCase.fetchPersonAccounts(withOffest: paginationMode.nextOffset) + useCase.fetchFavoritePersonAccounts() .replaceError(with: []) .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] accounts in guard let self else { return } + self.dataFromLocal = .init(list: accounts, + mode: .initial, + section: .favoriteBankAcconts) self.dataUpdated = self.dataFromLocal }) .store(in: &subscriptions) @@ -109,4 +113,40 @@ class TransferViewModel { public func accountSelected(_ account: PersonBankAccount) { changeView = .detail(account: account) } + + public func removeFromFavorite(account: PersonBankAccount) { + useCase.removePersonAccountFromFavorites(account) + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self else { return } + + switch completion { + case .finished: break + case .failure(let error): + self.updateViewState(newState: .error(message: error.localizedDescription)) + break + } + + } receiveValue: { [weak self] account in + self?.favoriteStatusUpdated.send(account) + }.store(in: &subscriptions) + } + + public func saveToFavorite(account: PersonBankAccount) { + useCase.savePersonAccountToFavorites(account) + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self else { return } + + switch completion { + case .finished: break + case .failure(let error): + self.updateViewState(newState: .error(message: error.localizedDescription)) + break + } + + } receiveValue: { [weak self] account in + self?.favoriteStatusUpdated.send(account) + }.store(in: &subscriptions) + } } From f86c65d651d07ae685df6b91d31e5185a6d74ace Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Mon, 6 Nov 2023 22:05:32 +0330 Subject: [PATCH 10/12] Added Handling of Save and Remove in Favorites, HomeViewController List Update - Added functionality for handling save or remove operations in the Favorites. --- .../View/Cell/VerticalAccountCell.swift | 37 +++++++++++++--- .../View/HomeCollectionViewDataSource.swift | 40 ++++++++++++++---- .../arrow.imageset/Contents.json | 22 ++++++++++ .../arrow.imageset/Down Arrow 1.pdf | Bin 0 -> 1999 bytes .../arrow.imageset/Down arrow.pdf | Bin 0 -> 1999 bytes .../star.imageset/Contents.json | 12 ++++++ .../Assets.xcassets/star.imageset/star.png | Bin 0 -> 18352 bytes 7 files changed, 97 insertions(+), 14 deletions(-) create mode 100644 TransferList/Resources/Assets.xcassets/arrow.imageset/Contents.json create mode 100644 TransferList/Resources/Assets.xcassets/arrow.imageset/Down Arrow 1.pdf create mode 100644 TransferList/Resources/Assets.xcassets/arrow.imageset/Down arrow.pdf create mode 100644 TransferList/Resources/Assets.xcassets/star.imageset/Contents.json create mode 100644 TransferList/Resources/Assets.xcassets/star.imageset/star.png diff --git a/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift b/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift index ac67db6..8dee70a 100644 --- a/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift +++ b/TransferList/Presentation/HomeScene/View/Cell/VerticalAccountCell.swift @@ -29,13 +29,20 @@ class VerticalAccountCell: AccountCell { labelsStackView.topAnchor.constraint(equalTo: topSafeMargin, constant: 16), labelsStackView.bottomAnchor.constraint(equalTo: bottomSafeMargin, constant: -16), labelsStackView.leadingAnchor.constraint(equalTo: personImageView.trailingAnchor, constant: 12), - labelsStackView.trailingAnchor.constraint(equalTo: trailingSafeMargin) -// labelsStackView.trailingAnchor.constraint(lessThanOrEqualTo: clock.leadingAnchor, constant: -12), -// clock.centerYAnchor.constraint(equalTo: centerYSafeMargin), -// clock.trailingAnchor.constraint(equalTo: trailingSafeMargin, constant: -18) + labelsStackView.trailingAnchor + .constraint(lessThanOrEqualTo: imagesStackView.leadingAnchor, + constant: -12), + imagesStackView.trailingAnchor.constraint(equalTo: trailingSafeMargin), + imagesStackView.centerYAnchor.constraint(equalTo: centerYSafeMargin), + imagesStackView.trailingAnchor.constraint(equalTo: trailingSafeMargin), + favoriteImageView.heightAnchor.constraint(equalToConstant: 15), + favoriteImageView.widthAnchor.constraint(equalToConstant: 15), + arrowImageView.widthAnchor.constraint(equalToConstant: 15), + arrowImageView.heightAnchor.constraint(equalToConstant: 15) ]) configureLabelsStackView() + configureImageStackView() } private func configureLabelsStackView() { @@ -47,10 +54,30 @@ class VerticalAccountCell: AccountCell { labelsStackView.addArrangedSubview(nameLabel) labelsStackView.addArrangedSubview(cardTypeLabel) } + + private func configureImageStackView() { + imagesStackView.spacing = 12 + imagesStackView.alignment = .center + imagesStackView.axis = .horizontal + imagesStackView.distribution = .fill + + imagesStackView.addArrangedSubview(favoriteImageView) + imagesStackView.addArrangedSubview(arrowImageView) + + configureImages() + } + + private func configureImages() { + favoriteImageView.image = UIImage(named: "star") + + arrowImageView.image = UIImage(named: "arrow") + arrowImageView.contentMode = .scaleAspectFit + arrowImageView.transform = CGAffineTransform(rotationAngle: -(.pi / 2)) + } override func setAccountItem(_ personAccount: PersonBankAccount) { super.setAccountItem(personAccount) - // set isFavorite + favoriteImageView.isHidden = !personAccount.isFavorite } } diff --git a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift index fca5f75..393c9e5 100644 --- a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift +++ b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift @@ -93,11 +93,15 @@ class HomeCollectionViewDataSource { let firstSection = snapshot.sectionIdentifiers.first if snapshot.sectionIdentifiers.isEmpty { snapshot.appendSections(newSections) + snapshot.appendItems([.header(title: "Favorite")], toSection: .FavoritesTitle) + } else { - snapshot.insertSections(newSections, beforeSection: firstSection!) + if !snapshot.sectionIdentifiers.contains(.FavoritesTitle) { + snapshot.insertSections(newSections, beforeSection: firstSection!) + snapshot.appendItems([.header(title: "Favorite")], toSection: .FavoritesTitle) + } } - snapshot.appendItems([.header(title: "Favorite")], toSection: .FavoritesTitle) snapshot.appendItems(items, toSection: dataTransfer.section) return snapshot @@ -137,14 +141,10 @@ class HomeCollectionViewDataSource { } public func sectionIdentifier(atIndexPath indexPath: IndexPath) -> HomeItem.Section? { - if #available(iOS 15.0, *) { - return dataSource.sectionIdentifier(for: indexPath.section) - } else { - guard let item = dataSource.itemIdentifier(for: indexPath) else { - return nil - } - return dataSource.snapshot().sectionIdentifier(containingItem: item) + guard let item = dataSource.itemIdentifier(for: indexPath) else { + return nil } + return dataSource.snapshot().sectionIdentifier(containingItem: item) } public func getAccount(at indexPath: IndexPath) -> PersonBankAccount? { @@ -163,13 +163,35 @@ class HomeCollectionViewDataSource { public func updateAccount(account: PersonBankAccount) { var snapshot: DiffableSnapshot + var fakeAccount = account if account.isFavorite { + // favorite snapshot = updateFavoriteSection(.init(list: [account], mode: .append, section: .favoriteBankAcconts)) + + // all + fakeAccount.update(favoriteStatus: false) + let fakeItem = HomeItem.personBankAccount(account: fakeAccount) + let item = HomeItem.personBankAccount(account: account) + snapshot.insertItems([item], beforeItem: fakeItem) + snapshot.deleteItems([fakeItem]) + } else { snapshot = dataSource.snapshot() + fakeAccount.update(favoriteStatus: true) + + let favItem = HomeItem.favoriteBankAccount(account: fakeAccount) + let fakeItem = HomeItem.personBankAccount(account: fakeAccount) + + let item = HomeItem.personBankAccount(account: account) + snapshot.insertItems([item], beforeItem: fakeItem) + snapshot.deleteItems([fakeItem, favItem]) + + if snapshot.itemIdentifiers(inSection: .favoriteBankAcconts).isEmpty { + snapshot.deleteSections([.FavoritesTitle, .favoriteBankAcconts]) + } } dataSource.apply(snapshot, animatingDifferences: true) diff --git a/TransferList/Resources/Assets.xcassets/arrow.imageset/Contents.json b/TransferList/Resources/Assets.xcassets/arrow.imageset/Contents.json new file mode 100644 index 0000000..8074b0a --- /dev/null +++ b/TransferList/Resources/Assets.xcassets/arrow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Down Arrow 1.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Down arrow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TransferList/Resources/Assets.xcassets/arrow.imageset/Down Arrow 1.pdf b/TransferList/Resources/Assets.xcassets/arrow.imageset/Down Arrow 1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fdaca32fb1db62f4ad35a9f40a51f053ed8e36fb GIT binary patch literal 1999 zcmZuy%Wm695WLS<^u>q-Y{+>(0>gkU#=&AYh$9Dk*dzyr8aoOk(jlq9o3Gy(o&}k6njDwZmbl@Zg#Mm z8wE{K63k<$VblOm9OXh$*mw1u(HO)N#hB$90)-x8i8WSh22&`*dX@>S7xD?Fl@-tj zAe#ZTKwG=!w(RHSAC^q3rT4|;>Y+|uIb{JcAVa7`6F)mERC0aX* z^NNFuNStnriy{+YP^yE6$xnHU~Z?#~iBB_5cTe=R(d^TTUBN2ODq`nc?9qB8Zl4< zG~4gc!$h4R5t|zTNqn1o8gNa2$)K z+WRFewwq-?lyqHHErf=<0jKxd-yYd_*JNI|T|0OkF>-P7_QUOHhq)Qx>)Z&C&OLx> zN7ZMdLmKt&uJCp6(4;DUUA1}Zfg1|xu?c1>jUsiul@Ar*Bv9(kj)Pnyu|cU>uo)B% z!FP4FITXHz#Xc`S+`~VeF;uQnbp9JVAW9<$KEB74K}f@cixB1@^1|5Ex2|CTc*fpu_gV6-0hZg3Hc{+L_@ zM52K^fhm&|*S6Ohhz2p@O3v`%T}`8*V0_QW=oKI@&2njkzSWslSC==}f~_>YUP+c^ s%vr})GP6$dOgs6O+N)rK$w|=L$c5SZUl9C*9RvRIMoZyOU2iG!KVgH%RR910 literal 0 HcmV?d00001 diff --git a/TransferList/Resources/Assets.xcassets/arrow.imageset/Down arrow.pdf b/TransferList/Resources/Assets.xcassets/arrow.imageset/Down arrow.pdf new file mode 100644 index 0000000000000000000000000000000000000000..31fb8de723f889ec263869de05cfa3bcc164a881 GIT binary patch literal 1999 zcmZuy+iuf95PjdTn3sw~0(xV(g`E8>cc|4H~1kKeiCzWE6P!L`3tH{>p3en_vLiK0t@Vi}4ixOd@5NWE@N+Elrexd{1a(G&NSDsZrW$j2xN7 zLr~`hK|h8XMh);Np$APG&9D5_X7b%6VcCQ^qQ5K}3m8 znDE^Q2p%-Gqg{T+zmIAntEJ$+h_*6~O|cPI(n>RyVIzy-vBFSl5^)SJ^kC;Zl0;f3 z6#Eq#7m(21U>8Wp&*b1U3v^8~3;j@$F~TB+J0g71N44(GalJd^(1!`;!R7vJLZ^v) zDo*J+_2;A9k%D|Aep31|7-q(gM>k7G%P>i(;c>#^MNyPZ4PU?G#NiWv`upG@zRB)g z54?AqZMG~Q;p;dyVQLudhJ~T9P+?js7#yu#T^_33)!;{Z-FsV=MdJz#=;wI=Z?Xz8 zP(3u6dJlzd5Wn4SFjMdatV5htc|cosl75ZI#FIYqH=y-=LKe3J~reMxi}H5g>ti`e%f!I)w}-c65tTX`e!jW4roWO_gnTt|GV|<2Na` zV^)`YH;37FGw+%bugkIlUvS&t_;&lxVfO8s%&Mj>2iFlJ7Za{su9?_o)yd zo@xNy4y;c|hdA)vuJE8FLa)S4FH4TD-;hvE}D?px#<iII vxmYALP4)7ctrn<0o61*`U3n3VN`g*CiSMoZ171H^G2ojwND5zdoutTr?Uu)G literal 0 HcmV?d00001 diff --git a/TransferList/Resources/Assets.xcassets/star.imageset/Contents.json b/TransferList/Resources/Assets.xcassets/star.imageset/Contents.json new file mode 100644 index 0000000..a8507e4 --- /dev/null +++ b/TransferList/Resources/Assets.xcassets/star.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "star.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TransferList/Resources/Assets.xcassets/star.imageset/star.png b/TransferList/Resources/Assets.xcassets/star.imageset/star.png new file mode 100644 index 0000000000000000000000000000000000000000..b1767ab76ac5eb06121f95b0674cbb5b0e56c839 GIT binary patch literal 18352 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 literal 0 HcmV?d00001 From c7234b316fe28e60c59c7662178dc080d5a9e8ba Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Mon, 6 Nov 2023 22:46:49 +0330 Subject: [PATCH 11/12] Fix Error Display for ViewControllers and Enhance Performance - Resolved the issue of displaying errors only for ViewControllers by adding an error publisher. - Improved performance by replacing the @Published property with PassthroughSubject. --- .../Domain/Error/PersonBankAccountError.swift | 2 +- TransferList.xcodeproj/project.pbxproj | 12 ++++++++++ .../Extension/UIViewController+Alert.swift | 19 +++++++++++++++ .../View/DetailAccountViewConroller.swift | 5 ++-- .../HomeScene/View/HomeViewController.swift | 17 +++++++++++--- .../HomeScene/View/Main.storyboard | 9 +++++--- .../ViewModel/TransferViewModel.swift | 23 +++++++++++-------- 7 files changed, 67 insertions(+), 20 deletions(-) create mode 100644 TransferList/Extra/Extension/UIViewController+Alert.swift diff --git a/Domain/Sources/Domain/Error/PersonBankAccountError.swift b/Domain/Sources/Domain/Error/PersonBankAccountError.swift index a69adb7..32a9922 100644 --- a/Domain/Sources/Domain/Error/PersonBankAccountError.swift +++ b/Domain/Sources/Domain/Error/PersonBankAccountError.swift @@ -15,7 +15,7 @@ public enum PersonBankAccountError: Error { case cannotFetchFavoritePersonAccounts case unexpectedError - var errorDescription: String? { + public var errorDescription: String? { switch self { case .cannotSavePersonAccountToFavorites: return "Failed to save person accounts to favorites" diff --git a/TransferList.xcodeproj/project.pbxproj b/TransferList.xcodeproj/project.pbxproj index b678995..476d384 100644 --- a/TransferList.xcodeproj/project.pbxproj +++ b/TransferList.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ DEFF1DEA2AF93B1200673B8C /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DE92AF93B1200673B8C /* Router.swift */; }; DEFF1DEC2AF93E2F00673B8C /* HeaderInformationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DEB2AF93E2F00673B8C /* HeaderInformationCell.swift */; }; DEFF1DEE2AF949FC00673B8C /* AddRemoveFavoriteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DED2AF949FC00673B8C /* AddRemoveFavoriteCell.swift */; }; + DEFF1DF12AF969F600673B8C /* UIViewController+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DF02AF969F600673B8C /* UIViewController+Alert.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -71,6 +72,7 @@ DEFF1DE92AF93B1200673B8C /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; DEFF1DEB2AF93E2F00673B8C /* HeaderInformationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderInformationCell.swift; sourceTree = ""; }; DEFF1DED2AF949FC00673B8C /* AddRemoveFavoriteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRemoveFavoriteCell.swift; sourceTree = ""; }; + DEFF1DF02AF969F600673B8C /* UIViewController+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Alert.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -207,6 +209,7 @@ DEFF1DC52AF8EAC700673B8C /* Extra */ = { isa = PBXGroup; children = ( + DEFF1DEF2AF969EB00673B8C /* Extension */, DEFF1DC62AF8EACB00673B8C /* Core */, ); path = Extra; @@ -269,6 +272,14 @@ path = Cell; sourceTree = ""; }; + DEFF1DEF2AF969EB00673B8C /* Extension */ = { + isa = PBXGroup; + children = ( + DEFF1DF02AF969F600673B8C /* UIViewController+Alert.swift */, + ); + path = Extension; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -364,6 +375,7 @@ DEFF1DE12AF9356200673B8C /* DetailAccountDataSource.swift in Sources */, DEFF1DEE2AF949FC00673B8C /* AddRemoveFavoriteCell.swift in Sources */, DEFF1DD52AF9122D00673B8C /* ViewState.swift in Sources */, + DEFF1DF12AF969F600673B8C /* UIViewController+Alert.swift in Sources */, DEFF1DD92AF92DB500673B8C /* AccountCell.swift in Sources */, DEBE4AC32AF6C3B000A58501 /* AppDelegate.swift in Sources */, DEBE4AC52AF6C3B000A58501 /* SceneDelegate.swift in Sources */, diff --git a/TransferList/Extra/Extension/UIViewController+Alert.swift b/TransferList/Extra/Extension/UIViewController+Alert.swift new file mode 100644 index 0000000..a574e54 --- /dev/null +++ b/TransferList/Extra/Extension/UIViewController+Alert.swift @@ -0,0 +1,19 @@ +// +// UIViewController+Alert.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/6/23. +// + +import UIKit + +extension UIViewController { + + func showAlert(title: String, message: String) { + let dialogMessage = UIAlertController(title: title, message: message, preferredStyle: .alert) + let ok = UIAlertAction(title: "OK", style: .default) + + dialogMessage.addAction(ok) + self.present(dialogMessage, animated: true, completion: nil) + } +} diff --git a/TransferList/Presentation/DetailAccountScene/View/DetailAccountViewConroller.swift b/TransferList/Presentation/DetailAccountScene/View/DetailAccountViewConroller.swift index 99ed2ff..8faf6ab 100644 --- a/TransferList/Presentation/DetailAccountScene/View/DetailAccountViewConroller.swift +++ b/TransferList/Presentation/DetailAccountScene/View/DetailAccountViewConroller.swift @@ -60,15 +60,14 @@ class DetailAccountViewConroller: BaseCollectionViewController { } private func observeDidChangeData() { - viewModel.$viewState - .compactMap { $0 } + viewModel.errorForSavingOrRemoving .drop(while: { $0 == .loading || $0 == .result }) .compactMap { String(describing: $0) } .sink { [weak self] errorMessage in guard let self else { return } - print(errorMessage) + self.showAlert(title: "Error", message: errorMessage) } .store(in: &subscriptions) diff --git a/TransferList/Presentation/HomeScene/View/HomeViewController.swift b/TransferList/Presentation/HomeScene/View/HomeViewController.swift index b225146..92064ae 100644 --- a/TransferList/Presentation/HomeScene/View/HomeViewController.swift +++ b/TransferList/Presentation/HomeScene/View/HomeViewController.swift @@ -53,15 +53,14 @@ class HomeViewController: BaseCollectionViewController { } private func observeDidChangeData() { - viewModel.$dataUpdated - .compactMap { $0 } + viewModel.dataUpdated .sink { [weak self] data in self?.refresher.endRefreshing() self?.dataSource.updateData(data) } .store(in: &subscriptions) - viewModel.$changeView + viewModel.changeView .compactMap { $0 } .sink { [weak self] route in guard case let .detail(account) = route else { @@ -75,6 +74,18 @@ class HomeViewController: BaseCollectionViewController { .sink { [weak self] account in self?.dataSource.updateAccount(account: account) }.store(in: &subscriptions) + + viewModel.$viewState + .compactMap { $0 } + .drop(while: { + $0 == .loading || $0 == .result + }) + .compactMap { String(describing: $0) } + .sink { [weak self] errorMessage in + guard let self else { return } + self.showAlert(title: "Error", message: errorMessage) + } + .store(in: &subscriptions) } private func navigateToDetailViewController(withAccount account: PersonBankAccount) { diff --git a/TransferList/Presentation/HomeScene/View/Main.storyboard b/TransferList/Presentation/HomeScene/View/Main.storyboard index dd79351..cb0e3e4 100644 --- a/TransferList/Presentation/HomeScene/View/Main.storyboard +++ b/TransferList/Presentation/HomeScene/View/Main.storyboard @@ -1,7 +1,9 @@ - + + - + + @@ -12,7 +14,7 @@ - + @@ -20,6 +22,7 @@ + diff --git a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift index 6b5d6dd..cb45e68 100644 --- a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift +++ b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift @@ -12,8 +12,9 @@ import Domain class TransferViewModel { @Published var viewState: ViewState! - @Published var dataUpdated: DataTransfer! - @Published var changeView: Router! + var dataUpdated = PassthroughSubject, Never>() + var errorForSavingOrRemoving = PassthroughSubject() + var changeView = PassthroughSubject() var favoriteStatusUpdated = PassthroughSubject() private let useCase: PersonBankAccountUseCase private var subscriptions = Set() @@ -49,7 +50,7 @@ class TransferViewModel { dataFromServer.append(contentsOf: accounts) } - dataUpdated = dataFromServer + dataUpdated.send(dataFromServer) } public func fetchFavoriteList() { @@ -61,7 +62,7 @@ class TransferViewModel { self.dataFromLocal = .init(list: accounts, mode: .initial, section: .favoriteBankAcconts) - self.dataUpdated = self.dataFromLocal + self.dataUpdated.send(self.dataFromLocal) }) .store(in: &subscriptions) } @@ -78,7 +79,7 @@ class TransferViewModel { case .finished: break case .failure(let error): - updateViewState(newState: .error(message: error.localizedDescription)) + self.updateViewState(newState: .error(message: error.errorDescription ?? "Unexpected error")) break } @@ -86,7 +87,7 @@ class TransferViewModel { guard let self else { return } self.updateAccounts(appendAccounts: accounts) self.paginationMode.moveToNextOffset() - updateViewState(newState: .result) + self.updateViewState(newState: .result) } .store(in: &subscriptions) } @@ -111,7 +112,7 @@ class TransferViewModel { } public func accountSelected(_ account: PersonBankAccount) { - changeView = .detail(account: account) + changeView.send(.detail(account: account)) } public func removeFromFavorite(account: PersonBankAccount) { @@ -123,7 +124,8 @@ class TransferViewModel { switch completion { case .finished: break case .failure(let error): - self.updateViewState(newState: .error(message: error.localizedDescription)) + let viewError = ViewState.error(message: error.errorDescription ?? "Unexpected error") + self.errorForSavingOrRemoving.send(viewError) break } @@ -141,11 +143,12 @@ class TransferViewModel { switch completion { case .finished: break case .failure(let error): - self.updateViewState(newState: .error(message: error.localizedDescription)) + let viewError = ViewState.error(message: error.errorDescription ?? "Unexpected error") + self.errorForSavingOrRemoving.send(viewError) break } - } receiveValue: { [weak self] account in + } receiveValue: { [weak self] account in self?.favoriteStatusUpdated.send(account) }.store(in: &subscriptions) } From 65f8cca348559ed809d512869c6f3975275df6d8 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Tue, 7 Nov 2023 00:21:16 +0330 Subject: [PATCH 12/12] Refactor Variable Names, Function Renaming, ViewModel Adjustments, and Cell Size Modification - Renamed multiple variables for improved clarity. - Updated function names and combined two functions in the ViewModel. - Set the width and height to 100 for the FavoriteAccountCell. --- .github/workflows/Data.yml | 8 +- TransferList.xcodeproj/project.pbxproj | 163 ++++++++++++++++-- .../xcschemes/xcschememanagement.plist | 2 +- .../DetailAccountScene/Model/DetailItem.swift | 2 + ...ueCell.swift => TitleAndContentCell.swift} | 4 +- .../View/DetailAccountDataSource.swift | 22 +-- .../HomeScene/Model/HomeItem.swift | 2 + .../View/Cell/FavoriteAccountCell.swift | 7 +- .../View/HomeCollectionViewDataSource.swift | 68 ++++---- .../HomeScene/View/HomeViewController.swift | 31 ++-- .../HomeScene/View/Main.storyboard | 33 ---- .../ViewModel/TransferViewModel.swift | 73 ++++---- .../AccentColor.colorset/Contents.json | 20 +++ .../AppIcon.appiconset/Contents.json | 1 + .../AppIcon.appiconset/Untitled design.png | Bin 0 -> 58124 bytes .../ViewModel/TransferViewModelTests.swift | 101 +++++++++++ UI/Package.swift | 3 - 17 files changed, 378 insertions(+), 162 deletions(-) rename TransferList/Presentation/DetailAccountScene/View/Cell/{TitleValueCell.swift => TitleAndContentCell.swift} (94%) delete mode 100644 TransferList/Presentation/HomeScene/View/Main.storyboard create mode 100644 TransferList/Resources/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 TransferList/Resources/Assets.xcassets/AppIcon.appiconset/Untitled design.png create mode 100644 TransferListTests/Presentation/HomeScene/ViewModel/TransferViewModelTests.swift diff --git a/.github/workflows/Data.yml b/.github/workflows/Data.yml index db32a1a..c03fc87 100644 --- a/.github/workflows/Data.yml +++ b/.github/workflows/Data.yml @@ -20,10 +20,4 @@ jobs: - name: Lint code run: | cd Data/Sources/ - swiftlint - - - name: run unit test - run: | - cd Data/ - swift build - swift test \ No newline at end of file + swiftlint \ No newline at end of file diff --git a/TransferList.xcodeproj/project.pbxproj b/TransferList.xcodeproj/project.pbxproj index 476d384..9347f8f 100644 --- a/TransferList.xcodeproj/project.pbxproj +++ b/TransferList.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ DE690EAA2AF842F800E8C451 /* DIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE690EA92AF842F800E8C451 /* DIContainer.swift */; }; DE690EAC2AF8432500E8C451 /* DIContainerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE690EAB2AF8432500E8C451 /* DIContainerImpl.swift */; }; DE690EB12AF8455400E8C451 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE690EB02AF8455400E8C451 /* HomeViewController.swift */; }; - DE690EB32AF84A1C00E8C451 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE690EB22AF84A1C00E8C451 /* Main.storyboard */; }; DEBE4AC32AF6C3B000A58501 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBE4AC22AF6C3B000A58501 /* AppDelegate.swift */; }; DEBE4AC52AF6C3B000A58501 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBE4AC42AF6C3B000A58501 /* SceneDelegate.swift */; }; DEBE4ACC2AF6C3B200A58501 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DEBE4ACB2AF6C3B200A58501 /* Assets.xcassets */; }; @@ -33,20 +32,30 @@ DEFF1DDF2AF9341800673B8C /* DetailAccountViewConroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DDE2AF9341800673B8C /* DetailAccountViewConroller.swift */; }; DEFF1DE12AF9356200673B8C /* DetailAccountDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DE02AF9356200673B8C /* DetailAccountDataSource.swift */; }; DEFF1DE42AF9358E00673B8C /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DE32AF9358E00673B8C /* DetailItem.swift */; }; - DEFF1DE72AF9366900673B8C /* TitleValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DE62AF9366900673B8C /* TitleValueCell.swift */; }; + DEFF1DE72AF9366900673B8C /* TitleAndContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DE62AF9366900673B8C /* TitleAndContentCell.swift */; }; DEFF1DEA2AF93B1200673B8C /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DE92AF93B1200673B8C /* Router.swift */; }; DEFF1DEC2AF93E2F00673B8C /* HeaderInformationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DEB2AF93E2F00673B8C /* HeaderInformationCell.swift */; }; DEFF1DEE2AF949FC00673B8C /* AddRemoveFavoriteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DED2AF949FC00673B8C /* AddRemoveFavoriteCell.swift */; }; DEFF1DF12AF969F600673B8C /* UIViewController+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1DF02AF969F600673B8C /* UIViewController+Alert.swift */; }; + DEFF1E032AF978E300673B8C /* TransferViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFF1E022AF978E300673B8C /* TransferViewModelTests.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + DEFF1DFA2AF978A600673B8C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DEBE4AB72AF6C3B000A58501 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DEBE4ABE2AF6C3B000A58501; + remoteInfo = TransferList; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ DE3EF77C2AF6F9B40071E5E4 /* Data */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Data; sourceTree = ""; }; DE690E9E2AF82D4300E8C451 /* UI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = UI; sourceTree = ""; }; DE690EA92AF842F800E8C451 /* DIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DIContainer.swift; sourceTree = ""; }; DE690EAB2AF8432500E8C451 /* DIContainerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DIContainerImpl.swift; sourceTree = ""; }; DE690EB02AF8455400E8C451 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; - DE690EB22AF84A1C00E8C451 /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; DEBE4ABF2AF6C3B000A58501 /* TransferList.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TransferList.app; sourceTree = BUILT_PRODUCTS_DIR; }; DEBE4AC22AF6C3B000A58501 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; DEBE4AC42AF6C3B000A58501 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -68,11 +77,13 @@ DEFF1DDE2AF9341800673B8C /* DetailAccountViewConroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailAccountViewConroller.swift; sourceTree = ""; }; DEFF1DE02AF9356200673B8C /* DetailAccountDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailAccountDataSource.swift; sourceTree = ""; }; DEFF1DE32AF9358E00673B8C /* DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailItem.swift; sourceTree = ""; }; - DEFF1DE62AF9366900673B8C /* TitleValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleValueCell.swift; sourceTree = ""; }; + DEFF1DE62AF9366900673B8C /* TitleAndContentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleAndContentCell.swift; sourceTree = ""; }; DEFF1DE92AF93B1200673B8C /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; DEFF1DEB2AF93E2F00673B8C /* HeaderInformationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderInformationCell.swift; sourceTree = ""; }; DEFF1DED2AF949FC00673B8C /* AddRemoveFavoriteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRemoveFavoriteCell.swift; sourceTree = ""; }; DEFF1DF02AF969F600673B8C /* UIViewController+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Alert.swift"; sourceTree = ""; }; + DEFF1DF62AF978A600673B8C /* TransferListTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TransferListTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DEFF1E022AF978E300673B8C /* TransferViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferViewModelTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -87,6 +98,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DEFF1DF32AF978A600673B8C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -141,7 +159,6 @@ children = ( DEFF1DBB2AF8DED200673B8C /* Cell */, DE690EB02AF8455400E8C451 /* HomeViewController.swift */, - DE690EB22AF84A1C00E8C451 /* Main.storyboard */, DEFF1DBF2AF8DFA500673B8C /* HomeCollectionViewDataSource.swift */, ); path = View; @@ -162,6 +179,7 @@ DE3EF77C2AF6F9B40071E5E4 /* Data */, DEBE4AD62AF6C8F600A58501 /* Domain */, DEBE4AC12AF6C3B000A58501 /* TransferList */, + DEFF1DF72AF978A600673B8C /* TransferListTests */, DEBE4AC02AF6C3B000A58501 /* Products */, DE690EA22AF842ED00E8C451 /* Frameworks */, ); @@ -171,6 +189,7 @@ isa = PBXGroup; children = ( DEBE4ABF2AF6C3B000A58501 /* TransferList.app */, + DEFF1DF62AF978A600673B8C /* TransferListTests.xctest */, ); name = Products; sourceTree = ""; @@ -265,7 +284,7 @@ DEFF1DE52AF9363700673B8C /* Cell */ = { isa = PBXGroup; children = ( - DEFF1DE62AF9366900673B8C /* TitleValueCell.swift */, + DEFF1DE62AF9366900673B8C /* TitleAndContentCell.swift */, DEFF1DEB2AF93E2F00673B8C /* HeaderInformationCell.swift */, DEFF1DED2AF949FC00673B8C /* AddRemoveFavoriteCell.swift */, ); @@ -280,6 +299,38 @@ path = Extension; sourceTree = ""; }; + DEFF1DF72AF978A600673B8C /* TransferListTests */ = { + isa = PBXGroup; + children = ( + DEFF1DFF2AF978C100673B8C /* Presentation */, + ); + path = TransferListTests; + sourceTree = ""; + }; + DEFF1DFF2AF978C100673B8C /* Presentation */ = { + isa = PBXGroup; + children = ( + DEFF1E002AF978C900673B8C /* HomeScene */, + ); + path = Presentation; + sourceTree = ""; + }; + DEFF1E002AF978C900673B8C /* HomeScene */ = { + isa = PBXGroup; + children = ( + DEFF1E012AF978D000673B8C /* ViewModel */, + ); + path = HomeScene; + sourceTree = ""; + }; + DEFF1E012AF978D000673B8C /* ViewModel */ = { + isa = PBXGroup; + children = ( + DEFF1E022AF978E300673B8C /* TransferViewModelTests.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -306,6 +357,24 @@ productReference = DEBE4ABF2AF6C3B000A58501 /* TransferList.app */; productType = "com.apple.product-type.application"; }; + DEFF1DF52AF978A600673B8C /* TransferListTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DEFF1DFC2AF978A600673B8C /* Build configuration list for PBXNativeTarget "TransferListTests" */; + buildPhases = ( + DEFF1DF22AF978A600673B8C /* Sources */, + DEFF1DF32AF978A600673B8C /* Frameworks */, + DEFF1DF42AF978A600673B8C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DEFF1DFB2AF978A600673B8C /* PBXTargetDependency */, + ); + name = TransferListTests; + productName = TransferListTests; + productReference = DEFF1DF62AF978A600673B8C /* TransferListTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -319,6 +388,11 @@ DEBE4ABE2AF6C3B000A58501 = { CreatedOnToolsVersion = 14.3.1; }; + DEFF1DF52AF978A600673B8C = { + CreatedOnToolsVersion = 14.3.1; + LastSwiftMigration = 1430; + TestTargetID = DEBE4ABE2AF6C3B000A58501; + }; }; }; buildConfigurationList = DEBE4ABA2AF6C3B000A58501 /* Build configuration list for PBXProject "TransferList" */; @@ -338,6 +412,7 @@ projectRoot = ""; targets = ( DEBE4ABE2AF6C3B000A58501 /* TransferList */, + DEFF1DF52AF978A600673B8C /* TransferListTests */, ); }; /* End PBXProject section */ @@ -347,12 +422,18 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - DE690EB32AF84A1C00E8C451 /* Main.storyboard in Resources */, DEBE4ACF2AF6C3B200A58501 /* LaunchScreen.storyboard in Resources */, DEBE4ACC2AF6C3B200A58501 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; + DEFF1DF42AF978A600673B8C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -382,13 +463,29 @@ DEFF1DC02AF8DFA500673B8C /* HomeCollectionViewDataSource.swift in Sources */, DEFF1DD32AF8FF8100673B8C /* DataTransfer.swift in Sources */, DE690EAC2AF8432500E8C451 /* DIContainerImpl.swift in Sources */, - DEFF1DE72AF9366900673B8C /* TitleValueCell.swift in Sources */, + DEFF1DE72AF9366900673B8C /* TitleAndContentCell.swift in Sources */, DEFF1DD72AF91A8A00673B8C /* FavoriteAccountCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + DEFF1DF22AF978A600673B8C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEFF1E032AF978E300673B8C /* TransferViewModelTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + DEFF1DFB2AF978A600673B8C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DEBE4ABE2AF6C3B000A58501 /* TransferList */; + targetProxy = DEFF1DFA2AF978A600673B8C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ DEBE4ACD2AF6C3B200A58501 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; @@ -526,7 +623,7 @@ INFOPLIST_FILE = TransferList/Resources/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UIMainStoryboardFile = ""; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -553,7 +650,7 @@ INFOPLIST_FILE = TransferList/Resources/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UIMainStoryboardFile = ""; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -569,6 +666,43 @@ }; name = Release; }; + DEFF1DFD2AF978A600673B8C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.github.helloitshessam.TransferListTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TransferList.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TransferList"; + }; + name = Debug; + }; + DEFF1DFE2AF978A600673B8C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.github.helloitshessam.TransferListTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TransferList.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TransferList"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -590,6 +724,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + DEFF1DFC2AF978A600673B8C /* Build configuration list for PBXNativeTarget "TransferListTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEFF1DFD2AF978A600673B8C /* Debug */, + DEFF1DFE2AF978A600673B8C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist index 5eb9d26..52e1fe9 100644 --- a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ TransferList.xcscheme_^#shared#^_ orderHint - 3 + 5 diff --git a/TransferList/Presentation/DetailAccountScene/Model/DetailItem.swift b/TransferList/Presentation/DetailAccountScene/Model/DetailItem.swift index 951b80c..bc5f1b5 100644 --- a/TransferList/Presentation/DetailAccountScene/Model/DetailItem.swift +++ b/TransferList/Presentation/DetailAccountScene/Model/DetailItem.swift @@ -23,9 +23,11 @@ enum DetailItem: Hashable { func hash(into hasher: inout Hasher) { switch self { case .header(let title): hasher.combine(title) + case .information(let title, let value): hasher.combine(title) hasher.combine(value) + case .addRemoveFavorite(let isFavorite): hasher.combine(isFavorite) hasher.combine("addRmoveFavorite") diff --git a/TransferList/Presentation/DetailAccountScene/View/Cell/TitleValueCell.swift b/TransferList/Presentation/DetailAccountScene/View/Cell/TitleAndContentCell.swift similarity index 94% rename from TransferList/Presentation/DetailAccountScene/View/Cell/TitleValueCell.swift rename to TransferList/Presentation/DetailAccountScene/View/Cell/TitleAndContentCell.swift index 71c7e13..cff7520 100644 --- a/TransferList/Presentation/DetailAccountScene/View/Cell/TitleValueCell.swift +++ b/TransferList/Presentation/DetailAccountScene/View/Cell/TitleAndContentCell.swift @@ -1,5 +1,5 @@ // -// TitleValueCell.swift +// TitleAndContentCell.swift // TransferList // // Created by Hessam Mahdiabadi on 11/6/23. @@ -8,7 +8,7 @@ import UIKit import UI -class TitleValueCell: BaseCollectionCell { +class TitleAndContentCell: BaseCollectionCell { @InstantiateView(type: ListLabel.self) var titleLabel @InstantiateView(type: SubTitleLabel.self) var valueLabel diff --git a/TransferList/Presentation/DetailAccountScene/View/DetailAccountDataSource.swift b/TransferList/Presentation/DetailAccountScene/View/DetailAccountDataSource.swift index 07510ee..2d461f1 100644 --- a/TransferList/Presentation/DetailAccountScene/View/DetailAccountDataSource.swift +++ b/TransferList/Presentation/DetailAccountScene/View/DetailAccountDataSource.swift @@ -25,7 +25,7 @@ class DetailAccountDataSource { } private func configureCollectionView() { - collectionView.registerReusableCell(type: TitleValueCell.self) + collectionView.registerReusableCell(type: TitleAndContentCell.self) collectionView.registerReusableCell(type: HeaderInformationCell.self) collectionView.registerReusableCell(type: AddRemoveFavoriteCell.self) } @@ -52,8 +52,8 @@ class DetailAccountDataSource { return cell } - private func createValueCell(for indexPath: IndexPath, title: String, value: Int) -> TitleValueCell { - let cell: TitleValueCell = collectionView.dequeueReusableCell(for: indexPath) + private func createValueCell(for indexPath: IndexPath, title: String, value: Int) -> TitleAndContentCell { + let cell: TitleAndContentCell = collectionView.dequeueReusableCell(for: indexPath) cell.set(title: title, value: value) return cell } @@ -85,22 +85,16 @@ class DetailAccountDataSource { } public func updateFavoriteStatus(isFavorite: Bool) { - let indexPath = IndexPath(row: 2, section: 1) - guard let oldItem = dataSource.itemIdentifier(for: indexPath) else { return } - let addRemove = DetailItem.addRemoveFavorite(isFavorite: isFavorite) - var newSnapShot = dataSource.snapshot() - newSnapShot.deleteItems([oldItem]) - newSnapShot.appendItems([addRemove], toSection: .information) + + let newItem = DetailItem.addRemoveFavorite(isFavorite: isFavorite) + newSnapShot.deleteItems([.addRemoveFavorite(isFavorite: !isFavorite)]) + newSnapShot.appendItems([newItem], toSection: .information) dataSource.apply(newSnapShot, animatingDifferences: true) } public func getItem(at indexPath: IndexPath) -> DetailItem? { - guard let detailItem = dataSource.itemIdentifier(for: indexPath) else { - return nil - } - - return detailItem + dataSource.itemIdentifier(for: indexPath) } } diff --git a/TransferList/Presentation/HomeScene/Model/HomeItem.swift b/TransferList/Presentation/HomeScene/Model/HomeItem.swift index b9980c8..9b71997 100644 --- a/TransferList/Presentation/HomeScene/Model/HomeItem.swift +++ b/TransferList/Presentation/HomeScene/Model/HomeItem.swift @@ -25,9 +25,11 @@ enum HomeItem: Hashable { func hash(into hasher: inout Hasher) { switch self { case .header(let title): hasher.combine(title) + case .favoriteBankAccount(let account): hasher.combine("favorite") hasher.combine(account) + case .personBankAccount(let account): hasher.combine("accounts") hasher.combine(account) diff --git a/TransferList/Presentation/HomeScene/View/Cell/FavoriteAccountCell.swift b/TransferList/Presentation/HomeScene/View/Cell/FavoriteAccountCell.swift index cff1ed6..72410d8 100644 --- a/TransferList/Presentation/HomeScene/View/Cell/FavoriteAccountCell.swift +++ b/TransferList/Presentation/HomeScene/View/Cell/FavoriteAccountCell.swift @@ -21,7 +21,9 @@ class FavoriteAccountCell: AccountCell { stackView.leadingAnchor.constraint(equalTo: leadingSafeMargin), stackView.trailingAnchor.constraint(equalTo: trailingSafeMargin), stackView.bottomAnchor.constraint(equalTo: bottomSafeMargin), - stackView.topAnchor.constraint(equalTo: topSafeMargin) + stackView.topAnchor.constraint(equalTo: topSafeMargin), + nameLabel.widthAnchor.constraint(equalToConstant: 100), + cardTypeLabel.widthAnchor.constraint(equalToConstant: 100) ]) configureStackView() @@ -45,7 +47,10 @@ class FavoriteAccountCell: AccountCell { private func configureLabels() { nameLabel.textAlignment = .center nameLabel.numberOfLines = 1 + nameLabel.minimumScaleFactor = 0.5 + cardTypeLabel.textAlignment = .center cardTypeLabel.numberOfLines = 1 + cardTypeLabel.minimumScaleFactor = 0.5 } } diff --git a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift index 393c9e5..31e7eec 100644 --- a/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift +++ b/TransferList/Presentation/HomeScene/View/HomeCollectionViewDataSource.swift @@ -66,7 +66,7 @@ class HomeCollectionViewDataSource { } } - public func updateData(_ dataTransfer: DataTransfer) { + public func updateList(_ dataTransfer: DataTransfer) { semaphore.wait() var snapshot: DiffableSnapshot! @@ -84,10 +84,10 @@ class HomeCollectionViewDataSource { private func updateFavoriteSection(_ dataTransfer: DataTransfer) -> DiffableSnapshot { var snapshot = dataSource.snapshot() + guard !dataTransfer.list.isEmpty else { return snapshot } let items = dataTransfer.list.map { return HomeItem.favoriteBankAccount(account: $0) } - guard !items.isEmpty else { return snapshot } let newSections = [.FavoritesTitle, dataTransfer.section] let firstSection = snapshot.sectionIdentifiers.first @@ -140,13 +140,6 @@ class HomeCollectionViewDataSource { return dataSource.snapshot().sectionIdentifiers[section] } - public func sectionIdentifier(atIndexPath indexPath: IndexPath) -> HomeItem.Section? { - guard let item = dataSource.itemIdentifier(for: indexPath) else { - return nil - } - return dataSource.snapshot().sectionIdentifier(containingItem: item) - } - public func getAccount(at indexPath: IndexPath) -> PersonBankAccount? { guard let homeItem = dataSource.itemIdentifier(for: indexPath) else { return nil @@ -161,37 +154,36 @@ class HomeCollectionViewDataSource { } } - public func updateAccount(account: PersonBankAccount) { - var snapshot: DiffableSnapshot + public func updateAccountToFavorites(account: PersonBankAccount) { + var accountNeedsToRemove = account + var snapshot = updateFavoriteSection(.init(list: [account], + mode: .append, + section: .favoriteBankAcconts)) + + // all + accountNeedsToRemove.update(favoriteStatus: false) + let itemNeedsToRemove = HomeItem.personBankAccount(account: accountNeedsToRemove) + let item = HomeItem.personBankAccount(account: account) + snapshot.insertItems([item], beforeItem: itemNeedsToRemove) + snapshot.deleteItems([itemNeedsToRemove]) + + dataSource.apply(snapshot, animatingDifferences: true) + } + + public func removeAccountfromFavorites(account: PersonBankAccount) { var fakeAccount = account + var snapshot = dataSource.snapshot() + fakeAccount.update(favoriteStatus: true) - if account.isFavorite { - // favorite - snapshot = updateFavoriteSection(.init(list: [account], - mode: .append, - section: .favoriteBankAcconts)) - - // all - fakeAccount.update(favoriteStatus: false) - let fakeItem = HomeItem.personBankAccount(account: fakeAccount) - let item = HomeItem.personBankAccount(account: account) - snapshot.insertItems([item], beforeItem: fakeItem) - snapshot.deleteItems([fakeItem]) - - } else { - snapshot = dataSource.snapshot() - fakeAccount.update(favoriteStatus: true) - - let favItem = HomeItem.favoriteBankAccount(account: fakeAccount) - let fakeItem = HomeItem.personBankAccount(account: fakeAccount) - - let item = HomeItem.personBankAccount(account: account) - snapshot.insertItems([item], beforeItem: fakeItem) - snapshot.deleteItems([fakeItem, favItem]) - - if snapshot.itemIdentifiers(inSection: .favoriteBankAcconts).isEmpty { - snapshot.deleteSections([.FavoritesTitle, .favoriteBankAcconts]) - } + let favItem = HomeItem.favoriteBankAccount(account: fakeAccount) + let fakeItem = HomeItem.personBankAccount(account: fakeAccount) + + let item = HomeItem.personBankAccount(account: account) + snapshot.insertItems([item], beforeItem: fakeItem) + snapshot.deleteItems([fakeItem, favItem]) + + if snapshot.itemIdentifiers(inSection: .favoriteBankAcconts).isEmpty { + snapshot.deleteSections([.FavoritesTitle, .favoriteBankAcconts]) } dataSource.apply(snapshot, animatingDifferences: true) diff --git a/TransferList/Presentation/HomeScene/View/HomeViewController.swift b/TransferList/Presentation/HomeScene/View/HomeViewController.swift index 92064ae..a2ae0fb 100644 --- a/TransferList/Presentation/HomeScene/View/HomeViewController.swift +++ b/TransferList/Presentation/HomeScene/View/HomeViewController.swift @@ -36,16 +36,15 @@ class HomeViewController: BaseCollectionViewController { } private func configureRefresher() { - collectionView.alwaysBounceVertical = true refresher.tintColor = Theme.supplementaryBackground - refresher.addTarget(self, action: #selector(refreshData), for: .valueChanged) + refresher.addTarget(self, action: #selector(beginRefreshing), for: .valueChanged) collectionView.addSubview(refresher) } - @objc func refreshData() { + @objc private func beginRefreshing() { self.refresher.beginRefreshing() - viewModel.refreshData() + viewModel.refreshList() } private func configureDataSource() { @@ -53,14 +52,14 @@ class HomeViewController: BaseCollectionViewController { } private func observeDidChangeData() { - viewModel.dataUpdated - .sink { [weak self] data in + viewModel.accountsNeedToShow + .sink { [weak self] accounts in self?.refresher.endRefreshing() - self?.dataSource.updateData(data) + self?.dataSource.updateList(accounts) } .store(in: &subscriptions) - viewModel.changeView + viewModel.router .compactMap { $0 } .sink { [weak self] route in guard case let .detail(account) = route else { @@ -71,8 +70,17 @@ class HomeViewController: BaseCollectionViewController { .store(in: &subscriptions) viewModel.favoriteStatusUpdated + .share() + .filter { $0.isFavorite } .sink { [weak self] account in - self?.dataSource.updateAccount(account: account) + self?.dataSource.updateAccountToFavorites(account: account) + }.store(in: &subscriptions) + + viewModel.favoriteStatusUpdated + .share() + .filter { !$0.isFavorite } + .sink { [weak self] account in + self?.dataSource.removeAccountfromFavorites(account: account) }.store(in: &subscriptions) viewModel.$viewState @@ -126,10 +134,9 @@ extension HomeViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - guard let section = dataSource.sectionIdentifier(atIndexPath: indexPath) else { + guard let section = dataSource.sectionIdentifier(atSection: indexPath.section) else { return } - viewModel.itemDisplay(atSection: section, row: indexPath.row) + viewModel.reachedToRow(row: indexPath.row, atSection: section) } } - diff --git a/TransferList/Presentation/HomeScene/View/Main.storyboard b/TransferList/Presentation/HomeScene/View/Main.storyboard deleted file mode 100644 index cb0e3e4..0000000 --- a/TransferList/Presentation/HomeScene/View/Main.storyboard +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift index cb45e68..19ed333 100644 --- a/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift +++ b/TransferList/Presentation/HomeScene/ViewModel/TransferViewModel.swift @@ -12,9 +12,9 @@ import Domain class TransferViewModel { @Published var viewState: ViewState! - var dataUpdated = PassthroughSubject, Never>() + var accountsNeedToShow = PassthroughSubject, Never>() var errorForSavingOrRemoving = PassthroughSubject() - var changeView = PassthroughSubject() + var router = PassthroughSubject() var favoriteStatusUpdated = PassthroughSubject() private let useCase: PersonBankAccountUseCase private var subscriptions = Set() @@ -38,7 +38,7 @@ class TransferViewModel { self.viewState = viewState } - private func updateAccounts(appendAccounts accounts: [PersonBankAccount]) { + private func updateAllAccounts(appendAccounts accounts: [PersonBankAccount]) { guard !accounts.isEmpty else { paginationMode.mode = .reachedToEnd return @@ -50,7 +50,7 @@ class TransferViewModel { dataFromServer.append(contentsOf: accounts) } - dataUpdated.send(dataFromServer) + accountsNeedToShow.send(dataFromServer) } public func fetchFavoriteList() { @@ -62,7 +62,7 @@ class TransferViewModel { self.dataFromLocal = .init(list: accounts, mode: .initial, section: .favoriteBankAcconts) - self.dataUpdated.send(self.dataFromLocal) + self.accountsNeedToShow.send(self.dataFromLocal) }) .store(in: &subscriptions) } @@ -85,14 +85,14 @@ class TransferViewModel { } receiveValue: { [weak self] accounts in guard let self else { return } - self.updateAccounts(appendAccounts: accounts) + self.updateAllAccounts(appendAccounts: accounts) self.paginationMode.moveToNextOffset() self.updateViewState(newState: .result) } .store(in: &subscriptions) } - public func refreshData() { + public func refreshList() { guard viewState != .loading else { return } paginationMode.reset() @@ -100,7 +100,7 @@ class TransferViewModel { fetchTransferList() } - public func itemDisplay(atSection section: HomeItem.Section, row: Int) { + public func reachedToRow(row: Int, atSection section: HomeItem.Section) { guard viewState != .loading else { return } guard section == .personBankAccounts else { return } guard paginationMode.mode == .continues else { return } @@ -112,44 +112,35 @@ class TransferViewModel { } public func accountSelected(_ account: PersonBankAccount) { - changeView.send(.detail(account: account)) + router.send(.detail(account: account)) + } + + private func updateFavorite(publisher: AnyPublisher) { + publisher + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self else { return } + + switch completion { + case .finished: break + case .failure(let error): + let viewError = ViewState.error(message: error.errorDescription ?? "Unexpected error") + self.errorForSavingOrRemoving.send(viewError) + break + } + + } receiveValue: { [weak self] account in + self?.favoriteStatusUpdated.send(account) + }.store(in: &subscriptions) } public func removeFromFavorite(account: PersonBankAccount) { - useCase.removePersonAccountFromFavorites(account) - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self else { return } - - switch completion { - case .finished: break - case .failure(let error): - let viewError = ViewState.error(message: error.errorDescription ?? "Unexpected error") - self.errorForSavingOrRemoving.send(viewError) - break - } - - } receiveValue: { [weak self] account in - self?.favoriteStatusUpdated.send(account) - }.store(in: &subscriptions) + let publisher = useCase.removePersonAccountFromFavorites(account) + updateFavorite(publisher: publisher) } public func saveToFavorite(account: PersonBankAccount) { - useCase.savePersonAccountToFavorites(account) - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self else { return } - - switch completion { - case .finished: break - case .failure(let error): - let viewError = ViewState.error(message: error.errorDescription ?? "Unexpected error") - self.errorForSavingOrRemoving.send(viewError) - break - } - - } receiveValue: { [weak self] account in - self?.favoriteStatusUpdated.send(account) - }.store(in: &subscriptions) + let publisher = useCase.savePersonAccountToFavorites(account) + updateFavorite(publisher: publisher) } } diff --git a/TransferList/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/TransferList/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..274babb --- /dev/null +++ b/TransferList/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TransferList/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/TransferList/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 13613e3..f5086e8 100644 --- a/TransferList/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/TransferList/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "Untitled design.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/TransferList/Resources/Assets.xcassets/AppIcon.appiconset/Untitled design.png b/TransferList/Resources/Assets.xcassets/AppIcon.appiconset/Untitled design.png new file mode 100644 index 0000000000000000000000000000000000000000..95dc491c8eeb5f25dc6870c06384b93db632c6d9 GIT binary patch literal 58124 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 + var viewModel: TransferViewModel! + + override func setUp() { + cancellables = [] + viewModel = ViewModelFactory.shared.createMediaViewModel() + } + + override func tearDown() { + viewModel = nil + cancellables.removeAll() + } + + func testViewState() { + + // given + let expectation = XCTestExpectation(description: "change view state") + var newState: ViewState? + + // when + viewModel.fetchTransferList() + + viewModel.$viewState + .first() + .sink { state in + newState = state + expectation.fulfill() + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 1.0) + + // then + XCTAssertNotNil(newState) + XCTAssertEqual(newState, .loading) + } + + func testFetchTransferList() { + + // given + let expectation = XCTestExpectation(description: "fetch transfer list") + var dataTransfer: DataTransfer! + + // when + viewModel.fetchTransferList() + + viewModel.accountsNeedToShow + .sink { data in + dataTransfer = data + expectation.fulfill() + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 10.0) + + // then + XCTAssertEqual(dataTransfer.list.count, 10) + } + + func testFetchFavoriteList() { + + // given + let expectation = XCTestExpectation(description: "fetch transfer list") + var dataTransfer: DataTransfer! + + // when + viewModel.saveToFavorite(account: .init(person: Person(name: "hes", email: nil, + avatar: nil), card: .init(cardNumber: "134", cardType: nil), cardTransferCount: nil, note: nil, lastDateTransfer: nil)) + + viewModel.favoriteStatusUpdated + .sink { [weak self] _ in + self?.viewModel.fetchFavoriteList() + }.store(in: &cancellables) + + viewModel.accountsNeedToShow + .sink { data in + dataTransfer = data + expectation.fulfill() + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 10.0) + + // then + XCTAssertEqual(dataTransfer.list.first?.person?.name, "hes") + } +} diff --git a/UI/Package.swift b/UI/Package.swift index d3bec7e..16aed61 100644 --- a/UI/Package.swift +++ b/UI/Package.swift @@ -24,8 +24,5 @@ let package = Package( dependencies: [], resources: [.process("Font/Resources"), .process("Color/Resources")]), - .testTarget( - name: "UITests", - dependencies: ["UI"]), ] )