From 02d061b42b7524e03ac92b8b6ef77b4e67b24ba0 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 20 Jun 2020 16:04:33 +0430 Subject: [PATCH 1/7] - added authorizeStatus use case. --- .../AuthenticationRepository.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/ZarinPal-Challenge/Repositories/Authentication/AuthenticationRepository.swift b/ZarinPal-Challenge/Repositories/Authentication/AuthenticationRepository.swift index fb7acc3..ab63fbd 100644 --- a/ZarinPal-Challenge/Repositories/Authentication/AuthenticationRepository.swift +++ b/ZarinPal-Challenge/Repositories/Authentication/AuthenticationRepository.swift @@ -12,16 +12,18 @@ import UIKit /// <#Description#> protocol AuthenticationUseCase : class { - + func authorizeUser() -> Observable func fetchCredential(with code: String) -> Observable + func authorizeStatus() -> Observable + } final class AuthenticationRepository : BaseRepository, AuthenticationUseCase { - var authorizeStatus: Observable { + var currentAuthorizeStatus: Observable { return authenticator? .isAuthenticated .map({ $0 ? AuthenticationStatus.authorized : .notAuthorized}) ?? .just(.unknown) @@ -39,6 +41,12 @@ final class AuthenticationRepository : BaseRepository, AuthenticationUseCase { } + //////////////////////////////////////////////////////////////// + //MARK:- + //MARK:UseCases implementation. + //MARK:- + //////////////////////////////////////////////////////////////// + func fetchCredential(with code: String) -> Observable { guard let authenticator = authenticator else { return .just(false) @@ -55,5 +63,9 @@ final class AuthenticationRepository : BaseRepository, AuthenticationUseCase { return authenticator.buildAuthentication(credential: clientCredential) } - + + func authorizeStatus() -> Observable { + return self.currentAuthorizeStatus + } + } From 5c5eb4ff755e7f45ae1af4e4b50c3b14154dc320 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 20 Jun 2020 16:04:58 +0430 Subject: [PATCH 2/7] - Added AppDIContainer file. - Added AppViewModel. --- ZarinPal-Challenge.xcodeproj/project.pbxproj | 20 +++- .../Utitlies/AppDIContainer.swift | 79 +++++++++++++ .../ViewModels/AppViewModel.swift | 108 ++++++++++++++++++ 3 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 ZarinPal-Challenge/Utitlies/AppDIContainer.swift create mode 100644 ZarinPal-Challenge/ViewModels/AppViewModel.swift diff --git a/ZarinPal-Challenge.xcodeproj/project.pbxproj b/ZarinPal-Challenge.xcodeproj/project.pbxproj index 4c2f728..46a4083 100644 --- a/ZarinPal-Challenge.xcodeproj/project.pbxproj +++ b/ZarinPal-Challenge.xcodeproj/project.pbxproj @@ -53,6 +53,8 @@ F8D2FA03249E04F50029FD63 /* XCGitHubUserRepositoryTestes.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA02249E04F50029FD63 /* XCGitHubUserRepositoryTestes.swift */; }; F8D2FA05249E1B260029FD63 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA04249E1B260029FD63 /* UserProfile.swift */; }; F8D2FA07249E1EBF0029FD63 /* XCGitHubProfileRepositoriy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA06249E1EBF0029FD63 /* XCGitHubProfileRepositoriy.swift */; }; + F8D2FA0A249E20620029FD63 /* AppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA09249E20620029FD63 /* AppViewModel.swift */; }; + F8D2FA0C249E271C0029FD63 /* AppDIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA0B249E271C0029FD63 /* AppDIContainer.swift */; }; F8E5888C2499A3070083BD93 /* UserRepositoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5888B2499A3070083BD93 /* UserRepositoryDetailView.swift */; }; F8E5888E2499B3130083BD93 /* UserRepositoryBranchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5888D2499B3130083BD93 /* UserRepositoryBranchListView.swift */; }; F8E588902499B3290083BD93 /* UserRepositoryIssueListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5888F2499B3290083BD93 /* UserRepositoryIssueListView.swift */; }; @@ -135,6 +137,8 @@ F8D2FA02249E04F50029FD63 /* XCGitHubUserRepositoryTestes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCGitHubUserRepositoryTestes.swift; sourceTree = ""; }; F8D2FA04249E1B260029FD63 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; F8D2FA06249E1EBF0029FD63 /* XCGitHubProfileRepositoriy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCGitHubProfileRepositoriy.swift; sourceTree = ""; }; + F8D2FA09249E20620029FD63 /* AppViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewModel.swift; sourceTree = ""; }; + F8D2FA0B249E271C0029FD63 /* AppDIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDIContainer.swift; sourceTree = ""; }; F8E5888B2499A3070083BD93 /* UserRepositoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryDetailView.swift; sourceTree = ""; }; F8E5888D2499B3130083BD93 /* UserRepositoryBranchListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryBranchListView.swift; sourceTree = ""; }; F8E5888F2499B3290083BD93 /* UserRepositoryIssueListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryIssueListView.swift; sourceTree = ""; }; @@ -226,12 +230,13 @@ F82103B92499697B00B1A211 /* ZarinPal-Challenge */ = { isa = PBXGroup; children = ( - F8D2F9FE249DFADF0029FD63 /* Utitlies */, - F85513C22499ED50006F309D /* Constant */, F875B8D024996B420052C1DC /* Views */, + F8D2FA08249E202F0029FD63 /* ViewModels */, F85513B92499E069006F309D /* Models */, F86A2115249A476900C8AE69 /* Repositories */, F85513B82499D136006F309D /* Service */, + F8D2F9FE249DFADF0029FD63 /* Utitlies */, + F85513C22499ED50006F309D /* Constant */, F82103BA2499697B00B1A211 /* AppDelegate.swift */, F82103C02499697D00B1A211 /* Assets.xcassets */, F82103C82499697D00B1A211 /* Info.plist */, @@ -400,6 +405,7 @@ isa = PBXGroup; children = ( F8D2F9FF249DFAF30029FD63 /* GitHubGraphQLFactory.swift */, + F8D2FA0B249E271C0029FD63 /* AppDIContainer.swift */, ); path = Utitlies; sourceTree = ""; @@ -413,6 +419,14 @@ path = RepositoriesTestes; sourceTree = ""; }; + F8D2FA08249E202F0029FD63 /* ViewModels */ = { + isa = PBXGroup; + children = ( + F8D2FA09249E20620029FD63 /* AppViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -686,6 +700,7 @@ F85513D5249A1436006F309D /* Authentication.swift in Sources */, F85513C42499ED5B006F309D /* AppConfig.swift in Sources */, F82103BB2499697B00B1A211 /* AppDelegate.swift in Sources */, + F8D2FA0C249E271C0029FD63 /* AppDIContainer.swift in Sources */, F86A2119249A498C00C8AE69 /* AuthenticationRepository.swift in Sources */, F85513D1249A1080006F309D /* OAuthCredential+AuthenticationCredential.swift in Sources */, F80E3127249A54440092000F /* GitHubUserProfileRepository.swift in Sources */, @@ -700,6 +715,7 @@ F87C13F424997779002170B9 /* AuthenticationView.swift in Sources */, F85513CD249A06E9006F309D /* KeyChainStorage.swift in Sources */, F85513C9249A04F4006F309D /* Storable.swift in Sources */, + F8D2FA0A249E20620029FD63 /* AppViewModel.swift in Sources */, F85513CF249A0F7F006F309D /* OAuthCredential.swift in Sources */, F85513C62499F71B006F309D /* INetworkServiceEndpoint.swift in Sources */, F80E3125249A51040092000F /* GitHubUserRepository.swift in Sources */, diff --git a/ZarinPal-Challenge/Utitlies/AppDIContainer.swift b/ZarinPal-Challenge/Utitlies/AppDIContainer.swift new file mode 100644 index 0000000..da05694 --- /dev/null +++ b/ZarinPal-Challenge/Utitlies/AppDIContainer.swift @@ -0,0 +1,79 @@ +// +// AppDIContainer.swift +// ZarinPal-Challenge +// +// Created by Farshad Mousalou on 6/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +struct AppDIContainer { + + static let clientCredential = AppConfig.clientCredetianl + + //////////////////////////////////////////////////////////////// + //MARK:- + //MARK:Storage DI Container + //MARK:- + //////////////////////////////////////////////////////////////// + + static let secureStorage: SecureStorage = KeyChainStorage(supportsSecureStore: true) + + //////////////////////////////////////////////////////////////// + //MARK:- + //MARK:Authroization DI Container + //MARK:- + //////////////////////////////////////////////////////////////// + + + static let authorization: Authentication = { + return authorizationInterceptor + }() + + static let authorizationInterceptor: AuthenticationInterceptable = { + return AuthenticationService(networkService: APIClient.instance, storage: secureStorage) + }() + + static let networkService: NetworkServiceInterceptable = { + return APIClient.default + }() + + //////////////////////////////////////////////////////////////// + //MARK:- + //MARK:Repository DI Container + //MARK:- + //////////////////////////////////////////////////////////////// + + static var authenticationRepository : AuthenticationRepository { + return AuthenticationRepository(authenticator: authorizationInterceptor, clientCredential: clientCredential) + } + + static var githubUserRepository: GitHubUserRepository { + return GitHubUserRepository(authenticator: authorizationInterceptor, networkService: networkService) + } + + static var githubUserProfileRepository: GitHubUserProfileRepository { + return GitHubUserProfileRepository(authenticator: authorizationInterceptor, networkService: networkService) + } + + //////////////////////////////////////////////////////////////// + //MARK:- + //MARK:Use Cases DI Container + //MARK:- + //////////////////////////////////////////////////////////////// + + static var authenticationUseCases: AuthenticationUseCase { + return authenticationRepository + } + + static var userRepositoryUseCases: GitHubUserRepositoryUseCases { + return githubUserRepository + } + + static var userProfileUseCases: GitHubUserProfileRepositoryUseCases { + return githubUserProfileRepository + } + + +} diff --git a/ZarinPal-Challenge/ViewModels/AppViewModel.swift b/ZarinPal-Challenge/ViewModels/AppViewModel.swift new file mode 100644 index 0000000..03c3921 --- /dev/null +++ b/ZarinPal-Challenge/ViewModels/AppViewModel.swift @@ -0,0 +1,108 @@ +// +// AppViewModel.swift +// ZarinPal-Challenge +// +// Created by Farshad Mousalou on 6/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import Combine + +class AppViewModel: ObservableObject { + + @Published var state: State = .notAuthorized + @Published var error: Error? = nil + + enum State { + case authorized + case notAuthorized + } + + enum Event { + case authorize + case recieved(code: String) + } + + private let authentication : AuthenticationUseCase + + let disposeBag = DisposeBag() + + init(authentication: AuthenticationUseCase) { + + self.authentication = authentication + observerAuthentication() + } + + func observerAuthentication() { + + authentication.authorizeStatus() + .asObservable() + .subscribe(onNext: {[unowned self] (status) in + let newState : State + + switch status { + case .authorized: + newState = .authorized + case .unknown: + fallthrough + case .notAuthorized: + newState = .notAuthorized + } + + self.state = newState + }) + .disposed(by: disposeBag) + + } + + func send(event: Event) { + switch event { + case .authorize: + authentication.authorizeUser() + .subscribeOn(MainScheduler.asyncInstance) + .subscribe {[weak self] (event) in + switch event { + case .completed: + return + case .next(let url): + self?.open(URL: url) + case .error(let error): + self?.error = error + } + } + .disposed(by: disposeBag) + + case .recieved(code: let code): + authentication.fetchCredential(with: code) + .subscribeOn(MainScheduler.asyncInstance) + .subscribe {[weak self] (event) in + switch event { + case .completed: + return + case .next(_): + return + case .error(let error): + self?.error = error + } + } + .disposed(by: disposeBag) + } + } + + func open(URL url : URL?) { + + guard let url = url, UIApplication.shared.canOpenURL(url) else { + return + } + + UIApplication.shared.open(url, + options: [:]) { (result) in + + } + + } + + +} From 1fbdddc87ce1473ab90b818b702a11b9317c45cb Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 20 Jun 2020 16:28:55 +0430 Subject: [PATCH 3/7] - Added `AppContainerView`. - Added `AppViewModel` class. - Added `AuthenticationViewModel` class. - Implemented AppViewModel into `SceneDelegate`. - Added: clearAll method to Storage and implemented KeyChainStorage. --- ZarinPal-Challenge.xcodeproj/project.pbxproj | 8 +++++ ZarinPal-Challenge/SceneDelegate.swift | 27 ++++----------- .../Service/Storage/KeyChainStorage.swift | 12 +++++++ .../Service/Storage/Storage.swift | 3 ++ .../ViewModels/AppViewModel.swift | 2 +- .../ViewModels/AuthenticationViewModel.swift | 13 +++++++ .../Views/AppContainerView.swift | 34 +++++++++++++++++++ .../Views/AuthenticationView.swift | 14 ++++++-- 8 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 ZarinPal-Challenge/ViewModels/AuthenticationViewModel.swift create mode 100644 ZarinPal-Challenge/Views/AppContainerView.swift diff --git a/ZarinPal-Challenge.xcodeproj/project.pbxproj b/ZarinPal-Challenge.xcodeproj/project.pbxproj index 46a4083..e6c26d2 100644 --- a/ZarinPal-Challenge.xcodeproj/project.pbxproj +++ b/ZarinPal-Challenge.xcodeproj/project.pbxproj @@ -55,6 +55,8 @@ F8D2FA07249E1EBF0029FD63 /* XCGitHubProfileRepositoriy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA06249E1EBF0029FD63 /* XCGitHubProfileRepositoriy.swift */; }; F8D2FA0A249E20620029FD63 /* AppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA09249E20620029FD63 /* AppViewModel.swift */; }; F8D2FA0C249E271C0029FD63 /* AppDIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA0B249E271C0029FD63 /* AppDIContainer.swift */; }; + F8D2FA0E249E2C0A0029FD63 /* AppContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA0D249E2C0A0029FD63 /* AppContainerView.swift */; }; + F8D2FA10249E2F180029FD63 /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA0F249E2F180029FD63 /* AuthenticationViewModel.swift */; }; F8E5888C2499A3070083BD93 /* UserRepositoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5888B2499A3070083BD93 /* UserRepositoryDetailView.swift */; }; F8E5888E2499B3130083BD93 /* UserRepositoryBranchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5888D2499B3130083BD93 /* UserRepositoryBranchListView.swift */; }; F8E588902499B3290083BD93 /* UserRepositoryIssueListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5888F2499B3290083BD93 /* UserRepositoryIssueListView.swift */; }; @@ -139,6 +141,8 @@ F8D2FA06249E1EBF0029FD63 /* XCGitHubProfileRepositoriy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCGitHubProfileRepositoriy.swift; sourceTree = ""; }; F8D2FA09249E20620029FD63 /* AppViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewModel.swift; sourceTree = ""; }; F8D2FA0B249E271C0029FD63 /* AppDIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDIContainer.swift; sourceTree = ""; }; + F8D2FA0D249E2C0A0029FD63 /* AppContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContainerView.swift; sourceTree = ""; }; + F8D2FA0F249E2F180029FD63 /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; }; F8E5888B2499A3070083BD93 /* UserRepositoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryDetailView.swift; sourceTree = ""; }; F8E5888D2499B3130083BD93 /* UserRepositoryBranchListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryBranchListView.swift; sourceTree = ""; }; F8E5888F2499B3290083BD93 /* UserRepositoryIssueListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryIssueListView.swift; sourceTree = ""; }; @@ -373,6 +377,7 @@ F8E5888F2499B3290083BD93 /* UserRepositoryIssueListView.swift */, F8E588912499B33F0083BD93 /* UserRepositoryPullRequestListView.swift */, F84D0AC52499BD0600B88CA0 /* UserProfileView.swift */, + F8D2FA0D249E2C0A0029FD63 /* AppContainerView.swift */, ); path = Views; sourceTree = ""; @@ -423,6 +428,7 @@ isa = PBXGroup; children = ( F8D2FA09249E20620029FD63 /* AppViewModel.swift */, + F8D2FA0F249E2F180029FD63 /* AuthenticationViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -702,6 +708,7 @@ F82103BB2499697B00B1A211 /* AppDelegate.swift in Sources */, F8D2FA0C249E271C0029FD63 /* AppDIContainer.swift in Sources */, F86A2119249A498C00C8AE69 /* AuthenticationRepository.swift in Sources */, + F8D2FA0E249E2C0A0029FD63 /* AppContainerView.swift in Sources */, F85513D1249A1080006F309D /* OAuthCredential+AuthenticationCredential.swift in Sources */, F80E3127249A54440092000F /* GitHubUserProfileRepository.swift in Sources */, F8D2F9F7249DE3620029FD63 /* Respositories.swift in Sources */, @@ -719,6 +726,7 @@ F85513CF249A0F7F006F309D /* OAuthCredential.swift in Sources */, F85513C62499F71B006F309D /* INetworkServiceEndpoint.swift in Sources */, F80E3125249A51040092000F /* GitHubUserRepository.swift in Sources */, + F8D2FA10249E2F180029FD63 /* AuthenticationViewModel.swift in Sources */, F85513BD2499E566006F309D /* NetworkService.swift in Sources */, F86A2117249A480A00C8AE69 /* BaseRepository.swift in Sources */, F8CF16C524998FBD00464F90 /* RepositoryData.swift in Sources */, diff --git a/ZarinPal-Challenge/SceneDelegate.swift b/ZarinPal-Challenge/SceneDelegate.swift index 11ad3cd..220cb3d 100644 --- a/ZarinPal-Challenge/SceneDelegate.swift +++ b/ZarinPal-Challenge/SceneDelegate.swift @@ -13,8 +13,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - let authentication = AuthenticationService() - + + let appViewModel = AppViewModel(authentication: AppDIContainer.authenticationUseCases) + 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. @@ -24,22 +25,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { print(#function,connectionOptions) - var items = [RepositoryData]() - let fakeBranche = RepositoryData.BranchData(title: "Branch", createdDate: Date(timeIntervalSinceNow: -100), updateDate: Date()) - let fakeIssue = RepositoryData.IssueData(number: "1", title: "Issue", date: Date(timeIntervalSinceNow: -50), description: "Issue Description") - let fakePR = RepositoryData.PullRequestData(number: "#2", title: "Issue", date: Date(timeIntervalSinceNow: -40), description: "PR Description") - for i in 0...20 { - - var respository = RepositoryData(title: "Repo Name \(i)", description: i % 2 == 0 ? nil : "Repo Desc") - respository.branches = i % 3 == 0 ? Array(repeating:fakeBranche , count: 5) : [] - respository.pullRequests = i % 4 == 0 ? Array(repeating: fakePR, count: 5) : [] - respository.issues = i % 5 == 0 ? Array(repeating: fakeIssue, count: 5) : [] - - items.append(respository) - } +// AppDIContainer.secureStorage.clearAll() - let contentView = UserRepositoryListView(items: items) + + let contentView = AppContainerView(viewModel: appViewModel) // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { @@ -49,9 +39,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window.makeKeyAndVisible() } - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { - _ = self.authentication.buildAuthentication(credential: AppConfig.clientCredetianl) - } } func sceneDidDisconnect(_ scene: UIScene) { @@ -100,7 +87,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { .rootViewController?.present(alert, animated: true, completion: nil) }else if let code = queryItems?.first(where: { $0.name == "code"}), let codeValue = code.value { - print("accessCode =>",codeValue) + appViewModel.send(event: .recieved(code: codeValue)) } } diff --git a/ZarinPal-Challenge/Service/Storage/KeyChainStorage.swift b/ZarinPal-Challenge/Service/Storage/KeyChainStorage.swift index 5bc8ada..478e71a 100644 --- a/ZarinPal-Challenge/Service/Storage/KeyChainStorage.swift +++ b/ZarinPal-Challenge/Service/Storage/KeyChainStorage.swift @@ -68,4 +68,16 @@ struct KeyChainStorage: SecureStorage { } + @discardableResult + func clearAll() -> Bool { + do { + try keychain.synchronizable(true) + .removeAll() + return true + }catch let error { + print("error \(error)") + return false + } + } + } diff --git a/ZarinPal-Challenge/Service/Storage/Storage.swift b/ZarinPal-Challenge/Service/Storage/Storage.swift index 8303ecd..8011895 100644 --- a/ZarinPal-Challenge/Service/Storage/Storage.swift +++ b/ZarinPal-Challenge/Service/Storage/Storage.swift @@ -10,6 +10,7 @@ import Foundation protocol Storage { + @discardableResult /// <#Description#> /// - Parameters: /// - object: <#object description#> @@ -22,6 +23,8 @@ protocol Storage { /// - forKey: <#forKey description#> func retreive(type:T.Type, forKey key: String) -> T? + @discardableResult + func clearAll() -> Bool } protocol SecureStorage: Storage { diff --git a/ZarinPal-Challenge/ViewModels/AppViewModel.swift b/ZarinPal-Challenge/ViewModels/AppViewModel.swift index 03c3921..296191f 100644 --- a/ZarinPal-Challenge/ViewModels/AppViewModel.swift +++ b/ZarinPal-Challenge/ViewModels/AppViewModel.swift @@ -25,7 +25,7 @@ class AppViewModel: ObservableObject { case recieved(code: String) } - private let authentication : AuthenticationUseCase + let authentication : AuthenticationUseCase let disposeBag = DisposeBag() diff --git a/ZarinPal-Challenge/ViewModels/AuthenticationViewModel.swift b/ZarinPal-Challenge/ViewModels/AuthenticationViewModel.swift new file mode 100644 index 0000000..0932e02 --- /dev/null +++ b/ZarinPal-Challenge/ViewModels/AuthenticationViewModel.swift @@ -0,0 +1,13 @@ +// +// AuthenticationViewModel.swift +// ZarinPal-Challenge +// +// Created by Farshad Mousalou on 6/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +class AuthenticationViewModel: AppViewModel { + +} diff --git a/ZarinPal-Challenge/Views/AppContainerView.swift b/ZarinPal-Challenge/Views/AppContainerView.swift new file mode 100644 index 0000000..8fb61be --- /dev/null +++ b/ZarinPal-Challenge/Views/AppContainerView.swift @@ -0,0 +1,34 @@ +// +// AppContainerView.swift +// ZarinPal-Challenge +// +// Created by Farshad Mousalou on 6/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import SwiftUI +import UIKit +import Combine +struct AppContainerView: View { + + @ObservedObject var viewModel : AppViewModel + + var body: some View { + + switch viewModel.state { + case .authorized: + return AnyView(UserRepositoryListView(items: [])) + case .notAuthorized: + return AnyView(AuthenticationView(viewModel: AuthenticationViewModel(authentication: viewModel.authentication))) + } + + } +} + + +struct AppContainerView_Previews: PreviewProvider { + static var previews: some View { + AppContainerView(viewModel: AppViewModel(authentication: AppDIContainer.authenticationUseCases)) + } +} + diff --git a/ZarinPal-Challenge/Views/AuthenticationView.swift b/ZarinPal-Challenge/Views/AuthenticationView.swift index f9defb9..db819d0 100644 --- a/ZarinPal-Challenge/Views/AuthenticationView.swift +++ b/ZarinPal-Challenge/Views/AuthenticationView.swift @@ -7,9 +7,13 @@ // import SwiftUI +import Combine +import Foundation struct AuthenticationView: View { + @ObservedObject var viewModel : AuthenticationViewModel + var body : some View { VStack(spacing: 16) { Text("Authentication") @@ -23,7 +27,7 @@ struct AuthenticationView: View { .font(.body) .fontWeight(.semibold) Button(action: { - UIApplication.shared.openURL(URL(string: "https://www.google.com")!) + self.viewModel.send(event: .authorize) }) { Text("Authenticate") .foregroundColor(.white) @@ -33,6 +37,12 @@ struct AuthenticationView: View { .background(Color.blue) .clipShape(Capsule()) + viewModel.error.map { (error) in + Text(error.localizedDescription) + .font(.body) + .fontWeight(.regular) + } + Spacer() } .padding(16) @@ -41,6 +51,6 @@ struct AuthenticationView: View { struct AuthenticationView_Previews: PreviewProvider { static var previews: some View { - AuthenticationView() + AuthenticationView(viewModel: AuthenticationViewModel(authentication: AppDIContainer.authenticationUseCases)) } } From 98e750725707334b44db0fd7d6250853bc7839e8 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 20 Jun 2020 19:33:23 +0430 Subject: [PATCH 4/7] - Fixed Pagination Issue in Repo. - Fixed DIContainer Session Bug. --- ZarinPal-Challenge/Models/Pagination.swift | 2 +- .../Repositories/GitHub/GitHubUserRepository.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ZarinPal-Challenge/Models/Pagination.swift b/ZarinPal-Challenge/Models/Pagination.swift index 7166e0e..3419436 100644 --- a/ZarinPal-Challenge/Models/Pagination.swift +++ b/ZarinPal-Challenge/Models/Pagination.swift @@ -10,7 +10,7 @@ import Foundation struct Pagination : Decodable { - let endCursor: String + let startCursor: String? let hasNextPage: Bool let hasPreviousPage: Bool } diff --git a/ZarinPal-Challenge/Repositories/GitHub/GitHubUserRepository.swift b/ZarinPal-Challenge/Repositories/GitHub/GitHubUserRepository.swift index 43bb553..c22e13d 100644 --- a/ZarinPal-Challenge/Repositories/GitHub/GitHubUserRepository.swift +++ b/ZarinPal-Challenge/Repositories/GitHub/GitHubUserRepository.swift @@ -46,7 +46,7 @@ final class GitHubUserRepository : BaseRepository, GitHubUserRepositoryUseCases self.pageSize = last return fetchRespositories(last:pageSize, - cursor: lastPage?.endCursor, + cursor: lastPage?.startCursor, networkService: network) } @@ -59,7 +59,7 @@ final class GitHubUserRepository : BaseRepository, GitHubUserRepositoryUseCases } return fetchRespositories(last:self.pageSize, - cursor: lastPage.endCursor, + cursor: lastPage.startCursor, networkService: network) } From fd29f1ba7ac5ebe480e4b0bde3f0e7db9b9ec310 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 20 Jun 2020 19:35:32 +0430 Subject: [PATCH 5/7] - Added and implemented ViewModel for GitHub Repository List. - Added: `ActivityIndicatorView` struct. - Bugs Fixed. --- Podfile | 2 +- Podfile.lock | 8 +- ZarinPal-Challenge.xcodeproj/project.pbxproj | 24 +++++ .../Utitlies/AppDIContainer.swift | 6 +- .../Utitlies/GitHubGraphQLFactory.swift | 2 +- ZarinPal-Challenge/Utitlies/UIColor+Hex.swift | 40 ++++++++ .../UserRepositoryListViewModel.swift | 97 +++++++++++++++++++ .../ViewModels/UserRepositoryViewModel.swift | 49 ++++++++++ .../Views/AppContainerView.swift | 2 +- .../CustomView/ActivityIndicatorView.swift | 32 ++++++ .../Views/UserRepositoryDetailView.swift | 4 +- .../Views/UserRepositoryListView.swift | 64 ++++++++++-- .../Views/UserRepositoryRowView.swift | 67 +++++++++---- 13 files changed, 363 insertions(+), 34 deletions(-) create mode 100644 ZarinPal-Challenge/Utitlies/UIColor+Hex.swift create mode 100644 ZarinPal-Challenge/ViewModels/UserRepositoryListViewModel.swift create mode 100644 ZarinPal-Challenge/ViewModels/UserRepositoryViewModel.swift create mode 100644 ZarinPal-Challenge/Views/CustomView/ActivityIndicatorView.swift diff --git a/Podfile b/Podfile index 3064563..7565a4e 100644 --- a/Podfile +++ b/Podfile @@ -6,7 +6,7 @@ target 'ZarinPal-Challenge' do use_frameworks! pod 'Alamofire' pod 'RxSwift' - pod 'Kingfisher' + pod 'Kingfisher/SwiftUI' pod 'RxAlamofire' pod 'KeychainAccess' pod 'AutoGraph', :git => 'https://github.com/farshadmb/AutoGraph.git' diff --git a/Podfile.lock b/Podfile.lock index b115eba..e005575 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -5,9 +5,9 @@ PODS: - JSONValueRX (~> 7.0.0) - JSONValueRX (7.0.0) - KeychainAccess (4.2.0) - - Kingfisher (5.14.0): - - Kingfisher/Core (= 5.14.0) - Kingfisher/Core (5.14.0) + - Kingfisher/SwiftUI (5.14.0): + - Kingfisher/Core - RxAlamofire (5.3.2): - RxAlamofire/Core (= 5.3.2) - RxAlamofire/Core (5.3.2): @@ -23,7 +23,7 @@ DEPENDENCIES: - Alamofire - AutoGraph (from `https://github.com/farshadmb/AutoGraph.git`) - KeychainAccess - - Kingfisher + - Kingfisher/SwiftUI - RxAlamofire - RxBlocking - RxSwift @@ -60,6 +60,6 @@ SPEC CHECKSUMS: RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa -PODFILE CHECKSUM: 8c8454106c69153332fa9e0d04ba52fb91fec444 +PODFILE CHECKSUM: 0ecdda1c6160f23eb1234f252627efb1eab92ee8 COCOAPODS: 1.9.3 diff --git a/ZarinPal-Challenge.xcodeproj/project.pbxproj b/ZarinPal-Challenge.xcodeproj/project.pbxproj index e6c26d2..2293a13 100644 --- a/ZarinPal-Challenge.xcodeproj/project.pbxproj +++ b/ZarinPal-Challenge.xcodeproj/project.pbxproj @@ -57,6 +57,10 @@ F8D2FA0C249E271C0029FD63 /* AppDIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA0B249E271C0029FD63 /* AppDIContainer.swift */; }; F8D2FA0E249E2C0A0029FD63 /* AppContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA0D249E2C0A0029FD63 /* AppContainerView.swift */; }; F8D2FA10249E2F180029FD63 /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA0F249E2F180029FD63 /* AuthenticationViewModel.swift */; }; + F8D2FA12249E31950029FD63 /* UserRepositoryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA11249E31950029FD63 /* UserRepositoryListViewModel.swift */; }; + F8D2FA14249E31C30029FD63 /* UserRepositoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA13249E31C30029FD63 /* UserRepositoryViewModel.swift */; }; + F8D2FA16249E33A50029FD63 /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA15249E33A50029FD63 /* UIColor+Hex.swift */; }; + F8D2FA19249E382F0029FD63 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA18249E382F0029FD63 /* ActivityIndicatorView.swift */; }; F8E5888C2499A3070083BD93 /* UserRepositoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5888B2499A3070083BD93 /* UserRepositoryDetailView.swift */; }; F8E5888E2499B3130083BD93 /* UserRepositoryBranchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5888D2499B3130083BD93 /* UserRepositoryBranchListView.swift */; }; F8E588902499B3290083BD93 /* UserRepositoryIssueListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5888F2499B3290083BD93 /* UserRepositoryIssueListView.swift */; }; @@ -143,6 +147,10 @@ F8D2FA0B249E271C0029FD63 /* AppDIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDIContainer.swift; sourceTree = ""; }; F8D2FA0D249E2C0A0029FD63 /* AppContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContainerView.swift; sourceTree = ""; }; F8D2FA0F249E2F180029FD63 /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; }; + F8D2FA11249E31950029FD63 /* UserRepositoryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryListViewModel.swift; sourceTree = ""; }; + F8D2FA13249E31C30029FD63 /* UserRepositoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryViewModel.swift; sourceTree = ""; }; + F8D2FA15249E33A50029FD63 /* UIColor+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Hex.swift"; sourceTree = ""; }; + F8D2FA18249E382F0029FD63 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; }; F8E5888B2499A3070083BD93 /* UserRepositoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryDetailView.swift; sourceTree = ""; }; F8E5888D2499B3130083BD93 /* UserRepositoryBranchListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryBranchListView.swift; sourceTree = ""; }; F8E5888F2499B3290083BD93 /* UserRepositoryIssueListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryIssueListView.swift; sourceTree = ""; }; @@ -369,6 +377,7 @@ F875B8D024996B420052C1DC /* Views */ = { isa = PBXGroup; children = ( + F8D2FA17249E381D0029FD63 /* CustomView */, F87C13F324997779002170B9 /* AuthenticationView.swift */, F8C97436249989F800D6C0EF /* UserRepositoryListView.swift */, F8C9743824998A0600D6C0EF /* UserRepositoryRowView.swift */, @@ -411,6 +420,7 @@ children = ( F8D2F9FF249DFAF30029FD63 /* GitHubGraphQLFactory.swift */, F8D2FA0B249E271C0029FD63 /* AppDIContainer.swift */, + F8D2FA15249E33A50029FD63 /* UIColor+Hex.swift */, ); path = Utitlies; sourceTree = ""; @@ -429,10 +439,20 @@ children = ( F8D2FA09249E20620029FD63 /* AppViewModel.swift */, F8D2FA0F249E2F180029FD63 /* AuthenticationViewModel.swift */, + F8D2FA11249E31950029FD63 /* UserRepositoryListViewModel.swift */, + F8D2FA13249E31C30029FD63 /* UserRepositoryViewModel.swift */, ); path = ViewModels; sourceTree = ""; }; + F8D2FA17249E381D0029FD63 /* CustomView */ = { + isa = PBXGroup; + children = ( + F8D2FA18249E382F0029FD63 /* ActivityIndicatorView.swift */, + ); + path = CustomView; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -698,10 +718,12 @@ F85513BF2499E5F6006F309D /* IGraphQLQueryRequest.swift in Sources */, F8C9743924998A0600D6C0EF /* UserRepositoryRowView.swift in Sources */, F86A211C249A4C4200C8AE69 /* AuthenticationStatus.swift in Sources */, + F8D2FA12249E31950029FD63 /* UserRepositoryListViewModel.swift in Sources */, F8D2FA05249E1B260029FD63 /* UserProfile.swift in Sources */, F8E5888C2499A3070083BD93 /* UserRepositoryDetailView.swift in Sources */, F85513D3249A126D006F309D /* AuthenticationService+Authentictor.swift in Sources */, F85513CB249A04FD006F309D /* Storage.swift in Sources */, + F8D2FA14249E31C30029FD63 /* UserRepositoryViewModel.swift in Sources */, F85513C12499EC0C006F309D /* APIClientService.swift in Sources */, F85513D5249A1436006F309D /* Authentication.swift in Sources */, F85513C42499ED5B006F309D /* AppConfig.swift in Sources */, @@ -718,6 +740,7 @@ F85513D7249A162D006F309D /* AppClientCredential.swift in Sources */, F8D2F9F5249DE30B0029FD63 /* Pagination.swift in Sources */, F8D2FA00249DFAF30029FD63 /* GitHubGraphQLFactory.swift in Sources */, + F8D2FA16249E33A50029FD63 /* UIColor+Hex.swift in Sources */, F8E5888E2499B3130083BD93 /* UserRepositoryBranchListView.swift in Sources */, F87C13F424997779002170B9 /* AuthenticationView.swift in Sources */, F85513CD249A06E9006F309D /* KeyChainStorage.swift in Sources */, @@ -726,6 +749,7 @@ F85513CF249A0F7F006F309D /* OAuthCredential.swift in Sources */, F85513C62499F71B006F309D /* INetworkServiceEndpoint.swift in Sources */, F80E3125249A51040092000F /* GitHubUserRepository.swift in Sources */, + F8D2FA19249E382F0029FD63 /* ActivityIndicatorView.swift in Sources */, F8D2FA10249E2F180029FD63 /* AuthenticationViewModel.swift in Sources */, F85513BD2499E566006F309D /* NetworkService.swift in Sources */, F86A2117249A480A00C8AE69 /* BaseRepository.swift in Sources */, diff --git a/ZarinPal-Challenge/Utitlies/AppDIContainer.swift b/ZarinPal-Challenge/Utitlies/AppDIContainer.swift index da05694..a8c4c00 100644 --- a/ZarinPal-Challenge/Utitlies/AppDIContainer.swift +++ b/ZarinPal-Challenge/Utitlies/AppDIContainer.swift @@ -7,6 +7,7 @@ // import Foundation +import Alamofire struct AppDIContainer { @@ -36,7 +37,10 @@ struct AppDIContainer { }() static let networkService: NetworkServiceInterceptable = { - return APIClient.default + let session = Session() + session.sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData + let apiClient = APIClient(session: session, decoder: APIClient.default.decoder) + return apiClient }() //////////////////////////////////////////////////////////////// diff --git a/ZarinPal-Challenge/Utitlies/GitHubGraphQLFactory.swift b/ZarinPal-Challenge/Utitlies/GitHubGraphQLFactory.swift index 8fa3676..537cce8 100644 --- a/ZarinPal-Challenge/Utitlies/GitHubGraphQLFactory.swift +++ b/ZarinPal-Challenge/Utitlies/GitHubGraphQLFactory.swift @@ -36,7 +36,7 @@ struct GitHubGraphQLFactory { } } pageInfo { - endCursor + startCursor hasNextPage hasPreviousPage } diff --git a/ZarinPal-Challenge/Utitlies/UIColor+Hex.swift b/ZarinPal-Challenge/Utitlies/UIColor+Hex.swift new file mode 100644 index 0000000..104ea85 --- /dev/null +++ b/ZarinPal-Challenge/Utitlies/UIColor+Hex.swift @@ -0,0 +1,40 @@ +// +// UIColor+Hex.swift +// ZarinPal-Challenge +// +// Created by Farshad Mousalou on 6/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +extension UIColor { + + convenience init?(hex: String) { + let r, g, b, a: CGFloat + + if hex.hasPrefix("#") { + let start = hex.index(hex.startIndex, offsetBy: 1) + let hexColor = String(hex[start...]) + + if hexColor.count == 8 { + let scanner = Scanner(string: hexColor) + var hexNumber: UInt64 = 0 + + if scanner.scanHexInt64(&hexNumber) { + r = CGFloat((hexNumber & 0xff000000) >> 24) / 255 + g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 + b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 + a = CGFloat(hexNumber & 0x000000ff) / 255 + + self.init(red: r, green: g, blue: b, alpha: a) + return + } + } + } + + return nil + } + +} diff --git a/ZarinPal-Challenge/ViewModels/UserRepositoryListViewModel.swift b/ZarinPal-Challenge/ViewModels/UserRepositoryListViewModel.swift new file mode 100644 index 0000000..7d51b35 --- /dev/null +++ b/ZarinPal-Challenge/ViewModels/UserRepositoryListViewModel.swift @@ -0,0 +1,97 @@ +// +// UserRepositoryListViewModel.swift +// ZarinPal-Challenge +// +// Created by Farshad Mousalou on 6/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import Combine +import RxSwift + +class UserRepositoryListViewModel : ObservableObject{ + + @Published var items: [UserRepositoryViewModel] = [] + @Published var state: State = .idle + + enum State { + case idle + case loading + case loadingMore + case loaded + case error(Error) + } + + enum Event { + case fetchRepositories(last: Int) + case fetchMoreRepositories + case refresh + } + + let githubRepository: GitHubUserRepositoryUseCases + + let disposeBag = DisposeBag() + + init(repositoriesUseCase: GitHubUserRepositoryUseCases) { + self.githubRepository = repositoriesUseCase + } + + func send(event: Event) { + + switch event { + case .fetchRepositories(last: let value): + state = .loading + githubRepository.fetchRepositories(last: value) + .observeOn(MainScheduler.asyncInstance) + .debug() + .subscribe {[weak self] (event) in + switch event { + case .next(let items): + self?.items = items.map { + UserRepositoryViewModel(repository: $0, userRepositoryUseCases: AppDIContainer.userRepositoryUseCases) + }.reversed() + case .error(let error): + self?.state = .error(error) + case .completed: + self?.state = .loaded + } + }.disposed(by: disposeBag) + + case .fetchMoreRepositories: + state = .loadingMore + + githubRepository.fetchMoreRepositories() + .observeOn(MainScheduler.asyncInstance) + .debug() + .subscribe {[weak self] (event) in + switch event { + case .next(let newItems): + guard let `self` = self else { + break + } + + let mapped = newItems.map { + UserRepositoryViewModel(repository: $0, userRepositoryUseCases: AppDIContainer.userRepositoryUseCases) + } + .filter { item in + !self.items.contains(where: { $0.model.id == item.model.id }) + } + .reversed() as Array + + self.items += mapped + + case .error(let error): + self?.state = .error(error) + case .completed: + self?.state = .loaded + } + }.disposed(by: disposeBag) + + case .refresh: + print("not implemented due the deadline time.") + } + + } + +} diff --git a/ZarinPal-Challenge/ViewModels/UserRepositoryViewModel.swift b/ZarinPal-Challenge/ViewModels/UserRepositoryViewModel.swift new file mode 100644 index 0000000..6ed2e16 --- /dev/null +++ b/ZarinPal-Challenge/ViewModels/UserRepositoryViewModel.swift @@ -0,0 +1,49 @@ +// +// UserRepositoryViewModel.swift +// ZarinPal-Challenge +// +// Created by Farshad Mousalou on 6/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import Combine +import RxSwift + +class UserRepositoryViewModel : ObservableObject, Identifiable { + + @Published var id: String? = nil + @Published var title: String? = nil + @Published var description: String? = nil + @Published var starCount: Int = 0 + @Published var forkCount: Int = 0 + @Published var avatarImage: URL? = nil + @Published var languageName: String? = nil + @Published var languageColor: UIColor = nil + + let userRepositoryUseCases: GitHubUserRepositoryUseCases + let model: Repository + + init(repository: Repository, userRepositoryUseCases: GitHubUserRepositoryUseCases) { + self.model = repository + self.userRepositoryUseCases = userRepositoryUseCases + initValues() + } + + func initValues() { + + id = model.id + title = model.name + description = model.description + starCount = model.starCount + forkCount = model.forkCount + avatarImage = model.avatarImage + languageName = model.language?.name + + if let color = model.language?.color { + languageColor = UIColor.init(hex: color) + } + + } + +} diff --git a/ZarinPal-Challenge/Views/AppContainerView.swift b/ZarinPal-Challenge/Views/AppContainerView.swift index 8fb61be..4fa295b 100644 --- a/ZarinPal-Challenge/Views/AppContainerView.swift +++ b/ZarinPal-Challenge/Views/AppContainerView.swift @@ -17,7 +17,7 @@ struct AppContainerView: View { switch viewModel.state { case .authorized: - return AnyView(UserRepositoryListView(items: [])) + return AnyView(UserRepositoryListView(viewModel: UserRepositoryListViewModel(repositoriesUseCase: AppDIContainer.userRepositoryUseCases))) case .notAuthorized: return AnyView(AuthenticationView(viewModel: AuthenticationViewModel(authentication: viewModel.authentication))) } diff --git a/ZarinPal-Challenge/Views/CustomView/ActivityIndicatorView.swift b/ZarinPal-Challenge/Views/CustomView/ActivityIndicatorView.swift new file mode 100644 index 0000000..5cc2a02 --- /dev/null +++ b/ZarinPal-Challenge/Views/CustomView/ActivityIndicatorView.swift @@ -0,0 +1,32 @@ +// +// ActivityIndicatorView.swift +// ZarinPal-Challenge +// +// Created by Farshad Mousalou on 6/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import SwiftUI +import UIKit + +struct ActivityIndicator: UIViewRepresentable { + + @Binding var isAnimating: Bool + + let style: UIActivityIndicatorView.Style + + func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView { + return UIActivityIndicatorView(style: style) + } + + func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) { + isAnimating ? uiView.startAnimating() : uiView.stopAnimating() + } +} + + +struct ActivityIndicatorView_Previews: PreviewProvider { + static var previews: some View { + ActivityIndicator(isAnimating: .constant(true), style: .medium) + } +} diff --git a/ZarinPal-Challenge/Views/UserRepositoryDetailView.swift b/ZarinPal-Challenge/Views/UserRepositoryDetailView.swift index 2356742..de13f6b 100644 --- a/ZarinPal-Challenge/Views/UserRepositoryDetailView.swift +++ b/ZarinPal-Challenge/Views/UserRepositoryDetailView.swift @@ -16,7 +16,9 @@ struct UserRepositoryDetailView: View { case pullRequest = "Pull Request" } - @State var data: RepositoryData + @ObservedObject var viewModel : UserRepositoryViewModel + + @State var data: RepositoryData = RepositoryData(title: "Hi") @State var mode : ViewMode = .branch var body: some View { diff --git a/ZarinPal-Challenge/Views/UserRepositoryListView.swift b/ZarinPal-Challenge/Views/UserRepositoryListView.swift index e95d9b1..134f0e6 100644 --- a/ZarinPal-Challenge/Views/UserRepositoryListView.swift +++ b/ZarinPal-Challenge/Views/UserRepositoryListView.swift @@ -10,8 +10,12 @@ import SwiftUI struct UserRepositoryListView: View { - var items : [RepositoryData] + @ObservedObject var viewModel: UserRepositoryListViewModel + @State private var selectProfile : Bool = false + @State private var previewError: Bool = false + @State private var error: Error? = nil + var body : some View { NavigationView { @@ -19,13 +23,10 @@ struct UserRepositoryListView: View { NavigationLink(destination: UserProfileView(),isActive: self.$selectProfile) { EmptyView() } - .hidden().frame(width: 0, height: 0, alignment: .center) - List(items) { item in - NavigationLink(destination: UserRepositoryDetailView(data:item)) { - UserRepositoryRowView(repository: item) - } - } + .hidden().frame(width: 0, height: 0.0, alignment: .center) + view(forState: viewModel.state) } + .onAppear(perform: observerState) .navigationViewStyle(StackNavigationViewStyle()) .navigationBarTitle(Text("Repositories")) .navigationBarItems(trailing:Button.init(action: { @@ -33,10 +34,57 @@ struct UserRepositoryListView: View { }, label: { Image(systemName: "person.circle").renderingMode(.template).accentColor(.green) })) + .alert(isPresented: self.$previewError) { + Alert(title: Text("Error"), + message: Text(self.error!.localizedDescription), + dismissButton: .default(Text("Got it!"))) + } + } + + } + + func view(forState state : UserRepositoryListViewModel.State) -> some View { + + switch state { + case .idle: + return AnyView(EmptyView()) + + case .loading: + return AnyView(ActivityIndicator(isAnimating: .constant(true), style: .medium)) + case .loadingMore,.error(_): + fallthrough + case .loaded: + return AnyView(List(viewModel.items) { itemViewModel in + NavigationLink(destination: UserRepositoryDetailView(viewModel: itemViewModel)) { + UserRepositoryRowView(viewModel: itemViewModel) + .onAppear { + + if let lastItem = self.viewModel.items.last, + lastItem === itemViewModel { + self.viewModel.send(event: .fetchMoreRepositories) + } + } + } + }) } } + func observerState() { + + switch viewModel.state { + case .idle: + viewModel.send(event: .fetchRepositories(last: 15)) + case .loading, .loadingMore: + break + case .loaded: + break + case .error(let error): + self.error = error + self.previewError.toggle() + } + } + } struct TempView : View { @@ -49,6 +97,6 @@ struct TempView : View { struct UserRepositoryListView_Previews: PreviewProvider { static var previews: some View { - UserRepositoryListView(items: []) + UserRepositoryListView(viewModel: .init(repositoriesUseCase: AppDIContainer.userRepositoryUseCases)) } } diff --git a/ZarinPal-Challenge/Views/UserRepositoryRowView.swift b/ZarinPal-Challenge/Views/UserRepositoryRowView.swift index 1edbd5c..4229e16 100644 --- a/ZarinPal-Challenge/Views/UserRepositoryRowView.swift +++ b/ZarinPal-Challenge/Views/UserRepositoryRowView.swift @@ -6,29 +6,62 @@ // Copyright © 2020 Farshad Mousalou. All rights reserved. // +import Combine import SwiftUI +import struct Kingfisher.KFImage struct UserRepositoryRowView: View { - var repository : RepositoryData + @ObservedObject var viewModel : UserRepositoryViewModel + var body: some View { - - VStack(alignment: .leading, spacing: 8) { - Text(repository.title) - .font(.system(.headline, design: .serif)) - .fontWeight(.semibold) - repository.description.map({ - Text($0) - .font(.system(.body, design: .serif)) - .fontWeight(.regular) - }) + HStack(alignment: .center, spacing: 8) { + avatarImage() + VStack(alignment: .leading, spacing: 5) { + viewModel.title.map({ + Text($0) + .fontWeight(.medium) + .font(.system(Font.TextStyle.subheadline)) + }) + viewModel.description.map({ + Text($0) + .fontWeight(.regular) + .font(.system(.caption)) + .lineLimit(3) + .truncationMode(.tail) + }) + } + Spacer() } - .padding() + .padding(8) } -} - -struct UserRepositoryRowView_Previews: PreviewProvider { - static var previews: some View { - UserRepositoryRowView(repository: RepositoryData(title:"",description: nil)) + + func avatarImage() -> some View { + return KFImage(viewModel.avatarImage,options: [.forceRefresh,.transition(.fade(0.3))]) + .resizable() + .onSuccess { r in + print(r) + } + .placeholder { + Image(systemName: "person.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 45, height: 45, alignment: .topLeading) + .foregroundColor(.green) + .clipShape(Circle()) + } + .aspectRatio(contentMode: .fit) + .frame(width: 45, height: 45, alignment: .topLeading) + .clipShape(Circle()) + } + } + +/* + struct UserRepositoryRowView_Previews: PreviewProvider { + static var previews: some View { + UserRepositoryRowView(viewModel: UserRepositoryViewModel(repository: Repository, userRepositoryUseCases: <#T##GitHubUserRepositoryUseCases#>)) + } + } + */ From 9623913a18baeb41c7311b315ae4b87c8e29abb5 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 20 Jun 2020 20:05:40 +0430 Subject: [PATCH 6/7] - some enchantment. --- ZarinPal-Challenge/Utitlies/UIColor+Hex.swift | 13 ++- .../ViewModels/UserRepositoryViewModel.swift | 2 +- .../Views/UserRepositoryRowView.swift | 87 +++++++++++++++---- 3 files changed, 77 insertions(+), 25 deletions(-) diff --git a/ZarinPal-Challenge/Utitlies/UIColor+Hex.swift b/ZarinPal-Challenge/Utitlies/UIColor+Hex.swift index 104ea85..45c37eb 100644 --- a/ZarinPal-Challenge/Utitlies/UIColor+Hex.swift +++ b/ZarinPal-Challenge/Utitlies/UIColor+Hex.swift @@ -12,23 +12,22 @@ import UIKit extension UIColor { convenience init?(hex: String) { - let r, g, b, a: CGFloat + let r, g, b: CGFloat if hex.hasPrefix("#") { let start = hex.index(hex.startIndex, offsetBy: 1) let hexColor = String(hex[start...]) - if hexColor.count == 8 { + if hexColor.count == 6 { let scanner = Scanner(string: hexColor) var hexNumber: UInt64 = 0 if scanner.scanHexInt64(&hexNumber) { - r = CGFloat((hexNumber & 0xff000000) >> 24) / 255 - g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 - b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 - a = CGFloat(hexNumber & 0x000000ff) / 255 + r = CGFloat((hexNumber & 0xff0000) >> 16) / 255 + g = CGFloat((hexNumber & 0x00ff00) >> 8) / 255 + b = CGFloat(hexNumber & 0x0000ff) / 255 - self.init(red: r, green: g, blue: b, alpha: a) + self.init(red: r, green: g, blue: b, alpha: 1.0) return } } diff --git a/ZarinPal-Challenge/ViewModels/UserRepositoryViewModel.swift b/ZarinPal-Challenge/ViewModels/UserRepositoryViewModel.swift index 6ed2e16..1a835e3 100644 --- a/ZarinPal-Challenge/ViewModels/UserRepositoryViewModel.swift +++ b/ZarinPal-Challenge/ViewModels/UserRepositoryViewModel.swift @@ -19,7 +19,7 @@ class UserRepositoryViewModel : ObservableObject, Identifiable { @Published var forkCount: Int = 0 @Published var avatarImage: URL? = nil @Published var languageName: String? = nil - @Published var languageColor: UIColor = nil + @Published var languageColor: UIColor? = nil let userRepositoryUseCases: GitHubUserRepositoryUseCases let model: Repository diff --git a/ZarinPal-Challenge/Views/UserRepositoryRowView.swift b/ZarinPal-Challenge/Views/UserRepositoryRowView.swift index 4229e16..ed3ea89 100644 --- a/ZarinPal-Challenge/Views/UserRepositoryRowView.swift +++ b/ZarinPal-Challenge/Views/UserRepositoryRowView.swift @@ -15,25 +15,58 @@ struct UserRepositoryRowView: View { @ObservedObject var viewModel : UserRepositoryViewModel var body: some View { - HStack(alignment: .center, spacing: 8) { - avatarImage() - VStack(alignment: .leading, spacing: 5) { - viewModel.title.map({ - Text($0) - .fontWeight(.medium) - .font(.system(Font.TextStyle.subheadline)) - }) - viewModel.description.map({ - Text($0) - .fontWeight(.regular) - .font(.system(.caption)) - .lineLimit(3) - .truncationMode(.tail) - }) + Group { + HStack(alignment: .center, spacing: 8) { + avatarImage() + VStack(alignment: .leading, spacing: 5) { + viewModel.title.map({ + Text($0) + .fontWeight(.medium) + .font(.system(Font.TextStyle.subheadline)) + }) + viewModel.description.map({ + Text($0) + .fontWeight(.regular) + .font(.system(.caption)) + .lineLimit(3) + .truncationMode(.tail) + }) + + HStack(alignment: .bottom, spacing: 2) { + languageView() + .frame(alignment: .bottomLeading) + + Spacer() + Group { + Image(systemName: "star.fill") + .resizable() + .scaledToFit() + .frame(width: 10, height: 10, alignment: .bottom) + .foregroundColor(.orange) + Text("\(viewModel.starCount)") + .fontWeight(.light) + .font(.system(size: 8)) + + Image(systemName: "tuningfork") + .resizable() + .scaledToFit() + .frame(width: 10, height: 10, alignment: .bottom) + .foregroundColor(.black) + + Text("\(viewModel.forkCount)") + .fontWeight(.light) + .font(.system(size: 8)) + } + .frame(alignment: .bottomTrailing) + } + + } + Spacer() } - Spacer() } .padding(8) + .listRowInsets(.none) + } func avatarImage() -> some View { @@ -55,7 +88,27 @@ struct UserRepositoryRowView: View { .clipShape(Circle()) } - + + func languageView() -> some View { + + viewModel.languageName.map { name in + + HStack(alignment: .bottom, spacing: 2) { + Text("Language:") + .fontWeight(.light) + .font(.system(size: 8)) + Text(name).fontWeight(.light) + .font(.system(size: 8)) + + viewModel.languageColor.map { color in + Circle() + .fill(Color(color)) + .frame(width: 10, height: 10, alignment: .bottom) + } + } + } + } + } /* From 4bb2ef9a2a450818cc4d8a3dcdf3ad4311277621 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 20 Jun 2020 20:43:16 +0430 Subject: [PATCH 7/7] - User Profile is done. --- ZarinPal-Challenge.xcodeproj/project.pbxproj | 4 + .../ViewModels/UserProfileViewModel.swift | 88 +++++++++++++++ .../Views/UserProfileView.swift | 104 ++++++++++++++---- .../Views/UserRepositoryListView.swift | 11 +- 4 files changed, 179 insertions(+), 28 deletions(-) create mode 100644 ZarinPal-Challenge/ViewModels/UserProfileViewModel.swift diff --git a/ZarinPal-Challenge.xcodeproj/project.pbxproj b/ZarinPal-Challenge.xcodeproj/project.pbxproj index 2293a13..122fd06 100644 --- a/ZarinPal-Challenge.xcodeproj/project.pbxproj +++ b/ZarinPal-Challenge.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ F8D2FA14249E31C30029FD63 /* UserRepositoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA13249E31C30029FD63 /* UserRepositoryViewModel.swift */; }; F8D2FA16249E33A50029FD63 /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA15249E33A50029FD63 /* UIColor+Hex.swift */; }; F8D2FA19249E382F0029FD63 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA18249E382F0029FD63 /* ActivityIndicatorView.swift */; }; + F8D2FA1B249E64CD0029FD63 /* UserProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D2FA1A249E64CD0029FD63 /* UserProfileViewModel.swift */; }; F8E5888C2499A3070083BD93 /* UserRepositoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5888B2499A3070083BD93 /* UserRepositoryDetailView.swift */; }; F8E5888E2499B3130083BD93 /* UserRepositoryBranchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5888D2499B3130083BD93 /* UserRepositoryBranchListView.swift */; }; F8E588902499B3290083BD93 /* UserRepositoryIssueListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5888F2499B3290083BD93 /* UserRepositoryIssueListView.swift */; }; @@ -151,6 +152,7 @@ F8D2FA13249E31C30029FD63 /* UserRepositoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryViewModel.swift; sourceTree = ""; }; F8D2FA15249E33A50029FD63 /* UIColor+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Hex.swift"; sourceTree = ""; }; F8D2FA18249E382F0029FD63 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = ""; }; + F8D2FA1A249E64CD0029FD63 /* UserProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileViewModel.swift; sourceTree = ""; }; F8E5888B2499A3070083BD93 /* UserRepositoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryDetailView.swift; sourceTree = ""; }; F8E5888D2499B3130083BD93 /* UserRepositoryBranchListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryBranchListView.swift; sourceTree = ""; }; F8E5888F2499B3290083BD93 /* UserRepositoryIssueListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryIssueListView.swift; sourceTree = ""; }; @@ -441,6 +443,7 @@ F8D2FA0F249E2F180029FD63 /* AuthenticationViewModel.swift */, F8D2FA11249E31950029FD63 /* UserRepositoryListViewModel.swift */, F8D2FA13249E31C30029FD63 /* UserRepositoryViewModel.swift */, + F8D2FA1A249E64CD0029FD63 /* UserProfileViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -727,6 +730,7 @@ F85513C12499EC0C006F309D /* APIClientService.swift in Sources */, F85513D5249A1436006F309D /* Authentication.swift in Sources */, F85513C42499ED5B006F309D /* AppConfig.swift in Sources */, + F8D2FA1B249E64CD0029FD63 /* UserProfileViewModel.swift in Sources */, F82103BB2499697B00B1A211 /* AppDelegate.swift in Sources */, F8D2FA0C249E271C0029FD63 /* AppDIContainer.swift in Sources */, F86A2119249A498C00C8AE69 /* AuthenticationRepository.swift in Sources */, diff --git a/ZarinPal-Challenge/ViewModels/UserProfileViewModel.swift b/ZarinPal-Challenge/ViewModels/UserProfileViewModel.swift new file mode 100644 index 0000000..778b6e2 --- /dev/null +++ b/ZarinPal-Challenge/ViewModels/UserProfileViewModel.swift @@ -0,0 +1,88 @@ +// +// UserProfileViewModel.swift +// ZarinPal-Challenge +// +// Created by Farshad Mousalou on 6/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import Combine +import RxSwift + +class UserProfileViewModel : ObservableObject, Identifiable { + + + @Published var id: String = "" + @Published var name: String = "" + @Published var username: String = "" + @Published var email: String? = nil + @Published var avatarURL: URL? = nil + @Published var url: String? = nil + @Published var twitterUsername: String? = nil + @Published var bio: String? = nil + @Published var company: String? = nil + @Published var websiteURL: String? = nil + + @Published var state: State = .idle + @Published var error: Error? = nil + + enum State { + case idle + case loading + case loaded + case error + } + + let userProfileUseCases: GitHubUserProfileRepositoryUseCases + + let disposeBag = DisposeBag() + + init(userProfileUseCases: GitHubUserProfileRepositoryUseCases) { + self.userProfileUseCases = userProfileUseCases + fetchUserProfileAndMap() + } + + func fetchUserProfile() -> Observable { + userProfileUseCases.fetchUserProfile() + } + + func fetchUserProfileAndMap() { + state = .loading + fetchUserProfile() + .observeOn(MainScheduler.asyncInstance) + .subscribe {[weak self] (event) in + switch event { + case .next(let profile): + self?.assign(userProfile: profile) + case .error(let error): + self?.state = .error + self?.error = error + case .completed: + self?.state = .loaded + } + } + .disposed(by: disposeBag) + } + + /// <#Description#> + /// - Parameter userProfile: <#userProfile description#> + func assign(userProfile: UserProfile) { + name = userProfile.name ?? userProfile.username + username = "@"+userProfile.username + id = userProfile.id + email = userProfile.email + + if let imageURL = userProfile.avatarURL { + avatarURL = URL(string: imageURL) + } + + bio = userProfile.bio + company = userProfile.company + + websiteURL = userProfile.websiteURL + + } + + +} diff --git a/ZarinPal-Challenge/Views/UserProfileView.swift b/ZarinPal-Challenge/Views/UserProfileView.swift index cfdbfdf..ce919d4 100644 --- a/ZarinPal-Challenge/Views/UserProfileView.swift +++ b/ZarinPal-Challenge/Views/UserProfileView.swift @@ -7,39 +7,103 @@ // import SwiftUI +import struct Kingfisher.KFImage +import Combine struct UserProfileView: View { + + @ObservedObject var viewModel : UserProfileViewModel + + var body: some View { - ScrollView(.vertical, showsIndicators: true) { - VStack(alignment: .leading, spacing: 5.0) { - UserProfileHeaderView() - UserProfileDetailRowView(title: "Email", value: "farshadm90@gmail.com") - UserProfileDetailRowView(title:"work",value:"Digipay") - UserProfileDetailRowView(title: "website", value: "Google") - UserProfileDetailRowView() - UserProfileDetailRowView() - UserProfileDetailRowView() - } - .padding() + + view(forState: viewModel.state) + .navigationBarTitle("Profile") + } + + func view(forState state : UserProfileViewModel.State) -> some View { + + switch state { + case .idle: + return AnyView(EmptyView()) + case .loading: + return AnyView(ActivityIndicator(isAnimating: .constant(true), style: .medium)) + case .error: + return AnyView( + viewModel.error.map { error in + Group { + Text(error.localizedDescription) + .fontWeight(.medium) + .font(.system(.headline)) + .padding(.bottom,5) + + Button(action: { + self.viewModel.fetchUserProfileAndMap() + }) { + Text("Retry") + .foregroundColor(.white) + .font(.callout) + } + .padding(16) + .background(Color.blue) + .clipShape(Capsule()) + } + } + ) + case .loaded: + return AnyView( + ScrollView(.vertical, showsIndicators: true) { + + VStack(alignment: .leading, spacing: 5.0) { + UserProfileHeaderView(name: viewModel.name, username: viewModel.username,avatarURL: viewModel.avatarURL) + + UserProfileDetailRowView(title: "Email:", value: viewModel.email) + UserProfileDetailRowView(title: "Company:",value: viewModel.company) + UserProfileDetailRowView(title: "Website:", value: viewModel.websiteURL) + UserProfileDetailRowView(title: "Bio:", value: viewModel.bio) + UserProfileDetailRowView(title: "GitHub:", value: viewModel.url) + + } + .padding() + } + ) } - .navigationBarTitle("Profile") + } + } struct UserProfileHeaderView: View { + + @State var name: String = "" + @State var username: String = "" + @State var avatarURL: URL? = nil + var body: some View { HStack(alignment: .center, spacing: 8) { - Image(systemName: "person.circle.fill") + + KFImage(avatarURL,options: [.forceRefresh,.transition(.fade(0.3))]) .resizable() - .scaledToFit() - .frame(width: 70, height: 70, alignment: .topLeading) - .foregroundColor(.green) + .onSuccess { r in + print(r) + } + .placeholder { + Image(systemName: "person.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 75, height: 75, alignment: .topLeading) + .foregroundColor(.green) + .clipShape(Circle()) + } + .aspectRatio(contentMode: .fit) + .frame(width: 75, height: 75, alignment: .topLeading) + .clipShape(Circle()) VStack(alignment: .leading, spacing: 5) { - Text("Farshad Mousalou") + Text(name) .fontWeight(.medium) .font(.system(.headline)) - Text("@farshadmb") + Text(username) .fontWeight(.light) .font(.system(.subheadline)) Spacer() @@ -53,6 +117,7 @@ struct UserProfileHeaderView: View { struct UserProfileDetailRowView : View { + @State var title: String? = nil @State var value: String? = nil var body: some View { @@ -68,6 +133,7 @@ struct UserProfileDetailRowView : View { Text($0) .fontWeight(.regular) .font(.system(.footnote)) + .lineLimit(0) }) } Spacer() @@ -79,6 +145,6 @@ struct UserProfileDetailRowView : View { struct UserProfileView_Previews: PreviewProvider { static var previews: some View { - UserProfileView() + UserProfileView(viewModel: .init(userProfileUseCases: AppDIContainer.userProfileUseCases)) } } diff --git a/ZarinPal-Challenge/Views/UserRepositoryListView.swift b/ZarinPal-Challenge/Views/UserRepositoryListView.swift index 134f0e6..a9fd1a1 100644 --- a/ZarinPal-Challenge/Views/UserRepositoryListView.swift +++ b/ZarinPal-Challenge/Views/UserRepositoryListView.swift @@ -20,7 +20,8 @@ struct UserRepositoryListView: View { NavigationView { Group { - NavigationLink(destination: UserProfileView(),isActive: self.$selectProfile) { + NavigationLink(destination: UserProfileView(viewModel: UserProfileViewModel(userProfileUseCases: AppDIContainer.userProfileUseCases)), + isActive: self.$selectProfile) { EmptyView() } .hidden().frame(width: 0, height: 0.0, alignment: .center) @@ -87,14 +88,6 @@ struct UserRepositoryListView: View { } -struct TempView : View { - var body: some View { - NavigationLink(destination: UserProfileView()) { - Image(systemName: "person.circle").accentColor(.green) - } - } -} - struct UserRepositoryListView_Previews: PreviewProvider { static var previews: some View { UserRepositoryListView(viewModel: .init(repositoriesUseCase: AppDIContainer.userRepositoryUseCases))