From 3dd71ada3d7a8b2962d501ba3c6a223936d2c5a1 Mon Sep 17 00:00:00 2001 From: LeeSeungmin Date: Tue, 23 Apr 2024 11:07:11 +0900 Subject: [PATCH] =?UTF-8?q?[#74]Feat:=20=EA=B2=BD=EA=B3=A0=EC=B0=BD=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=BB=A4=EC=8A=A4=ED=85=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ReportAction enum 커스텀 - title, message, contentView, 왼쪽, 오른쪽, dim action 추가 함수 구현 - UIViewController에서 경고창 표시하는 함수 구현 - Color -> Image로 변환 함수 구현 => button에 State에 따른 색상을 넣기 위함 - addAction Custom -> button 혹은 view에서 동작 구현을 위함 --- .../UIComponent/TFAlertViewController.swift | 241 ++++++++++++++++++ .../DesignSystem/Src/Util/UIColor+Util.swift | 18 ++ .../Src/Util/UIControl+Util.swift | 45 ++++ .../Src/Util/UITapGestureRecognizer.swift | 51 ++++ .../Src/Util/UIVIewControler+Utils.swift | 82 ++++++ 5 files changed, 437 insertions(+) create mode 100644 Projects/Modules/DesignSystem/Src/UIComponent/TFAlertViewController.swift create mode 100644 Projects/Modules/DesignSystem/Src/Util/UIColor+Util.swift create mode 100644 Projects/Modules/DesignSystem/Src/Util/UIControl+Util.swift create mode 100644 Projects/Modules/DesignSystem/Src/Util/UITapGestureRecognizer.swift create mode 100644 Projects/Modules/DesignSystem/Src/Util/UIVIewControler+Utils.swift diff --git a/Projects/Modules/DesignSystem/Src/UIComponent/TFAlertViewController.swift b/Projects/Modules/DesignSystem/Src/UIComponent/TFAlertViewController.swift new file mode 100644 index 00000000..c9b6b38d --- /dev/null +++ b/Projects/Modules/DesignSystem/Src/UIComponent/TFAlertViewController.swift @@ -0,0 +1,241 @@ +// +// TFAlertViewController.swift +// DSKit +// +// Created by SeungMin on 4/3/24. +// + +import UIKit + +public enum ReportAction { + case complaints, block, withdraw + + var title: String { + switch self { + case .complaints: + return "차단할까요?" + case .block: + return "어떤 문제가 있나요?" + case .withdraw: + return "계정 탈퇴하기" + } + } + + var message: String? { + switch self { + case .block: + return "해당 사용자와 서로 차단됩니다." + case .withdraw: + return "정말 탈퇴하시겠어요? 회원님의 모든 정보 및\n사용 내역은 복구 불가합니다. 블링 환불 관련 문의는\nteamtht23@gmail.com 으로 부탁드립니다." + default: + return nil + } + } + + var leftActionTitle: String { + switch self { + case .complaints: + return "차단할까요?" + case .block: + return "차단하기" + case .withdraw: + return "탈퇴하기" + } + } + + var rightActionTitle: String { + switch self { + default: + return "취소" + } + } +} + +public final class TFAlertViewController: TFBaseViewController { + private var titleText: String? + private var messageText: String? + private var contentView: UIView? + + private lazy var dimView: UIView = { + let view = UIView() + view.backgroundColor = DSKitAsset.Color.DimColor.default.color + return view + }() + + private lazy var containerStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.alignment = .center + stackView.backgroundColor = DSKitAsset.Color.neutral600.color + stackView.layer.cornerRadius = 8 + stackView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) + stackView.layoutMargins = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0) + stackView.isLayoutMarginsRelativeArrangement = true + return stackView + }() + + private lazy var labelStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 6 + stackView.alignment = .center + return stackView + }() + + private lazy var buttonStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 16.0 + stackView.alignment = .center + return stackView + }() + + private lazy var titleLabel: UILabel? = { + guard titleText != nil else { return nil } + + let label = UILabel() + label.text = titleText + label.font = UIFont.thtH5Sb + label.textAlignment = .center + label.textColor = DSKitAsset.Color.neutral50.color + label.numberOfLines = 0 + return label + }() + + private lazy var messageLabel: UILabel? = { + guard messageText != nil else { return nil } + + let label = UILabel() + label.text = messageText + label.font = UIFont.thtP1R + label.textAlignment = .center + label.textColor = DSKitAsset.Color.neutral300.color + label.numberOfLines = 0 + return label + }() + + let separatorView: UIView = { + let view = UIView() + view.backgroundColor = DSKitAsset.Color.neutral400.color + return view + }() + + init( + titleText: String? = nil, + messageText: String? = nil + ) { + super.init(nibName: nil, bundle: nil) + self.titleText = titleText + self.messageText = messageText + + modalPresentationStyle = .overFullScreen + } + + convenience init(contentView: UIView) { + self.init() + + self.contentView = contentView + modalPresentationStyle = .overFullScreen + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // curveEaseOut: 시작은 천천히, 끝날 땐 빠르게 + UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveEaseOut) { [weak self] in + self?.containerStackView.transform = .identity + self?.containerStackView.isHidden = false + } + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + // curveEaseIn: 시작은 빠르게, 끝날 땐 천천히 + UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveEaseIn) { [weak self] in + self?.containerStackView.transform = .identity + self?.containerStackView.isHidden = true + } + } + + public override func makeUI() { + view.addSubviews([dimView, containerStackView]) + containerStackView.addArrangedSubviews([labelStackView, buttonStackView]) + + dimView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + containerStackView.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.trailing.equalToSuperview().inset(28) + } + + labelStackView.snp.makeConstraints { + $0.width.equalToSuperview() + } + + buttonStackView.snp.makeConstraints { + $0.width.equalToSuperview() + } + + if let titleLabel = titleLabel { + labelStackView.addArrangedSubview(titleLabel) + + } + + if let messageLabel = messageLabel { + labelStackView.addArrangedSubview(messageLabel) + } + + if !labelStackView.subviews.isEmpty { + containerStackView.setCustomSpacing(20, after: labelStackView) + } + + separatorView.snp.makeConstraints { + $0.width.equalToSuperview() + $0.height.equalTo(1.5) + } + } + + public func addActionToButton( + title: String? = nil, + completion: (() -> Void)? = nil + ) { + guard let title = title else { return } + + let button = UIButton() + button.titleLabel?.font = UIFont.thtSubTitle2Sb + + // enable + button.setTitle(title, for: .normal) + button.setTitleColor(DSKitAsset.Color.neutral50.color, for: .normal) + button.setBackgroundImage(DSKitAsset.Color.neutral600.color.image(), for: .normal) + + // disable + button.setTitleColor(DSKitAsset.Color.neutral50.color, for: .disabled) + button.setBackgroundImage(DSKitAsset.Color.neutral300.color.image(), for: .disabled) + + button.addAction(for: .touchUpInside) { _ in + completion?() + } + + if let _ = buttonStackView.subviews.first { + buttonStackView.addArrangedSubview(separatorView) + } + + buttonStackView.addArrangedSubview(button) + button.snp.makeConstraints { $0.width.equalToSuperview() } + } + + public func addActionToDim(completion: (() -> Void)? = nil) { + let tapGestureRecognizer = UITapGestureRecognizer() + tapGestureRecognizer.addAction(closure: { _ in completion?() }) + + dimView.addGestureRecognizer(tapGestureRecognizer) + } +} diff --git a/Projects/Modules/DesignSystem/Src/Util/UIColor+Util.swift b/Projects/Modules/DesignSystem/Src/Util/UIColor+Util.swift new file mode 100644 index 00000000..99ebf819 --- /dev/null +++ b/Projects/Modules/DesignSystem/Src/Util/UIColor+Util.swift @@ -0,0 +1,18 @@ +// +// UIColor+Util.swift +// DSKit +// +// Created by SeungMin on 4/20/24. +// + +import UIKit + +extension UIColor { + /// Convert color to image + func image(_ size: CGSize = CGSize(width: 1, height: 1)) -> UIImage { + return UIGraphicsImageRenderer(size: size).image { rendererContext in + self.setFill() + rendererContext.fill(CGRect(origin: .zero, size: size)) + } + } +} diff --git a/Projects/Modules/DesignSystem/Src/Util/UIControl+Util.swift b/Projects/Modules/DesignSystem/Src/Util/UIControl+Util.swift new file mode 100644 index 00000000..dd963cad --- /dev/null +++ b/Projects/Modules/DesignSystem/Src/Util/UIControl+Util.swift @@ -0,0 +1,45 @@ +// +// UIControl+Util.swift +// DSKit +// +// Created by SeungMin on 4/20/24. +// + +import UIKit + +extension UIControl { + public typealias UIControlTargetClosure = (UIControl) -> () + + private class UIControlClosureWrapper: NSObject { + let closure: UIControlTargetClosure + init(_ closure: @escaping UIControlTargetClosure) { + self.closure = closure + } + } + + private struct AssociatedKeys { + static var targetClosure = "targetClosure" + } + + private var targetClosure: UIControlTargetClosure? { + get { + guard let closureWrapper = objc_getAssociatedObject(self, &AssociatedKeys.targetClosure) as? UIControlClosureWrapper else { return nil } + return closureWrapper.closure + + } set(newValue) { + guard let newValue = newValue else { return } + objc_setAssociatedObject(self, &AssociatedKeys.targetClosure, UIControlClosureWrapper(newValue), + objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + @objc func closureAction() { + guard let targetClosure = targetClosure else { return } + targetClosure(self) + } + + public func addAction(for event: UIControl.Event, closure: @escaping UIControlTargetClosure) { + targetClosure = closure + addTarget(self, action: #selector(UIControl.closureAction), for: event) + } +} diff --git a/Projects/Modules/DesignSystem/Src/Util/UITapGestureRecognizer.swift b/Projects/Modules/DesignSystem/Src/Util/UITapGestureRecognizer.swift new file mode 100644 index 00000000..9f5d01c7 --- /dev/null +++ b/Projects/Modules/DesignSystem/Src/Util/UITapGestureRecognizer.swift @@ -0,0 +1,51 @@ +// +// UITapGestureRecognizer.swift +// DSKit +// +// Created by SeungMin on 4/20/24. +// + +import UIKit + +extension UITapGestureRecognizer { + public typealias UITapGestureTargetClosure = (UITapGestureRecognizer) -> () + + private class UITapGestureClosureWrapper: NSObject { + let closure: UITapGestureTargetClosure + init(_ closure: @escaping UITapGestureTargetClosure) { + self.closure = closure + } + } + + private struct AssociatedKeys { + static var targetClosure = "targetClosure" + } + + private var targetClosure: UITapGestureTargetClosure? { + get { + guard let closureWrapper = objc_getAssociatedObject( + self, + &AssociatedKeys.targetClosure + ) as? UITapGestureClosureWrapper else { return nil } + return closureWrapper.closure + + } set(newValue) { + guard let newValue = newValue else { return } + objc_setAssociatedObject( + self, + &AssociatedKeys.targetClosure, + UITapGestureClosureWrapper(newValue), + objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + @objc func closureAction() { + guard let targetClosure = targetClosure else { return } + targetClosure(self) + } + + public func addAction(closure: @escaping UITapGestureTargetClosure) { + targetClosure = closure + addTarget(self, action: #selector(UITapGestureRecognizer.closureAction)) + } +} diff --git a/Projects/Modules/DesignSystem/Src/Util/UIVIewControler+Utils.swift b/Projects/Modules/DesignSystem/Src/Util/UIVIewControler+Utils.swift new file mode 100644 index 00000000..016b945f --- /dev/null +++ b/Projects/Modules/DesignSystem/Src/Util/UIVIewControler+Utils.swift @@ -0,0 +1,82 @@ +// +// UIVIewControler+Utils.swift +// DSKit +// +// Created by SeungMin on 4/17/24. +// + +import UIKit + +extension UIViewController { + public func showAlert(title: String? = nil, + message: String? = nil, + attributedMessage: NSAttributedString? = nil, + leftActionTitle: String? = "확인", + rightActionTitle: String = "취소", + leftActionCompletion: (() -> Void)? = nil, + rightActionCompletion: (() -> Void)? = nil, + dimActionCompletion: (() -> Void)? = nil) { + let alertViewController = TFAlertViewController(titleText: title, + messageText: message) + showAlert(alertViewController: alertViewController, + leftActionTitle: leftActionTitle, + rightActionTitle: rightActionTitle, + leftActionCompletion: leftActionCompletion, + rightActionCompletion: rightActionCompletion, + dimActionCompletion: dimActionCompletion) + } + + public func showAlert(contentView: UIView, + leftActionTitle: String? = "확인", + rightActionTitle: String = "취소", + leftActionCompletion: (() -> Void)? = nil, + rightActionCompletion: (() -> Void)? = nil, + dimActionCompletion: (() -> Void)? = nil) { + let alertViewController = TFAlertViewController(contentView: contentView) + + showAlert(alertViewController: alertViewController, + leftActionTitle: leftActionTitle, + rightActionTitle: rightActionTitle, + leftActionCompletion: leftActionCompletion, + rightActionCompletion: rightActionCompletion, + dimActionCompletion: dimActionCompletion) + } + + public func showAlert(action: ReportAction, + leftActionCompletion: (() -> Void)? = nil, + rightActionCompletion: (() -> Void)? = nil, + dimActionCompletion: (() -> Void)? = nil) { + let alertViewController = TFAlertViewController( + titleText: action.title, + messageText: action.message + ) + + showAlert(alertViewController: alertViewController, + leftActionTitle: action.leftActionTitle, + rightActionTitle: action.rightActionTitle, + leftActionCompletion: leftActionCompletion, + rightActionCompletion: rightActionCompletion, + dimActionCompletion: dimActionCompletion) + } + + private func showAlert(alertViewController: TFAlertViewController, + leftActionTitle: String?, + rightActionTitle: String, + leftActionCompletion: (() -> Void)?, + rightActionCompletion: (() -> Void)?, + dimActionCompletion: (() -> Void)?) { + alertViewController.addActionToButton(title: leftActionTitle) { + alertViewController.dismiss(animated: false, completion: leftActionCompletion) + } + + alertViewController.addActionToButton(title: rightActionTitle) { + alertViewController.dismiss(animated: false, completion: rightActionCompletion) + } + + alertViewController.addActionToDim() { + alertViewController.dismiss(animated: false, completion: dimActionCompletion) + } + + present(alertViewController, animated: false, completion: nil) + } +}