From 6aebc02fd78d5f665136ab1187235f6384babdee Mon Sep 17 00:00:00 2001 From: J0onYEong Date: Thu, 18 Jul 2024 18:42:39 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[IDLE-180]=20=EC=84=BC=ED=84=B0=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=EB=B0=8F=20=EC=84=BC=ED=84=B0=20=EC=86=8C=EA=B0=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BDUI=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CenterFeatureDependency.swift | 13 + .../Domain/Entity/VO/AlertContentVO.swift | 24 + .../ExampleApp/Sources/SceneDelegate.swift | 2 +- .../ExampleApp/Sources/ViewController3.swift | 52 ++ .../EditPhoto.imageset/Contents.json | 21 + .../EditPhoto.imageset/EditImage.png | Bin 0 -> 1552 bytes .../location_small.imageset/Contents.json | 23 + .../location_image 1.png | Bin 0 -> 1166 bytes .../location_image 2.png | Bin 0 -> 1166 bytes .../location_image.png | Bin 0 -> 1166 bytes .../Component/Button/TextButtonType2.swift | 2 +- .../Component/Button/TextButtonType3.swift | 2 +- .../Component/ImageView/IdleImageView.swift | 29 ++ .../Sources/Component/Label/IdleLabel.swift | 5 +- .../Sources/Component/Stack/HStack.swift | 29 ++ .../Stack.swift => Stack/VStack.swift} | 3 +- .../TextField/IdleOneLineInputField.swift | 7 - .../TextField/MultiLineTextField.swift | 93 ++++ .../Resources/LaunchScreen.storyboard | 45 ++ .../ExampleApp/Sources/AppDelegate.swift | 36 ++ .../ExampleApp/Sources/SceneDelegate.swift | 30 ++ .../ExampleApp/Sources/ViewController.swift | 29 ++ .../Presentation/Feature/Center/Project.swift | 84 ++++ .../Feature/Center/Resources/Empty.md | 2 + .../Profile/CenterProfileViewController.swift | 470 ++++++++++++++++++ .../Profile/CenterProfileViewModel.swift | 196 ++++++++ .../Auth/RegisterSuccessOutputable.swift | 0 .../CTAButton.swift | 0 28 files changed, 1185 insertions(+), 12 deletions(-) create mode 100644 project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/CenterFeatureDependency.swift create mode 100644 project/Projects/Domain/Entity/VO/AlertContentVO.swift create mode 100644 project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift create mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/EditPhoto.imageset/Contents.json create mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/EditPhoto.imageset/EditImage.png create mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/Contents.json create mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/location_image 1.png create mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/location_image 2.png create mode 100644 project/Projects/Presentation/DSKit/Resources/Icons.xcassets/location_small.imageset/location_image.png create mode 100644 project/Projects/Presentation/DSKit/Sources/Component/ImageView/IdleImageView.swift create mode 100644 project/Projects/Presentation/DSKit/Sources/Component/Stack/HStack.swift rename project/Projects/Presentation/DSKit/Sources/Component/{UIExtension/Stack.swift => Stack/VStack.swift} (96%) create mode 100644 project/Projects/Presentation/DSKit/Sources/Component/TextField/MultiLineTextField.swift create mode 100644 project/Projects/Presentation/Feature/Center/ExampleApp/Resources/LaunchScreen.storyboard create mode 100644 project/Projects/Presentation/Feature/Center/ExampleApp/Sources/AppDelegate.swift create mode 100644 project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift create mode 100644 project/Projects/Presentation/Feature/Center/ExampleApp/Sources/ViewController.swift create mode 100644 project/Projects/Presentation/Feature/Center/Project.swift create mode 100644 project/Projects/Presentation/Feature/Center/Resources/Empty.md create mode 100644 project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift create mode 100644 project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift rename project/Projects/Presentation/PresentationCore/Sources/ViewModelType/{Constraint => InputOuputConstraint}/Auth/RegisterSuccessOutputable.swift (100%) rename project/Projects/Presentation/PresentationCore/Sources/ViewModelType/{Constraint => InputOuputConstraint}/CTAButton.swift (100%) diff --git a/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/CenterFeatureDependency.swift b/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/CenterFeatureDependency.swift new file mode 100644 index 00000000..7dac642d --- /dev/null +++ b/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/CenterFeatureDependency.swift @@ -0,0 +1,13 @@ +// +// CenterFeatureDependency.swift +// DependencyPlugin +// +// Created by 최준영 on 6/21/24. +// + +import ProjectDescription + +public extension ModuleDependency.Presentation { + + static let CenterFeature: TargetDependency = .project(target: "CenterFeature", path: .relativeToRoot("Projects/Presentation/Feature/Center")) +} diff --git a/project/Projects/Domain/Entity/VO/AlertContentVO.swift b/project/Projects/Domain/Entity/VO/AlertContentVO.swift new file mode 100644 index 00000000..7a4d34fe --- /dev/null +++ b/project/Projects/Domain/Entity/VO/AlertContentVO.swift @@ -0,0 +1,24 @@ +// +// AlertContentVO.swift +// Entity +// +// Created by choijunios on 7/18/24. +// + +import Foundation + +public struct DefaultAlertContentVO { + + public let title: String + public let message: String + + public init(title: String, message: String) { + self.title = title + self.message = message + } + + public static let `default` = DefaultAlertContentVO( + title: "오류", + message: "동작을 수행하지 못했습니다." + ) +} diff --git a/project/Projects/Presentation/DSKit/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/DSKit/ExampleApp/Sources/SceneDelegate.swift index d9dce24f..4691396d 100644 --- a/project/Projects/Presentation/DSKit/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/DSKit/ExampleApp/Sources/SceneDelegate.swift @@ -17,7 +17,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window = UIWindow(windowScene: windowScene) - window?.rootViewController = ViewController2() + window?.rootViewController = ViewController3() window?.makeKeyAndVisible() } } diff --git a/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift b/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift new file mode 100644 index 00000000..1319cc37 --- /dev/null +++ b/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift @@ -0,0 +1,52 @@ +// +// ViewController3.swift +// DSKitExampleApp +// +// Created by choijunios on 7/17/24. +// + +import UIKit +import DSKit +import RxSwift + +class ViewController3: UIViewController { + + let disposeBag = DisposeBag() + + override func viewDidLoad() { + + view.backgroundColor = .white + + view.layoutMargins = .init( + top: 0, + left: 20, + bottom: 0, + right: 20 + ) + + let field = MultiLineTextField(typography: .Body3, placeholderText: "Hello world") + + let label = IdleLabel(typography: .Body3) + label.textString = "엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장" + label.numberOfLines = 0 + [ + field, + label + ] + .forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + field.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + field.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + field.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + + label.topAnchor.constraint(equalTo: field.bottomAnchor, constant: 30), + label.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + label.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + ]) + } +} + diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/EditPhoto.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/EditPhoto.imageset/Contents.json new file mode 100644 index 00000000..266dfb04 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/EditPhoto.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "EditImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/EditPhoto.imageset/EditImage.png b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/EditPhoto.imageset/EditImage.png new file mode 100644 index 0000000000000000000000000000000000000000..aa701b013be6344a4658042723d4af3578602329 GIT binary patch literal 1552 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 { tapGesture.rx.event.asSignal() } + public var eventPublisher: Observable { tapGesture.rx.event.map { _ in () } } public init( labelText: String, diff --git a/project/Projects/Presentation/DSKit/Sources/Component/Button/TextButtonType3.swift b/project/Projects/Presentation/DSKit/Sources/Component/Button/TextButtonType3.swift index 60beee65..8934545d 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/Button/TextButtonType3.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/Button/TextButtonType3.swift @@ -15,7 +15,7 @@ public class TextButtonType3: IdleLabel { private var tapGesture: UITapGestureRecognizer! - public var eventPublisher: Signal { tapGesture.rx.event.asSignal().map { _ in () } } + public var eventPublisher: Observable { tapGesture.rx.event.map { _ in () } } public override init(typography: Typography) { diff --git a/project/Projects/Presentation/DSKit/Sources/Component/ImageView/IdleImageView.swift b/project/Projects/Presentation/DSKit/Sources/Component/ImageView/IdleImageView.swift new file mode 100644 index 00000000..cadbfbaa --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/Component/ImageView/IdleImageView.swift @@ -0,0 +1,29 @@ +// +// IdleImageView.swift +// DSKit +// +// Created by choijunios on 7/17/24. +// + +import UIKit + +public extension UIImageView { + + static let backButton: UIImageView = { + let view = UIImageView(image: DSKitAsset.Icons.back.image) + view.contentMode = .scaleAspectFit + return view + }() + + static let locationMark: UIImageView = { + let view = UIImageView(image: DSKitAsset.Icons.locationSmall.image) + view.contentMode = .scaleAspectFit + return view + }() + + static let editPhotoImage: UIImageView = { + let view = UIImageView(image: DSKitAsset.Icons.editPhoto.image) + view.contentMode = .scaleAspectFit + return view + }() +} diff --git a/project/Projects/Presentation/DSKit/Sources/Component/Label/IdleLabel.swift b/project/Projects/Presentation/DSKit/Sources/Component/Label/IdleLabel.swift index d895d18e..c9731718 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/Label/IdleLabel.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/Label/IdleLabel.swift @@ -32,7 +32,10 @@ public class IdleLabel: UILabel { let size = super.intrinsicContentSize - return CGSize(width: size.width, height: typography.lineHeight * CGFloat(currentLineCount)) + if currentLineCount != 0 { + return CGSize(width: size.width, height: typography.lineHeight * CGFloat(currentLineCount)) + } + return super.intrinsicContentSize } public var typography: Typography { diff --git a/project/Projects/Presentation/DSKit/Sources/Component/Stack/HStack.swift b/project/Projects/Presentation/DSKit/Sources/Component/Stack/HStack.swift new file mode 100644 index 00000000..4e511120 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/Component/Stack/HStack.swift @@ -0,0 +1,29 @@ +// +// HStack.swift +// DSKit +// +// Created by choijunios on 7/18/24. +// + +import UIKit + +public class HStack: UIStackView { + + public init(_ elements: [UIView], spacing: CGFloat = 0.0, alignment: UIStackView.Alignment = .center) { + + super.init(frame: .zero) + + self.spacing = spacing + self.axis = .horizontal + self.distribution = .fill + self.alignment = alignment + + elements + .forEach { + self.addArrangedSubview($0) + } + } + + required init(coder: NSCoder) { fatalError() } +} + diff --git a/project/Projects/Presentation/DSKit/Sources/Component/UIExtension/Stack.swift b/project/Projects/Presentation/DSKit/Sources/Component/Stack/VStack.swift similarity index 96% rename from project/Projects/Presentation/DSKit/Sources/Component/UIExtension/Stack.swift rename to project/Projects/Presentation/DSKit/Sources/Component/Stack/VStack.swift index d7c30d3f..aaf52921 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/UIExtension/Stack.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/Stack/VStack.swift @@ -1,5 +1,5 @@ // -// Stack.swift +// VStack.swift // DSKit // // Created by choijunios on 7/15/24. @@ -27,3 +27,4 @@ public class VStack: UIStackView { required init(coder: NSCoder) { fatalError() } } + diff --git a/project/Projects/Presentation/DSKit/Sources/Component/TextField/IdleOneLineInputField.swift b/project/Projects/Presentation/DSKit/Sources/Component/TextField/IdleOneLineInputField.swift index c04817bb..d96d779d 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/TextField/IdleOneLineInputField.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/TextField/IdleOneLineInputField.swift @@ -9,13 +9,6 @@ import UIKit import RxSwift import RxCocoa -/// 총 Height 44(42 + border(1pt)x2) -/// -/// TextBox사이즈 24 -/// inset -/// vertical: 11 -/// horizontal: 20 - public class IdleOneLineInputField: UIView { public var isEnabled: Bool = true diff --git a/project/Projects/Presentation/DSKit/Sources/Component/TextField/MultiLineTextField.swift b/project/Projects/Presentation/DSKit/Sources/Component/TextField/MultiLineTextField.swift new file mode 100644 index 00000000..1f4e79bc --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/Component/TextField/MultiLineTextField.swift @@ -0,0 +1,93 @@ +// +// MultiLineTextField.swift +// DSKit +// +// Created by choijunios on 7/17/24. +// + +import UIKit +import RxSwift +import RxCocoa + +public class MultiLineTextField: UITextView { + + private var currentText: String = "" + + public var placeholderText: String + public let typography: Typography + + public var textString: String { + get { + return currentText + } + set { + currentText = newValue + updateText() + } + } + + public init(typography: Typography, placeholderText: String = "") { + self.placeholderText = placeholderText + self.typography = typography + + super.init(frame: .zero, textContainer: nil) + + setAppearance() + setPlaceholderText(textView: self) + } + + required init?(coder: NSCoder) { fatalError() } + + public override var intrinsicContentSize: CGSize { + .init(width: super.intrinsicContentSize.width, height: 156) + } + + + func setAppearance() { + // Delegate + self.delegate = self + + // border + self.layer.borderColor = DSKitAsset.Colors.gray100.color.cgColor + self.layer.borderWidth = 1.0 + self.layer.cornerRadius = 6 + + // textContainer + self.textContainerInset = .init(top: 12, left: 16, bottom: 16, right: 16) + self.textContainer.lineFragmentPadding = 0 + + // font + self.typingAttributes = typography.attributes + + // Scroll + self.isScrollEnabled = true + } + + private func updateText() { + self.rx.attributedText.onNext(NSAttributedString(string: textString, attributes: typography.attributes)) + } +} + +extension MultiLineTextField: UITextViewDelegate { + + // UITextViewDelegate 메서드: 텍스트 뷰가 편집을 시작할 때 호출 + public func textViewDidBeginEditing(_ textView: UITextView) { + if textView.text == placeholderText { + textView.attributedText = .none + textView.textColor = DSKitAsset.Colors.gray900.color + } + } + + // UITextViewDelegate 메서드: 텍스트 뷰가 편집을 끝낼 때 호출 + public func textViewDidEndEditing(_ textView: UITextView) { + setPlaceholderText(textView: textView) + } + + private func setPlaceholderText(textView: UITextView) { + if textView.attributedText.string.isEmpty { + textView.attributedText = self.typography.attributes.toString(placeholderText) + textView.textColor = DSKitAsset.Colors.gray200.color + } + } + +} diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Resources/LaunchScreen.storyboard b/project/Projects/Presentation/Feature/Center/ExampleApp/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000..a2157a3e --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Resources/LaunchScreen.storyboard @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/AppDelegate.swift b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/AppDelegate.swift new file mode 100644 index 00000000..00267bb5 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// +// +// Created by 최준영 on 6/19/24. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift new file mode 100644 index 00000000..29606e91 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift @@ -0,0 +1,30 @@ +// +// SceneDelegate.swift +// +// +// Created by 최준영 on 6/19/24. +// + +import UIKit +import CenterFeature + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + + guard let windowScene = scene as? UIWindowScene else { return } + + + window = UIWindow(windowScene: windowScene) + + let viewModel = CenterProfileViewModel() + let viewController = CenterProfileViewController() + + viewController.bind(viewModel: viewModel) + + window?.rootViewController = viewController + window?.makeKeyAndVisible() + } +} diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/ViewController.swift b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/ViewController.swift new file mode 100644 index 00000000..e439d432 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/ViewController.swift @@ -0,0 +1,29 @@ +// +// ViewController.swift +// +// +// Created by 최준영 on 6/19/24. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + + let initialLabel = UILabel() + + initialLabel.text = "Example app" + + view.backgroundColor = .white + + view.addSubview(initialLabel) + initialLabel.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + initialLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + initialLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } +} + diff --git a/project/Projects/Presentation/Feature/Center/Project.swift b/project/Projects/Presentation/Feature/Center/Project.swift new file mode 100644 index 00000000..5c87b31d --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Project.swift @@ -0,0 +1,84 @@ +// +// Project.swift +// ProjectDescriptionHelpers +// +// Created by choijunios on 2024/07/17 +// + +import ProjectDescription +import ProjectDescriptionHelpers +import ConfigurationPlugin +import DependencyPlugin + +let project = Project( + name: "Center", + settings: .settings( + configurations: IdleConfiguration.emptyConfigurations + ), + targets: [ + + /// FeatureConcrete + .target( + name: "CenterFeature", + destinations: DeploymentSettings.platform, + product: .staticFramework, + bundleId: "$(PRODUCT_BUNDLE_IDENTIFIER)", + deploymentTargets: DeploymentSettings.deployment_version, + sources: ["Sources/**"], + resources: ["Resources/**"], + dependencies: [ + // Presentation + D.Presentation.PresentationCore, + D.Presentation.DSKit, + + // Domain + D.Domain.UseCaseInterface, + D.Domain.RepositoryInterface, + + // ThirdParty + D.ThirdParty.RxSwift, + D.ThirdParty.RxCocoa, + ], + settings: .settings( + configurations: IdleConfiguration.presentationConfigurations + ) + ), + + /// FeatureConcrete ExampleApp + .target( + name: "Center_ExampleApp", + destinations: DeploymentSettings.platform, + product: .app, + bundleId: "$(PRODUCT_BUNDLE_IDENTIFIER)", + deploymentTargets: DeploymentSettings.deployment_version, + infoPlist: IdleInfoPlist.exampleAppDefault, + sources: ["ExampleApp/Sources/**"], + resources: ["ExampleApp/Resources/**"], + dependencies: [ + .target(name: "CenterFeature"), + + D.Domain.ConcreteUseCase, + D.Data.ConcreteRepository, + ], + settings: .settings( + configurations: IdleConfiguration.presentationConfigurations + ) + ), + ], + schemes: [ + Scheme.makeSchemes( + .target("CenterFeature"), + configNames: [ + IdleConfiguration.debugConfigName, + IdleConfiguration.releaseConfigName + ] + ), + Scheme.makeSchemes( + .target("Center_ExampleApp"), + configNames: [ + IdleConfiguration.debugConfigName, + IdleConfiguration.releaseConfigName + ] + ) + ].flatMap { $0 } +) diff --git a/project/Projects/Presentation/Feature/Center/Resources/Empty.md b/project/Projects/Presentation/Feature/Center/Resources/Empty.md new file mode 100644 index 00000000..64e53d46 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Resources/Empty.md @@ -0,0 +1,2 @@ +# <#Title#> + diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift new file mode 100644 index 00000000..59f900b1 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift @@ -0,0 +1,470 @@ +// +// CenterProfileViewController.swift +// CenterFeature +// +// Created by choijunios on 7/17/24. +// + +import UIKit +import PresentationCore +import RxSwift +import RxCocoa +import DSKit +import Entity + +public protocol CenterProfileViewModelable where Input: CenterProfileInputable, Output: CenterProfileOutputable { + associatedtype Input + associatedtype Output + var input: Input { get } + var output: Output? { get } +} + +public protocol CenterProfileInputable { + var editingButtonPressed: PublishRelay { get } + var editingFinishButtonPressed: PublishRelay { get } + var editingPhoneNumber: BehaviorRelay { get } + var editingInstruction: BehaviorRelay { get } + var editingImage: BehaviorRelay { get } +} + +public protocol CenterProfileOutputable { + var centerName: Driver { get } + var centerLocation: Driver { get } + var centerPhoneNumber: Driver { get } + var centerIntroduction: Driver { get } + var centerImage: Driver { get } + var isEditingMode: Driver { get } + var editingValidation: Driver { get } + var alert: Driver { get } +} + +public class CenterProfileViewController: DisposableViewController { + + var viewModel: (any CenterProfileViewModelable)? + + let navigationBar: NavigationBarType1 = { + let bar = NavigationBarType1(navigationTitle: "내 센터 정보") + return bar + }() + + let editingCompleteButton: TextButtonType3 = { + let btn = TextButtonType3(typography: .Subtitle2) + btn.textString = "저장" + btn.attrTextColor = DSKitAsset.Colors.orange500.color + return btn + }() + + // View + + /// Center name label + let centerNameLabel: IdleLabel = { + let label = IdleLabel(typography: .Heading1) + return label + }() + + /// Center location label + let centerLocationLabel: IdleLabel = { + let label = IdleLabel(typography: .Body2) + + return label + }() + + /// ☑️ 센터 상세정보 ☑️ + let centerDetailLabel: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.textString = "센터 상세 정보" + return label + }() + let profileEditButton: TextButtonType2 = { + let button = TextButtonType2(labelText: "수정하기") + + button.label.typography = .Body3 + button.label.attrTextColor = DSKitAsset.Colors.gray300.color + button.layoutMargins = .init(top: 5.5, left:12, bottom: 5.5, right: 12) + button.layer.cornerRadius = 16 + return button + }() + + /// ☑️ "전화번호" 라벨 ☑️ + let centerPhoneNumeberTitleLabel: IdleLabel = { + let label = IdleLabel(typography: .Subtitle4) + label.textString = "전화번호" + label.textColor = DSKitAsset.Colors.gray500.color + return label + }() + + /// 센터 전화번호가 표시되는 라벨 + let centerPhoneNumeberLabel: IdleLabel = { + let label = IdleLabel(typography: .Body3) + return label + }() + /// 센터 전화번호를 편집할 수 있는 텍스트 필드 + let centerPhoneNumeberField: IdleOneLineInputField = { + let field = IdleOneLineInputField(placeHolderText: "") + + return field + }() + + /// ☑️ "센토 소개" 라벨 ☑️ + let centerIntroductionTitleLabel: IdleLabel = { + let label = IdleLabel(typography: .Subtitle4) + label.textString = "센터 소개" + label.textColor = DSKitAsset.Colors.gray500.color + return label + }() + + /// 센터 소개가 표시되는 라벨 + let centerIntroductionLabel: IdleLabel = { + let label = IdleLabel(typography: .Body3) + return label + }() + /// 센터 소개를 수정하는 텍스트 필드 + let centerIntroductionTextView: MultiLineTextField = { + let textView = MultiLineTextField( + typography: .Body3, + placeholderText: "추가적으로 요구사항이 있다면 작성해주세요." + ) + return textView + }() + + /// ☑️ "센토 사진" 라벨 ☑️ + let centerPictureLabel: IdleLabel = { + let label = IdleLabel(typography: .Subtitle4) + label.textString = "센터 사진" + label.textColor = DSKitAsset.Colors.gray500.color + return label + }() + let centerImageView: UIImageView = { + let view = UIImageView() + view.layer.cornerRadius = 6 + view.clipsToBounds = true + view.backgroundColor = DSKitAsset.Colors.gray100.color + return view + }() + let centerImageEditButton: UIButton = { + let btn = UIButton() + btn.setImage(DSKitAsset.Icons.editPhoto.image, for: .normal) + return btn + }() + + let edtingImage: PublishRelay = .init() + + public init() { + + super.init(nibName: nil, bundle: nil) + + setApearance() + setAutoLayout() + } + + required init?(coder: NSCoder) { fatalError() } + + func setApearance() { + view.backgroundColor = .white + } + + func setAutoLayout() { + + let navigationStack = HStack([ + navigationBar, + editingCompleteButton, + ]) + navigationStack.distribution = .equalSpacing + navigationStack.backgroundColor = .white + + let navigationStackBackground = UIView() + navigationStackBackground.addSubview(navigationStack) + navigationStack.translatesAutoresizingMaskIntoConstraints = false + navigationStackBackground.backgroundColor = .white + navigationStackBackground.layoutMargins = .init(top: 0, left: 12, bottom: 0, right: 28) + NSLayoutConstraint.activate([ + navigationStack.topAnchor.constraint(equalTo: navigationStackBackground.layoutMarginsGuide.topAnchor), + navigationStack.leadingAnchor.constraint(equalTo: navigationStackBackground.layoutMarginsGuide.leadingAnchor), + navigationStack.trailingAnchor.constraint(equalTo: navigationStackBackground.layoutMarginsGuide.trailingAnchor), + navigationStack.bottomAnchor.constraint(equalTo: navigationStackBackground.layoutMarginsGuide.bottomAnchor), + ]) + + let locationIcon = UIImageView.locationMark + + let centerLocationStack = HStack( + [ + locationIcon, + centerLocationLabel + ], + spacing: 2, + alignment: .center + ) + + let centerPhoneNumberStack = VStack( + [ + centerPhoneNumeberTitleLabel, + centerPhoneNumeberLabel, + centerPhoneNumeberField, + ], + spacing: 6, + alignment: .fill + ) + + let centerIntroductionStack = VStack( + [ + centerIntroductionTitleLabel, + centerIntroductionLabel, + centerIntroductionTextView, + ], + spacing: 6, + alignment: .fill + ) + + // 센터 이미지뷰 세팅 + centerImageView.addSubview(centerImageEditButton) + centerImageEditButton.translatesAutoresizingMaskIntoConstraints = false + + let scrollView = UIScrollView() + + let divider = UIView() + divider.backgroundColor = DSKitAsset.Colors.gray050.color + + [ + centerNameLabel, + centerLocationStack, + + divider, + + centerDetailLabel, + profileEditButton, + + centerPhoneNumberStack, + + centerIntroductionStack, + + centerPictureLabel, + centerImageView + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview($0) + } + + [ + navigationStackBackground, + scrollView + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + // view 서브뷰 zindex설정 + navigationStackBackground.layer.zPosition = 1 + scrollView.layer.zPosition = 0 + + // 전체 뷰 + NSLayoutConstraint.activate([ + navigationStackBackground.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + navigationStackBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor), + navigationStackBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + scrollView.topAnchor.constraint(equalTo: navigationStackBackground.bottomAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + // 뷰 고정 사이즈 + NSLayoutConstraint.activate([ + locationIcon.widthAnchor.constraint(equalToConstant: 24), + locationIcon.heightAnchor.constraint(equalTo: locationIcon.widthAnchor), + + centerImageEditButton.widthAnchor.constraint(equalToConstant: 28), + centerImageEditButton.heightAnchor.constraint(equalTo: centerImageEditButton.widthAnchor), + ]) + + let contentGuide = scrollView.contentLayoutGuide + scrollView.layoutMargins = .init(top: 0, left: 20, bottom: 0, right: 20) + + // 스크롤 뷰의 서브뷰 + NSLayoutConstraint.activate([ + + centerNameLabel.topAnchor.constraint(equalTo: contentGuide.topAnchor, constant: 25), + centerNameLabel.leadingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.leadingAnchor), + + centerLocationStack.topAnchor.constraint(equalTo: centerNameLabel.bottomAnchor, constant: 12), + centerLocationStack.leadingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.leadingAnchor), + + divider.topAnchor.constraint(equalTo: centerLocationStack.bottomAnchor, constant: 20), + divider.leadingAnchor.constraint(equalTo: view.leadingAnchor), + divider.trailingAnchor.constraint(equalTo: view.trailingAnchor), + divider.heightAnchor.constraint(equalToConstant: 8), + + centerDetailLabel.topAnchor.constraint(equalTo: divider.bottomAnchor, constant: 24), + centerDetailLabel.leadingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.leadingAnchor), + + profileEditButton.topAnchor.constraint(equalTo: divider.bottomAnchor, constant: 24), + profileEditButton.trailingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.trailingAnchor), + + centerPhoneNumberStack.topAnchor.constraint(equalTo: centerDetailLabel.bottomAnchor, constant: 20), + centerPhoneNumberStack.leadingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.leadingAnchor), + centerPhoneNumberStack.trailingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.trailingAnchor), + + centerIntroductionStack.topAnchor.constraint(equalTo: centerPhoneNumberStack.bottomAnchor, constant: 20), + centerIntroductionStack.leadingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.leadingAnchor), + centerIntroductionStack.trailingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.trailingAnchor), + + centerPictureLabel.topAnchor.constraint(equalTo: centerIntroductionStack.bottomAnchor, constant: 20), + centerPictureLabel.leadingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.leadingAnchor), + + centerImageView.topAnchor.constraint(equalTo: centerPictureLabel.bottomAnchor, constant: 20), + + centerImageView.leadingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.leadingAnchor), + centerImageView.trailingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.trailingAnchor), + centerImageView.heightAnchor.constraint(equalToConstant: 250), + centerImageView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -38), + + centerImageEditButton.trailingAnchor.constraint(equalTo: centerImageView.trailingAnchor, constant: -16), + centerImageEditButton.bottomAnchor.constraint(equalTo: centerImageView.bottomAnchor, constant: -16), + ]) + } + + let disposBag = DisposeBag() + + public func bind(viewModel: any CenterProfileViewModelable) { + + self.viewModel = viewModel + + // input + let input = viewModel.input + + profileEditButton + .eventPublisher + .bind(to: input.editingButtonPressed) + .disposed(by: disposBag) + + editingCompleteButton + .eventPublisher + .bind(to: input.editingFinishButtonPressed) + .disposed(by: disposBag) + + centerPhoneNumeberField.textField.rx.text + .compactMap { $0 } + .bind(to: input.editingPhoneNumber) + .disposed(by: disposBag) + + centerIntroductionTextView.rx.text + .compactMap { $0 } + .bind(to: input.editingInstruction) + .disposed(by: disposBag) + + edtingImage + .bind(to: input.editingImage) + .disposed(by: disposBag) + + // output + guard let output = viewModel.output else { fatalError() } + + output + .centerName + .drive(centerNameLabel.rx.textString) + .disposed(by: disposBag) + + output + .centerLocation + .drive(centerLocationLabel.rx.textString) + .disposed(by: disposBag) + + output + .centerPhoneNumber + .drive(centerPhoneNumeberLabel.rx.textString) + .disposed(by: disposBag) + output + .centerPhoneNumber + .drive(centerPhoneNumeberField.textField.rx.text) + .disposed(by: disposBag) + + output + .centerIntroduction + .drive(centerIntroductionLabel.rx.textString) + .disposed(by: disposBag) + output + .centerIntroduction + .drive(centerIntroductionTextView.rx.textString) + .disposed(by: disposBag) + + output + .centerImage + .drive(centerImageView.rx.image) + .disposed(by: disposBag) + + // MARK: Edit Mode + output + .isEditingMode + .map { !$0 } + .drive(centerPhoneNumeberField.rx.isHidden) + .disposed(by: disposBag) + output + .isEditingMode + .drive(centerPhoneNumeberLabel.rx.isHidden) + .disposed(by: disposBag) + + output + .isEditingMode + .map { !$0 } + .drive(centerIntroductionTextView.rx.isHidden) + .disposed(by: disposBag) + output + .isEditingMode + .drive(centerIntroductionLabel.rx.isHidden) + .disposed(by: disposBag) + + output + .isEditingMode + .map { !$0 } + .drive(centerImageEditButton.rx.isHidden) + .disposed(by: disposBag) + + output + .isEditingMode + .map { !$0 } + .drive(editingCompleteButton.rx.isHidden) + .disposed(by: disposBag) + output + .isEditingMode + .drive(profileEditButton.rx.isHidden) + .disposed(by: disposBag) + + + output + .alert + .drive { [weak self] vo in + self?.showAlert(vo: vo) + } + .disposed(by: disposBag) + + output + .editingValidation + .drive { _ in + // do something when editing success + } + .disposed(by: disposBag) + } + + public func showAlert(vo: DefaultAlertContentVO) { + let alret = UIAlertController(title: vo.title, message: vo.message, preferredStyle: .alert) + let close = UIAlertAction(title: "닫기", style: .default, handler: nil) + alret.addAction(close) + present(alret, animated: true, completion: nil) + } + + public func cleanUp() { + + } +} + +extension CenterProfileViewController { + + private func onEditMode() { + + } + + private func onDisplayMode() { + editingCompleteButton.isHidden = true + profileEditButton.isHidden = false + + } +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift new file mode 100644 index 00000000..d5d288ac --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift @@ -0,0 +1,196 @@ +// +// CenterProfileViewModel.swift +// CenterFeature +// +// Created by choijunios on 7/18/24. +// + +import UIKit +import Entity +import RxSwift +import RxCocoa +import PresentationCore + +public struct ChangeCenterInformation { + let phoneNumber: String? + let introduction: String? + let image: UIImage? +} + +public class CenterProfileViewModel: CenterProfileViewModelable { + + public var input: Input + public var output: Output? = nil + + func checkModification( + prev_phoneNumber: String, + prev_introduction: String, + prev_image: UIImage) -> (String?, String?, UIImage?) + { + ( + input.editingPhoneNumber.value == prev_phoneNumber ? nil : input.editingPhoneNumber.value, + input.editingInstruction.value == prev_introduction ? nil : input.editingInstruction.value, + input.editingImage.value == prev_image ? nil : input.editingImage.value + ) + } + + public init() { + self.input = Input() + + let centerName = BehaviorRelay(value: "") + let centerLocation = BehaviorRelay(value: "") + let centerPhoneNumber = BehaviorRelay(value: "") + let centerIntroduction = BehaviorRelay(value: "") + let centerImage = BehaviorRelay(value: .init()) + + // 서버로 부터 데이터를 요청하는 API + centerName.accept("네 얼간이 방문요양센터") + centerLocation.accept("강남구 삼성동 512-3") + centerPhoneNumber.accept("(02) 123-4567") + centerIntroduction.accept("안녕하세요 반갑습니다!") + centerImage.accept(UIImage()) + + + // 최신 값들 + 버튼이 눌릴 경우 변경 로직이 실행된다. + let editingRequestResult = input + .editingFinishButtonPressed + .map({ [unowned self] _ in + self.checkModification( + prev_phoneNumber: centerPhoneNumber.value, + prev_introduction: centerIntroduction.value, + prev_image: centerImage.value + ) + }) + .flatMap { (inputs) in + + let (phoneNumber, introduction, image) = inputs + + // 변경이 발생하지 않은 곳은 nil값이 전달된다. + + // API 호출 + return Single.just(Result.success( + ChangeCenterInformation( + phoneNumber: phoneNumber, + introduction: introduction, + image: image + ) + )) + } + .share() + + // 스트림을 유지하기위해 생성한 Driver로 필수적으로 사용되지 않는다. + let editingValidation = editingRequestResult + .compactMap { $0.value } + .map { info in + + if let phoneNumber = info.phoneNumber { + printIfDebug("✅ 전화번호 변경 반영되었음") + centerPhoneNumber.accept(phoneNumber) + } + + if let introduction = info.introduction { + printIfDebug("✅ 센터소개 반영되었음") + centerIntroduction.accept(introduction) + } + + if let image = info.image { + printIfDebug("✅ 센터 이미지 변경 반영되었음") + centerImage.accept(image) + } + + return () + } + .asDriver(onErrorJustReturn: ()) + + enum Mode { + case editing, display + } + + let initialMode = BehaviorRelay(value: Mode.display) + + let buttonPress = Observable + .merge( + input.editingButtonPressed.map { Mode.editing }, + input.editingFinishButtonPressed.map { Mode.display } + ) + .map { mode in + switch mode { + case .editing: + return true + case .display: + return false + } + } + + let isEditingMode = Observable + .merge( + initialMode.map({ $0 == .editing }), + buttonPress + ) + .asDriver(onErrorJustReturn: false) + + + let alertDriver = editingRequestResult + .compactMap({ $0.error }) + .map({ error in + // 변경 실패 Alert + return DefaultAlertContentVO( + title: "변경 실패", + message: "변경 싪패 이유" + ) + }) + .asDriver(onErrorJustReturn: .default) + + self.output = .init( + centerName: centerName.asDriver(onErrorJustReturn: ""), + centerLocation: centerLocation.asDriver(onErrorJustReturn: ""), + centerPhoneNumber: centerPhoneNumber.asDriver(onErrorJustReturn: ""), + centerIntroduction: centerIntroduction.asDriver(onErrorJustReturn: ""), + centerImage: centerImage.asDriver(onErrorJustReturn: UIImage()), + isEditingMode: isEditingMode, + editingValidation: editingValidation, + alert: alertDriver + ) + } +} + + +public extension CenterProfileViewModel { + + class Input: CenterProfileInputable { + // 모드설정 + public var editingButtonPressed: PublishRelay = .init() + public var editingFinishButtonPressed: PublishRelay = .init() + public var editingPhoneNumber: BehaviorRelay = .init(value: "") + public var editingInstruction: BehaviorRelay = .init(value: "") + public var editingImage: BehaviorRelay = .init(value: .init()) + } + + class Output: CenterProfileOutputable { + // 기본 데이터 + public var centerName: Driver + public var centerLocation: Driver + public var centerPhoneNumber: Driver + public var centerIntroduction: Driver + public var centerImage: Driver + + // 수정 상태 여부 + public var isEditingMode: Driver + + // 요구사항 X + public var editingValidation: Driver + + public var alert: Driver + + init(centerName: Driver, centerLocation: Driver, centerPhoneNumber: Driver, centerIntroduction: Driver, centerImage: Driver, isEditingMode: Driver, editingValidation: Driver, alert: Driver) { + self.centerName = centerName + self.centerLocation = centerLocation + self.centerPhoneNumber = centerPhoneNumber + self.centerIntroduction = centerIntroduction + self.centerImage = centerImage + self.isEditingMode = isEditingMode + self.editingValidation = editingValidation + self.alert = alert + } + } +} diff --git a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/Constraint/Auth/RegisterSuccessOutputable.swift b/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/Auth/RegisterSuccessOutputable.swift similarity index 100% rename from project/Projects/Presentation/PresentationCore/Sources/ViewModelType/Constraint/Auth/RegisterSuccessOutputable.swift rename to project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/Auth/RegisterSuccessOutputable.swift diff --git a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/Constraint/CTAButton.swift b/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/CTAButton.swift similarity index 100% rename from project/Projects/Presentation/PresentationCore/Sources/ViewModelType/Constraint/CTAButton.swift rename to project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/CTAButton.swift From b167a252237b5c02684cf30d55c494139a6b14e4 Mon Sep 17 00:00:00 2001 From: J0onYEong Date: Thu, 18 Jul 2024 22:25:59 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[IDLE-180]=20=EC=82=AC=EC=A7=84=EC=95=B1?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=94=84=EB=A1=9C=ED=95=84=EC=9D=84=20?= =?UTF-8?q?=EB=B6=88=EB=9F=AC=EB=93=A4=EC=9D=B4=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectDescriptionHelpers/InfoPlist.swift | 1 + .../ExampleApp/Sources/ViewController3.swift | 13 +- .../Component/Label/IdleTextField.swift | 12 ++ .../TextField/MultiLineTextField.swift | 33 ++++- .../Profile/CenterProfileViewController.swift | 120 ++++++++++++------ .../Profile/CenterProfileViewModel.swift | 56 ++++---- 6 files changed, 164 insertions(+), 71 deletions(-) diff --git a/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift index 3f03edf7..e7737ab5 100644 --- a/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift +++ b/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift @@ -29,6 +29,7 @@ public enum IdleInfoPlist { ]) public static let exampleAppDefault: InfoPlist = .extendingDefault(with: [ + "Privacy - Photo Library Usage Description" : "프로필 사진 설정을 위해 사진 라이브러리에 접근합니다.", "UILaunchStoryboardName": "LaunchScreen.storyboard", "CFBundleDisplayName" : "$(BUNDLE_DISPLAY_NAME)", "UIApplicationSceneManifest": [ diff --git a/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift b/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift index 1319cc37..bedb8399 100644 --- a/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift +++ b/project/Projects/Presentation/DSKit/ExampleApp/Sources/ViewController3.swift @@ -29,9 +29,17 @@ class ViewController3: UIViewController { let label = IdleLabel(typography: .Body3) label.textString = "엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장엄청나게 긴 문장" label.numberOfLines = 0 + + let centerImageEditButton: UIButton = { + let btn = UIButton() + btn.setImage(DSKitAsset.Icons.editPhoto.image, for: .normal) + btn.isUserInteractionEnabled = true + return btn + }() [ field, - label + label, + centerImageEditButton ] .forEach { $0.translatesAutoresizingMaskIntoConstraints = false @@ -46,6 +54,9 @@ class ViewController3: UIViewController { label.topAnchor.constraint(equalTo: field.bottomAnchor, constant: 30), label.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), label.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + + centerImageEditButton.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 30), + centerImageEditButton.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), ]) } } diff --git a/project/Projects/Presentation/DSKit/Sources/Component/Label/IdleTextField.swift b/project/Projects/Presentation/DSKit/Sources/Component/Label/IdleTextField.swift index 8c3a31d1..53ec7ca2 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/Label/IdleTextField.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/Label/IdleTextField.swift @@ -18,6 +18,15 @@ public class IdleTextField: UITextField { bottom: 10, right: 24 ) + public var textString: String { + get { + return currentText + } + set { + currentText = newValue + updateText() + } + } public init(typography: Typography) { @@ -99,6 +108,9 @@ public class IdleTextField: UITextField { ) } } + private func updateText() { + self.rx.attributedText.onNext(NSAttributedString(string: textString, attributes: typography.attributes)) + } } diff --git a/project/Projects/Presentation/DSKit/Sources/Component/TextField/MultiLineTextField.swift b/project/Projects/Presentation/DSKit/Sources/Component/TextField/MultiLineTextField.swift index 1f4e79bc..ba6ca60f 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/TextField/MultiLineTextField.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/TextField/MultiLineTextField.swift @@ -26,6 +26,8 @@ public class MultiLineTextField: UITextView { } } + private let disposeBag = DisposeBag() + public init(typography: Typography, placeholderText: String = "") { self.placeholderText = placeholderText self.typography = typography @@ -38,11 +40,6 @@ public class MultiLineTextField: UITextView { required init?(coder: NSCoder) { fatalError() } - public override var intrinsicContentSize: CGSize { - .init(width: super.intrinsicContentSize.width, height: 156) - } - - func setAppearance() { // Delegate self.delegate = self @@ -63,6 +60,32 @@ public class MultiLineTextField: UITextView { self.isScrollEnabled = true } + public func addToolbar() { + // TextField toolbar + let toolbar = UIToolbar() + toolbar.sizeToFit() + + // flexibleSpace 추가 + let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + + let closeButton = UIBarButtonItem() + closeButton.title = "완료" + closeButton.style = .done + toolbar.setItems([ + flexibleSpace, + closeButton + ], animated: false) + toolbar.isUserInteractionEnabled = true + + self.inputAccessoryView = toolbar + + closeButton.rx.tap.subscribe { [weak self] _ in + + self?.resignFirstResponder() + } + .disposed(by: disposeBag) + } + private func updateText() { self.rx.attributedText.onNext(NSAttributedString(string: textString, attributes: typography.attributes)) } diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift index 59f900b1..051fbff9 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift @@ -17,6 +17,8 @@ public protocol CenterProfileViewModelable where Input: CenterProfileInputable, associatedtype Output var input: Input { get } var output: Output? { get } + + func requestData() } public protocol CenterProfileInputable { @@ -38,7 +40,7 @@ public protocol CenterProfileOutputable { var alert: Driver { get } } -public class CenterProfileViewController: DisposableViewController { +public class CenterProfileViewController: DisposableViewController { var viewModel: (any CenterProfileViewModelable)? @@ -99,10 +101,14 @@ public class CenterProfileViewController: DisposableViewController { return label }() /// 센터 전화번호를 편집할 수 있는 텍스트 필드 - let centerPhoneNumeberField: IdleOneLineInputField = { - let field = IdleOneLineInputField(placeHolderText: "") - - return field + let centerPhoneNumeberField: MultiLineTextField = { + let textView = MultiLineTextField( + typography: .Body3, + placeholderText: "추가적으로 요구사항이 있다면 작성해주세요." + ) + textView.textContainerInset = .init(top: 10, left: 16, bottom: 10, right: 24) + textView.isScrollEnabled = false + return textView }() /// ☑️ "센토 소개" 라벨 ☑️ @@ -144,17 +150,21 @@ public class CenterProfileViewController: DisposableViewController { let centerImageEditButton: UIButton = { let btn = UIButton() btn.setImage(DSKitAsset.Icons.editPhoto.image, for: .normal) + btn.isUserInteractionEnabled = true return btn }() let edtingImage: PublishRelay = .init() + let disposeBag = DisposeBag() + public init() { super.init(nibName: nil, bundle: nil) setApearance() setAutoLayout() + setObservable() } required init?(coder: NSCoder) { fatalError() } @@ -216,8 +226,8 @@ public class CenterProfileViewController: DisposableViewController { ) // 센터 이미지뷰 세팅 - centerImageView.addSubview(centerImageEditButton) - centerImageEditButton.translatesAutoresizingMaskIntoConstraints = false +// centerImageView.addSubview(centerImageEditButton) +// centerImageEditButton.translatesAutoresizingMaskIntoConstraints = false let scrollView = UIScrollView() @@ -238,7 +248,8 @@ public class CenterProfileViewController: DisposableViewController { centerIntroductionStack, centerPictureLabel, - centerImageView + centerImageView, + centerImageEditButton ].forEach { $0.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview($0) @@ -274,10 +285,13 @@ public class CenterProfileViewController: DisposableViewController { centerImageEditButton.widthAnchor.constraint(equalToConstant: 28), centerImageEditButton.heightAnchor.constraint(equalTo: centerImageEditButton.widthAnchor), + + centerIntroductionTextView.heightAnchor.constraint(equalToConstant: 156), ]) let contentGuide = scrollView.contentLayoutGuide scrollView.layoutMargins = .init(top: 0, left: 20, bottom: 0, right: 20) + scrollView.delaysContentTouches = false // 스크롤 뷰의 서브뷰 NSLayoutConstraint.activate([ @@ -322,7 +336,15 @@ public class CenterProfileViewController: DisposableViewController { ]) } - let disposBag = DisposeBag() + func setObservable() { + + centerImageEditButton + .rx.tap + .subscribe { [weak self] _ in + self?.showPhotoGalley() + } + .disposed(by: disposeBag) + } public func bind(viewModel: any CenterProfileViewModelable) { @@ -334,26 +356,26 @@ public class CenterProfileViewController: DisposableViewController { profileEditButton .eventPublisher .bind(to: input.editingButtonPressed) - .disposed(by: disposBag) + .disposed(by: disposeBag) editingCompleteButton .eventPublisher .bind(to: input.editingFinishButtonPressed) - .disposed(by: disposBag) + .disposed(by: disposeBag) - centerPhoneNumeberField.textField.rx.text + centerPhoneNumeberField.rx.text .compactMap { $0 } .bind(to: input.editingPhoneNumber) - .disposed(by: disposBag) + .disposed(by: disposeBag) centerIntroductionTextView.rx.text .compactMap { $0 } .bind(to: input.editingInstruction) - .disposed(by: disposBag) + .disposed(by: disposeBag) edtingImage .bind(to: input.editingImage) - .disposed(by: disposBag) + .disposed(by: disposeBag) // output guard let output = viewModel.output else { fatalError() } @@ -361,87 +383,88 @@ public class CenterProfileViewController: DisposableViewController { output .centerName .drive(centerNameLabel.rx.textString) - .disposed(by: disposBag) + .disposed(by: disposeBag) output .centerLocation .drive(centerLocationLabel.rx.textString) - .disposed(by: disposBag) + .disposed(by: disposeBag) output .centerPhoneNumber .drive(centerPhoneNumeberLabel.rx.textString) - .disposed(by: disposBag) + .disposed(by: disposeBag) output .centerPhoneNumber - .drive(centerPhoneNumeberField.textField.rx.text) - .disposed(by: disposBag) + .drive(centerPhoneNumeberField.rx.textString) + .disposed(by: disposeBag) output .centerIntroduction .drive(centerIntroductionLabel.rx.textString) - .disposed(by: disposBag) + .disposed(by: disposeBag) output .centerIntroduction .drive(centerIntroductionTextView.rx.textString) - .disposed(by: disposBag) + .disposed(by: disposeBag) output .centerImage .drive(centerImageView.rx.image) - .disposed(by: disposBag) + .disposed(by: disposeBag) // MARK: Edit Mode output .isEditingMode .map { !$0 } .drive(centerPhoneNumeberField.rx.isHidden) - .disposed(by: disposBag) + .disposed(by: disposeBag) output .isEditingMode .drive(centerPhoneNumeberLabel.rx.isHidden) - .disposed(by: disposBag) + .disposed(by: disposeBag) output .isEditingMode .map { !$0 } .drive(centerIntroductionTextView.rx.isHidden) - .disposed(by: disposBag) + .disposed(by: disposeBag) output .isEditingMode .drive(centerIntroductionLabel.rx.isHidden) - .disposed(by: disposBag) + .disposed(by: disposeBag) output .isEditingMode .map { !$0 } .drive(centerImageEditButton.rx.isHidden) - .disposed(by: disposBag) + .disposed(by: disposeBag) output .isEditingMode .map { !$0 } .drive(editingCompleteButton.rx.isHidden) - .disposed(by: disposBag) + .disposed(by: disposeBag) output .isEditingMode .drive(profileEditButton.rx.isHidden) - .disposed(by: disposBag) + .disposed(by: disposeBag) - output .alert .drive { [weak self] vo in self?.showAlert(vo: vo) } - .disposed(by: disposBag) + .disposed(by: disposeBag) output .editingValidation .drive { _ in // do something when editing success } - .disposed(by: disposBag) + .disposed(by: disposeBag) + + viewModel.requestData() } public func showAlert(vo: DefaultAlertContentVO) { @@ -455,16 +478,35 @@ public class CenterProfileViewController: DisposableViewController { } } - + extension CenterProfileViewController { - private func onEditMode() { + func showPhotoGalley() { + + let imagePickerVC = UIImagePickerController() + imagePickerVC.delegate = self + + if !UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { + return + } + + imagePickerVC.sourceType = .photoLibrary +// let modiaTypes = UIImagePickerController.availableMediaTypes(for: .photoLibrary) + present(imagePickerVC, animated: true) } +} - private func onDisplayMode() { - editingCompleteButton.isHidden = true - profileEditButton.isHidden = false - +extension CenterProfileViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + + if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + + edtingImage.accept(image) + centerImageView.image = image + + picker.dismiss(animated: true) + } } } diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift index d5d288ac..314fc6d6 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift @@ -37,28 +37,14 @@ public class CenterProfileViewModel: CenterProfileViewModelable { public init() { self.input = Input() - let centerName = BehaviorRelay(value: "") - let centerLocation = BehaviorRelay(value: "") - let centerPhoneNumber = BehaviorRelay(value: "") - let centerIntroduction = BehaviorRelay(value: "") - let centerImage = BehaviorRelay(value: .init()) - - // 서버로 부터 데이터를 요청하는 API - centerName.accept("네 얼간이 방문요양센터") - centerLocation.accept("강남구 삼성동 512-3") - centerPhoneNumber.accept("(02) 123-4567") - centerIntroduction.accept("안녕하세요 반갑습니다!") - centerImage.accept(UIImage()) - - // 최신 값들 + 버튼이 눌릴 경우 변경 로직이 실행된다. let editingRequestResult = input .editingFinishButtonPressed .map({ [unowned self] _ in self.checkModification( - prev_phoneNumber: centerPhoneNumber.value, - prev_introduction: centerIntroduction.value, - prev_image: centerImage.value + prev_phoneNumber: self.input.centerPhoneNumber.value, + prev_introduction: self.input.centerIntroduction.value, + prev_image: self.input.centerImage.value ) }) .flatMap { (inputs) in @@ -81,21 +67,21 @@ public class CenterProfileViewModel: CenterProfileViewModelable { // 스트림을 유지하기위해 생성한 Driver로 필수적으로 사용되지 않는다. let editingValidation = editingRequestResult .compactMap { $0.value } - .map { info in + .map { [weak input] info in if let phoneNumber = info.phoneNumber { printIfDebug("✅ 전화번호 변경 반영되었음") - centerPhoneNumber.accept(phoneNumber) + input?.centerPhoneNumber.accept(phoneNumber) } if let introduction = info.introduction { printIfDebug("✅ 센터소개 반영되었음") - centerIntroduction.accept(introduction) + input?.centerIntroduction.accept(introduction) } if let image = info.image { printIfDebug("✅ 센터 이미지 변경 반영되었음") - centerImage.accept(image) + input?.centerImage.accept(image) } return () @@ -142,22 +128,40 @@ public class CenterProfileViewModel: CenterProfileViewModelable { .asDriver(onErrorJustReturn: .default) self.output = .init( - centerName: centerName.asDriver(onErrorJustReturn: ""), - centerLocation: centerLocation.asDriver(onErrorJustReturn: ""), - centerPhoneNumber: centerPhoneNumber.asDriver(onErrorJustReturn: ""), - centerIntroduction: centerIntroduction.asDriver(onErrorJustReturn: ""), - centerImage: centerImage.asDriver(onErrorJustReturn: UIImage()), + centerName: input.centerName.asDriver(onErrorJustReturn: ""), + centerLocation: input.centerLocation.asDriver(onErrorJustReturn: ""), + centerPhoneNumber: input.centerPhoneNumber.asDriver(onErrorJustReturn: ""), + centerIntroduction: input.centerIntroduction.asDriver(onErrorJustReturn: ""), + centerImage: input.centerImage.asDriver(onErrorJustReturn: UIImage()), isEditingMode: isEditingMode, editingValidation: editingValidation, alert: alertDriver ) } + + public func requestData() { + + // 서버로 부터 데이터를 요청하는 API + input.centerName.accept("네 얼간이 방문요양센터") + input.centerLocation.accept("강남구 삼성동 512-3") + input.centerPhoneNumber.accept("(02) 123-4567") + input.centerIntroduction.accept("안녕하세요 반갑습니다!") + input.centerImage.accept(UIImage()) + } } public extension CenterProfileViewModel { class Input: CenterProfileInputable { + + // 서버에서 받아오는데이터 + public var centerName = BehaviorRelay(value: "") + public var centerLocation = BehaviorRelay(value: "") + public var centerPhoneNumber = BehaviorRelay(value: "") + public var centerIntroduction = BehaviorRelay(value: "") + public var centerImage = BehaviorRelay(value: .init()) + // 모드설정 public var editingButtonPressed: PublishRelay = .init() public var editingFinishButtonPressed: PublishRelay = .init() From 4a1402c99faf9a9694394f8735b6517438c96297 Mon Sep 17 00:00:00 2001 From: J0onYEong Date: Fri, 19 Jul 2024 09:18:45 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[IDLE-180]=20=EC=98=B5=EC=A0=80=EB=B2=84?= =?UTF-8?q?=EB=B8=94=20=ED=81=B4=EB=A1=9C=EC=A0=80=20=EA=B0=84=EC=86=8C?= =?UTF-8?q?=ED=99=94=EB=B0=8F=20=EC=84=BC=ED=84=B0=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=B7=B0=20=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Profile/CenterProfileViewController.swift | 74 +++++++++---------- .../Profile/CenterProfileViewModel.swift | 2 +- 2 files changed, 35 insertions(+), 41 deletions(-) diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift index 051fbff9..e9bb3a8b 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift @@ -125,7 +125,7 @@ public class CenterProfileViewController: DisposableViewController { return label }() /// 센터 소개를 수정하는 텍스트 필드 - let centerIntroductionTextView: MultiLineTextField = { + let centerIntroductionField: MultiLineTextField = { let textView = MultiLineTextField( typography: .Body3, placeholderText: "추가적으로 요구사항이 있다면 작성해주세요." @@ -145,6 +145,11 @@ public class CenterProfileViewController: DisposableViewController { view.layer.cornerRadius = 6 view.clipsToBounds = true view.backgroundColor = DSKitAsset.Colors.gray100.color + view.contentMode = .scaleAspectFill + + /// 이미지 뷰는 버튼을 자식으로 가지는데 기본적으로 isUserInteractionEnabled값이 fale라 자식 버튼에도 영향을 미친다. + /// 따라서 이터렉션이 필요한 자식이 있는 경우 명시적으로 아래 프로퍼티값을 true로 설정해야한다. + view.isUserInteractionEnabled = true return view }() let centerImageEditButton: UIButton = { @@ -175,6 +180,7 @@ public class CenterProfileViewController: DisposableViewController { func setAutoLayout() { + // 상단 네비게이션바 세팅 let navigationStack = HStack([ navigationBar, editingCompleteButton, @@ -219,15 +225,15 @@ public class CenterProfileViewController: DisposableViewController { [ centerIntroductionTitleLabel, centerIntroductionLabel, - centerIntroductionTextView, + centerIntroductionField, ], spacing: 6, alignment: .fill ) // 센터 이미지뷰 세팅 -// centerImageView.addSubview(centerImageEditButton) -// centerImageEditButton.translatesAutoresizingMaskIntoConstraints = false + centerImageView.addSubview(centerImageEditButton) + centerImageEditButton.translatesAutoresizingMaskIntoConstraints = false let scrollView = UIScrollView() @@ -249,7 +255,6 @@ public class CenterProfileViewController: DisposableViewController { centerPictureLabel, centerImageView, - centerImageEditButton ].forEach { $0.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview($0) @@ -286,7 +291,7 @@ public class CenterProfileViewController: DisposableViewController { centerImageEditButton.widthAnchor.constraint(equalToConstant: 28), centerImageEditButton.heightAnchor.constraint(equalTo: centerImageEditButton.widthAnchor), - centerIntroductionTextView.heightAnchor.constraint(equalToConstant: 156), + centerIntroductionField.heightAnchor.constraint(equalToConstant: 156), ]) let contentGuide = scrollView.contentLayoutGuide @@ -368,7 +373,7 @@ public class CenterProfileViewController: DisposableViewController { .bind(to: input.editingPhoneNumber) .disposed(by: disposeBag) - centerIntroductionTextView.rx.text + centerIntroductionField.rx.text .compactMap { $0 } .bind(to: input.editingInstruction) .disposed(by: disposeBag) @@ -405,7 +410,7 @@ public class CenterProfileViewController: DisposableViewController { .disposed(by: disposeBag) output .centerIntroduction - .drive(centerIntroductionTextView.rx.textString) + .drive(centerIntroductionField.rx.textString) .disposed(by: disposeBag) output @@ -416,38 +421,21 @@ public class CenterProfileViewController: DisposableViewController { // MARK: Edit Mode output .isEditingMode - .map { !$0 } - .drive(centerPhoneNumeberField.rx.isHidden) - .disposed(by: disposeBag) - output - .isEditingMode - .drive(centerPhoneNumeberLabel.rx.isHidden) - .disposed(by: disposeBag) - - output - .isEditingMode - .map { !$0 } - .drive(centerIntroductionTextView.rx.isHidden) - .disposed(by: disposeBag) - output - .isEditingMode - .drive(centerIntroductionLabel.rx.isHidden) - .disposed(by: disposeBag) - - output - .isEditingMode - .map { !$0 } - .drive(centerImageEditButton.rx.isHidden) - .disposed(by: disposeBag) - - output - .isEditingMode - .map { !$0 } - .drive(editingCompleteButton.rx.isHidden) - .disposed(by: disposeBag) - output - .isEditingMode - .drive(profileEditButton.rx.isHidden) + .drive { [weak self] in + guard let self else { return } + + centerPhoneNumeberField.isHidden = !$0 + centerPhoneNumeberLabel.isHidden = $0 + + centerIntroductionField.isHidden = !$0 + centerIntroductionLabel.isHidden = $0 + + centerImageEditButton.isHidden = !$0 + + editingCompleteButton.isHidden = !$0 + profileEditButton.isHidden = $0 + + } .disposed(by: disposeBag) output @@ -487,6 +475,12 @@ extension CenterProfileViewController { imagePickerVC.delegate = self if !UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { + + showAlert(vo: .init( + title: "오류", + message: "사진함을 열 수 없습니다.") + ) + return } diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift index 314fc6d6..0c2ec4f4 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Profile/CenterProfileViewModel.swift @@ -162,7 +162,7 @@ public extension CenterProfileViewModel { public var centerIntroduction = BehaviorRelay(value: "") public var centerImage = BehaviorRelay(value: .init()) - // 모드설정 + // ViewController에서 받아오는 데이터 public var editingButtonPressed: PublishRelay = .init() public var editingFinishButtonPressed: PublishRelay = .init() public var editingPhoneNumber: BehaviorRelay = .init(value: "") From 98be2cc77f6dfaf562e74a27dfee5c901f7942e6 Mon Sep 17 00:00:00 2001 From: J0onYEong Date: Fri, 19 Jul 2024 09:21:27 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[IDLE-180]=20=ED=82=A4=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=ED=88=B4=EB=B0=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DSKit/Sources/Component/TextField/MultiLineTextField.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/project/Projects/Presentation/DSKit/Sources/Component/TextField/MultiLineTextField.swift b/project/Projects/Presentation/DSKit/Sources/Component/TextField/MultiLineTextField.swift index ba6ca60f..02b3cfac 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/TextField/MultiLineTextField.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/TextField/MultiLineTextField.swift @@ -36,6 +36,7 @@ public class MultiLineTextField: UITextView { setAppearance() setPlaceholderText(textView: self) + addToolbar() } required init?(coder: NSCoder) { fatalError() } From 52e9f0121d79d27e453e753cd25e4d5e2d1b0a5b Mon Sep 17 00:00:00 2001 From: J0onYEong Date: Fri, 19 Jul 2024 09:30:44 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[IDLE-000]=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EC=9D=98=20=EC=98=B5=EC=A0=80=EB=B2=84=EB=B8=94?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Auth/Sources/View/Center/CenterAuthMainViewController.swift | 2 +- .../Sources/View/Center/Login/CenterLoginViewController.swift | 2 +- .../View/Worker/Register/EnterAddressViewController.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/CenterAuthMainViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/CenterAuthMainViewController.swift index 5effad35..8368333f 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/CenterAuthMainViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/CenterAuthMainViewController.swift @@ -129,7 +129,7 @@ public class CenterAuthMainViewController: DisposableViewController { loginButton .eventPublisher - .emit { [weak self] _ in + .subscribe { [weak self] _ in self?.coordinator?.parent?.login() } .disposed(by: disposeBag) diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterLoginViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterLoginViewController.swift index 74eff375..840f78bf 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterLoginViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterLoginViewController.swift @@ -211,7 +211,7 @@ public class CenterLoginViewController: DisposableViewController { forgotPasswordButton .eventPublisher - .emit { [weak self] _ in + .subscribe { [weak self] _ in self?.coordinator?.parent?.setNewPassword() } .disposed(by: disposeBag) diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EnterAddressViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EnterAddressViewController.swift index f32c5c89..737e255d 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EnterAddressViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EnterAddressViewController.swift @@ -177,7 +177,7 @@ where T.Input: EnterAddressInputable & CTAButtonEnableInputable, T.Output: Enter addressSearchButton .eventPublisher - .emit { [weak self] _ in + .subscribe { [weak self] _ in self?.showDaumSearchView() } .disposed(by: disposeBag)