From 951e04fb962f3df51d9aa7e74ec71edae642f29a Mon Sep 17 00:00:00 2001 From: hyerin Date: Mon, 3 Jul 2023 12:12:09 +0900 Subject: [PATCH] Squashed commit of the following: develop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 7da14c6c3ff5bc36cc54257baed27ef57bffc7aa Author: chansooo <89574881+chansooo@users.noreply.github.com> Date: Fri Jun 30 00:04:13 2023 +0900 Feature/sign up (#59) * feat: DesignSystemType case chip 추가 * feat: NavigationBar 구현 * feat: Navigationdemo 구현 * fix: NavigationDemoViewController 이름 변경 * fix: NavigationBar titleLabel 중앙 정렬 수정 * feat: NavigationColorType 구현 두가지 case로 나눠서 각각 tintcolor와 backgroundcolor 설정할 수 있도록 함 * feat: 버튼 tintcolor 변경 위해 렌더링 모드 변경 * add: `MOITTabPager` 생성 * add: `MOITTabPager` 생성 * remove: dummy file 삭제 * feat: MOITChipType 구현 * feat: MOITChip 구현 * feat: MOITChipDemoViewController 구현 * feat: DemoViewController 연결 * feat: PagerType 구현 * feat: TapPager 구현 * feat: SegmentPager 구현 * feat: PagerDemoViewController 추가 * fix: unavailable 추가 * feat: DemoApp Chip 폴더 추가 * fix: enum case AssociatedValue 사용하지 않는 부분 삭제 * feat: StudyPreview 구현중 * feat: 스와이프 -> 삭제 버튼 애니메이션 구현 * add: secondary color set 추가 * remove: 필요 없는 파일 삭제 * feat: optinal인 view addItem 쉽게 할 수 있는 메소드 구현 * refactor: 스와이프, 탭 부드럽게 변경 * fix: StudyPreview UI 변경 * feat: 버튼 누를 수 있는 뷰 추가 * fix: StudyPreviewDemoViewController 수정 * refactor: StudyPreview UI Center 맞게 수정 * refactor: panGesture 로직 변경 * refactor: final 추가 * redactor: `layoutIfNeeded` 삭제 * feat: SignUp 기본 세팅 * feat: Utils Target 추가 * feat: SignUp 접근 제한자 변경 * feat: Utils 타겟에 pinlayout, flexlayout 추가 * feat: BaseViewController 상속받을 경우 navigateBar 설정 편리하게 구현 * feat: StudyPreview configure 함수 구현 * feat: ProfileView image 없이 생성, configureImage 함수 추가 * refactor: 린트 맞게 코드 수정 * fix: navigationBar와 flexRootView 겹치지 않게 수정 * feat: scrollView 자동 설정 * feat: 탭 시 키보드 사라지도록 구현 * feat: SignUpViewController 구현 * feat: 다음 버튼 추가 * refactor: `MOITProfileView` 이미지 로드 로직 변경 * feat: NavigationBar logo 추가 * feat: ViewControllable extension 추가 * feat: StudyList shortcut 추가 * feat: dependency 수정 * feat: SignUp 관련 UseCase 추가 * feat: SignUpRepository 임시 추가 * fix: `MOITProfileView` border 추가 * fix: SignUpDemoApp RIBs 기반으로 생성하도록 수정 * fix: DemoApp Dependency 설정할 수 있도록 수정 * fix: DemoApp Dependency 설정 * feat: SignUpDependency 추가 * chore: 주석 삭제 및 코드 스타일 수정 * feat: SignUp 비즈니스 로직 수정 * fix: interactor.activate 추가 * profileselect * fix: baseView flexLayout 사용에 맞게 수정 * fix: `ProfileImageType` 참조할 수 있도록 수정 * feat: uikit 관련 extension 추가 * fix: bottomsheetViewController 탭 contentView 외부에서 작동하도록 수정 * fix: MOITProfileView tap 작동방식 변경 * feat: ProfileSelectView 구현 * feat: ProfileSelectViewController 구현 * feat: ProfileSelectInteractor 구현 * fix: ProfileSelect RIBlet 생성 시 현재 이미지 인덱스 전달 * fix: SignUpDemoApp 수정 * feat: SignUpInteractor 로직 구현 * feat: SignUpRouter 구현 * feat: SignUpViewController 프로필 이미지 업데이트 구현 * fix: imageIndex 정보 함께 넘기도록 수정 * fix: 이미지 인덱스 파라미터 추가 * chore: 주석 삭제 * fix: baseViewController에 removeObserver 추가 * chore: print문 삭제 --------- Co-authored-by: hyerin Co-authored-by: chansooooo commit 8bdabbd4cf25073a243df6de3c72670126eefa23 Author: hyerin Date: Thu Jun 29 22:26:02 2023 +0900 Fix/network layer fix (#55) * feat: Response 모델 선언 * feat: Common Error 추가 * feat: NetworkImpl 수정 * feat: 옵셔널 대응 commit 74b0f885ac910eec0c88e49587c0d0edefbd009a Author: hyerin Date: Tue Jun 27 10:09:22 2023 +0900 Feature/adjust line height (#58) * feat: font 타입 별 line height return 해주는 메서드 구현 * feat: ParagraphStyle 적용하는 UILabel Extension 구현 commit e256b51c6356eb8a3085b9b1836ec0855d38e373 Author: hyerin Date: Mon Jun 26 11:38:29 2023 +0900 Fix/design system fix (#56) * fix: Chip Configure 메서드 및 Convenience init 추가 * fix: MOITTextField title 없는 경우 대응 * fix: List에 MOITProfileView 적용 * fix: List StudyOrder 및 Fine Formatter 적용 * fix: Lint 수정 --- Core/Project.swift | 27 ++- Core/Utils/Base/BaseView.swift | 49 +++++ Core/Utils/Base/BaseViewController.swift | 122 +++++++++++++ Core/Utils/RIBs/ViewControllable+.swift | 77 ++++++++ ...aptivePresentationControllerDelegate.swift | 20 ++ Core/Utils/UIKit/UIViewController+.swift | 23 +++ .../Chip/MOITChipDemoViewController.swift | 19 ++ .../MOITTextFieldDemoViewController.swift | 6 + .../List/MOITListDemoViewController.swift | 14 +- .../NavigationDemoViewController.swift | 2 +- .../Profile/ProfileDemoViewController.swift | 10 +- .../StudyPreviewDemoViewController.swift | 15 +- .../BottomSheetViewController.swift | 23 +-- DesignSystem/Sources/Chip/MOITChip.swift | 52 +++++- DesignSystem/Sources/Chip/MOITChipType.swift | 2 +- DesignSystem/Sources/Common/FlexLayout+.swift | 9 +- .../Sources/Input/MOITTextField.swift | 38 ++-- DesignSystem/Sources/List/MOITList.swift | 113 +++++++++--- .../NavigationBar/MOITNavigationBar.swift | 63 +++++-- .../NavigationBar/NavigationColorType.swift | 1 - .../NavigationBar/NavigationItem.swift | 6 +- .../NavigationBar/NavigationItemType.swift | 4 +- .../Sources/Profile/MOITProfileView.swift | 41 +++-- .../Sources/Profile/ProfileType.swift | 35 +++- .../StudyPreview/MOITStudyPreview.swift | 55 ++++-- .../StudyPreview/TouchThroughView.swift | 4 +- .../Sources/TapPager/MOITSegmentPager.swift | 2 +- .../Sources/TapPager/MOITTabPager.swift | 2 +- .../Sources/TapPager/TapPagerType.swift | 1 - .../SignUp/SignUpData/Implement/dummy.swift | 4 + .../SignUpData/Interface/JoinRepository.swift | 15 ++ .../SignUp/SignUpData/Interface/dummy.swift | 4 + Features/SignUp/SignUpData/Project.swift | 22 +++ Features/SignUp/SignUpData/Tests/dummy.swift | 4 + .../FetchRandomNumberUseCaseImpl.swift | 21 +++ .../Implement/PostJoinInfoUseCaseImpl.swift | 31 ++++ .../Interface/FetchRandomNumberUseCase.swift | 13 ++ .../Interface/PostJoinInfoUseCase.swift | 15 ++ Features/SignUp/SignUpDomain/Project.swift | 22 +++ .../SignUp/SignUpDomain/Tests/dummy.swift | 4 + .../DemoApp/Resources/LaunchScreen.storyboard | 26 +++ .../DemoApp/Sources/MockComponent.swift | 42 +++++ .../SignUpUserInterfaceAppDelegate.swift | 39 ++++ .../ProfileSelect/ProfileSelectBuilder.swift | 31 ++++ .../ProfileSelectInteractor.swift | 56 ++++++ .../ProfileSelect/ProfileSelectRouter.swift | 28 +++ .../ProfileSelect/ProfileSelectView.swift | 121 ++++++++++++ .../ProfileSelectViewController.swift | 85 +++++++++ .../Implement/SignUpBuilder.swift | 44 +++++ .../Implement/SignUpInteractor.swift | 172 ++++++++++++++++++ .../Implement/SignUpRouter.swift | 68 +++++++ .../Implement/SignUpViewController.swift | 169 +++++++++++++++++ .../ProfileSelectBuildable.swift | 13 ++ .../ProfileSelectDependency.swift | 13 ++ .../ProfileSelect/ProfileSelectListener.swift | 15 ++ .../Interface/SignUpBuildable.swift | 15 ++ .../Interface/SignUpDependency.swift | 20 ++ .../Interface/SignUpListener.swift | 15 ++ .../SignUp/SignUpUserInterface/Project.swift | 39 ++++ .../SignUpUserInterface/Tests/dummy.swift | 3 + MOITNetwork/Implement/NetworkImpl.swift | 38 ++-- MOITNetwork/Interface/NetworkError.swift | 6 +- MOITNetwork/Interface/Response.swift | 20 ++ .../Dependency+Project.swift | 59 ++++-- .../Icon.xcassets/logo.imageset/Contents.json | 12 ++ .../Icon.xcassets/logo.imageset/logo.svg | 9 + ResourceKit/Sources/Font.swift | 45 ++++- Scripts/SwiftLintRunScript.sh | 2 +- .../Project+Templates.swift | 3 +- 69 files changed, 2013 insertions(+), 185 deletions(-) create mode 100644 Core/Utils/Base/BaseView.swift create mode 100644 Core/Utils/Base/BaseViewController.swift create mode 100644 Core/Utils/RIBs/ViewControllable+.swift create mode 100644 Core/Utils/UIKit/AdaptivePresentationControllerDelegate.swift create mode 100644 Core/Utils/UIKit/UIViewController+.swift create mode 100644 Features/SignUp/SignUpData/Implement/dummy.swift create mode 100644 Features/SignUp/SignUpData/Interface/JoinRepository.swift create mode 100644 Features/SignUp/SignUpData/Interface/dummy.swift create mode 100644 Features/SignUp/SignUpData/Project.swift create mode 100644 Features/SignUp/SignUpData/Tests/dummy.swift create mode 100644 Features/SignUp/SignUpDomain/Implement/FetchRandomNumberUseCaseImpl.swift create mode 100644 Features/SignUp/SignUpDomain/Implement/PostJoinInfoUseCaseImpl.swift create mode 100644 Features/SignUp/SignUpDomain/Interface/FetchRandomNumberUseCase.swift create mode 100644 Features/SignUp/SignUpDomain/Interface/PostJoinInfoUseCase.swift create mode 100644 Features/SignUp/SignUpDomain/Project.swift create mode 100644 Features/SignUp/SignUpDomain/Tests/dummy.swift create mode 100644 Features/SignUp/SignUpUserInterface/DemoApp/Resources/LaunchScreen.storyboard create mode 100644 Features/SignUp/SignUpUserInterface/DemoApp/Sources/MockComponent.swift create mode 100644 Features/SignUp/SignUpUserInterface/DemoApp/Sources/SignUpUserInterfaceAppDelegate.swift create mode 100644 Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectBuilder.swift create mode 100644 Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectInteractor.swift create mode 100644 Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectRouter.swift create mode 100644 Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectView.swift create mode 100644 Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectViewController.swift create mode 100644 Features/SignUp/SignUpUserInterface/Implement/SignUpBuilder.swift create mode 100644 Features/SignUp/SignUpUserInterface/Implement/SignUpInteractor.swift create mode 100644 Features/SignUp/SignUpUserInterface/Implement/SignUpRouter.swift create mode 100644 Features/SignUp/SignUpUserInterface/Implement/SignUpViewController.swift create mode 100644 Features/SignUp/SignUpUserInterface/Interface/ProfileSelect/ProfileSelectBuildable.swift create mode 100644 Features/SignUp/SignUpUserInterface/Interface/ProfileSelect/ProfileSelectDependency.swift create mode 100644 Features/SignUp/SignUpUserInterface/Interface/ProfileSelect/ProfileSelectListener.swift create mode 100644 Features/SignUp/SignUpUserInterface/Interface/SignUpBuildable.swift create mode 100644 Features/SignUp/SignUpUserInterface/Interface/SignUpDependency.swift create mode 100644 Features/SignUp/SignUpUserInterface/Interface/SignUpListener.swift create mode 100644 Features/SignUp/SignUpUserInterface/Project.swift create mode 100644 Features/SignUp/SignUpUserInterface/Tests/dummy.swift create mode 100644 MOITNetwork/Interface/Response.swift create mode 100644 ResourceKit/Resources/Icon.xcassets/logo.imageset/Contents.json create mode 100644 ResourceKit/Resources/Icon.xcassets/logo.imageset/logo.svg diff --git a/Core/Project.swift b/Core/Project.swift index a78a8381..e480e914 100644 --- a/Core/Project.swift +++ b/Core/Project.swift @@ -9,11 +9,22 @@ import ProjectDescription import ProjectDescriptionHelpers import UtilityPlugin -let project = Project(name: "Core", - targets: [ - Project.makeTarget( - name: "CSLogger", - dependencies: [ - ] - ), - ]) +let project = Project( + name: "Core", + targets: [ + Project.makeTarget( + name: "CSLogger", + dependencies: [] + ), + Project.makeTarget( + name: "Utils", + dependencies: [ + .DesignSystem, + .ResourceKit, + + .ThirdParty.PinLayout, + .ThirdParty.FlexLayout, + ] + ), + ] +) diff --git a/Core/Utils/Base/BaseView.swift b/Core/Utils/Base/BaseView.swift new file mode 100644 index 00000000..de60972c --- /dev/null +++ b/Core/Utils/Base/BaseView.swift @@ -0,0 +1,49 @@ +// +// BaseView.swift +// Utils +// +// Created by 김찬수 on 2023/06/14. +// + +import UIKit + +import RxSwift +import FlexLayout +import PinLayout + +open class BaseView: UIView { + + // MARK: - UI + public let flexRootView = UIView() + + // MARK: - Properties + public var disposebag = DisposeBag() + + // MARK: - Methods + public override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = .white + configureAttributes() + configureHierarchy() + configureConstraints() + bind() + } + + open func configureHierarchy() { + self.addSubview(flexRootView) + } + open func configureConstraints() {} + open func configureAttributes() {} + open func bind() {} + + open override func layoutSubviews() { + flexRootView.pin.all() + flexRootView.flex.layout(mode: .adjustHeight) + } + + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("init(coder:) is called.") + } +} diff --git a/Core/Utils/Base/BaseViewController.swift b/Core/Utils/Base/BaseViewController.swift new file mode 100644 index 00000000..906366fe --- /dev/null +++ b/Core/Utils/Base/BaseViewController.swift @@ -0,0 +1,122 @@ +// +// BaseViewController.swift +// Utils +// +// Created by 김찬수 on 2023/06/14. +// + +import UIKit + +import DesignSystem + +import RxSwift +import PinLayout +import FlexLayout + +open class BaseViewController: UIViewController { + + // MARK: - UI + public let scrollView = UIScrollView() + public let flexRootView = UIView() + public lazy var navigationBar = MOITNavigationBar() + + // MARK: - Properties + public var disposebag = DisposeBag() + + private var indicator: UIActivityIndicatorView? + + // MARK: - Initializer + public init() { + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required public init(coder: NSCoder) { + fatalError("init(coder:) is called.") + } + + // MARK: - LifeCycle + open override func viewDidLoad() { + super.viewDidLoad() + + self.view.backgroundColor = .white + + configureAttributes() + configureHierarchy() + configureConstraints() + bind() + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) + } + + open override func viewDidDisappear(_ animated: Bool) { + NotificationCenter.default.removeObserver(UIResponder.keyboardWillShowNotification) + NotificationCenter.default.removeObserver(UIResponder.keyboardWillHideNotification) + } + + @objc func keyboardWillShow(notification: NSNotification) { + if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { + scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0) + } + } + + @objc func keyboardWillHide(notification: NSNotification) { + scrollView.contentInset = UIEdgeInsets.zero + } + + open override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + scrollView.pin.all(view.pin.safeArea) + flexRootView.pin.all() + flexRootView.flex.layout(mode: .adjustHeight) + scrollView.contentSize = flexRootView.frame.size + } + // MARK: - Methods + /// addSubView 하는 함수 + public func configureHierarchy() { + self.scrollView.addSubview(flexRootView) + self.view.addSubview(scrollView) + } + /// layout 잡는 함수 + open func configureConstraints() {} + /// function to set the value + open func configureAttributes() { + hideKeyboardWhenTapped() + } + /// Function to bind. + open func bind() {} + + /// navigationBar 사용하면 flexRootView에 추가해주는 함수 + /// `viewDidLayoutSubviews`에 추가해야함 + public func configureNavigationBar( + leftItems: [NavigationItemType], + title: String?, + rightItems: [NavigationItemType] + ) { + navigationBar.configure( + leftItems: leftItems, + title: title, + rightItems: rightItems + ) + + view.addSubview(navigationBar) + + navigationBar.pin.top(self.view.pin.safeArea.top).horizontally() + configureRootView() + + guard let back = navigationBar.leftItems?.first(where: { $0.type == .back }) else { return } + back.rx.tap + .subscribe(onNext: { [weak self] in + self?.navigationController?.popViewController(animated: true) + }) + .disposed(by: disposebag) + } + + private func configureRootView() { + self.scrollView.pin.top(self.view.pin.safeArea.top + 56).left().right().bottom() // How to solve it other than absolute value..? + // scrollview에 딱 맞게 flexRootView를 pinlayout으로 설정 + self.flexRootView.pin.all() + self.flexRootView.flex.layout() + self.flexRootView.flex.markDirty() + } +} diff --git a/Core/Utils/RIBs/ViewControllable+.swift b/Core/Utils/RIBs/ViewControllable+.swift new file mode 100644 index 00000000..bc585dea --- /dev/null +++ b/Core/Utils/RIBs/ViewControllable+.swift @@ -0,0 +1,77 @@ +// +// ViewControllable+.swift +// CSLogger +// +// Created by 김찬수 on 2023/06/16. +// + +import UIKit + +import RIBs + +public final class NavigationControllerable: ViewControllable { + + public var uiviewController: UIViewController { self.navigationController } + public let navigationController: UINavigationController + + public init(root: ViewControllable) { + let navigation = UINavigationController(rootViewController: root.uiviewController) + navigation.navigationBar.isTranslucent = false + navigation.navigationBar.backgroundColor = .white + navigation.navigationBar.scrollEdgeAppearance = navigation.navigationBar.standardAppearance + + self.navigationController = navigation + } +} + +public extension ViewControllable { + + func present(_ viewControllable: ViewControllable, animated: Bool, completion: (() -> Void)?) { + self.uiviewController.present(viewControllable.uiviewController, animated: animated, completion: completion) + } + + func dismiss(completion: (() -> Void)?) { + self.uiviewController.dismiss(animated: true, completion: completion) + } + + func pushViewController(_ viewControllable: ViewControllable, animated: Bool) { + if let nav = self.uiviewController as? UINavigationController { + nav.pushViewController(viewControllable.uiviewController, animated: animated) + } else { + self.uiviewController.navigationController?.pushViewController(viewControllable.uiviewController, animated: animated) + } + } + + func popViewController(animated: Bool) { + if let nav = self.uiviewController as? UINavigationController { + nav.popViewController(animated: animated) + } else { + self.uiviewController.navigationController?.popViewController(animated: animated) + } + } + + func popToRoot(animated: Bool) { + if let nav = self.uiviewController as? UINavigationController { + nav.popToRootViewController(animated: animated) + } else { + self.uiviewController.navigationController?.popToRootViewController(animated: animated) + } + } + + func setViewControllers(_ viewControllerables: [ViewControllable]) { + if let nav = self.uiviewController as? UINavigationController { + nav.setViewControllers(viewControllerables.map(\.uiviewController), animated: true) + } else { + self.uiviewController.navigationController?.setViewControllers(viewControllerables.map(\.uiviewController), animated: true) + } + } + + var topViewControllable: ViewControllable { + var top: ViewControllable = self + + while let presented = top.uiviewController.presentedViewController as? ViewControllable { + top = presented + } + return top + } +} diff --git a/Core/Utils/UIKit/AdaptivePresentationControllerDelegate.swift b/Core/Utils/UIKit/AdaptivePresentationControllerDelegate.swift new file mode 100644 index 00000000..8c1da2ff --- /dev/null +++ b/Core/Utils/UIKit/AdaptivePresentationControllerDelegate.swift @@ -0,0 +1,20 @@ +// +// AdaptivePresentationControllerDelegate.swift +// Utils +// +// Created by 김찬수 on 2023/06/26. +// + +import UIKit + +public protocol AdaptivePresentationControllerDelegate: AnyObject { + func presentationControllerDidDismiss() +} + +public final class AdaptivePresentationControllerDelegateProxy: NSObject, UIAdaptivePresentationControllerDelegate { + public weak var delegate: AdaptivePresentationControllerDelegate? + + public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + delegate?.presentationControllerDidDismiss() + } +} diff --git a/Core/Utils/UIKit/UIViewController+.swift b/Core/Utils/UIKit/UIViewController+.swift new file mode 100644 index 00000000..1d8b9408 --- /dev/null +++ b/Core/Utils/UIKit/UIViewController+.swift @@ -0,0 +1,23 @@ +// +// UIViewController+.swift +// Utils +// +// Created by 김찬수 on 2023/06/15. +// + +import UIKit + +extension UIViewController { + + func hideKeyboardWhenTapped() { + let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + tap.cancelsTouchesInView = false + // 기본값이 true이면 제스쳐 발동시 터치 이벤트가 뷰로 전달x + // 즉 제스쳐가 동작하면 뷰의 터치이벤트는 발생하지 않는것 false면 둘 다 작동한다는 뜻 + view.addGestureRecognizer(tap) + } + + @objc func dismissKeyboard() { + self.view.endEditing(true) + } +} diff --git a/DesignSystem/DemoApp/Sources/Chip/MOITChipDemoViewController.swift b/DesignSystem/DemoApp/Sources/Chip/MOITChipDemoViewController.swift index 8b8f2757..fc85a960 100644 --- a/DesignSystem/DemoApp/Sources/Chip/MOITChipDemoViewController.swift +++ b/DesignSystem/DemoApp/Sources/Chip/MOITChipDemoViewController.swift @@ -16,6 +16,8 @@ import PinLayout public final class MOITChipDemoViewController: UIViewController { private let flexRootContainer = UIView() + private let defaultChip = MOITChip() + private let secondDefaultChip = MOITChip() override public func viewDidLoad() { super.viewDidLoad() @@ -30,6 +32,17 @@ public final class MOITChipDemoViewController: UIViewController { flexRootContainer.flex.layout(mode: .adjustWidth) } + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in + self?.defaultChip.setType(to: .dueDate(date: 10)) + self?.secondDefaultChip.setType(to: .attend) + self?.view.flex.markDirty() + self?.view.setNeedsLayout() + } + } + private func configure() { view.addSubview(flexRootContainer) view.backgroundColor = . white @@ -56,6 +69,12 @@ public final class MOITChipDemoViewController: UIViewController { flex.addItem(finishChip()) .margin(10) + + flex.addItem(defaultChip) + .margin(10) + + flex.addItem(secondDefaultChip) + .margin(10) } } diff --git a/DesignSystem/DemoApp/Sources/Input/MOITTextFieldDemoViewController.swift b/DesignSystem/DemoApp/Sources/Input/MOITTextFieldDemoViewController.swift index a0c6decd..8774be73 100644 --- a/DesignSystem/DemoApp/Sources/Input/MOITTextFieldDemoViewController.swift +++ b/DesignSystem/DemoApp/Sources/Input/MOITTextFieldDemoViewController.swift @@ -21,6 +21,11 @@ final class MOITTextFieldDemoViewController: UIViewController { placeHolder: "비어있는 텍스트 필드 테스트입니다" ) private let label = UILabel() + + private let noTitleTextField = MOITTextField( + title: nil, + placeHolder: "제목이 없는 텍스트 필드 테스트입니다." + ) private let disposeBag = DisposeBag() @@ -49,6 +54,7 @@ final class MOITTextFieldDemoViewController: UIViewController { .define { flex in flex.addItem(emptyTextField) flex.addItem(label).height(20).marginTop(30) + flex.addItem(noTitleTextField).marginTop(30) } } diff --git a/DesignSystem/DemoApp/Sources/List/MOITListDemoViewController.swift b/DesignSystem/DemoApp/Sources/List/MOITListDemoViewController.swift index 9946ba79..2291414e 100644 --- a/DesignSystem/DemoApp/Sources/List/MOITListDemoViewController.swift +++ b/DesignSystem/DemoApp/Sources/List/MOITListDemoViewController.swift @@ -23,7 +23,7 @@ final class MOITListDemoViewController: UIViewController { label.text = "0번 탭 됐음!" return label }() - + private let disposeBag = DisposeBag() override func viewDidLoad() { @@ -71,7 +71,7 @@ final class MOITListDemoViewController: UIViewController { private func allAttendList() -> MOITList { MOITList( type: .allAttend, - image: DesignSystemDemoAppAsset.profileImage.image, + imageUrlString: "https://github.com/hope1053.png", title: "가나다라마바사아자차카타", detail: "17:02", chipType: .attend @@ -93,6 +93,7 @@ final class MOITListDemoViewController: UIViewController { title: "가나다라마바사아자차카타", detail: "15,000원", chipType: .late, + studyOrder: 3, button: MOITButton( type: .mini, title: "납부 인증하기", @@ -121,24 +122,25 @@ final class MOITListDemoViewController: UIViewController { type: .sendMoney, title: "가나다라마바사아자차카타", detail: "15,000원", - chipType: .late + chipType: .late, + studyOrder: 3 ) } private func myMoneyList() -> MOITList { MOITList( type: .myMoney, - title: "3차스터디", detail: "2023.04.21 17:02", chipType: .late, - fine: "+ 12,000원" + studyOrder: 3, + fine: 12000 ) } private func peopleList() -> MOITList { MOITList( type: .people, - image:DesignSystemDemoAppAsset.profileImage.image, + imageUrlString: "https://github.com/hope1053.png", title: "가나다라마바사아자차카타" ) } diff --git a/DesignSystem/DemoApp/Sources/NavigationBar/NavigationDemoViewController.swift b/DesignSystem/DemoApp/Sources/NavigationBar/NavigationDemoViewController.swift index f78a709a..295b7df0 100644 --- a/DesignSystem/DemoApp/Sources/NavigationBar/NavigationDemoViewController.swift +++ b/DesignSystem/DemoApp/Sources/NavigationBar/NavigationDemoViewController.swift @@ -44,7 +44,7 @@ final class MOITNavigationDemoViewController: UIViewController { self.configureLayouts() self.navigationController?.navigationItem.title = "MOITNavigationBar" self.view.backgroundColor = .white - self.moitNavigationBar.leftItems[0].rx.tap.subscribe(onNext: { [weak self] in + self.moitNavigationBar.leftItems?[0].rx.tap.subscribe(onNext: { [weak self] in self?.navigationController?.popViewController(animated: true) }).disposed(by: self.disposeBag) } diff --git a/DesignSystem/DemoApp/Sources/Profile/ProfileDemoViewController.swift b/DesignSystem/DemoApp/Sources/Profile/ProfileDemoViewController.swift index 26aa5e2b..94bd9ac7 100644 --- a/DesignSystem/DemoApp/Sources/Profile/ProfileDemoViewController.swift +++ b/DesignSystem/DemoApp/Sources/Profile/ProfileDemoViewController.swift @@ -17,21 +17,23 @@ final class ProfileDemoViewController: UIViewController { // MARK: - UI private let largeProfileView = MOITProfileView( - urlString: "https://avatars.githubusercontent.com/u/15011638?s=64&v=4", + profileImageType: .one, profileType: .large, addButton: true ) private let mediumProfileView = MOITProfileView( - urlString: "https://avatars.githubusercontent.com/u/15011638?s=64&v=4", + profileImageType: .one, profileType: .medium ) private let smallProfileView = MOITProfileView( - urlString: "https://avatars.githubusercontent.com/u/15011638?s=64&v=4", + profileImageType: .one, profileType: .small ) + private lazy var lazyProfileView = MOITProfileView(profileType: .large) + private let flexRootView = UIView() // MARK: - Properties @@ -40,6 +42,7 @@ final class ProfileDemoViewController: UIViewController { // MARK: - Initializers public init() { super.init(nibName: nil, bundle: nil) + lazyProfileView.configureImage(with: .one) } @available(*, unavailable) @@ -75,6 +78,7 @@ final class ProfileDemoViewController: UIViewController { flex.addItem(largeProfileView) flex.addItem(mediumProfileView) flex.addItem(smallProfileView) + flex.addItem(lazyProfileView) } } diff --git a/DesignSystem/DemoApp/Sources/StudyPreview/StudyPreviewDemoViewController.swift b/DesignSystem/DemoApp/Sources/StudyPreview/StudyPreviewDemoViewController.swift index 35625b8b..6431a5b1 100644 --- a/DesignSystem/DemoApp/Sources/StudyPreview/StudyPreviewDemoViewController.swift +++ b/DesignSystem/DemoApp/Sources/StudyPreview/StudyPreviewDemoViewController.swift @@ -20,11 +20,13 @@ final class StudyPreviewDemoViewController: UIViewController { // MARK: - UI private let studyPreview = MOITStudyPreview( remainingDate: 19, - profileURL: URL(string: "https://avatars.githubusercontent.com/u/15011638?s=64&v=4")!, + profileURLString: "https://avatars.githubusercontent.com/u/15011638?s=64&v=4", studyName: "공부스터디", studyProgressDescription: "격주 금요일 17:00 - 20:00" ) + private let studyPreview2 = MOITStudyPreview() + private let flexRootView = UIView() // MARK: - Properties @@ -33,6 +35,12 @@ final class StudyPreviewDemoViewController: UIViewController { // MARK: - Initializers public init() { super.init(nibName: nil, bundle: nil) + studyPreview2.configure( + remainingDate: 20, + profileURL: "https://avatars.githubusercontent.com/u/15011638?s=64&v=4", + studyName: "스터디스터디", + studyProgressDescription: "3주마다 토요일 15:00 - 23:00" + ) } @available(*, unavailable) @@ -67,6 +75,11 @@ final class StudyPreviewDemoViewController: UIViewController { .marginTop(40) .width(80%) .height(100) + flex.addItem(self.studyPreview2) + .marginTop(40) + .width(80%) + .height(100) + } } diff --git a/DesignSystem/Sources/BottomSheet/BottomSheetViewController.swift b/DesignSystem/Sources/BottomSheet/BottomSheetViewController.swift index b4105303..5f15601c 100644 --- a/DesignSystem/Sources/BottomSheet/BottomSheetViewController.swift +++ b/DesignSystem/Sources/BottomSheet/BottomSheetViewController.swift @@ -33,10 +33,6 @@ open class BottomSheetViewController: UIViewController { self.view.addSubview(self.flexRootView) self.flexRootView.pin.all() self.flexRootView.flex.layout() - self.contentView.pin.top(20) - .left() - .right() - .bottom() } public init(contentView: UIView) { @@ -65,23 +61,22 @@ extension BottomSheetViewController { private func configureLayouts() { self.view.addSubview(self.flexRootView) - self.dimmedView.backgroundColor = UIColor(red: 24/255, green: 24/255, blue: 24/255, alpha: 0.5) + self.flexRootView.backgroundColor = UIColor(red: 24/255, green: 24/255, blue: 24/255, alpha: 0.5) - self.flexRootView.backgroundColor = .clear self.flexRootView.flex +// .backgroundColor(.clear) .define { flex in - flex.addItem(self.dimmedView) + flex.addItem() .define { flex in - flex.addItem() + flex.addItem(dimmedView) .grow(1) - - flex.addItem(self.contentRootView) + + flex.addItem(contentRootView) + .paddingVertical(20) + .paddingTop(20) .define { flex in - flex.addItem(self.contentView) - .position(.absolute) + flex.addItem(contentView) } - .grow(1) - .marginBottom(0) } .grow(1) } diff --git a/DesignSystem/Sources/Chip/MOITChip.swift b/DesignSystem/Sources/Chip/MOITChip.swift index 9d1d3482..91e6596d 100644 --- a/DesignSystem/Sources/Chip/MOITChip.swift +++ b/DesignSystem/Sources/Chip/MOITChip.swift @@ -12,24 +12,34 @@ import ResourceKit import FlexLayout import PinLayout +import RxSwift +import RxRelay public final class MOITChip: UIView { +// MARK: - UI + private let flexRootView = UIView() private let titleLabel = UILabel() - private let type: MOITChipType +// MARK: - property + + private let type = PublishRelay() + private let disposeBag = DisposeBag() + +// MARK: - init public init( type: MOITChipType ) { - self.type = type - super.init(frame: .zero) - configureFlexRootView() - configureComponent() - configureLayout() + bind() + self.type.accept(type) + } + + public convenience init() { + self.init(type: .absent) } @available (*, unavailable) @@ -37,6 +47,8 @@ public final class MOITChip: UIView { fatalError("init(coder:) has not been implemented") } +// MARK: - override + override public func layoutSubviews() { super.layoutSubviews() @@ -44,19 +56,41 @@ public final class MOITChip: UIView { flexRootView.flex.layout(mode: .adjustWidth) } - private func configureFlexRootView() { +// MARK: - public + + public func setType(to type: MOITChipType) { + self.type.accept(type) + } + +// MARK: - private + + private func bind() { + type + .subscribe( + onNext: { [weak self] type in + guard let self else { return } + + self.configureLayout(type: type) + self.configureFlexRootView(type: type) + self.configureComponent(type: type) + } + ) + .disposed(by: disposeBag) + } + + private func configureFlexRootView(type: MOITChipType) { flexRootView.backgroundColor = type.backgroundColor flexRootView.layer.cornerRadius = type.cornerRadius flexRootView.clipsToBounds = true } - private func configureComponent() { + private func configureComponent(type: MOITChipType) { titleLabel.textColor = type.textColor titleLabel.text = type.title titleLabel.font = type.font } - private func configureLayout() { + private func configureLayout(type: MOITChipType) { addSubview(flexRootView) flexRootView.flex diff --git a/DesignSystem/Sources/Chip/MOITChipType.swift b/DesignSystem/Sources/Chip/MOITChipType.swift index df7aba2e..f051fad2 100644 --- a/DesignSystem/Sources/Chip/MOITChipType.swift +++ b/DesignSystem/Sources/Chip/MOITChipType.swift @@ -58,7 +58,7 @@ public enum MOITChipType { switch self { case .attend, .absent, .late: return ResourceKitAsset.Color.white.color - case .dueDate(_), .finish: + case .dueDate, .finish: return ResourceKitAsset.Color.gray900.color } } diff --git a/DesignSystem/Sources/Common/FlexLayout+.swift b/DesignSystem/Sources/Common/FlexLayout+.swift index 35ea13f6..c8871ecb 100644 --- a/DesignSystem/Sources/Common/FlexLayout+.swift +++ b/DesignSystem/Sources/Common/FlexLayout+.swift @@ -12,9 +12,14 @@ import FlexLayout public extension FlexLayout.Flex { @discardableResult - func addOptionalItem(_ view: UIView?) -> Flex { + func addOptionalItem( + _ view: UIView?, + height: CGFloat? = nil, + marginRight: CGFloat = 0, + marginBottom: CGFloat = 0 + ) -> Flex { if let view { - self.addItem(view) + self.addItem(view).height(height).marginRight(marginRight).marginBottom(marginBottom) } return self diff --git a/DesignSystem/Sources/Input/MOITTextField.swift b/DesignSystem/Sources/Input/MOITTextField.swift index f0baa2f1..e0a86cff 100644 --- a/DesignSystem/Sources/Input/MOITTextField.swift +++ b/DesignSystem/Sources/Input/MOITTextField.swift @@ -17,14 +17,23 @@ import RxSwift public final class MOITTextField: UIView { +// MARK: - UI + private let flexRootView = UIView() - private var titleLabel: UILabel = { - let label = UILabel() - label.textColor = ResourceKitAsset.Color.gray700.color - label.font = ResourceKitFontFamily.p2 - return label + + private lazy var titleLabel: UILabel? = { + if let title { + let label = UILabel() + label.textColor = ResourceKitAsset.Color.gray700.color + label.font = ResourceKitFontFamily.p2 + label.text = title + return label + } + + return nil }() - public var textField: UITextField = { + + fileprivate var textField: UITextField = { let textField = UITextField() textField.layer.cornerRadius = 12 textField.clipsToBounds = true @@ -37,14 +46,17 @@ public final class MOITTextField: UIView { textField.addHorizontalPadding() return textField }() + +// MARK: - property private let disposeBag = DisposeBag() - private let title: String + private let title: String? private let placeHolder: String // MARK: - init + public init( - title: String, + title: String?, placeHolder: String ) { self.title = title @@ -69,23 +81,21 @@ public final class MOITTextField: UIView { } // MARK: - private func + private func configureLayout() { self.addSubview(flexRootView) flexRootView.flex .direction(.column) .define { flex in - flex.addItem(titleLabel) - .height(22) - flex.addItem(textField) - .height(53) - .marginTop(10) + flex.addOptionalItem(titleLabel, height: 22, marginBottom: 10) + + flex.addItem(textField).height(53) } } private func configureComponent() { - titleLabel.text = title textField.attributedPlaceholder = NSAttributedString( string: placeHolder, attributes: [ diff --git a/DesignSystem/Sources/List/MOITList.swift b/DesignSystem/Sources/List/MOITList.swift index 1dbc6e7d..d4b14c62 100644 --- a/DesignSystem/Sources/List/MOITList.swift +++ b/DesignSystem/Sources/List/MOITList.swift @@ -16,27 +16,30 @@ import RxSwift public final class MOITList: UIView { +// MARK: - UI + private let flexRootView = UIView() - private lazy var profileImageView: UIImageView? = { - if let image { - let imageView = UIImageView() - imageView.image = image - imageView.layer.cornerRadius = 16 - imageView.clipsToBounds = true - imageView.layer.borderColor = ResourceKitAsset.Color.gray100.color.cgColor - imageView.layer.borderWidth = 1 + + private lazy var profileImageView: MOITProfileView? = { + if let imageUrlString { + let imageView = MOITProfileView( + urlString: imageUrlString, + profileType: .small + ) return imageView } return nil }() - private lazy var titleLabel: UILabel = { + + private lazy var titleLabel: UILabel? = { let label = UILabel() label.text = title label.font = ResourceKitFontFamily.h6 label.textColor = ResourceKitAsset.Color.gray900.color return label }() + private lazy var detailLabel: UILabel? = { if let detail { let label = UILabel() @@ -48,10 +51,30 @@ public final class MOITList: UIView { return nil }() + + private lazy var studyOrderLabel: UILabel? = { + if let studyOrder { + let label = UILabel() + label.text = "\(studyOrder)차 스터디" + return label + } + + return nil + }() + + private lazy var separtaorLabel: UILabel = { + let label = UILabel() + label.text = "|" + label.textColor = ResourceKitAsset.Color.gray600.color + label.font = ResourceKitFontFamily.caption + return label + }() + private lazy var fineLabel: UILabel? = { if let fine { let label = UILabel() - label.text = fine + let formattedFine = Formatter.fineFormatter.string(from: NSNumber(value: fine)) ?? "" + label.text = "+ \(formattedFine) 원" label.textColor = ResourceKitAsset.Color.gray900.color label.font = ResourceKitFontFamily.h5 return label @@ -59,6 +82,7 @@ public final class MOITList: UIView { return nil }() + private lazy var chip: MOITChip? = { if let chipType { return MOITChip(type: chipType) @@ -66,30 +90,36 @@ public final class MOITList: UIView { return nil }() + fileprivate let button: MOITButton? +// MARK: - property + private let type: MOITListType - private let image: UIImage? - private let title: String + private let imageUrlString: String? + private let title: String? private let detail: String? private let chipType: MOITChipType? - private let fine: String? // TODO: 이부분도 서버에서 String, Int 중 어떤 형식으로 내려줄지? + private let studyOrder: Int? + private let fine: Int? // MARK: - init public init( type: MOITListType, - image: UIImage? = nil, - title: String, + imageUrlString: String? = nil, + title: String? = nil, detail: String? = nil, chipType: MOITChipType? = nil, - fine: String? = nil, + studyOrder: Int? = nil, + fine: Int? = nil, button: MOITButton? = nil ) { self.type = type - self.image = image + self.imageUrlString = imageUrlString self.title = title self.detail = detail self.chipType = chipType + self.studyOrder = studyOrder self.fine = fine self.button = button @@ -117,33 +147,43 @@ public final class MOITList: UIView { flexRootView.flex .direction(.row) .alignItems(.center) + .height(type.height) .define { flex in if let profileImageView { - flex.addItem(profileImageView).width(40).height(40).marginRight(10) + flex.addItem(profileImageView).marginRight(10) } if [.sendMoney, .myMoney].contains(where: { $0 == type }) { - if let chip { - flex.addItem(chip).marginRight(10) - } + flex.addOptionalItem(chip, marginRight: 10) } flex.addItem().direction(.column) .grow(1) .justifyContent(.center) .define { flex in - flex.addItem(titleLabel) - - if let detailLabel { - flex.addItem(detailLabel) - } + + if let studyOrderLabel, type == .myMoney { + studyOrderLabel.setTitleStyle() + flex.addItem(studyOrderLabel) + } else { + flex.addOptionalItem(titleLabel) + } + + flex.addItem().direction(.row).define { flex in + flex.addOptionalItem(detailLabel) + + if let studyOrderLabel, type == .sendMoney { + studyOrderLabel.setDetailLabelStyle() + flex.addItem(separtaorLabel).marginHorizontal(3) + flex.addItem(studyOrderLabel) + } + } } if let additionalView = selectAdditionalView(type: type) { flex.addItem(additionalView) } } - .height(type.height) } private func selectAdditionalView(type: MOITListType) -> UIView? { @@ -170,3 +210,22 @@ extension Reactive where Base: MOITList { } } +extension UILabel { + func setDetailLabelStyle() { + self.textColor = ResourceKitAsset.Color.gray600.color + self.font = ResourceKitFontFamily.caption + } + + func setTitleStyle() { + self.font = ResourceKitFontFamily.h6 + self.textColor = ResourceKitAsset.Color.gray900.color + } +} + +enum Formatter { + static let fineFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter + }() +} diff --git a/DesignSystem/Sources/NavigationBar/MOITNavigationBar.swift b/DesignSystem/Sources/NavigationBar/MOITNavigationBar.swift index 568b3d26..62722ca3 100644 --- a/DesignSystem/Sources/NavigationBar/MOITNavigationBar.swift +++ b/DesignSystem/Sources/NavigationBar/MOITNavigationBar.swift @@ -30,12 +30,17 @@ public final class MOITNavigationBar: UIView { // MARK: - Properties - public let leftItems: [NavigationItem] - public let rightItems: [NavigationItem] + public private(set) var leftItems: [NavigationItem]? + public private(set) var rightItems: [NavigationItem]? - private let colorType: NavigationColorType + private var colorType: NavigationColorType? // MARK: - Initializers + public init() { + + super.init(frame: .zero) + } + /// leftItems와 rightItems에 좌우측에 들어갈 item을 정의합니다. /// MOITNavigationBar.leftItems[n].rx.tap으로 이벤트를 받아올 수 있습니다. public init( @@ -44,8 +49,8 @@ public final class MOITNavigationBar: UIView { rightItems: [NavigationItemType], colorType: NavigationColorType = .normal ) { - self.leftItems = leftItems.map { NavigationItem(type: $0)} - self.rightItems = rightItems.map { NavigationItem(type: $0)} + self.leftItems = leftItems.map { NavigationItem(type: $0) } + self.rightItems = rightItems.map { NavigationItem(type: $0) } self.colorType = colorType super.init(frame: .zero) @@ -53,7 +58,6 @@ public final class MOITNavigationBar: UIView { self.configureTitle(title) self.configureLayout() self.configureColor() - } @available(*, unavailable) @@ -71,6 +75,20 @@ public final class MOITNavigationBar: UIView { } // MARK: - Methods + public func configure( + leftItems: [NavigationItemType], + title: String?, + rightItems: [NavigationItemType], + colorType: NavigationColorType = .normal + ) { + self.leftItems = leftItems.map { NavigationItem(type: $0) } + self.rightItems = rightItems.map { NavigationItem(type: $0) } + self.colorType = colorType + + self.configureTitle(title) + self.configureLayout() + self.configureColor() + } } // MARK: - Private Functions @@ -92,14 +110,24 @@ extension MOITNavigationBar { .height(56) .direction(.row) .alignItems(.center) - .backgroundColor(colorType.backgroundColor) + .backgroundColor(colorType?.backgroundColor ?? .white) .define { flex in flex.addItem() .direction(.row) .width(80) .justifyContent(.start) .define { flex in - self.leftItems.forEach { flex.addItem($0).size(24)} + self.leftItems?.forEach { item in + switch item.type { + case .logo: + flex.addItem(item) + .width(72) + .height(24) + default: + flex.addItem(item) + .size(24) + } + } } .marginLeft(16) @@ -112,24 +140,21 @@ extension MOITNavigationBar { .width(80) .justifyContent(.end) .define { flex in - self.rightItems.forEach { flex.addItem($0).size(24).marginLeft(20)} + self.rightItems?.forEach { flex.addItem($0).size(24).marginLeft(20) } } .marginRight(16) } } private func configureColor() { + leftItems?.forEach { $0.tintColor = self.colorType?.tintColor } + rightItems?.forEach { $0.tintColor = self.colorType?.tintColor } + titleLabel.textColor = colorType?.tintColor + leftItems?.forEach { $0.backgroundColor = self.colorType?.backgroundColor } + rightItems?.forEach { $0.backgroundColor = self.colorType?.backgroundColor } + titleLabel.backgroundColor = colorType?.backgroundColor - leftItems.forEach { $0.tintColor = self.colorType.tintColor} - rightItems.forEach { $0.tintColor = self.colorType.tintColor} - titleLabel.textColor = colorType.tintColor - - leftItems.forEach { $0.backgroundColor = self.colorType.backgroundColor} - rightItems.forEach { $0.backgroundColor = self.colorType.backgroundColor} - titleLabel.backgroundColor = colorType.backgroundColor - - self.backgroundColor = colorType.backgroundColor + self.backgroundColor = colorType?.backgroundColor } - } diff --git a/DesignSystem/Sources/NavigationBar/NavigationColorType.swift b/DesignSystem/Sources/NavigationBar/NavigationColorType.swift index f99312e9..a35b2e54 100644 --- a/DesignSystem/Sources/NavigationBar/NavigationColorType.swift +++ b/DesignSystem/Sources/NavigationBar/NavigationColorType.swift @@ -31,5 +31,4 @@ public enum NavigationColorType { return ResourceKitAsset.Color.gray500.color } } - } diff --git a/DesignSystem/Sources/NavigationBar/NavigationItem.swift b/DesignSystem/Sources/NavigationBar/NavigationItem.swift index d3d0470a..64c26110 100644 --- a/DesignSystem/Sources/NavigationBar/NavigationItem.swift +++ b/DesignSystem/Sources/NavigationBar/NavigationItem.swift @@ -14,7 +14,7 @@ import RxSwift public final class NavigationItem: UIButton { - let type: NavigationItemType + public let type: NavigationItemType public init(type: NavigationItemType) { self.type = type @@ -28,7 +28,9 @@ public final class NavigationItem: UIButton { } private func configureIcon(icon: UIImage?) { - guard let tintedImage = type.icon?.withRenderingMode(.alwaysTemplate) else { + guard let tintedImage = type.icon?.withRenderingMode(.alwaysTemplate), + self.type != .logo + else { self.setImage(type.icon, for: .normal) return } diff --git a/DesignSystem/Sources/NavigationBar/NavigationItemType.swift b/DesignSystem/Sources/NavigationBar/NavigationItemType.swift index b7a50e9d..7be079bc 100644 --- a/DesignSystem/Sources/NavigationBar/NavigationItemType.swift +++ b/DesignSystem/Sources/NavigationBar/NavigationItemType.swift @@ -11,6 +11,7 @@ import UIKit import ResourceKit public enum NavigationItemType { + case logo case close case back case share @@ -19,6 +20,8 @@ public enum NavigationItemType { var icon: UIImage? { switch self { + case .logo: + return ResourceKitAsset.Icon.logo.image case .close: return ResourceKitAsset.Icon.x.image case .back: @@ -31,5 +34,4 @@ public enum NavigationItemType { return ResourceKitAsset.Icon.bell.image } } - } diff --git a/DesignSystem/Sources/Profile/MOITProfileView.swift b/DesignSystem/Sources/Profile/MOITProfileView.swift index 92ec8503..7a7c16f5 100644 --- a/DesignSystem/Sources/Profile/MOITProfileView.swift +++ b/DesignSystem/Sources/Profile/MOITProfileView.swift @@ -14,7 +14,6 @@ import FlexLayout import PinLayout import RxSwift import RxGesture -import Kingfisher public final class MOITProfileView: UIView { @@ -28,23 +27,36 @@ public final class MOITProfileView: UIView { }() // MARK: - Properties - private let urlString: String + public private(set) var profileImageType: ProfileImageType? private let profileType: ProfileType fileprivate let containAddButton: Bool // MARK: - Initializers + public init ( - urlString: String, profileType: ProfileType, addButton: Bool = false ) { - self.urlString = urlString self.profileType = profileType self.containAddButton = addButton super.init(frame: .zero) - configureAttributes() + configureLayout() + } + + public init ( + profileImageType: ProfileImageType, + profileType: ProfileType, + addButton: Bool = false + ) { + self.profileImageType = profileImageType + self.profileType = profileType + self.containAddButton = addButton + + super.init(frame: .zero) + + configureImage(with: profileImageType) configureLayout() } @@ -52,7 +64,7 @@ public final class MOITProfileView: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Lifecycle public override func layoutSubviews() { super.layoutSubviews() @@ -67,19 +79,17 @@ public final class MOITProfileView: UIView { // MARK: - Functions - private func configureAttributes() { - profileImageView.kf.setImage( - with: URL(string: urlString), - placeholder: nil - ) + public func configureImage(with profileImageType: ProfileImageType) { + self.profileImageType = profileImageType + profileImageView.image = profileImageType.image } private func configureLayout() { addSubview(profileImageView) profileImageView.layer.cornerRadius = profileType.radius profileImageView.clipsToBounds = true - profileImageView.backgroundColor = ResourceKitAsset.Color.blue700.color - + profileImageView.layer.borderWidth = 1 + profileImageView.layer.borderColor = ResourceKitAsset.Color.gray100.color.cgColor profileImageView.flex.size(profileType.size) } } @@ -87,9 +97,6 @@ public final class MOITProfileView: UIView { public extension Reactive where Base: MOITProfileView { var tap: Observable { - if base.containAddButton { - return base.rx.tapGesture().when(.recognized).map { _ in } - } - return .empty() + base.rx.tapGesture().when(.recognized).map { _ in } } } diff --git a/DesignSystem/Sources/Profile/ProfileType.swift b/DesignSystem/Sources/Profile/ProfileType.swift index a54ca02b..8b974df0 100644 --- a/DesignSystem/Sources/Profile/ProfileType.swift +++ b/DesignSystem/Sources/Profile/ProfileType.swift @@ -6,7 +6,9 @@ // Copyright © 2023 chansoo.MOIT. All rights reserved. // -import Foundation +import UIKit + +import ResourceKit public enum ProfileType { case large @@ -33,6 +35,35 @@ public enum ProfileType { case .small: return 40 } - + } +} + +public enum ProfileImageType: Int { + case zero = 0 + case one, two, three, four, five, six, seven, eight, nine + // TODO: - 프로필 이미지 시안 나오면 변경 + var image: UIImage { + switch self { + case .zero: + return ResourceKitAsset.Icon.apple.image + case .one: + return ResourceKitAsset.Icon.apple.image + case .two: + return ResourceKitAsset.Icon.apple.image + case .three: + return ResourceKitAsset.Icon.apple.image + case .four: + return ResourceKitAsset.Icon.apple.image + case .five: + return ResourceKitAsset.Icon.apple.image + case .six: + return ResourceKitAsset.Icon.apple.image + case .seven: + return ResourceKitAsset.Icon.apple.image + case .eight: + return ResourceKitAsset.Icon.apple.image + case .nine: + return ResourceKitAsset.Icon.apple.image + } } } diff --git a/DesignSystem/Sources/StudyPreview/MOITStudyPreview.swift b/DesignSystem/Sources/StudyPreview/MOITStudyPreview.swift index 0eb4a14a..fb107f01 100644 --- a/DesignSystem/Sources/StudyPreview/MOITStudyPreview.swift +++ b/DesignSystem/Sources/StudyPreview/MOITStudyPreview.swift @@ -50,33 +50,51 @@ public final class MOITStudyPreview: UIView { fileprivate let didTapSubject = PublishSubject() // MARK: - Initializers + public init() { + super.init(frame: .zero) + } + public init( remainingDate: Int, - profileURL: URL, + profileURLString: String, studyName: String, studyProgressDescription: String? ) { super.init(frame: .zero) - - configureAttributes( + configure( remainingDate: remainingDate, - profileURL: profileURL, + profileURL: profileURLString, studyName: studyName, studyProgressDescription: studyProgressDescription ) - configureLayout() - setupGesture() } @available (*, unavailable) required init?(coder: NSCoder) { - fatalError() + fatalError("required init called") } // MARK: - Lifecycle // MARK: - Methods + public func configure( + remainingDate: Int, + profileURL: String, + studyName: String, + studyProgressDescription: String? + ) { + + configureAttributes( + remainingDate: remainingDate, + profileURLString: profileURL, + studyName: studyName, + studyProgressDescription: studyProgressDescription + ) + configureLayout() + setupGesture() + } + private func setupGesture() { let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:))) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTap(_:))) @@ -99,7 +117,7 @@ public final class MOITStudyPreview: UIView { self.flexRootView.flex .direction(.row) - .define { (flex) in + .define { flex in flex.addItem() // 보이는 뷰 .paddingLeft(16) .marginRight(10) @@ -138,7 +156,7 @@ public final class MOITStudyPreview: UIView { private func configureAttributes( remainingDate: Int, - profileURL: URL, + profileURLString: String, studyName: String, studyProgressDescription: String? ) { @@ -148,10 +166,13 @@ public final class MOITStudyPreview: UIView { remainingDateLabel = MOITChip(type: .dueDate(date: remainingDate)) - profileImageView.kf.setImage( - with: profileURL, - options: [.processor(RoundCornerImageProcessor(cornerRadius: 20))] - ) + if let url = URL(string: profileURLString) { + profileImageView.kf.setImage( + with: url, + options: [.processor(RoundCornerImageProcessor(cornerRadius: 20))] + ) + } + studyNameLabel.text = studyName studyDescriptionLabel.text = studyProgressDescription @@ -177,7 +198,10 @@ public final class MOITStudyPreview: UIView { case .ended: if velocity.x < 0 { UIView.animate(withDuration: 0.2) { - self.flexRootView.transform = CGAffineTransform(translationX: -(self.deleteButton.frame.width + 10), y: 0) + self.flexRootView.transform = CGAffineTransform( + translationX: -(self.deleteButton.frame.width + 10), + y: 0 + ) } } else { UIView.animate(withDuration: 0.2) { @@ -200,7 +224,7 @@ public final class MOITStudyPreview: UIView { message: "스터디를 삭제하면 해당 데이터도 모두 삭제됩니다.", preferredStyle: .alert ) - let confirmAction = UIAlertAction(title: "확인", style: .default) { action in + let confirmAction = UIAlertAction(title: "확인", style: .default) { _ in self.deleteConfirmSubject.onNext(()) } let cancelAction = UIAlertAction(title: "취소", style: .destructive) @@ -208,7 +232,6 @@ public final class MOITStudyPreview: UIView { alert.addAction(cancelAction) self.window?.rootViewController?.present(alert, animated: true) } - } // MARK: - UIGestureRecognizerDelegate diff --git a/DesignSystem/Sources/StudyPreview/TouchThroughView.swift b/DesignSystem/Sources/StudyPreview/TouchThroughView.swift index 8a1f004f..21612d7c 100644 --- a/DesignSystem/Sources/StudyPreview/TouchThroughView.swift +++ b/DesignSystem/Sources/StudyPreview/TouchThroughView.swift @@ -9,7 +9,9 @@ import UIKit final class TouchThroughView: UIView { - var button: UIButton? = nil + + var button: UIButton? + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { if let button = self.button { return self.bounds.contains(point) || button.frame.contains(point) diff --git a/DesignSystem/Sources/TapPager/MOITSegmentPager.swift b/DesignSystem/Sources/TapPager/MOITSegmentPager.swift index d1e835a5..73aebed0 100644 --- a/DesignSystem/Sources/TapPager/MOITSegmentPager.swift +++ b/DesignSystem/Sources/TapPager/MOITSegmentPager.swift @@ -126,7 +126,7 @@ public final class MOITSegmentPager: UIView { flex.marginLeft(xPosition) .width(width) .height(height) - .cornerRadius(height/2) + .cornerRadius(height / 2) } } } diff --git a/DesignSystem/Sources/TapPager/MOITTabPager.swift b/DesignSystem/Sources/TapPager/MOITTabPager.swift index 78806f73..ad31c996 100644 --- a/DesignSystem/Sources/TapPager/MOITTabPager.swift +++ b/DesignSystem/Sources/TapPager/MOITTabPager.swift @@ -72,7 +72,7 @@ public final class MOITTabPager: UIView { flex.addItem() .direction(.row) .define { flex in - self.pages.forEach { flex.addItem($0).marginHorizontal(12)} + self.pages.forEach { flex.addItem($0).marginHorizontal(12) } } .marginBottom(5) diff --git a/DesignSystem/Sources/TapPager/TapPagerType.swift b/DesignSystem/Sources/TapPager/TapPagerType.swift index 9f26b703..543e0dc9 100644 --- a/DesignSystem/Sources/TapPager/TapPagerType.swift +++ b/DesignSystem/Sources/TapPager/TapPagerType.swift @@ -40,5 +40,4 @@ public enum PagerType { return ResourceKitFontFamily.p2 } } - } diff --git a/Features/SignUp/SignUpData/Implement/dummy.swift b/Features/SignUp/SignUpData/Implement/dummy.swift new file mode 100644 index 00000000..9997fb44 --- /dev/null +++ b/Features/SignUp/SignUpData/Implement/dummy.swift @@ -0,0 +1,4 @@ +// +// dummy.swift +// + diff --git a/Features/SignUp/SignUpData/Interface/JoinRepository.swift b/Features/SignUp/SignUpData/Interface/JoinRepository.swift new file mode 100644 index 00000000..3ae7effe --- /dev/null +++ b/Features/SignUp/SignUpData/Interface/JoinRepository.swift @@ -0,0 +1,15 @@ +// +// JoinRepository.swift +// SignUpData +// +// Created by 김찬수 on 2023/06/16. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import RxSwift + +public protocol JoinRepository { + func post(imageIndex: Int, name: String, inviteCode: String?) -> Single +} diff --git a/Features/SignUp/SignUpData/Interface/dummy.swift b/Features/SignUp/SignUpData/Interface/dummy.swift new file mode 100644 index 00000000..9997fb44 --- /dev/null +++ b/Features/SignUp/SignUpData/Interface/dummy.swift @@ -0,0 +1,4 @@ +// +// dummy.swift +// + diff --git a/Features/SignUp/SignUpData/Project.swift b/Features/SignUp/SignUpData/Project.swift new file mode 100644 index 00000000..b62c4eb7 --- /dev/null +++ b/Features/SignUp/SignUpData/Project.swift @@ -0,0 +1,22 @@ +// +// SignUpAppDelegate.swift +// +// SignUp +// +// Created by kimchansoo +// + +import ProjectDescription +import ProjectDescriptionHelpers +import UtilityPlugin + +let project = Project.invertedDualTargetProject( + name: "SignUpData", + platform: .iOS, + iOSTargetVersion: "15.0.0", + interfaceDependencies: [ + ], + implementDependencies: [ + ] +) + diff --git a/Features/SignUp/SignUpData/Tests/dummy.swift b/Features/SignUp/SignUpData/Tests/dummy.swift new file mode 100644 index 00000000..9997fb44 --- /dev/null +++ b/Features/SignUp/SignUpData/Tests/dummy.swift @@ -0,0 +1,4 @@ +// +// dummy.swift +// + diff --git a/Features/SignUp/SignUpDomain/Implement/FetchRandomNumberUseCaseImpl.swift b/Features/SignUp/SignUpDomain/Implement/FetchRandomNumberUseCaseImpl.swift new file mode 100644 index 00000000..6bc0d23a --- /dev/null +++ b/Features/SignUp/SignUpDomain/Implement/FetchRandomNumberUseCaseImpl.swift @@ -0,0 +1,21 @@ +// +// FetchRandomNumberUseCaseImpl.swift +// SignUpDomain +// +// Created by 김찬수 on 2023/06/16. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import SignUpDomain + +public final class FetchRandomNumberUseCaseImpl: FetchRandomNumberUseCase { + + public init() { + } + + public func execute(with range: Range) -> Int { + return Int.random(in: range) + } +} diff --git a/Features/SignUp/SignUpDomain/Implement/PostJoinInfoUseCaseImpl.swift b/Features/SignUp/SignUpDomain/Implement/PostJoinInfoUseCaseImpl.swift new file mode 100644 index 00000000..7724ff7e --- /dev/null +++ b/Features/SignUp/SignUpDomain/Implement/PostJoinInfoUseCaseImpl.swift @@ -0,0 +1,31 @@ +// +// PostJoinInfoUseCaseImpl.swift +// SignUpDomain +// +// Created by 김찬수 on 2023/06/16. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import SignUpData +import SignUpDomain + +import RxSwift + +public final class PostJoinInfoUseCaseImpl: PostJoinInfoUseCase { + // MARK: - UI + + // MARK: - Properties + private let joinRepository: JoinRepository + + // MARK: - Initializers + public init(joinRepository: JoinRepository) { + self.joinRepository = joinRepository + } + + // MARK: - Functions + public func execute(imageIndex: Int, name: String, inviteCode: String?) -> Single { + return joinRepository.post(imageIndex: imageIndex, name: name, inviteCode: inviteCode) + } +} diff --git a/Features/SignUp/SignUpDomain/Interface/FetchRandomNumberUseCase.swift b/Features/SignUp/SignUpDomain/Interface/FetchRandomNumberUseCase.swift new file mode 100644 index 00000000..48ca5a49 --- /dev/null +++ b/Features/SignUp/SignUpDomain/Interface/FetchRandomNumberUseCase.swift @@ -0,0 +1,13 @@ +// +// FetchRandomNumberUseCase.swift +// SignUpDomain +// +// Created by 김찬수 on 2023/06/16. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +public protocol FetchRandomNumberUseCase { + func execute(with range: Range) -> Int +} diff --git a/Features/SignUp/SignUpDomain/Interface/PostJoinInfoUseCase.swift b/Features/SignUp/SignUpDomain/Interface/PostJoinInfoUseCase.swift new file mode 100644 index 00000000..db30a727 --- /dev/null +++ b/Features/SignUp/SignUpDomain/Interface/PostJoinInfoUseCase.swift @@ -0,0 +1,15 @@ +// +// PostJoinInfoUseCase.swift +// SignUpDomain +// +// Created by 김찬수 on 2023/06/16. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import RxSwift + +public protocol PostJoinInfoUseCase { + func execute(imageIndex: Int, name: String, inviteCode: String?) -> Single +} diff --git a/Features/SignUp/SignUpDomain/Project.swift b/Features/SignUp/SignUpDomain/Project.swift new file mode 100644 index 00000000..e7142521 --- /dev/null +++ b/Features/SignUp/SignUpDomain/Project.swift @@ -0,0 +1,22 @@ +// +// SignUpAppDelegate.swift +// +// SignUp +// +// Created by kimchansoo +// + +import ProjectDescription +import ProjectDescriptionHelpers +import UtilityPlugin + +let project = Project.invertedDualTargetProject( + name: "SignUpDomain", + platform: .iOS, + iOSTargetVersion: "15.0.0", + interfaceDependencies: [ + ], + implementDependencies: [ + ] +) + diff --git a/Features/SignUp/SignUpDomain/Tests/dummy.swift b/Features/SignUp/SignUpDomain/Tests/dummy.swift new file mode 100644 index 00000000..9997fb44 --- /dev/null +++ b/Features/SignUp/SignUpDomain/Tests/dummy.swift @@ -0,0 +1,4 @@ +// +// dummy.swift +// + diff --git a/Features/SignUp/SignUpUserInterface/DemoApp/Resources/LaunchScreen.storyboard b/Features/SignUp/SignUpUserInterface/DemoApp/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000..eae8f562 --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/DemoApp/Resources/LaunchScreen.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Features/SignUp/SignUpUserInterface/DemoApp/Sources/MockComponent.swift b/Features/SignUp/SignUpUserInterface/DemoApp/Sources/MockComponent.swift new file mode 100644 index 00000000..2b0b4258 --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/DemoApp/Sources/MockComponent.swift @@ -0,0 +1,42 @@ +// +// AppRoot.swift +// SignUpUserInterfaceDemoApp +// +// Created by 김찬수 on 2023/06/19. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +import SignUpUserInterface +import SignUpUserInterfaceImpl +import SignUpDomain +import SignUpDomainImpl +import SignUpData +import SignUpDataImpl + +import RIBs +import RxSwift + +final class MOCKSignUpComponent: Component, + SignUpDependency, + ProfileSelectDependency { + + init() { + super.init(dependency: EmptyComponent()) + } + + var fetchRandomNumberUseCase: FetchRandomNumberUseCase = FetchRandomNumberUseCaseImpl() + + var postJoinInfoUseCase: PostJoinInfoUseCase = PostJoinInfoUseCaseImpl(joinRepository: MockJoinRepository()) + lazy var profileSelectBuildable: ProfileSelectBuildable = { + return ProfileSelectBuilder(dependency: self) + }() +} + +final class MockJoinRepository: JoinRepository { + + func post(imageIndex: Int, name: String, inviteCode: String?) -> Single { + Single.just(3) + } +} diff --git a/Features/SignUp/SignUpUserInterface/DemoApp/Sources/SignUpUserInterfaceAppDelegate.swift b/Features/SignUp/SignUpUserInterface/DemoApp/Sources/SignUpUserInterfaceAppDelegate.swift new file mode 100644 index 00000000..38b7384b --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/DemoApp/Sources/SignUpUserInterfaceAppDelegate.swift @@ -0,0 +1,39 @@ +// +// SignUpAppDelegate.swift +// +// SignUp +// +// Created by kimchansoo on . +// + +import UIKit + +import SignUpUserInterfaceImpl +import SignUpUserInterface +import DesignSystem + +import RIBs + +@main +final class SignUpAppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + var router: ViewableRouting? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + let window = UIWindow(frame: UIScreen.main.bounds) + + let signUpBuilder = SignUpBuilder(dependency: MOCKSignUpComponent()) + self.router = signUpBuilder.build(withListener: MOCKMOITSignUpListener()) + + router?.load() + router?.interactable.activate() + window.rootViewController = router?.viewControllable.uiviewController + + window.makeKeyAndVisible() + self.window = window + return true + } + + private final class MOCKMOITSignUpListener: SignUpListener {} +} diff --git a/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectBuilder.swift b/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectBuilder.swift new file mode 100644 index 00000000..279fcbd3 --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectBuilder.swift @@ -0,0 +1,31 @@ +// +// ProfileSelectBuilder.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/21. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// +import SignUpUserInterface + +import RIBs + +final class ProfileSelectComponent: Component { + +} + +// MARK: - Builder + +public final class ProfileSelectBuilder: Builder, ProfileSelectBuildable { + + public override init(dependency: ProfileSelectDependency) { + super.init(dependency: dependency) + } + + public func build(withListener listener: ProfileSelectListener, currentImageIndex: Int?) -> ViewableRouting { + let component = ProfileSelectComponent(dependency: dependency) + let viewController = ProfileSelectViewContoller() + let interactor = ProfileSelectInteractor(presenter: viewController, currentImageIndex: currentImageIndex) + interactor.listener = listener + return ProfileSelectRouter(interactor: interactor, viewController: viewController) + } +} diff --git a/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectInteractor.swift b/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectInteractor.swift new file mode 100644 index 00000000..34d1b696 --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectInteractor.swift @@ -0,0 +1,56 @@ +// +// ProfileSelectInteractor.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/21. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// +import SignUpUserInterface + +import RIBs +import RxSwift + +protocol ProfileSelectRouting: ViewableRouting { + +} + +protocol ProfileSelectPresentable: Presentable { + var listener: ProfileSelectPresentableListener? { get set } + + func updateProfileIndex(index: Int?) +} + +final class ProfileSelectInteractor: PresentableInteractor, ProfileSelectInteractable, ProfileSelectPresentableListener { + + weak var router: ProfileSelectRouting? + weak var listener: ProfileSelectListener? + + init(presenter: ProfileSelectPresentable, currentImageIndex: Int?) { + super.init(presenter: presenter) + presenter.listener = self + presenter.updateProfileIndex(index: currentImageIndex) + } + + deinit { + debugPrint("\(self) deinit") + } + + override func didBecomeActive() { + super.didBecomeActive() + } + + override func willResignActive() { + super.willResignActive() + } +} + +extension ProfileSelectInteractor { + + func didTapSelectButton(with profileTypeIdx: Int) { + listener?.profileSelectDidFinish(imageTypeIdx: profileTypeIdx) + } + + func didTapDimmedView() { + listener?.profileSelectDidClose() + } +} diff --git a/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectRouter.swift b/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectRouter.swift new file mode 100644 index 00000000..5bf313ad --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectRouter.swift @@ -0,0 +1,28 @@ +// +// ProfileSelectRouter.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/21. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import SignUpUserInterface + +import RIBs + +protocol ProfileSelectInteractable: Interactable { + var router: ProfileSelectRouting? { get set } + var listener: ProfileSelectListener? { get set } +} + +protocol ProfileSelectViewControllable: ViewControllable { + +} + +final class ProfileSelectRouter: ViewableRouter, ProfileSelectRouting { + + override init(interactor: ProfileSelectInteractable, viewController: ProfileSelectViewControllable) { + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } +} diff --git a/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectView.swift b/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectView.swift new file mode 100644 index 00000000..fc931079 --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectView.swift @@ -0,0 +1,121 @@ +// +// ProfileSelectViewController.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/21. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import UIKit + +import SignUpUserInterface +import Utils +import ResourceKit +import DesignSystem + +import RIBs +import RxSwift +import FlexLayout +import PinLayout + +public final class ProfileSelectView: BaseView { + + // MARK: - UI + private let titleLabel: UILabel = { + let label = UILabel() + label.font = ResourceKitFontFamily.h5 + label.textColor = ResourceKitAsset.Color.gray900.color + label.text = "프로필 이미지 선택하기" + return label + }() + + public let currentProfileImage = MOITProfileView(profileType: .large) + + public var profileImageList: [MOITProfileView] = [] + + public let selectButton = MOITButton( + type: .large, + title: "선택하기", + titleColor: ResourceKitAsset.Color.white.color, + backgroundColor: ResourceKitAsset.Color.blue800.color + ) + + // MARK: - Properties + + // MARK: - Initializers + public init() { + super.init(frame: .zero) + } + + deinit { + debugPrint("\(self) deinit") + } + + // MARK: - Lifecycle + + + // MARK: - Functions + public func configureProfileImage(with imageType: ProfileImageType) { + currentProfileImage.configureImage(with: imageType) + } + + public override func configureAttributes() { + let profileRange = 0...10 + self.profileImageList = profileRange + .compactMap { ProfileImageType(rawValue: $0) } + .map { + return MOITProfileView( + profileImageType: $0, + profileType: .medium + ) + } + } + + public override func configureConstraints() { + flexRootView.flex + .alignItems(.center) + .paddingHorizontal(20) + .define { flex in + flex.addItem(titleLabel) + .alignSelf(.start) + .height(56) + .marginBottom(20) + + flex.addItem(currentProfileImage) + .marginBottom(20) + + flex.addItem() + .alignContent(.spaceAround) + .justifyContent(.center) + .direction(.row) + .wrap(.wrap) + .define { flex in + self.profileImageList.forEach { + flex.addItem($0) + .marginHorizontal(5) + .marginBottom(10) + } + } + .marginBottom(20) + + flex.addItem(selectButton) + .marginBottom(26) + .width(100%) + } + } +} + +public extension Reactive where Base: ProfileSelectView { + + var imageTapped: Observable { + Observable.merge( + base.profileImageList.enumerated().map { idx, image in + image.rx.tap.map { idx } + } + ) + } + + var selectButtonTapped: Observable { + return base.selectButton.rx.tap.asObservable() + } +} diff --git a/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectViewController.swift b/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectViewController.swift new file mode 100644 index 00000000..5e023f50 --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Implement/ProfileSelect/ProfileSelectViewController.swift @@ -0,0 +1,85 @@ +// +// ProfileSelectViewController.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/22. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import UIKit + +import DesignSystem + +import FlexLayout +import PinLayout +import RxSwift +import RIBs + +public protocol ProfileSelectPresentableListener: AnyObject { + + func didTapSelectButton(with: Int) + func didTapDimmedView() +} + +public final class ProfileSelectViewContoller: BottomSheetViewController, + ProfileSelectPresentable, + ProfileSelectViewControllable { + + // MARK: - UI + public let profileView = ProfileSelectView() + + // MARK: - Properties + weak var listener: ProfileSelectPresentableListener? + + private var disposebag = DisposeBag() + + // MARK: - Initializers + public init() { + super.init(contentView: profileView) + bind() + } + + @available (*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + debugPrint("\(self) deinit") + } + + // MARK: - Lifecycle + + // MARK: - Functions + + private func bind() { + profileView.rx.imageTapped + .withUnretained(self) + .subscribe(onNext: { owner, idx in + guard let profileImageType = ProfileImageType(rawValue: idx) else { return } + owner.profileView.currentProfileImage.configureImage(with: profileImageType) + }) + .disposed(by: disposebag) + + profileView.rx.selectButtonTapped + .withUnretained(self) + .subscribe(onNext: { owner, _ in + guard let imageType = owner.profileView.currentProfileImage.profileImageType else { return } + owner.listener?.didTapSelectButton(with: imageType.rawValue) + }) + .disposed(by: disposebag) + + self.rx.didTapDimmedView + .subscribe(onNext: { [weak self] _ in + + self?.listener?.didTapDimmedView() + }) + .disposed(by: disposebag) + } + + func updateProfileIndex(index: Int?) { + guard let index = index, + let imageType = ProfileImageType(rawValue: index) else { return } + profileView.configureProfileImage(with: imageType) + } +} diff --git a/Features/SignUp/SignUpUserInterface/Implement/SignUpBuilder.swift b/Features/SignUp/SignUpUserInterface/Implement/SignUpBuilder.swift new file mode 100644 index 00000000..6282a53c --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Implement/SignUpBuilder.swift @@ -0,0 +1,44 @@ +// +// SignUpBuilder.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/14. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import SignUpUserInterface + +import SignUpDomain + +import RIBs + +public final class SignUpComponent: Component, SignUpInteractorDependency { + + var fetchRandomNumberUseCase: FetchRandomNumberUseCase { dependency.fetchRandomNumberUseCase } + var postJoinInfoUseCase: PostJoinInfoUseCase { dependency.postJoinInfoUseCase } + var profileSelectBuildable: ProfileSelectBuildable { dependency.profileSelectBuildable } +} + +// MARK: - Builder + +public final class SignUpBuilder: Builder, SignUpBuildable { + + public override init(dependency: SignUpDependency) { + super.init(dependency: dependency) + } + + public func build(withListener listener: SignUpListener) -> ViewableRouting { + let component = SignUpComponent(dependency: dependency) + let viewController = SignUpViewController() + let interactor = SignUpInteractor( + presenter: viewController, + dependency: component + ) + interactor.listener = listener + return SignUpRouter( + interactor: interactor, + viewController: viewController, + profileSelectBuildable: dependency.profileSelectBuildable + ) + } +} diff --git a/Features/SignUp/SignUpUserInterface/Implement/SignUpInteractor.swift b/Features/SignUp/SignUpUserInterface/Implement/SignUpInteractor.swift new file mode 100644 index 00000000..828f25d2 --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Implement/SignUpInteractor.swift @@ -0,0 +1,172 @@ +// +// SignUpInteractor.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/14. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import SignUpUserInterface +import SignUpData +import SignUpDomain +import DesignSystem +import Utils + +import RIBs +import RxSwift +import RxCocoa + +protocol SignUpRouting: ViewableRouting { + + func attachMOITList() + + func attachProfileSelect(currentImageIndex: Int?) + func detachProfileSelect() +} + +protocol SignUpPresentable: Presentable { + + var listener: SignUpPresentableListener? { get set } + + func updateProfileIndex(index: Int) +} + +protocol SignUpInteractorDependency { + var fetchRandomNumberUseCase: FetchRandomNumberUseCase { get } + var postJoinInfoUseCase: PostJoinInfoUseCase { get } +} + +final class SignUpInteractor: PresentableInteractor, + SignUpInteractable { + + // MARK: - Properties + weak var router: SignUpRouting? + weak var listener: SignUpListener? + + private let dependency: SignUpInteractorDependency + + private let profileImageIndex = PublishRelay() + private let nickName = PublishRelay() + private let inviteCode = PublishRelay() + private let nextButtonTapped = PublishRelay() + private let profileViewTapped = PublishRelay() + + // MARK: - Initializers + public init( + presenter: SignUpPresentable, + dependency: SignUpInteractorDependency + ) { + self.dependency = dependency + super.init(presenter: presenter) + presenter.listener = self + } + + // MARK: - Lifecycle + override func didBecomeActive() { + super.didBecomeActive() + + bind() + configureImageType() + } + + override func willResignActive() { + super.willResignActive() + } + + // MARK: - Functions + private func configureImageType() { + self.profileImageIndex.accept(dependency.fetchRandomNumberUseCase.execute(with: 0..<9)) + } + + private func bind() { + // nextButtonTapped가 발동 시 nickname과 inviteCode 스트림을 합쳐서 postJoinInfoUseCase에 전달 + nextButtonTapped + .withLatestFrom(Observable.combineLatest(profileImageIndex, nickName, inviteCode)) + .distinctUntilChanged({ old, new in + return old.0 == new.0 && old.1 == new.1 + }) + .flatMap { [weak self] profileImageIndex, nickName, inviteCode -> Observable in + guard let self = self else { return .empty() } + print("profileImageIndex, nickName, inviteCode: ", profileImageIndex, nickName, inviteCode) + return self.dependency.postJoinInfoUseCase.execute( + imageIndex: profileImageIndex, + name: nickName, + inviteCode: inviteCode + ) + .asObservable() + } + // 성공하면 화면 moitlist로, 실패하면 처리 따로 + .subscribe( + onNext: { [weak self] code in + // 코드 저장 + + // 뷰 옮기기 + self?.router?.attachMOITList() + }, + onError: { error in + // 토스트?! + print(error) + }) + .disposeOnDeactivate(interactor: self) + + profileImageIndex + .withUnretained(self) + .subscribe(onNext: { owner, index in + // 이미지 업데이트 + owner.presenter.updateProfileIndex(index: index) + }) + .disposeOnDeactivate(interactor: self) + + profileViewTapped + .withLatestFrom(profileImageIndex) + .withUnretained(self) + .debug() + .subscribe(onNext: { owner, index in + owner.router?.attachProfileSelect(currentImageIndex: index) + }) + .disposeOnDeactivate(interactor: self) + } +} + +// MARK: - SignUpPresentableListener +extension SignUpInteractor: SignUpPresentableListener { + + func didSwipeBack() { + + } + + func didTapNextButton() { + nextButtonTapped.accept(()) + } + + func didTapProfileView() { + profileViewTapped.accept(()) + } + + func didTypeName(name: String) { + self.nickName.accept(name) + } + + func didTypeInviteCode(inviteCode: String) { + self.inviteCode.accept(inviteCode) + } +} + +// MARK: - ProfileSelectListener +extension SignUpInteractor: ProfileSelectListener { + + func profileSelectDidClose() { + router?.detachProfileSelect() + } + + func profileSelectDidFinish(imageTypeIdx: Int) { + profileImageIndex.accept(imageTypeIdx) + router?.detachProfileSelect() + } +} + +extension SignUpInteractor: AdaptivePresentationControllerDelegate { + func presentationControllerDidDismiss() { + router?.detachProfileSelect() + } +} diff --git a/Features/SignUp/SignUpUserInterface/Implement/SignUpRouter.swift b/Features/SignUp/SignUpUserInterface/Implement/SignUpRouter.swift new file mode 100644 index 00000000..d98157c2 --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Implement/SignUpRouter.swift @@ -0,0 +1,68 @@ +// +// SignUpRouter.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/14. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import SignUpUserInterface + +import RIBs +import Utils + +protocol SignUpInteractable: Interactable, ProfileSelectListener { + + var router: SignUpRouting? { get set } + var listener: SignUpListener? { get set } +} + +protocol SignUpViewControllable: ViewControllable { + +} + +final class SignUpRouter: ViewableRouter, SignUpRouting { + + // MARK: - Properties + private let profileSelectBuildable: ProfileSelectBuildable + private var profileSelectRouting: Routing? + + // MARK: - Initializers + public init( + interactor: SignUpInteractable, + viewController: SignUpViewControllable, + profileSelectBuildable: ProfileSelectBuildable + ) { + self.profileSelectBuildable = profileSelectBuildable + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } + + // MARK: - Functions + func attachMOITList() { + } + + func attachProfileSelect(currentImageIndex: Int?) { + if profileSelectRouting != nil { return } + + let router = profileSelectBuildable.build(withListener: interactor, currentImageIndex: currentImageIndex) + + self.viewControllable.present(router.viewControllable, animated: true, completion: nil) + + self.profileSelectRouting = router + attachChild(router) + } + + func detachProfileSelect() { + guard let router = profileSelectRouting else { + print("profileSelectRouting is nil") + return + } + + viewControllable.dismiss(completion: nil) + self.profileSelectRouting = nil + + detachChild(router) + } + +} diff --git a/Features/SignUp/SignUpUserInterface/Implement/SignUpViewController.swift b/Features/SignUp/SignUpUserInterface/Implement/SignUpViewController.swift new file mode 100644 index 00000000..b1b6d9cc --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Implement/SignUpViewController.swift @@ -0,0 +1,169 @@ +// +// SignUpViewController.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/14. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// +import UIKit + +import SignUpUserInterface +import DesignSystem +import Utils +import ResourceKit + +import RIBs +import RxSwift +import FlexLayout +import PinLayout + +protocol SignUpPresentableListener: AnyObject { + + func didSwipeBack() + func didTapNextButton() + func didTapProfileView() + func didTypeName(name: String) + func didTypeInviteCode(inviteCode: String) +} + +public final class SignUpViewController: BaseViewController, SignUpViewControllable { + + // MARK: - UI + private let titleLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 2 + label.font = ResourceKitFontFamily.h4 + label.textColor = ResourceKitAsset.Color.black.color + var paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = 1.34 + label.attributedText = NSMutableAttributedString( + string: "모잇에서 사용할\n프로필을 만들어 주세요.", + attributes: [ + NSAttributedString.Key.paragraphStyle: paragraphStyle + ] + ) + return label + }() + + private let profileView: MOITProfileView = { + // 랜덤으로 띄워주기로 했었나.. + let profileView = MOITProfileView( + profileImageType: .one, + profileType: .large, + addButton: true + ) + return profileView + }() + + private let nameTextField = MOITTextField( + title: "이름 (필수)", + placeHolder: "이름을 입력해주세요." + ) + private let inviteCodeTextField = MOITTextField( + title: "스터디 초대 코드 (선택)", + placeHolder: "공유받은 스터디 초대코드를 입력하세요." + ) + private let nextButton = MOITButton( + type: .large, + title: "다음", + titleColor: ResourceKitAsset.Color.white.color, + backgroundColor: ResourceKitAsset.Color.blue800.color + ) + + // MARK: - Properties + weak var listener: SignUpPresentableListener? + + // MARK: - Initializers + public override init() { + super.init() + } + + // MARK: - Lifecycle + public override func viewDidLoad() { + super.viewDidLoad() + self.navigationController?.navigationBar.isHidden = true + } + + public override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + if self.isMovingFromParent { + self.listener?.didSwipeBack() + } + } + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + configureNavigationBar( + leftItems: [.back], + title: "", + rightItems: [] + ) + } + + deinit { + debugPrint("\(self) deinit") + } + + // MARK: - Functions + public override func configureConstraints() { + flexRootView.flex + .alignItems(.start) + .paddingHorizontal(20) + .define { flex in + flex.addItem(titleLabel) + .marginTop(20) + flex.addItem(profileView) + .marginTop(20) + .alignSelf(.center) + flex.addItem(nameTextField) + .marginTop(20) + .width(100%) + flex.addItem(inviteCodeTextField) + .marginTop(20) + .width(100%) + flex.addItem() + .grow(1) + flex.addItem(nextButton) + .width(100%) + .marginBottom(36) + } + } + + public override func bind() { + profileView.rx.tap + .withUnretained(self) + .bind(onNext: { owner, _ in + owner.listener?.didTapProfileView() + }) + .disposed(by: disposebag) + + nameTextField.rx.text + .withUnretained(self) + .bind(onNext: { owner, name in + owner.listener?.didTypeName(name: name) + }) + .disposed(by: disposebag) + + inviteCodeTextField.rx.text + .withUnretained(self) + .bind(onNext: { owner, inviteCode in + owner.listener?.didTypeInviteCode(inviteCode: inviteCode) + }) + .disposed(by: disposebag) + + nextButton.rx.tap + .withUnretained(self) + .bind(onNext: { owner, _ in + owner.listener?.didTapNextButton() + }) + .disposed(by: disposebag) + } +} + +extension SignUpViewController: SignUpPresentable { + + func updateProfileIndex(index: Int) { + guard let imageType = ProfileImageType(rawValue: index) else { return } + self.profileView.configureImage(with: imageType) + } +} diff --git a/Features/SignUp/SignUpUserInterface/Interface/ProfileSelect/ProfileSelectBuildable.swift b/Features/SignUp/SignUpUserInterface/Interface/ProfileSelect/ProfileSelectBuildable.swift new file mode 100644 index 00000000..11ba502b --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Interface/ProfileSelect/ProfileSelectBuildable.swift @@ -0,0 +1,13 @@ +// +// SignUpBuildable.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/14. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import RIBs + +public protocol ProfileSelectBuildable: Buildable { + func build(withListener listener: ProfileSelectListener, currentImageIndex: Int?) -> ViewableRouting +} diff --git a/Features/SignUp/SignUpUserInterface/Interface/ProfileSelect/ProfileSelectDependency.swift b/Features/SignUp/SignUpUserInterface/Interface/ProfileSelect/ProfileSelectDependency.swift new file mode 100644 index 00000000..94655c35 --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Interface/ProfileSelect/ProfileSelectDependency.swift @@ -0,0 +1,13 @@ +// +// ProfileSelectDependency.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/21. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import RIBs + +public protocol ProfileSelectDependency: Dependency { + +} diff --git a/Features/SignUp/SignUpUserInterface/Interface/ProfileSelect/ProfileSelectListener.swift b/Features/SignUp/SignUpUserInterface/Interface/ProfileSelect/ProfileSelectListener.swift new file mode 100644 index 00000000..ffa959fe --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Interface/ProfileSelect/ProfileSelectListener.swift @@ -0,0 +1,15 @@ +// +// ProfileSelectListener.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/21. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import RIBs + +public protocol ProfileSelectListener: AnyObject { + + func profileSelectDidClose() + func profileSelectDidFinish(imageTypeIdx: Int) +} diff --git a/Features/SignUp/SignUpUserInterface/Interface/SignUpBuildable.swift b/Features/SignUp/SignUpUserInterface/Interface/SignUpBuildable.swift new file mode 100644 index 00000000..e6a841b3 --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Interface/SignUpBuildable.swift @@ -0,0 +1,15 @@ +// +// SignUpBuildable.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/14. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import UIKit + +import RIBs + +public protocol SignUpBuildable: Buildable { + func build(withListener listener: SignUpListener) -> ViewableRouting +} diff --git a/Features/SignUp/SignUpUserInterface/Interface/SignUpDependency.swift b/Features/SignUp/SignUpUserInterface/Interface/SignUpDependency.swift new file mode 100644 index 00000000..64fefa05 --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Interface/SignUpDependency.swift @@ -0,0 +1,20 @@ +// +// SignUpDependency.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/14. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import UIKit + +import SignUpDomain + +import RIBs + +public protocol SignUpDependency: Dependency { + + var fetchRandomNumberUseCase: FetchRandomNumberUseCase { get } + var postJoinInfoUseCase: PostJoinInfoUseCase { get } + var profileSelectBuildable: ProfileSelectBuildable { get } +} diff --git a/Features/SignUp/SignUpUserInterface/Interface/SignUpListener.swift b/Features/SignUp/SignUpUserInterface/Interface/SignUpListener.swift new file mode 100644 index 00000000..ebcf6f44 --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Interface/SignUpListener.swift @@ -0,0 +1,15 @@ +// +// SignUpListener.swift +// SignUpUserInterfaceImpl +// +// Created by 김찬수 on 2023/06/14. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import UIKit + +import RIBs + +public protocol SignUpListener: AnyObject { + +} diff --git a/Features/SignUp/SignUpUserInterface/Project.swift b/Features/SignUp/SignUpUserInterface/Project.swift new file mode 100644 index 00000000..e8620790 --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Project.swift @@ -0,0 +1,39 @@ +// +// SignUpAppDelegate.swift +// +// SignUp +// +// Created by kimchansoo +// + +import ProjectDescription +import ProjectDescriptionHelpers +import UtilityPlugin + +let project = Project.invertedDualTargetProjectWithDemoApp( + name: "SignUpUserInterface", + platform: .iOS, + iOSTargetVersion: "15.0.0", + interfaceDependencies: [ + .ThirdParty.RIBs, + .ThirdParty.RxSwift, + ], + implementDependencies: [ + .ThirdParty.RxGesture, + .ThirdParty.RxSwift, + .ThirdParty.RxCocoa, + .ThirdParty.RIBs, + + .ResourceKit, + .DesignSystem, + .Core.Utils, + + .Feature.SignUp.Domain.Interface, + .Feature.SignUp.Data.Interface, + ], + demoAppDependencies: [ + .Feature.SignUp.Domain.Implement, + .Feature.SignUp.Data.Implement, + ], + isUserInterface: true +) diff --git a/Features/SignUp/SignUpUserInterface/Tests/dummy.swift b/Features/SignUp/SignUpUserInterface/Tests/dummy.swift new file mode 100644 index 00000000..cfe6c36c --- /dev/null +++ b/Features/SignUp/SignUpUserInterface/Tests/dummy.swift @@ -0,0 +1,3 @@ +// +// dummy.swift +// diff --git a/MOITNetwork/Implement/NetworkImpl.swift b/MOITNetwork/Implement/NetworkImpl.swift index fe21435b..92bd8692 100644 --- a/MOITNetwork/Implement/NetworkImpl.swift +++ b/MOITNetwork/Implement/NetworkImpl.swift @@ -26,16 +26,11 @@ public final class NetworkImpl: Network { self?.session.dataTask(with: urlRequest) { [weak self] data, response, error in guard let self else { return } - let result = self.checkError(with: data, response, error) + let result = self.checkError(with: data, response, error, E.Response.self) switch result { - case .success(let data): - do { - let response = try JSONDecoder().decode(E.Response.self, from: data) - single(.success(response)) - } catch { - single(.failure(NetworkError.decodingError)) - } + case .success(let response): + single(.success(response)) case .failure(let error): single(.failure(error)) } @@ -47,11 +42,12 @@ public final class NetworkImpl: Network { } } - private func checkError( + private func checkError( with data: Data?, _ response: URLResponse?, - _ error: Error? - ) -> Result { + _ error: Error?, + _ model: M.Type + ) -> Result { if let error = error { return .failure(error) } @@ -59,16 +55,22 @@ public final class NetworkImpl: Network { guard let response = response as? HTTPURLResponse else { return .failure(NetworkError.unknownError) } - - guard (200...299).contains(response.statusCode) else { - let serverError = ServerError(fromRawValue: response.statusCode) - return .failure(NetworkError.serverError(serverError)) - } - + guard let data = data else { return .failure(NetworkError.emptyData) } - return .success(data) + do { + let responseModel = try JSONDecoder().decode(MOITResponse.self, from: data) + + if responseModel.success, let data = responseModel.data { + return .success(data) + } else { + let serverError = ServerError(fromRawValue: response.statusCode) + return .failure(NetworkError.serverError(serverError)) + } + } catch { + return .failure(NetworkError.decodingError) + } } } diff --git a/MOITNetwork/Interface/NetworkError.swift b/MOITNetwork/Interface/NetworkError.swift index b4e43e7d..d95df4b4 100644 --- a/MOITNetwork/Interface/NetworkError.swift +++ b/MOITNetwork/Interface/NetworkError.swift @@ -18,8 +18,10 @@ public enum NetworkError: Error { public enum ServerError: Int { case unknownError - case badReqeust = 400 - case notFound = 404 + case systemFail = 500 + case invalidAccess = 403 + case notExist = 404 + case alreadyExist = 409 public init(fromRawValue rawValue: Int) { self = ServerError(rawValue: rawValue) ?? .unknownError diff --git a/MOITNetwork/Interface/Response.swift b/MOITNetwork/Interface/Response.swift new file mode 100644 index 00000000..9c55b3fe --- /dev/null +++ b/MOITNetwork/Interface/Response.swift @@ -0,0 +1,20 @@ +// +// Response.swift +// MOITNetwork +// +// Created by 최혜린 on 2023/06/19. +// Copyright © 2023 chansoo.MOIT. All rights reserved. +// + +import Foundation + +public struct MOITErrorResponse: Decodable { + let code: String + let message: String +} + +public struct MOITResponse: Decodable where R: Decodable { + public let success: Bool + public let data: R? + public let error: MOITErrorResponse? +} diff --git a/Plugins/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift b/Plugins/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift index 7749b1c3..d44d3ea7 100644 --- a/Plugins/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift +++ b/Plugins/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift @@ -5,13 +5,19 @@ import ProjectDescription extension TargetDependency { public struct Feature { - public struct Home { + public struct StudyList { public struct Data {} public struct Domain {} public struct UserInterface {} } - - public struct MOITShare { + + public struct SignUp { + public struct Data {} + public struct Domain {} + public struct UserInterface {} + } + + public struct MOITShare { public struct Data {} public struct Domain {} public struct UserInterface {} @@ -52,30 +58,55 @@ public extension TargetDependency.Core { } static let CSLogger = project(name: "CSLogger", isInterface: true) + static let Utils = project(name: "Utils", isInterface: true) } // MARK: - Features/Home -public extension TargetDependency.Feature.Home { - static let folderName = "Home" +public extension TargetDependency.Feature.StudyList { + static let folderName = "StudyList" + static func project(name: String, isInterface: Bool) -> TargetDependency { + let postfix: String = isInterface ? "" : "Impl" + return .project(target: "\(folderName)\(name)\(postfix)", + path: .relativeToRoot("Features/\(folderName)/\(folderName)\(name)")) + }} + +public extension TargetDependency.Feature.StudyList.UserInterface { + static let Interface = TargetDependency.Feature.StudyList.project(name: "UserInterface", isInterface: true) + static let Implement = TargetDependency.Feature.StudyList.project(name: "UserInterface", isInterface: false) +} + +public extension TargetDependency.Feature.StudyList.Domain { + static let Interface = TargetDependency.Feature.StudyList.project(name: "Domain", isInterface: true) + static let Implement = TargetDependency.Feature.StudyList.project(name: "Domain", isInterface: false) +} + +public extension TargetDependency.Feature.StudyList.Data { + static let Interface = TargetDependency.Feature.StudyList.project(name: "Data", isInterface: true) + static let Implement = TargetDependency.Feature.StudyList.project(name: "Data", isInterface: false) +} + +// MARK: - Features/SignUp +public extension TargetDependency.Feature.SignUp { + static let folderName = "SignUp" static func project(name: String, isInterface: Bool) -> TargetDependency { let postfix: String = isInterface ? "" : "Impl" return .project(target: "\(folderName)\(name)\(postfix)", path: .relativeToRoot("Features/\(folderName)/\(folderName)\(name)")) }} -public extension TargetDependency.Feature.Home.UserInterface { - static let Interface = TargetDependency.Feature.Home.project(name: "UserInterface", isInterface: true) - static let Implement = TargetDependency.Feature.Home.project(name: "UserInterface", isInterface: false) +public extension TargetDependency.Feature.SignUp.UserInterface { + static let Interface = TargetDependency.Feature.SignUp.project(name: "UserInterface", isInterface: true) + static let Implement = TargetDependency.Feature.SignUp.project(name: "UserInterface", isInterface: false) } -public extension TargetDependency.Feature.Home.Domain { - static let Interface = TargetDependency.Feature.Home.project(name: "Domain", isInterface: true) - static let Implement = TargetDependency.Feature.Home.project(name: "Domain", isInterface: false) +public extension TargetDependency.Feature.SignUp.Domain { + static let Interface = TargetDependency.Feature.SignUp.project(name: "Domain", isInterface: true) + static let Implement = TargetDependency.Feature.SignUp.project(name: "Domain", isInterface: false) } -public extension TargetDependency.Feature.Home.Data { - static let Interface = TargetDependency.Feature.Home.project(name: "Data", isInterface: true) - static let Implement = TargetDependency.Feature.Home.project(name: "Data", isInterface: false) +public extension TargetDependency.Feature.SignUp.Data { + static let Interface = TargetDependency.Feature.SignUp.project(name: "Data", isInterface: true) + static let Implement = TargetDependency.Feature.SignUp.project(name: "Data", isInterface: false) } diff --git a/ResourceKit/Resources/Icon.xcassets/logo.imageset/Contents.json b/ResourceKit/Resources/Icon.xcassets/logo.imageset/Contents.json new file mode 100644 index 00000000..ad42a58d --- /dev/null +++ b/ResourceKit/Resources/Icon.xcassets/logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "logo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ResourceKit/Resources/Icon.xcassets/logo.imageset/logo.svg b/ResourceKit/Resources/Icon.xcassets/logo.imageset/logo.svg new file mode 100644 index 00000000..3ec6aa49 --- /dev/null +++ b/ResourceKit/Resources/Icon.xcassets/logo.imageset/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ResourceKit/Sources/Font.swift b/ResourceKit/Sources/Font.swift index 9debabe4..5d5def9c 100644 --- a/ResourceKit/Sources/Font.swift +++ b/ResourceKit/Sources/Font.swift @@ -6,7 +6,8 @@ // Copyright © 2023 chansoo.MOIT. All rights reserved. // -import Foundation +// TODO: extension 메서드 위치 이동 후 Foundation으로 변경 필요 +import UIKit extension ResourceKitFontFamily { public static let h1 = Pretendard.bold.font(size: 36) @@ -19,4 +20,46 @@ extension ResourceKitFontFamily { public static let p2 = Pretendard.medium.font(size: 14) public static let p3 = Pretendard.regular.font(size: 14) public static let caption = Pretendard.medium.font(size: 12) + + public static func lineHeight(of font: ResourceKitFontConvertible.Font) -> CGFloat { + if font == h1 { return 50 } + else if [h2, h4].contains(where: { $0 == font }) { return 32 } + else if font == h3 { return 36 } + else if font == h5 { return 27} + else if [h6, p1].contains(where: { $0 == font }) { return 23 } + else if [p2, p3].contains(where: { $0 == font }) { return 22 } + else if font == caption { return 18 } + else { return 0 } + } } + +// TODO: 추후 Utils로 위치 이동 필요 +public extension UILabel { + func setTextWithParagraphStyle( + text: String, + alignment: NSTextAlignment = .left, + font: ResourceKitFontConvertible.Font, + textColor: UIColor + ) { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = alignment + + let fontHeight = ResourceKitFontFamily.lineHeight(of: font) + paragraphStyle.maximumLineHeight = fontHeight + paragraphStyle.minimumLineHeight = fontHeight + + let attributes: [NSAttributedString.Key : Any] = [ + .paragraphStyle : paragraphStyle, + .font: font, + .foregroundColor: textColor, + .baselineOffset: (fontHeight - font.lineHeight) / 4 + ] + + debugPrint(font.lineHeight) + + let attrString = NSAttributedString(string: text, + attributes: attributes) + self.attributedText = attrString + } +} + diff --git a/Scripts/SwiftLintRunScript.sh b/Scripts/SwiftLintRunScript.sh index 79348dac..9cb71e9c 100755 --- a/Scripts/SwiftLintRunScript.sh +++ b/Scripts/SwiftLintRunScript.sh @@ -15,4 +15,4 @@ if which swiftlint >/dev/null; then else echo "warning: SwiftLint not installed, download from" -fi \ No newline at end of file +fi diff --git a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift index f3165b93..ebb59586 100644 --- a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift @@ -108,6 +108,7 @@ extension Project { iOSTargetVersion: String = "15.0.0", interfaceDependencies: [TargetDependency] = [], implementDependencies: [TargetDependency] = [], + demoAppDependencies: [TargetDependency] = [], useTestTarget: Bool = true, infoPlist: InfoPlist = .default, isUserInterface: Bool = true @@ -153,7 +154,7 @@ extension Project { [ .target(name: name), .target(name: "\(name)Impl"), - ] + ] + demoAppDependencies ) let testTarget = makeTestTarget(name: name,