-
Notifications
You must be signed in to change notification settings - Fork 0
추천 기능 UI에 맞게 컴포넌트들을 수정 / 구현합니다. #320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
6cabad5
e288405
71f3d25
062f7ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| 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) | ||
|
Comment on lines
+140
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 } | ||
| } | ||
|
Comment on lines
+11
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| 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 | ||
| } | ||
|
Comment on lines
+99
to
+105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TooltipView내부에서UIBezierPath를 사용하여 직접 화살표를 그리고 있으므로, 이TooltipArrowView클래스는 현재 프로젝트 내에서 사용되지 않는 것으로 보입니다. 불필요한 파일은 제거하여 프로젝트 구조를 깔끔하게 유지하는 것이 좋습니다.