From 6cabad54dcaec0471a71b03bfca31f64cb50ccfa Mon Sep 17 00:00:00 2001 From: p2glet Date: Thu, 9 Apr 2026 00:09:06 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat/#315:=20TooptipView=20=EB=B0=8F=20Tool?= =?UTF-8?q?tipFactory=20=EC=83=9D=EC=84=B1=20=ED=8A=B9=EC=A0=95=20View?= =?UTF-8?q?=EB=A5=BC=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=ED=88=B4?= =?UTF-8?q?=ED=8C=81=EC=9D=84=20=EB=9D=84=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Tooltip/TooltipArrowView.swift | 24 +++ .../Tooltip/TooltipOverlayView.swift | 29 ++++ .../Components/Tooltip/TooltipView.swift | 143 ++++++++++++++++++ .../Layouts/Factory/ToastFactory.swift | 2 +- .../Layouts/Factory/TooltipFactory.swift | 106 +++++++++++++ .../TooltipTestViewController.swift | 124 +++++++++++++++ .../ViewController.swift | 3 +- 7 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipArrowView.swift create mode 100644 MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipOverlayView.swift create mode 100644 MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift create mode 100644 MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift create mode 100644 MLS/MLSDesignSystemExample/ComponentsTest/TooltipTestViewController.swift diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipArrowView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipArrowView.swift new file mode 100644 index 00000000..34afa4ff --- /dev/null +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipArrowView.swift @@ -0,0 +1,24 @@ +import UIKit + +import SnapKit + +final class TooltipArrowView: UIView { + + override class var layerClass: AnyClass { + CAShapeLayer.self + } + + override func layoutSubviews() { + super.layoutSubviews() + + let path = UIBezierPath() + path.move(to: CGPoint(x: bounds.midX, y: bounds.maxY)) + path.addLine(to: CGPoint(x: bounds.minX, y: bounds.minY)) + path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY)) + path.close() + + let shape = layer as! CAShapeLayer + shape.path = path.cgPath + shape.fillColor = UIColor.whiteMLS.cgColor + } +} diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipOverlayView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipOverlayView.swift new file mode 100644 index 00000000..a319bfe0 --- /dev/null +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipOverlayView.swift @@ -0,0 +1,29 @@ +import UIKit + +import RxCocoa +import RxSwift + +final class TooltipOverlayView: UIView { + // MARK: - Properties + private let disposeBag = DisposeBag() + + var onDismiss: (() -> Void)? + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .clear + + let tapGesture = UITapGestureRecognizer() + addGestureRecognizer(tapGesture) + + tapGesture.rx.event + .bind { [weak self] _ in + self?.onDismiss?() + } + .disposed(by: disposeBag) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift new file mode 100644 index 00000000..b04b55c0 --- /dev/null +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift @@ -0,0 +1,143 @@ +import UIKit + +import SnapKit + +/// Tooltip이 뻗어나가는 방향 +public enum TooltipPosition { + case topLeading + case topTrailing + case bottomLeading + case bottomTrailing +} + +private enum Constants { + static let arrowSize = CGSize(width: 16, height: 10) + static let cornerRadius: CGFloat = 16 +} + +final class TooltipView: UIView { + + // MARK: - Properties + private let tooltipPosition: TooltipPosition + + // MARK: - Components + private let label = UILabel() + + // MARK: - init + init(text: String, tooltipPosition: TooltipPosition) { + self.tooltipPosition = tooltipPosition + super.init(frame: .zero) + addViews() + setupConstraints() + configureUI(text: text) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } +} + +// MARK: - Layout +private extension TooltipView { + + func addViews() { + addSubview(label) + } + + func setupConstraints() { + switch tooltipPosition { + + /// 툴팁이 아래에 위치 + case .bottomLeading, .bottomTrailing: + label.snp.makeConstraints { make in + make.top.equalToSuperview().inset(11) + make.horizontalEdges.equalToSuperview().inset(18) + make.bottom.equalToSuperview().inset(11 + Constants.arrowSize.height) + } + + /// 툴팁이 위에 위치 + case .topLeading, .topTrailing: + label.snp.makeConstraints { make in + make.top.equalToSuperview().inset(11 + Constants.arrowSize.height) + make.horizontalEdges.equalToSuperview().inset(18) + make.bottom.equalToSuperview().inset(11) + } + } + } + + func configureUI(text: String) { + backgroundColor = .clear + + label.attributedText = .makeStyledString(font: .b_s_r, text: text) + label.numberOfLines = 0 + } +} + +// MARK: - Draw Bubble +extension TooltipView { + + override func layoutSubviews() { + super.layoutSubviews() + drawBubble() + } + + /// 말풍선 + arrow path 생성 + private func drawBubble() { + + let rect = bounds + let arrowHeight = Constants.arrowSize.height + let arrowWidth = Constants.arrowSize.width + + /// 툴팁 상하 위치 판단 + let isTop = tooltipPosition == .bottomLeading || tooltipPosition == .bottomTrailing + + /// 실제 툴팁 영역 + let bubbleRect = CGRect( + x: 0, + y: isTop ? 0 : arrowHeight, + width: rect.width, + height: rect.height - arrowHeight + ) + + let bubblePath = UIBezierPath( + roundedRect: bubbleRect, + cornerRadius: Constants.cornerRadius + ) + + /// arrow는 툴팁 내부에서 고정 위치 + let arrowInset: CGFloat = 28 + + let arrowX: CGFloat + switch tooltipPosition { + case .topLeading, .bottomLeading: + arrowX = arrowInset + + case .topTrailing, .bottomTrailing: + arrowX = bubbleRect.width - arrowInset + } + + let arrowPath = UIBezierPath() + + if isTop { + arrowPath.move(to: CGPoint(x: arrowX - arrowWidth/2, y: bubbleRect.minY)) + arrowPath.addLine(to: CGPoint(x: arrowX, y: bubbleRect.minY - arrowHeight)) + arrowPath.addLine(to: CGPoint(x: arrowX + arrowWidth/2, y: bubbleRect.minY)) + } else { + arrowPath.move(to: CGPoint(x: arrowX - arrowWidth/2, y: bubbleRect.maxY)) + arrowPath.addLine(to: CGPoint(x: arrowX, y: bubbleRect.maxY + arrowHeight)) + arrowPath.addLine(to: CGPoint(x: arrowX + arrowWidth/2, y: bubbleRect.maxY)) + } + + arrowPath.close() + bubblePath.append(arrowPath) + + let shapeLayer = CAShapeLayer() + shapeLayer.path = bubblePath.cgPath + shapeLayer.fillColor = UIColor.whiteMLS.cgColor + + /// 기존 shapeLayer 제거 후 다시 추가 + layer.sublayers?.removeAll(where: { $0 is CAShapeLayer }) + layer.insertSublayer(shapeLayer, at: 0) + } +} diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/ToastFactory.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/ToastFactory.swift index 849298ba..096afadd 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/ToastFactory.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/ToastFactory.swift @@ -4,7 +4,7 @@ import RxSwift import SnapKit @MainActor -public final class ToastFactory { +public enum ToastFactory { // MARK: - Properties diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift new file mode 100644 index 00000000..146ed9bc --- /dev/null +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift @@ -0,0 +1,106 @@ +import UIKit + +import RxSwift +import SnapKit + +@MainActor +public enum TooltipFactory { + // MARK: - Properties + + /// 현재 디바이스 최상단 Window를 지정 + static var window: UIWindow? { + UIApplication.shared + .connectedScenes + .flatMap { ($0 as? UIWindowScene)?.windows ?? [] } + .first { $0.isKeyWindow } + } + + private static var currentTooltip: TooltipView? + + /// 전체 터치 dismiss용 overlay + private static var overlayView: TooltipOverlayView? +} + +public extension TooltipFactory { + + /// Tooltip 노출 메소드 + static func show( + text: String, + anchorView: UIView, + tooltipPosition: TooltipPosition + ) { + currentTooltip?.removeFromSuperview() + currentTooltip = nil + + guard let window = window else { return } + + /// 전체 영역 터치 dismiss overlay + let overlay = TooltipOverlayView(frame: window.bounds) + window.addSubview(overlay) + overlayView = overlay + + let tooltip = TooltipView(text: text, tooltipPosition: tooltipPosition) + overlay.addSubview(tooltip) + currentTooltip = tooltip + + overlay.onDismiss = { + dismiss() + } + + let frame = anchorView.convert(anchorView.bounds, to: window) + + tooltip.frame.origin = CGPoint(x: 0, y: 0) + tooltip.setNeedsLayout() + tooltip.layoutIfNeeded() + + let tooltipSize = tooltip.systemLayoutSizeFitting( + UIView.layoutFittingCompressedSize + ) + + /// arrow가 툴팁 내부에서 위치하는 고정 inset + let arrowInset: CGFloat = 28 + let anchorCenterX = frame.midX + + /// 툴팁 내부 arrow 중심 위치 + let arrowCenterInTooltip: CGFloat + switch tooltipPosition { + case .topLeading, .bottomLeading: + arrowCenterInTooltip = arrowInset + + case .topTrailing, .bottomTrailing: + arrowCenterInTooltip = tooltipSize.width - arrowInset + } + + /// arrow 중심 = 버튼 중심 + let x = anchorCenterX - arrowCenterInTooltip + + let y: CGFloat + switch tooltipPosition { + case .topLeading, .topTrailing: + y = frame.minY - tooltipSize.height - 8 + case .bottomLeading, .bottomTrailing: + y = frame.maxY + 8 + } + + tooltip.frame = CGRect( + x: x, + y: y, + width: tooltipSize.width, + height: tooltipSize.height + ) + + tooltip.alpha = 0 + UIView.animate(withDuration: 0.25) { + tooltip.alpha = 1 + } + } + + /// 툴팁 제거 + static func dismiss() { + currentTooltip?.removeFromSuperview() + currentTooltip = nil + + overlayView?.removeFromSuperview() + overlayView = nil + } +} diff --git a/MLS/MLSDesignSystemExample/ComponentsTest/TooltipTestViewController.swift b/MLS/MLSDesignSystemExample/ComponentsTest/TooltipTestViewController.swift new file mode 100644 index 00000000..45e08203 --- /dev/null +++ b/MLS/MLSDesignSystemExample/ComponentsTest/TooltipTestViewController.swift @@ -0,0 +1,124 @@ +import UIKit + +import MLSDesignSystem + +import RxCocoa +import RxSwift +import SnapKit + +final class TooltipTestViewController: UIViewController { + + // MARK: - Properties + var disposeBag = DisposeBag() + + let button1 = { + let button = UIButton(type: .system) + button.setTitle("우상단 버튼입니다", for: .normal) + return button + }() + + let button2 = { + let button = UIButton(type: .system) + button.setTitle("좌상단", for: .normal) + return button + }() + + + let button3 = { + let button = UIButton(type: .system) + button.setTitle("우하단", for: .normal) + return button + }() + + let button4 = { + let button = UIButton(type: .system) + button.setTitle("좌하단", for: .normal) + return button + }() + + init() { + super.init(nibName: nil, bundle: nil) + self.title = "툴팁" + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Life Cycle +extension TooltipTestViewController { + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .black + addViews() + setupConstraints() + bind() + } +} + +// MARK: - SetUp +private extension TooltipTestViewController { + func addViews() { + view.addSubview(button1) + view.addSubview(button2) + view.addSubview(button3) + view.addSubview(button4) + } + + func setupConstraints() { + button1.snp.makeConstraints { make in + make.center.equalToSuperview() + } + + button2.snp.makeConstraints { make in + make.top.equalTo(button1.snp.bottom).offset(10) + make.centerX.equalToSuperview() + } + + button3.snp.makeConstraints { make in + make.top.equalTo(button2.snp.bottom).offset(10) + make.centerX.equalToSuperview() + } + + button4.snp.makeConstraints { make in + make.top.equalTo(button3.snp.bottom).offset(10) + make.centerX.equalToSuperview() + } + } + + func bind() { + button1.rx.tap + .withUnretained(self) + .subscribe { owner, _ in + TooltipFactory.show(text: "같은 레벨·직업 유저들이 자주 언급한\n사냥터를 기반으로 추천해요.", anchorView: owner.button1, tooltipPosition: .topLeading + ) + } + .disposed(by: disposeBag) + + button2.rx.tap + .withUnretained(self) + .subscribe { owner, _ in + TooltipFactory.show(text: "같은 레벨·직업 유저들이 자주 언급한\n사냥터를 기반으로 추천해요.", anchorView: owner.button2, tooltipPosition: .topTrailing + ) + } + .disposed(by: disposeBag) + + button3.rx.tap + .withUnretained(self) + .subscribe { owner, _ in + TooltipFactory.show(text: "같은 레벨·직업 유저들이 자주 언급한\n사냥터를 기반으로 추천해요.", anchorView: owner.button3, tooltipPosition: .bottomLeading + ) + } + .disposed(by: disposeBag) + + button4.rx.tap + .withUnretained(self) + .subscribe { owner, _ in + TooltipFactory.show(text: "같은 레벨·직업 유저들이 자주 언급한\n사냥터를 기반으로 추천해요.", anchorView: owner.button4, tooltipPosition: .bottomTrailing + ) + } + .disposed(by: disposeBag) + } +} diff --git a/MLS/MLSDesignSystemExample/ViewController.swift b/MLS/MLSDesignSystemExample/ViewController.swift index 7651950d..76a25237 100644 --- a/MLS/MLSDesignSystemExample/ViewController.swift +++ b/MLS/MLSDesignSystemExample/ViewController.swift @@ -39,7 +39,8 @@ class ViewController: UIViewController { SnackBarTestViewController(), BadgeTestController(), DictionaryDetailViewTestController(), - TextButtonTestViewController() + TextButtonTestViewController(), + TooltipTestViewController(), ] override func viewDidLoad() { From 71f3d25ca7e8f0363b023aeb0078cc527a3620e1 Mon Sep 17 00:00:00 2001 From: p2glet Date: Thu, 9 Apr 2026 18:21:35 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix/#315:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=EC=9C=A0=EB=8F=84=20UI=20/=20=EB=A0=88=EB=B2=A8,=20=EC=A7=81?= =?UTF-8?q?=EC=97=85=20=EC=9E=85=EB=A0=A5=20UI=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=EC=97=B4=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=EA=B5=AC=EC=B2=B4=EC=A0=81=EC=9D=B8=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=82=AC=EC=9A=A9=EC=84=B1=20=EC=A6=9D?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Layouts/CharacterInputView.swift | 22 +++++++++--- .../MLSDesignSystem/Layouts/ToLoginView.swift | 35 +++++++++++++++---- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/CharacterInputView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/CharacterInputView.swift index d0d2d557..7978f861 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/CharacterInputView.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/CharacterInputView.swift @@ -4,6 +4,20 @@ import RxCocoa import RxSwift import SnapKit +public enum CharacterViewType { + case normal + case recommend + + var title: String { + switch self { + case .normal: + "현재 레벨과 직업을\n입력해주세요." + case .recommend: + "사냥터 추천을 위해\n현재 레벨과 직업을 입력해주세요." + } + } +} + open class CharacterInputView: UIView { // MARK: - Type public enum Constant { @@ -40,11 +54,11 @@ open class CharacterInputView: UIView { public let nextButton = CommonButton(style: .normal, title: "다음", disabledTitle: "다음") // MARK: - init - public init(title: String? = nil) { + public init(type: CharacterViewType = .normal) { super.init(frame: .zero) addViews() setupConstraints() - configureUI(title: title) + configureUI(type: type) setGesture() } @@ -89,11 +103,11 @@ private extension CharacterInputView { } } - func configureUI(title: String? = nil) { + func configureUI(type: CharacterViewType) { inputBox.textField.delegate = self errorMessage.isHidden = true - descriptionLabel.attributedText = .makeStyledString(font: .h_xxl_b, text: title ?? "현재 레벨과 직업을\n입력해주세요.", alignment: .left) + descriptionLabel.attributedText = .makeStyledString(font: .h_xxl_b, text: type.title, alignment: .left) } /// inputBox를 제외한 영역 선택시 키보드 제거 diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/ToLoginView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/ToLoginView.swift index f2914040..ecd4c756 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/ToLoginView.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/ToLoginView.swift @@ -2,6 +2,29 @@ import UIKit import SnapKit +public enum LoginViewType { + case bookmark + case recommend + + var mainText: String { + switch self { + case .bookmark: + "북마크는 로그인 후 이용 가능해요!" + case .recommend: + "로그인하면 추천 기능이 열려요!" + } + } + + var subText: String { + switch self { + case .bookmark: + "자주 보는 정보, 검색 없이 바로 확인 할 수 있어요" + case .recommend: + "내 레벨과 직업에 맞춰\n사냥터를 추천받을 수 있어요" + } + } +} + public final class ToLoginView: UIView { // MARK: - Type enum Constant { @@ -19,11 +42,11 @@ public final class ToLoginView: UIView { public let button = CommonButton() // MARK: - Init - public init(mainText: String, subText: String, buttonText: String? = nil) { + public init(type: LoginViewType) { super.init(frame: .zero) addViews() setupConstraints() - configureUI(mainText: mainText, subText: subText, buttonText: buttonText) + configureUI(type: type) } @available(*, unavailable) @@ -65,20 +88,20 @@ private extension ToLoginView { } } - func configureUI(mainText: String, subText: String, buttonText: String? = nil) { + func configureUI(type: LoginViewType) { backgroundColor = .neutral100 imageView.image = DesignSystemAsset.image(named: "noShowList") mainLabel.attributedText = .makeStyledString( font: .h_xl_b, - text: mainText + text: type.mainText ) subLabel.attributedText = .makeStyledString( font: .cp_s_r, - text: subText, + text: type.subText, color: .neutral600 ) - button.updateTitle(title: buttonText ?? "로그인하러 가기") + button.updateTitle(title: "로그인하러 가기") } } From 062f7eefad5780ffb0bc07ca862dbfa2ad686f4b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 9 Apr 2026 09:34:59 +0000 Subject: [PATCH 3/6] style/#315: Apply SwiftLint autocorrect --- .../Components/Tooltip/TooltipView.swift | 4 ++-- .../Layouts/CharacterInputView.swift | 2 +- .../Layouts/Factory/TooltipFactory.swift | 6 ++--- .../MLSDesignSystem/Layouts/ToLoginView.swift | 4 ++-- .../TooltipTestViewController.swift | 23 +++++++++---------- .../ViewController.swift | 2 +- 6 files changed, 20 insertions(+), 21 deletions(-) diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift index b04b55c0..5c4ec86b 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift @@ -19,7 +19,7 @@ final class TooltipView: UIView { // MARK: - Properties private let tooltipPosition: TooltipPosition - + // MARK: - Components private let label = UILabel() @@ -47,7 +47,7 @@ private extension TooltipView { func setupConstraints() { switch tooltipPosition { - + /// 툴팁이 아래에 위치 case .bottomLeading, .bottomTrailing: label.snp.makeConstraints { make in diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/CharacterInputView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/CharacterInputView.swift index 7978f861..b69613e0 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/CharacterInputView.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/CharacterInputView.swift @@ -7,7 +7,7 @@ import SnapKit public enum CharacterViewType { case normal case recommend - + var title: String { switch self { case .normal: diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift index 146ed9bc..667921ba 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift @@ -16,13 +16,13 @@ public enum TooltipFactory { } private static var currentTooltip: TooltipView? - + /// 전체 터치 dismiss용 overlay private static var overlayView: TooltipOverlayView? } public extension TooltipFactory { - + /// Tooltip 노출 메소드 static func show( text: String, @@ -94,7 +94,7 @@ public extension TooltipFactory { tooltip.alpha = 1 } } - + /// 툴팁 제거 static func dismiss() { currentTooltip?.removeFromSuperview() diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/ToLoginView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/ToLoginView.swift index ecd4c756..3cad3a23 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/ToLoginView.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/ToLoginView.swift @@ -5,7 +5,7 @@ import SnapKit public enum LoginViewType { case bookmark case recommend - + var mainText: String { switch self { case .bookmark: @@ -14,7 +14,7 @@ public enum LoginViewType { "로그인하면 추천 기능이 열려요!" } } - + var subText: String { switch self { case .bookmark: diff --git a/MLS/MLSDesignSystemExample/ComponentsTest/TooltipTestViewController.swift b/MLS/MLSDesignSystemExample/ComponentsTest/TooltipTestViewController.swift index 45e08203..48bb3a51 100644 --- a/MLS/MLSDesignSystemExample/ComponentsTest/TooltipTestViewController.swift +++ b/MLS/MLSDesignSystemExample/ComponentsTest/TooltipTestViewController.swift @@ -10,26 +10,25 @@ final class TooltipTestViewController: UIViewController { // MARK: - Properties var disposeBag = DisposeBag() - + let button1 = { let button = UIButton(type: .system) button.setTitle("우상단 버튼입니다", for: .normal) return button }() - + let button2 = { let button = UIButton(type: .system) button.setTitle("좌상단", for: .normal) return button }() - - + let button3 = { let button = UIButton(type: .system) button.setTitle("우하단", for: .normal) return button }() - + let button4 = { let button = UIButton(type: .system) button.setTitle("좌하단", for: .normal) @@ -71,23 +70,23 @@ private extension TooltipTestViewController { button1.snp.makeConstraints { make in make.center.equalToSuperview() } - + button2.snp.makeConstraints { make in make.top.equalTo(button1.snp.bottom).offset(10) make.centerX.equalToSuperview() } - + button3.snp.makeConstraints { make in make.top.equalTo(button2.snp.bottom).offset(10) make.centerX.equalToSuperview() } - + button4.snp.makeConstraints { make in make.top.equalTo(button3.snp.bottom).offset(10) make.centerX.equalToSuperview() } } - + func bind() { button1.rx.tap .withUnretained(self) @@ -96,7 +95,7 @@ private extension TooltipTestViewController { ) } .disposed(by: disposeBag) - + button2.rx.tap .withUnretained(self) .subscribe { owner, _ in @@ -104,7 +103,7 @@ private extension TooltipTestViewController { ) } .disposed(by: disposeBag) - + button3.rx.tap .withUnretained(self) .subscribe { owner, _ in @@ -112,7 +111,7 @@ private extension TooltipTestViewController { ) } .disposed(by: disposeBag) - + button4.rx.tap .withUnretained(self) .subscribe { owner, _ in diff --git a/MLS/MLSDesignSystemExample/ViewController.swift b/MLS/MLSDesignSystemExample/ViewController.swift index 76a25237..0c211648 100644 --- a/MLS/MLSDesignSystemExample/ViewController.swift +++ b/MLS/MLSDesignSystemExample/ViewController.swift @@ -40,7 +40,7 @@ class ViewController: UIViewController { BadgeTestController(), DictionaryDetailViewTestController(), TextButtonTestViewController(), - TooltipTestViewController(), + TooltipTestViewController() ] override func viewDidLoad() { From 679b0a520d77b94089869a1f5658716243f20535 Mon Sep 17 00:00:00 2001 From: p2glet Date: Sun, 19 Apr 2026 15:17:29 +0900 Subject: [PATCH 4/6] =?UTF-8?q?fix/#315:=20=EC=A0=9C=EB=AF=B8=EB=82=98?= =?UTF-8?q?=EC=9D=B4=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Tooltip/TooltipArrowView.swift | 24 ------------------- .../Components/Tooltip/TooltipView.swift | 19 +++++++-------- .../Layouts/Factory/TooltipFactory.swift | 22 ++++++++++------- 3 files changed, 22 insertions(+), 43 deletions(-) delete mode 100644 MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipArrowView.swift diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipArrowView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipArrowView.swift deleted file mode 100644 index 34afa4ff..00000000 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipArrowView.swift +++ /dev/null @@ -1,24 +0,0 @@ -import UIKit - -import SnapKit - -final class TooltipArrowView: UIView { - - override class var layerClass: AnyClass { - CAShapeLayer.self - } - - override func layoutSubviews() { - super.layoutSubviews() - - let path = UIBezierPath() - path.move(to: CGPoint(x: bounds.midX, y: bounds.maxY)) - path.addLine(to: CGPoint(x: bounds.minX, y: bounds.minY)) - path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY)) - path.close() - - let shape = layer as! CAShapeLayer - shape.path = path.cgPath - shape.fillColor = UIColor.whiteMLS.cgColor - } -} diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift index b04b55c0..9c5639c8 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift @@ -13,13 +13,15 @@ public enum TooltipPosition { private enum Constants { static let arrowSize = CGSize(width: 16, height: 10) static let cornerRadius: CGFloat = 16 + static let arrowInset: CGFloat = 28 /// arrow는 툴팁 내부에서 고정 위치 } final class TooltipView: UIView { // MARK: - Properties private let tooltipPosition: TooltipPosition - + private let shapeLayer = CAShapeLayer() + // MARK: - Components private let label = UILabel() @@ -71,6 +73,8 @@ private extension TooltipView { label.attributedText = .makeStyledString(font: .b_s_r, text: text) label.numberOfLines = 0 + + layer.insertSublayer(shapeLayer, at: 0) } } @@ -105,16 +109,13 @@ extension TooltipView { cornerRadius: Constants.cornerRadius ) - /// arrow는 툴팁 내부에서 고정 위치 - let arrowInset: CGFloat = 28 - let arrowX: CGFloat switch tooltipPosition { case .topLeading, .bottomLeading: - arrowX = arrowInset + arrowX = Constants.arrowInset case .topTrailing, .bottomTrailing: - arrowX = bubbleRect.width - arrowInset + arrowX = bubbleRect.width - Constants.arrowInset } let arrowPath = UIBezierPath() @@ -132,12 +133,8 @@ extension TooltipView { arrowPath.close() bubblePath.append(arrowPath) - let shapeLayer = CAShapeLayer() + shapeLayer.frame = bounds shapeLayer.path = bubblePath.cgPath shapeLayer.fillColor = UIColor.whiteMLS.cgColor - - /// 기존 shapeLayer 제거 후 다시 추가 - layer.sublayers?.removeAll(where: { $0 is CAShapeLayer }) - layer.insertSublayer(shapeLayer, at: 0) } } diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift index 146ed9bc..f6a47b84 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift @@ -16,13 +16,12 @@ public enum TooltipFactory { } private static var currentTooltip: TooltipView? - + /// 전체 터치 dismiss용 overlay private static var overlayView: TooltipOverlayView? } public extension TooltipFactory { - /// Tooltip 노출 메소드 static func show( text: String, @@ -94,13 +93,20 @@ public extension TooltipFactory { tooltip.alpha = 1 } } - + /// 툴팁 제거 static func dismiss() { - currentTooltip?.removeFromSuperview() - currentTooltip = nil - - overlayView?.removeFromSuperview() - overlayView = nil + guard let tooltip = currentTooltip else { return } + + UIView.animate(withDuration: 0.2, animations: { + tooltip.alpha = 0 + overlayView?.alpha = 0 + }, completion: { _ in + tooltip.removeFromSuperview() + overlayView?.removeFromSuperview() + + currentTooltip = nil + overlayView = nil + }) } } From 7b4aefdaed16bc540b1ffe8c026b6a558456f630 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 19 Apr 2026 06:55:34 +0000 Subject: [PATCH 5/6] style/#315: Apply SwiftLint autocorrect --- .../MLSDesignSystem/Components/Tooltip/TooltipView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift index 69bfaa52..4eddb2ed 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift @@ -73,7 +73,7 @@ private extension TooltipView { label.attributedText = .makeStyledString(font: .b_s_r, text: text) label.numberOfLines = 0 - + layer.insertSublayer(shapeLayer, at: 0) } } From c36073896c011e2d598f7ec17386b4ef9d9e737a Mon Sep 17 00:00:00 2001 From: p2glet Date: Mon, 20 Apr 2026 14:54:24 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix/#315:=20rxGesture=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20+=20=EC=A0=91=EA=B7=BC=EC=A0=9C=EC=96=B4=EC=9E=90=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MLS/MLSDesignSystem/Package.swift | 6 ++++-- .../Components/Tabbar/BottomTabBar.swift | 2 +- .../Components/Tabbar/TabButton.swift | 2 +- .../Components/Tooltip/TooltipOverlayView.swift | 17 +++++++---------- .../Layouts/Factory/TooltipFactory.swift | 9 ++++++--- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/MLS/MLSDesignSystem/Package.swift b/MLS/MLSDesignSystem/Package.swift index 60408739..259c918d 100644 --- a/MLS/MLSDesignSystem/Package.swift +++ b/MLS/MLSDesignSystem/Package.swift @@ -17,7 +17,8 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.7.1"), - .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.9.1") + .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.9.1"), + .package(url: "https://github.com/RxSwiftCommunity/RxGesture.git", from: "4.0.4") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -28,7 +29,8 @@ let package = Package( .product(name: "SnapKit", package: "SnapKit"), .product(name: "RxSwift", package: "RxSwift"), .product(name: "RxCocoa", package: "RxSwift"), - .product(name: "RxRelay", package: "RxSwift") + .product(name: "RxRelay", package: "RxSwift"), + .product(name: "RxGesture", package: "RxGesture") // 추가 ], resources: [ .process("Resources") diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBar.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBar.swift index bf1c2a71..03dc2c5d 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBar.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBar.swift @@ -8,7 +8,7 @@ public struct TabItem { var icon: UIImage } -public final class BottomTabBar: UIStackView { +internal final class BottomTabBar: UIStackView { // MARK: - Type private enum Constant { static let height: CGFloat = 64 diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/TabButton.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/TabButton.swift index 664c2ea1..07646fac 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/TabButton.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/TabButton.swift @@ -2,7 +2,7 @@ import UIKit import SnapKit -public final class TabButton: UIButton { +internal final class TabButton: UIButton { // MARK: - Type private enum Constant { static let spacing: CGFloat = 4 diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipOverlayView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipOverlayView.swift index a319bfe0..db5fe8fa 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipOverlayView.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipOverlayView.swift @@ -1,26 +1,23 @@ import UIKit import RxCocoa +import RxGesture import RxSwift -final class TooltipOverlayView: UIView { +internal final class TooltipOverlayView: UIView { // MARK: - Properties private let disposeBag = DisposeBag() - - var onDismiss: (() -> Void)? + let dismiss = PublishRelay() // MARK: - Init override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear - let tapGesture = UITapGestureRecognizer() - addGestureRecognizer(tapGesture) - - tapGesture.rx.event - .bind { [weak self] _ in - self?.onDismiss?() - } + rx.tapGesture() + .when(.recognized) + .map { _ in } + .bind(to: dismiss) .disposed(by: disposeBag) } diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift index f6a47b84..55c86e10 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift @@ -6,6 +6,7 @@ import SnapKit @MainActor public enum TooltipFactory { // MARK: - Properties + private static let disposeBag = DisposeBag() /// 현재 디바이스 최상단 Window를 지정 static var window: UIWindow? { @@ -42,9 +43,11 @@ public extension TooltipFactory { overlay.addSubview(tooltip) currentTooltip = tooltip - overlay.onDismiss = { - dismiss() - } + overlay.dismiss + .bind { _ in + dismiss() + } + .disposed(by: disposeBag) let frame = anchorView.convert(anchorView.bounds, to: window)