Skip to content

Commit

Permalink
Localize Toast component and extract subcomponents
Browse files Browse the repository at this point in the history
  • Loading branch information
PavelHolec committed Jun 17, 2024
1 parent 0980474 commit 72a6bdd
Show file tree
Hide file tree
Showing 5 changed files with 391 additions and 208 deletions.
203 changes: 7 additions & 196 deletions Sources/Orbit/Components/Toast.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,19 @@ public struct Toast: View {
public var body: some View {
if let toast = toastQueue.toast {
ToastWrapper(
toast.description,
icon: toast.icon,
progress: toast.progress,
pauseAction: toastQueue.pause,
resumeAction: toastQueue.resume,
dismissAction: toastQueue.dismiss
)
) {
toast.description
} icon: {
toast.icon
}
.id(toast.id)
.padding(.xSmall)
.transition(.move(edge: .top).combined(with: .opacity))
.animation(.spring)
}
}

Expand All @@ -51,211 +54,24 @@ public struct Toast: View {
}
}

/// Orbit ``Toast`` with no gesture handling or queue management.
public struct ToastContent: View {

@Environment(\.colorScheme) private var colorScheme
@Environment(\.textColor) private var textColor

private let description: String
private let icon: Icon.Symbol?
private let progress: CGFloat

public var body: some View {
HStack(alignment: .top, spacing: .xSmall) {
Icon(icon)
Text(description)
Spacer(minLength: 0)
}
.textColor(textColor ?? foregroundColor)
.padding(.small)
.contentShape(Rectangle())
.background(background)
}

@ViewBuilder private var background: some View {
backgroundColor
.overlay(progressIndicator, alignment: .leading)
.clipShape(shape)
.elevation(.level3, shape: .roundedRectangle())
}

@ViewBuilder private var progressIndicator: some View {
GeometryReader { geometry in
progressColor
.opacity(max(0, progress * 2 - 0.5) * 0.3)
.clipShape(shape)
.frame(width: geometry.size.width * progress, alignment: .bottomLeading)
.animation(ToastQueue.animationIn, value: progress)
}
}

private var foregroundColor: Color {
colorScheme == .light ? .whiteNormal : .inkDark
}

private var backgroundColor: Color {
colorScheme == .light ? .inkDark : .whiteDarker
}

private var progressColor: Color {
colorScheme == .light ? .inkNormal : .cloudNormal
}

private var shape: some Shape {
RoundedRectangle(cornerRadius: BorderRadius.default, style: .continuous)
}

/// Creates Orbit ``ToastContent`` component with no gesture handling or queue management.
public init(_ description: String, icon: Icon.Symbol? = nil, progress: CGFloat = 0) {
self.description = description
self.icon = icon
self.progress = progress
}
}

/// Orbit ``ToastWrapper`` component with gesture handling, but no queue management.
public struct ToastWrapper: View {

static let minOffsetY: CGFloat = -10
static let maxOffsetY: CGFloat = 10

@Environment(\.isHapticsEnabled) private var isHapticsEnabled

private let description: String
private let icon: Icon.Symbol?
private let progress: CGFloat
private let pauseAction: () -> Void
private let resumeAction: () -> Void
private let dismissAction: () -> Void

@State private var offsetY: CGFloat = 0
@State private var gaveFeedback: Bool = false

public var body: some View {
ToastContent(description, icon: icon, progress: progress)
.opacity(opacity)
.offset(y: cappedOffsetY)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
pauseAction()
offsetY = gesture.translation.height / 2
processDragChanged()
}
.onEnded { _ in
if isOffsetDismiss {
dismissAction()
} else {
resumeAction()
withAnimation(ToastQueue.animationIn) {
offsetY = 0
}
}
}
)
}

private var isOffsetDismiss: Bool {
offsetY < Self.minOffsetY
}

private var dismissProgress: CGFloat {
min(0, cappedOffsetY) / Self.minOffsetY
}

private var opacity: CGFloat {
return 1 - dismissProgress * 0.2
}

private var cappedOffsetY: CGFloat {
min(Self.maxOffsetY, offsetY)
}

private func processDragChanged() {
if dismissProgress >= 1, gaveFeedback == false {
if isHapticsEnabled {
HapticsProvider.sendHapticFeedback(.notification(.warning))
}

gaveFeedback = true
}

if dismissProgress == 0 {
gaveFeedback = false
}
}

/// Creates Orbit ``ToastWrapper`` component.
public init(
_ description: String,
icon: Icon.Symbol? = nil,
progress: CGFloat = 0,
pauseAction: @escaping () -> Void,
resumeAction: @escaping() -> Void,
dismissAction: @escaping () -> Void
) {
self.description = description
self.icon = icon
self.progress = progress
self.pauseAction = pauseAction
self.resumeAction = resumeAction
self.dismissAction = dismissAction
}

}

// MARK: - Previews
struct ToastPreviews: PreviewProvider {

static let description = "Toast shows a brief message that's clear & understandable."
static let toastQueue = ToastQueue()
static let toastLiveQueue = ToastQueue()

static var previews: some View {
PreviewWrapper {
standalone
standaloneWrapper
progress
}
.padding(.xLarge)
.previewLayout(.sizeThatFits)

PreviewWrapper {
interactive
}
}

static var standalone: some View {
ToastContent(description, icon: .grid)
.previewDisplayName()

}

static var standaloneWrapper: some View {
ToastWrapper(description, icon: .checkCircle, progress: 0.6, pauseAction: {}, resumeAction: {}, dismissAction: {})
.previewDisplayName()
}

static var progress: some View {
VStack(alignment: .leading, spacing: .xxxLarge) {
ToastContent(description, progress: 0.01)
ToastContent(description, progress: 0.2)
ToastContent(description, progress: 0.8)
ToastContent(description, progress: 1.1)
ToastContent("Toast shows a brief message that's clear & understandable.", icon: .checkCircle, progress: 0.6)
}
.padding(.top, .large)
.padding(.bottom, .xxxLarge)
.previewDisplayName()
}

static var interactive: some View {
NavigationView {
VStack(alignment: .leading, spacing: .medium) {
Spacer()

Button("Add toast 1") { toastLiveQueue.add(description) }
Button("Add toast 1") { toastLiveQueue.add(ToastContentPreviews.description) }
Button("Add toast 2") { toastLiveQueue.add("Another toast message.") }
}
.padding(.medium)
Expand All @@ -269,9 +85,4 @@ struct ToastPreviews: PreviewProvider {
static var toast: some View {
Toast(toastQueue: toastLiveQueue)
}

static var snapshot: some View {
progress
.padding(.medium)
}
}
142 changes: 142 additions & 0 deletions Sources/Orbit/Support/Toast/ToastContent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import SwiftUI

/// Orbit ``Toast`` with no gesture handling or queue management.
public struct ToastContent<Description: View, Icon: View>: View {

@Environment(\.colorScheme) private var colorScheme
@Environment(\.textColor) private var textColor

private let progress: CGFloat
@ViewBuilder private let icon: Icon
@ViewBuilder private let description: Description

public var body: some View {
HStack(alignment: .top, spacing: .xSmall) {
icon
description
Spacer(minLength: 0)
}
.textColor(textColor ?? foregroundColor)
.padding(.small)
.contentShape(Rectangle())
.background(background)
}

@ViewBuilder private var background: some View {
backgroundColor
.overlay(progressIndicator, alignment: .leading)
.clipShape(shape)
.elevation(.level3, shape: .roundedRectangle())
}

@ViewBuilder private var progressIndicator: some View {
GeometryReader { geometry in
progressColor
.opacity(max(0, progress * 2 - 0.5) * 0.3)
.clipShape(shape)
.frame(width: geometry.size.width * progress, alignment: .bottomLeading)
.animation(ToastQueue.animationIn, value: progress)
}
}

private var foregroundColor: Color {
colorScheme == .light ? .whiteNormal : .inkDark
}

private var backgroundColor: Color {
colorScheme == .light ? .inkDark : .whiteDarker
}

private var progressColor: Color {
colorScheme == .light ? .inkNormal : .cloudNormal
}

private var shape: some Shape {
RoundedRectangle(cornerRadius: BorderRadius.default, style: .continuous)
}

/// Creates Orbit ``ToastContent`` component with custom content.
public init(
progress: CGFloat = 0,
@ViewBuilder description: () -> Description,
@ViewBuilder icon: () -> Icon = { EmptyView() }
) {
self.progress = progress
self.description = description()
self.icon = icon()
}
}

// MARK: - Convenience Inits
public extension ToastContent where Description == Text, Icon == Orbit.Icon {

/// Creates Orbit ``ToastContent`` component.
@_disfavoredOverload
init(
_ description: some StringProtocol = String(""),
icon: Icon.Symbol? = nil,
progress: CGFloat = 0
) {
self.init(progress: progress) {
Text(description)
} icon: {
Icon(icon)
}
}

/// Creates Orbit ``ToastContent`` component with localized description.
@_semantics("swiftui.init_with_localization")
init(
_ description: LocalizedStringKey,
icon: Icon.Symbol? = nil,
progress: CGFloat = 0,
tableName: String? = nil,
bundle: Bundle? = nil,
comment: StaticString? = nil
) {
self.init(progress: progress) {
Text(description, tableName: tableName, bundle: bundle)
} icon: {
Icon(icon)
}
}
}

// MARK: - Previews
struct ToastContentPreviews: PreviewProvider {

static let description = "Toast shows a brief message that's clear & understandable."

static var previews: some View {
PreviewWrapper {
standalone
progress
}
.padding(.xLarge)
.previewLayout(.sizeThatFits)
}

static var standalone: some View {
ToastContent(description, icon: .grid)
.previewDisplayName()

}

static var progress: some View {
VStack(alignment: .leading, spacing: .xxxLarge) {
ToastContent(description, progress: 0.01)
ToastContent(description, progress: 0.2)
ToastContent(description, progress: 0.8)
ToastContent(description, progress: 1.1)
ToastContent("Toast shows a brief message that's clear & understandable.", icon: .checkCircle, progress: 0.6)
}
.padding(.top, .large)
.padding(.bottom, .xxxLarge)
.previewDisplayName()
}

static var snapshot: some View {
progress
.padding(.medium)
}
}
Loading

0 comments on commit 72a6bdd

Please sign in to comment.