Skip to content
Merged
6 changes: 4 additions & 2 deletions MLS/MLSDesignSystem/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import UIKit

import RxCocoa
import RxGesture
import RxSwift

internal final class TooltipOverlayView: UIView {
// MARK: - Properties
private let disposeBag = DisposeBag()
let dismiss = PublishRelay<Void>()

// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear

rx.tapGesture()
.when(.recognized)
.map { _ in }
.bind(to: dismiss)
.disposed(by: disposeBag)
}

@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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
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()

// 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

layer.insertSublayer(shapeLayer, at: 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
)

let arrowX: CGFloat
switch tooltipPosition {
case .topLeading, .bottomLeading:
arrowX = Constants.arrowInset

case .topTrailing, .bottomTrailing:
arrowX = bubbleRect.width - Constants.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)

shapeLayer.frame = bounds
shapeLayer.path = bubblePath.cgPath
shapeLayer.fillColor = UIColor.whiteMLS.cgColor
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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를 제외한 영역 선택시 키보드 제거
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import RxSwift
import SnapKit

@MainActor
public final class ToastFactory {
public enum ToastFactory {

// MARK: - Properties

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import UIKit

import RxSwift
import SnapKit

@MainActor
public enum TooltipFactory {
// MARK: - Properties
private static let disposeBag = DisposeBag()

/// 현재 디바이스 최상단 Window를 지정
static var window: UIWindow? {
UIApplication.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }
}
Comment on lines +12 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

최상단 window를 찾는 로직이 ToastFactory.swift와 완전히 중복되어 구현되어 있습니다. 향후 윈도우 탐색 로직이 변경될 경우 두 곳을 모두 수정해야 하는 번거로움이 있으므로, UIWindow extension 등으로 분리하여 공통으로 사용하는 것이 좋습니다.


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.dismiss
.bind { _ in
dismiss()
}
.disposed(by: disposeBag)

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() {
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
})
}
Comment thread
pinocchio22 marked this conversation as resolved.
}
Loading
Loading