Skip to content

Commit

Permalink
✨ Add ToastItem, ToastView, ToastGroup, RootView, and Toast classes
Browse files Browse the repository at this point in the history
Added ToastItem struct with custom properties and Timing enum for toast item
Added ToastView struct for displaying toast item with symbol image and title
Added ToastGroup struct for managing multiple toast views in a ZStack
Added RootView struct for setting up UIWindow for overlay window to display toast
Added Toast class with present method to add toast items to display on RootView
  • Loading branch information
WhiteHyun committed Mar 24, 2024
1 parent b92589c commit 4a9f425
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 0 deletions.
33 changes: 33 additions & 0 deletions Shared/Sources/DesignSystem/Components/Toast/Toast.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// Toast.swift
//
//
// Created by 홍승현 on 3/24/24.
//

import SwiftUI

@Observable
public final class Toast {
/// Since the class conforms to the Observable protocol,
/// we can use this singleton object as a state object to receive UI updates on the overlay window root controller
public static let shared = Toast()
var toasts: [ToastItem] = []

/// 토스트를 띄웁니다.
/// - Parameters:
/// - title: 토스트 제목
/// - symbol: 토스트에 들어갈 `SF Symbol` 이미지 문자열 값
/// - tint: 토스트의 틴트 색상
/// - isUserInteractionEnabled: 사용자의 상호작용 여부, 스스로 지우거나 못하도록 설정할 수 있습니다.
/// - duration: 토스트 유지 시간
public func present(
title: String,
symbol: String?,
tint: Color = .gray900,
isUserInteractionEnabled: Bool = false,
duration: ToastTime = .medium
) {
toasts.append(.init(title: title, symbol: symbol, tint: tint, isUseInteractionEnabled: isUserInteractionEnabled, duration: duration))
}
}
49 changes: 49 additions & 0 deletions Shared/Sources/DesignSystem/Components/Toast/ToastItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// ToastItem.swift
//
//
// Created by 홍승현 on 3/24/24.
//

import SwiftUI

// MARK: - ToastItem

struct ToastItem: Identifiable {
let id: UUID = .init()

// MARK: Custom Properties

/// 토스트 제목
let title: String

/// 토스트 심볼 이미지 문자열
let symbol: String?

/// 토스트 틴트 색상
let tint: Color

/// 사용자 상호작용 여부
let isUseInteractionEnabled: Bool

// MARK: Timing

/// 토스트 유지 시간
let duration: ToastTime

init(title: String, symbol: String?, tint: Color, isUseInteractionEnabled: Bool, duration: ToastTime) {
self.title = title
self.symbol = symbol
self.tint = tint
self.isUseInteractionEnabled = isUseInteractionEnabled
self.duration = duration
}
}

// MARK: - ToastTime

public enum ToastTime: CGFloat {
case short = 1.0
case medium = 2.0
case long = 3.5
}
55 changes: 55 additions & 0 deletions Shared/Sources/DesignSystem/Components/Toast/View/RootView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// RootView.swift
//
//
// Created by 홍승현 on 3/24/24.
//

import SwiftUI

// MARK: - RootView

public struct RootView<Content: View>: View {
@ViewBuilder public var content: Content

@inlinable public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content()
}

// MARK: View Properties

@State private var overlayWindow: UIWindow?

public var body: some View {
content
.onAppear {
for scene in UIApplication.shared.connectedScenes where scene.activationState == .foregroundActive && overlayWindow == nil {
if let windowScene = scene as? UIWindowScene {
let window = PassthroughWindow(windowScene: windowScene)
window.backgroundColor = .clear
window.isUserInteractionEnabled = true
window.isHidden = false

// view controller part

let rootController = UIHostingController(rootView: ToastGroup())
rootController.view.frame = windowScene.screen.bounds
rootController.view.backgroundColor = .clear
window.rootViewController = rootController

overlayWindow = window
break
}
}
}
}
}

// MARK: - PassthroughWindow

private class PassthroughWindow: UIWindow {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let view = super.hitTest(point, with: event) else { return nil }
return rootViewController?.view == view ? nil : view
}
}
26 changes: 26 additions & 0 deletions Shared/Sources/DesignSystem/Components/Toast/View/ToastGroup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// ToastGroup.swift
//
//
// Created by 홍승현 on 3/24/24.
//

import SwiftUI

struct ToastGroup: View {
let model = Toast.shared
var body: some View {
GeometryReader { proxy in
let size = proxy.size
let safeArea = proxy.safeAreaInsets

ZStack {
ForEach(model.toasts) {
ToastView(size: size, item: $0)
}
}
.padding(.bottom, safeArea.top == .zero ? 15 : 10)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
}
}
}
35 changes: 35 additions & 0 deletions Shared/Sources/DesignSystem/Components/Toast/View/ToastView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// ToastView.swift
//
//
// Created by 홍승현 on 3/24/24.
//

import SwiftUI

struct ToastView: View {
var size: CGSize
var item: ToastItem

var body: some View {
HStack(spacing: 0) {
if let symbol = item.symbol {
Image(systemName: symbol)
.font(.title3)
.padding(.trailing, 10)
}

Text(item.title)
}
.foregroundStyle(item.tint)
.padding(.horizontal, 15)
.padding(.vertical, 8)
.background(
.background
.shadow(.drop(color: .primary.opacity(0.06), radius: 5, x: 5, y: 5))
.shadow(.drop(color: .primary.opacity(0.06), radius: 5, x: -5, y: -5)),
in: .capsule
)
.contentShape(.capsule)
}
}

0 comments on commit 4a9f425

Please sign in to comment.