20220126 iOS 일일 개발일지
===
###### tags: `develop`
[TOC]
## 아키텍쳐


## 개발
### 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