-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #32 from Noostak/feat/NST-62/toast
[Feat/NST-62] #31 AppToast, ToastManager 작성
- Loading branch information
Showing
9 changed files
with
241 additions
and
3 deletions.
There are no files selected for viewing
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
108 changes: 108 additions & 0 deletions
108
Noostak_iOS/Noostak_iOS/Global/Components/AppToastView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
// | ||
// AppToast.swift | ||
// Noostak_iOS | ||
// | ||
// Created by 박민서 on 2/2/25. | ||
// | ||
|
||
import UIKit | ||
import SnapKit | ||
import Then | ||
import RxSwift | ||
import RxCocoa | ||
|
||
final class AppToastView: UIView { | ||
|
||
// MARK: Properties | ||
private var status: Status | ||
|
||
// MARK: Views | ||
private let messageLabel = UILabel() | ||
private let backgroundView = UIView() | ||
|
||
// MARK: Init | ||
init(status: Status) { | ||
self.status = status | ||
super.init(frame: .zero) | ||
setUpHierarchy() | ||
setUpUI() | ||
setUpLayout() | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
override func layoutSubviews() { | ||
super.layoutSubviews() | ||
backgroundView.layer.cornerRadius = backgroundView.frame.height / 2 | ||
} | ||
|
||
// MARK: setUpHierarchy | ||
private func setUpHierarchy() { | ||
[ | ||
backgroundView, | ||
messageLabel | ||
].forEach { self.addSubview($0) } | ||
} | ||
|
||
// MARK: setUpUI | ||
private func setUpUI() { | ||
messageLabel.do { | ||
$0.attributedText = status.attributedText | ||
$0.textAlignment = .center | ||
$0.numberOfLines = 0 | ||
} | ||
|
||
backgroundView.do { | ||
$0.backgroundColor = status.backgroundColor | ||
$0.layer.cornerRadius = 21 // layoutSubviews에서 조정됩니다 | ||
$0.clipsToBounds = true | ||
} | ||
} | ||
|
||
// MARK: setUpLayout | ||
private func setUpLayout() { | ||
messageLabel.snp.makeConstraints { | ||
$0.center.equalToSuperview() | ||
} | ||
|
||
backgroundView.snp.makeConstraints { | ||
$0.horizontalEdges.equalTo(messageLabel).inset(-20) | ||
$0.verticalEdges.equalTo(messageLabel).inset(-12) | ||
} | ||
} | ||
} | ||
|
||
extension AppToastView { | ||
enum Status { | ||
case `default`(message: String) | ||
case `error`(message: String) | ||
|
||
var backgroundColor: UIColor { | ||
switch self { | ||
|
||
case .default: | ||
return .appGray800 | ||
case .error: | ||
return .appPink | ||
} | ||
} | ||
|
||
var attributedText: NSAttributedString { | ||
switch self { | ||
|
||
case .default(let message): | ||
return message.pretendardStyled(style: .c3_r, color: .appWhite) | ||
case .error(let message): | ||
// TODO: 폰트 시스템 C3_SB 추가되면 수정 | ||
return message.pretendardStyled(style: .c3_r, color: .appRed01) | ||
} | ||
} | ||
} | ||
} | ||
|
||
#Preview { | ||
// AppToastView(status: .default(message: "그룹 코드가 복사되었습니다")) | ||
AppToastView(status: .error(message: "최대 7일까지 선택할 수 있어요")) | ||
} |
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
Noostak_iOS/Noostak_iOS/Global/Utility/ToastManager.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
// | ||
// ToastManager.swift | ||
// Noostak_iOS | ||
// | ||
// Created by 박민서 on 2/2/25. | ||
// | ||
|
||
import UIKit | ||
import SnapKit | ||
|
||
final class ToastManager { | ||
static let shared = ToastManager() | ||
|
||
private init() {} | ||
|
||
private var overlayView: UIView? | ||
private var toastQueue: [(AppToastView, TimeInterval)] = [] | ||
private var isShowingToast = false | ||
|
||
/// 토스트 메시지를 화면에 표시합니다. | ||
/// | ||
/// - Parameters: | ||
/// - status: 표시할 토스트의 상태 (텍스트, 색상 등 지정) | ||
/// - duration: 토스트가 화면에 유지되는 시간 (기본값: 2.0초) | ||
/// - bottomFrom: 토스트가 화면 하단에서부터 얼마나 떨어져서 표시될지 지정 (기본값: 80pt) | ||
func showToast(status: AppToastView.Status, duration: TimeInterval = 2.0, bottomFrom: CGFloat = 80) { | ||
guard let windowScene = UIApplication.shared.connectedScenes | ||
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene, | ||
let window = windowScene.windows.first else { return } | ||
|
||
// 기존 overlayView가 없으면 생성 | ||
if overlayView == nil { | ||
self.overlayView = makeOverlayView(window: window) | ||
} | ||
|
||
// 토스트 뷰 생성 후 큐에 추가 | ||
let toastView = makeToastView(status: status, bottomFrom: bottomFrom) | ||
toastQueue.append((toastView, duration)) | ||
|
||
// 현재 토스트가 표시 중이 아닐 때만 새로 표시 | ||
if !isShowingToast { | ||
showNextToast() | ||
} | ||
} | ||
|
||
/// 큐에서 다음 토스트를 표시합니다 | ||
private func showNextToast() { | ||
// 토스트 큐에 토스트 있을 때 다음으로 | ||
guard !toastQueue.isEmpty else { | ||
isShowingToast = false | ||
return | ||
} | ||
|
||
isShowingToast = true | ||
let (toastView, duration) = toastQueue.removeFirst() // FIFO | ||
|
||
// 애니메이션 (페이드 인 -> 유지 -> 페이드 아웃) | ||
UIView.animate(withDuration: 0.3, animations: { | ||
toastView.alpha = 1 | ||
}, completion: { _ in | ||
DispatchQueue.main.asyncAfter(deadline: .now() + duration) { | ||
self.dismissToast(toastView) | ||
} | ||
}) | ||
} | ||
|
||
/// overlay 뷰가 없는 경우 새로운 뷰를 생성합니다 | ||
private func makeOverlayView(window: UIWindow) -> UIView { | ||
let overlay = UIView(frame: window.bounds) | ||
overlay.backgroundColor = UIColor.clear | ||
window.addSubview(overlay) | ||
|
||
// 제스처 추가 (화면 터치 시 모든 토스트 닫기) | ||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissAllToasts)) | ||
overlay.addGestureRecognizer(tapGesture) | ||
|
||
return overlay | ||
} | ||
|
||
/// 새로운 토스트 뷰를 생성합니다 | ||
private func makeToastView(status: AppToastView.Status, bottomFrom: CGFloat) -> AppToastView { | ||
let toastView = AppToastView(status: status) | ||
toastView.alpha = 0 // clear 상태로 추가 | ||
overlayView?.addSubview(toastView) | ||
|
||
toastView.snp.makeConstraints { | ||
$0.centerX.equalToSuperview() | ||
$0.bottom.equalToSuperview().inset(bottomFrom) | ||
} | ||
|
||
toastView.layoutIfNeeded() | ||
return toastView | ||
} | ||
|
||
/// 해당 토스트를 dimiss합니다 with Animation | ||
/// 큐에서 해당 토스트를 삭제하고, 다음 토스트를 실행합니다 | ||
private func dismissToast(_ toastView: AppToastView) { | ||
DispatchQueue.main.async { | ||
UIView.animate(withDuration: 0.3, animations: { | ||
toastView.alpha = 0 | ||
}, completion: { _ in | ||
toastView.removeFromSuperview() | ||
|
||
// 다음 토스트 실행 | ||
if !self.toastQueue.isEmpty { | ||
self.showNextToast() | ||
} else { | ||
// 모든 토스트가 사라지면 overlayView도 제거 | ||
self.overlayView?.removeFromSuperview() | ||
self.overlayView = nil | ||
self.isShowingToast = false | ||
} | ||
}) | ||
} | ||
} | ||
|
||
/// 큐에 쌓인 토스트를 일괄 삭제합니다 | ||
@objc private func dismissAllToasts() { | ||
DispatchQueue.main.async { | ||
for (toast, _) in self.toastQueue { | ||
toast.removeFromSuperview() | ||
} | ||
self.toastQueue.removeAll() | ||
self.overlayView?.removeFromSuperview() | ||
self.overlayView = nil | ||
self.isShowingToast = false | ||
} | ||
} | ||
} |