-
Notifications
You must be signed in to change notification settings - Fork 0
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
[Feat/NST-62] #31 AppToast, ToastManager 작성 #32
Merged
Merged
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
832a5fb
[Feat/#31] 기존 컴포넌트 위치 수정
FpRaArNkK f8183e0
[Feat/#31] AppToastView 작성
FpRaArNkK 57478bb
[Feat] String.pretendardStyle color argument 추가
FpRaArNkK e08c66e
[Feat/#31] ToastManager 작성
FpRaArNkK 9b904b8
[Fix] ToastManager Queue 미반영 이슈 처리
FpRaArNkK 12a70f2
[Chore] AppToastView Constraint 설정 수정
FpRaArNkK File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
110 changes: 110 additions & 0 deletions
110
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,110 @@ | ||
// | ||
// 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.leading.equalTo(messageLabel).offset(-20) | ||
$0.trailing.equalTo(messageLabel).offset(20) | ||
$0.top.equalTo(messageLabel).offset(-12) | ||
$0.bottom.equalTo(messageLabel).offset(12) | ||
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. 왼쪽오른쪽 위아래 값이 같다면, horizontalEdge , verticalEdge 를 써도 좋을 것 같네요! 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. 와 맞네요 까마득하게 잊고 있었슴다 horizontal/vertical inset으로 수정해놓겠습니다! |
||
} | ||
} | ||
} | ||
|
||
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
107 changes: 107 additions & 0 deletions
107
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,107 @@ | ||
// | ||
// 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] = [] | ||
|
||
/// 토스트 메시지를 화면에 표시합니다. | ||
/// | ||
/// - 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) // 토스트 큐에 추가 | ||
|
||
// 애니메이션 (페이드 인 -> 유지 -> 페이드 아웃) | ||
UIView.animate(withDuration: 0.3, animations: { | ||
toastView.alpha = 1 | ||
}) { _ 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 | ||
}) { _ in | ||
toastView.removeFromSuperview() | ||
self.toastQueue.removeAll { $0 == toastView } | ||
|
||
// 모든 토스트가 사라지면 overlayView도 제거 | ||
if self.toastQueue.isEmpty { | ||
self.overlayView?.removeFromSuperview() | ||
self.overlayView = nil | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// 큐에 쌓인 토스트를 일괄 삭제합니다 | ||
@objc private func dismissAllToasts() { | ||
DispatchQueue.main.async { | ||
for toast in self.toastQueue { | ||
toast.removeFromSuperview() | ||
} | ||
self.toastQueue.removeAll() | ||
self.overlayView?.removeFromSuperview() | ||
self.overlayView = nil | ||
} | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
토스트 메세지가 현재 뷰가 바뀌어도 유지가 되나요? 아니면 뷰가 dismiss 또는 pop 되는 순간 사라지나요?
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.
아뇨! 현재 앱에서 표시되고 있는 화면의 window에 추가되는 것이라서 UIWindow에 붙어있는 토스트라고 생각하시면 될 것 같습니다.
화면 전환 시에도 남아있게됩니다.