20220126 iOS 일일 개발일지 === ###### tags: `develop` [TOC] ## 아키텍쳐 ![layer_1](https://i.imgur.com/MmUWhiz.png) ![layer_2](https://i.imgur.com/on6dXjR.png) ## 개발 ### Clean Architecture 적용과 관련해 구조적인 흐름을 동일하게 가져가기 현재 프로젝트가 가진 구조 ViewController - ViewModel - Usecase - Repository - service -> Entity의 구조로 진행 중임. Service Layer의 위치를 어디에 둘지에 대한 고민에 대해 현재는 Repository 내부에서 의존 관계를 가지도록 설계하였음. -> 실질적인 API 통신은 Repository에서 이뤄지고 있으며, 구현해야하는 service는 현재는 네트워크 서비스만 있는 상태. User Login State에 따라 LoginView가 뜰 수 있도록 해야 함. TabBarController에서 해당 부분에 대한 점검이 필요함. 하지만, TabBarController 내부적으로 모두 Navigation Controller에 의해 감싸진 상태 ```swift= if let naviVC = viewController as? UINavigationController, (naviVC.viewControllers.first as? MyPageViewController != nil) && isLogout { } ``` NavigationController에 대한 형 변환 및 특정 타입이 맞는지 점검을 진행했음. <details> <summary>해당 Delegate 메소드 전체 코드</summary> <div markdown="1"> ```swift= func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { // Test를 위한 변수 선언 let isLogout = true if let naviVC = viewController as? UINavigationController, (naviVC.viewControllers.first as? MyPageViewController != nil) && isLogout { let loginViewController = SigninViewController() let navigationViewController = UINavigationController(rootViewController: loginViewController) navigationViewController.modalPresentationStyle = .fullScreen tabBarController.present(navigationViewController, animated: true, completion: nil) return false } return true } ``` </div> </details> <details> <summary> clean architecture 적용을 위한 작성 중 코드</summary> <div markdown="1"> ```swift= struct UserDTO: Decodable { } protocol UserNetwork { func confirmUser(email: String, password: String, completion: @escaping (Result<UserDTO, Error>) -> Void) } final class DefaultNetworkService { static let shared = DefaultNetworkService() private init() { } func } extension DefaultNetworkService: UserNetwork { func confirmUser(email: String, password: String, completion: @escaping (Result<UserDTO, Error>) -> Void) { } } // MARK: Repository protocol UserRepositoryInterface { func confirm(userEmail: String, userPassword: String, completion: @escaping (Result<User, Error>) -> Void) } final class UserRepository: UserRepositoryInterface { private let service: UserNetwork init(_ service: UserNetwork) { self.service = service } func confirm(userEmail: String, userPassword: String, completion: @escaping (Result<User, Error>) -> Void) { self.service.confirmUser(email: userEmail, password: userPassword) { result in switch result { case .success(let userDTO): completion(.success(userDTO.toDomain())) case .failure() } } } } // MARK: UseCase protocol UserUseCaseInterface { func confirm(userEmail: String, userPassword: String, completion: @escaping (Result<User, Error>) -> Void) } final class UserUseCase: UserUseCaseInterface { private let repository: UserRepositoryInterface init(_ userRepository: UserRepositoryInterface) { self.repository = userRepository } func confirm(userEmail: String, userPassword: String, completion: @escaping (Result<User, Error>) -> Void) { repository.confirm(userEmail: userEmail, userPassword: userPassword) { <#Result<User, Error>#> in <#code#> } } } protocol LoginViewModelProperty { var usecase: UserUseCaseInterface { get } var emailMessage: Observable<String> { get set } var passwordMessage: Observable<String> { get set } } protocol LoginViewModelValidatable { func validateEmail(_ email: String) func validatePassword(_ password: String) } protocol LoginViewModel: LoginViewModelProperty, LoginViewModelValidatable { func confirmUser(email: String, password: String, completion: @escaping (Result<User, Error>) -> Void) } final class DefaultLoginViewModel: LoginViewModel { var usecase: UserUseCaseInterface var emailMessage: Observable<String> = Observable("") var passwordMessage: Observable<String> = Observable("") var isLoginAvailable: Observable<Bool> = Observable(false) init(usecase: UserUseCaseInterface = ) { self.usecase = usecase } func validateEmail(_ email: String) { if ValidationHelper.validate(email: email) { self.emailMessage.value = ValidationHelper.State.valid.description } else { self.emailMessage.value = ValidationHelper.State.emailFormatInvalid.description } } func validatePassword(_ password: String) { if ValidationHelper.validate(password: password) { self.passwordMessage.value = ValidationHelper.State.valid.description } else { self.passwordMessage.value = ValidationHelper.State.passwordFormatInvalid.description } } func confirmUser(email: String, password: String, completion: @escaping (Result<User, Error>) -> Void) { // usecase.confirm(userEmail: email, userPassword: password) { [weak self] result in // switch result { // case .success(let user): // completion(.success(user)) // case .failure(<#T##Error#>) // } } } final class LoginViewController: BaseDIViewController<LoginViewModel> { override init(_ viewModel: LoginViewModel = DefaultLoginViewModel(usecase: <#UserUseCaseInterface#>)) { super.init(viewModel) } } ``` </div> </details> <details> <summary> ItemView, ViewController로부터 View를 분리(ItemView.swift) </summary> <div markdown="1"> ```swift= // // ItemView.swift // Cream // // Created by wankikim-MN on 2022/01/19. // import UIKit import SnapKit class ItemView: UIView { lazy var ItemInfoListView: UICollectionView = { let cv = UICollectionView(frame: .zero, collectionViewLayout: configureCollectionViewLayout()) return cv }() lazy var buyButton: TradeButton = { let button = TradeButton(tradeType: .buy) return button }() lazy var sellButton: TradeButton = { let button = TradeButton(tradeType: .sell) return button }() lazy var wishButton: VerticalButton = { let button = VerticalButton.init(frame: .zero) button.imageView?.image = UIImage(systemName: "bookmark") button.titleLabel?.text = "찜 목록 개수" return button }() lazy var tradeStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [buyButton, sellButton]) stackView.axis = .horizontal stackView.distribution = .fillEqually stackView.spacing = 5 return stackView }() lazy var containerStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [wishButton, tradeStackView]) stackView.axis = .horizontal stackView.spacing = 10 return stackView }() override init(frame: CGRect) { super.init(frame: frame) applyViewSettings() } required init?(coder: NSCoder) { super.init(coder: coder) applyViewSettings() } } // MARK: CollectionViewLayout extension ItemView { func configureCollectionViewLayout() -> UICollectionViewCompositionalLayout { return UICollectionViewCompositionalLayout { (section, env) -> NSCollectionLayoutSection? in switch section { case 0: return self.configureImageSectionLayout() case 1: return self.configureItemInfoSectionLayout() case 2: return self.configureReleaseSectionLayout() case 3: return self.configureDeliverySectionLayout() case 4: return self.configureAdvertiseSectionLayout() case 5: return self.configurePriceChartSectionLayout() default: return self.configureSimilarItemSectionLayout() } } } private func configureImageSectionLayout() -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(1)) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets.bottom = 15 let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(1)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) group.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0) let section = NSCollectionLayoutSection(group: group) section.orthogonalScrollingBehavior = .groupPaging return section } private func configureItemInfoSectionLayout() -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.52)) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets.bottom = 15 let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.52)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) group.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0) let section = NSCollectionLayoutSection(group: group) section.orthogonalScrollingBehavior = .groupPaging return section } private func configureReleaseSectionLayout() -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.4), heightDimension: .fractionalWidth(0.3)) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets.bottom = 15 let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.35)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) group.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0) let section = NSCollectionLayoutSection(group: group) section.orthogonalScrollingBehavior = .groupPaging return section } private func configureDeliverySectionLayout() -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.3)) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets.bottom = 0 let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.3)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) group.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0) let section = NSCollectionLayoutSection(group: group) section.orthogonalScrollingBehavior = .groupPaging return section } private func configureAdvertiseSectionLayout() -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.256)) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets.bottom = 0 let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.256)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) group.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0) let section = NSCollectionLayoutSection(group: group) section.orthogonalScrollingBehavior = .groupPaging return section } private func configurePriceChartSectionLayout() -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(1.2)) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets.bottom = 15 let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(1.2)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) group.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0) let section = NSCollectionLayoutSection(group: group) section.orthogonalScrollingBehavior = .groupPaging return section } private func configureSimilarItemSectionLayout() -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.4), heightDimension: .fractionalHeight(0.464)) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets = .init(top: 0, leading: 0, bottom: 5, trailing: 5) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(464)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) section.orthogonalScrollingBehavior = .groupPaging section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 5, bottom: 20, trailing: 0) section.boundarySupplementaryItems = [ NSCollectionLayoutBoundarySupplementaryItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(40)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading) ] return section } } extension ItemView: ViewConfiguration { func buildHierarchy() { self.addSubviews(ItemInfoListView, containerStackView) } func setupConstraints() { ItemInfoListView.snp.makeConstraints { // $0.top.equalTo(self.snp.top) // $0.leading.equalTo(self.snp.leading) // $0.trailing.equalTo(self.snp.trailing) $0.top.leading.trailing.equalToSuperview() $0.bottom.equalTo(self.containerStackView.snp.top) } containerStackView.snp.makeConstraints { $0.leading.trailing.bottom.equalToSuperview() } } func viewConfigure() { collectionViewConfigure() } func collectionViewConfigure() { ItemInfoListView.register(ItemInfoCell.self, forCellWithReuseIdentifier: ItemInfoCell.reuseIdentifier) ItemInfoListView.register(ReleaseInfoCell.self, forCellWithReuseIdentifier: ReleaseInfoCell.reuseIdentifier) ItemInfoListView.register(ShopBannerCell.self, forCellWithReuseIdentifier: ShopBannerCell.reuseIdentifier) ItemInfoListView.register(HomeViewItemCell.self, forCellWithReuseIdentifier: HomeViewItemCell.reuseIdentifier) } } ``` </div> </details> <br> > Network Layer 고려사항. **Clean Architecture**을 활용할 경우, DIP를 준수하여 network에 대해서도 testable하도록 설계가 가능해보임. 고려사항 - Network를 기능별로 나누어 사용하기 편리하도록 설계하기. 참고 링크: https://ios-development.tistory.com/712