Skip to content

Commit

Permalink
Feat SwiftUI Toast (#245)
Browse files Browse the repository at this point in the history
* Add Tooltip view

* Refactor Tooltip style

* Can adjust tooltip position

* Add tooltip playground for testing

* Refine layout tips logic

* Refine layout logic

* Update CharcoalTooltip.swift

* Refine tooltip spacing

* Add CharcoalIdentifiableOverlayView

* Use Actor to prevent Data Race

* Remove CharcoalIdentifiableOverlayView out

* Clean code

* Only update view when it is isPresenting

* Clean access control

* Add TooltipsView

* Fix tooltipY layout logic

* Use main actor and remove CharcoalContainerManagerKey

* Fix access control on CharcoalContainerManager

* Make viewID as @State

* Use EnviromentObject to create CharcoalContainerManager for each container

* Use ObservedObject on  CharcoalContainerManager

* Add use charcoal button as demo trigger

* Add arrow logic on tooltip

* Refine arrow logic

* Refine arrow layout logic

* Use StateObject to prevent unexpected reinit

* Refactor TooltipBubbleShape

* Fix edge layout logic

* Add comment

* Format code

* Use new approach to remove adaptiveMaxWidth

* Fix the tip bubble's position latency

* Add dismiss when interaction

* Reformat

* Add initial Snackbar

* Add thumbnail image

* Add support for thumbnailImage and action

* Clean code

* Reformat code

* Rename ActionContent

* Replace thumbnailImage type

* Add dismissOnTouchOutside control

* Add comment on CharcoalIdentifiableOverlayView

* Update CharcoalTooltip.swift

* Add SnackBar demo

* Replace thumbnail with charcoal logo

* Use @ViewBuilder

* Clean Code

* Made code more readable

* Update ToastsView.swift

* Add auto dismiss logic

* Fix dismiss comment

* Add Identifiable to CharcoalIdentifiableOverlayView

* Make all CharcoalPopupView identifiable

* Move all control logic into CharcoalPopupView

* Reformat

* Refine CharcoalOverlayContainerChild logic of updating view

* Rename to CharcoalOverlayUpdaterContainer

* Add CharcoalToast

* Refine toast control

* Refine screen edge of toast

* Refine comments

* Rename CharcoalPopupProtocol

* Refine isActuallyPresenting logic

* Clean animation

* Add animation configuration

* Add custom animation

* Add CharcoalToastProtocol

* Makes CharcoalSnackBar adapt CharcoalToastProtocol

* Remove time delay

* Refine SnackBar Animation logic

* Add CharcoalToastAnimationModifier

* Reformat code

* Update CharcoalPopupViewEdge of direction

* Refine demo

* Fix missing animation

* Rename charcoalAnimatedToast to charcoalAnimatableToast

* Rename CharcoalAnimatableToastProtocol

* Rename for clean

* Simplify protocols

* Add drag control

* Add Dismiss timer control logic

* Refine drag damping logic

* Add CharcoalToastDraggable

* Use CharcoalToastDraggableModifier on CharcoalSnackBar

* Format code

* Init CharcoalTooltipView

* Init Bubble shape

* Refine tooltip preview

* Rename as Charcoal Bubble Shape

* Add Label to tooltip

* Update text frame when traitCollection did change

* Update CharcoalTooltipView.swift

* Add CharcoalTooltip

* Can debug show on method

* Can layout point

* Can redraw target point

* Update CharcoalTooltip.swift

* Refine tooltip display

* Share the logic

* Use interaction mode

* Use CharcoalOverlayContainerView

* Update CharcoalOverlay.swift

* Refactor to ChacoalOverlayManager

* Makes CharcoalIdentifiableOverlayView  Identifiable

* Refine layout logic

* Add display(view: CharcoalIdentifiableOverlayView)

* Add tooltip to uikit example

* Update Tooltips.swift

* Add CharcoalIdentifiableOverlayDelegate

* Reformat

* Update StringExtension.swift

* Add to UIKitSample

* Fix public requirements

* Reformat

* Use touch began to handle dismiss on touch

* Update CharcoalIdentifiableOverlayView.swift

* Move show logic out

* Reformat

* Refine dismiss method

* Add ActionContent and ActionComplete callback

* Update CharcoalBubbleShape_UIKit.swift

* Reformat

* Fix name

* Fix geometry

* Revert "Fix geometry"

This reverts commit a89bf66.

* Fix proxy name

* Adjust unused text

* Replace charcoal logo

* Remove conditional modifier

* Add default dismiss time to toasts

* Reformat
  • Loading branch information
kevinneko authored Jul 24, 2024
1 parent 5cfd721 commit b238c37
Show file tree
Hide file tree
Showing 15 changed files with 1,140 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,16 @@ public struct ContentView: View {
NavigationLink(destination: TooltipsView()) {
Text("Tooltips")
}
NavigationLink(destination: ToastsView()) {
Text("Toasts")
}
NavigationLink(destination: SpinnersView()) {
Text("Spinners")
}
}
.navigationBarTitle("Charcoal")
}
.charcoalOverlayContainer()
.preferredColorScheme(isDarkModeOn ? .dark : .light)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "charcoal-logo.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
170 changes: 170 additions & 0 deletions CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import Charcoal
import SwiftUI

public struct ToastsView: View {
@State var isPresenting = false
@State var isPresenting2 = false
@State var isPresenting3 = false
@State var isPresenting4 = false

@State var isPresentingToast = false
@State var isPresentingToast2 = false
@State var isPresentingToast3 = false
@State var isPresentingToast4 = false

public var body: some View {
List {
Section(header: Text("SnackBar")) {
Button {
isPresenting.toggle()
} label: {
Text("SnackBar")
}
.charcoalSnackBar(
isPresenting: $isPresenting,
text: "ブックマークしました"
)

VStack(alignment: .leading) {
Button {
isPresenting2.toggle()
} label: {
Text("SnackBar")
}
Text("with Action")
}
.charcoalSnackBar(
isPresenting: $isPresenting2,
screenEdge: .top,
text: "ブックマークしました",
action: {
Button {
print("Tapped")
} label: {
Text("編集")
}
}
)

VStack(alignment: .leading) {
Button {
isPresenting3.toggle()
} label: {
Text("SnackBar")
}
Text("with Action and Thumbnail")
}
.charcoalSnackBar(
isPresenting: $isPresenting3,
text: "ブックマークしました",
thumbnailImage: Image("SnackbarDemo", bundle: Bundle.module),
action: {
Button {
print("Tapped")
} label: {
Text("編集")
}
}
)

VStack(alignment: .leading) {
Button {
isPresenting4.toggle()
} label: {
Text("SnackBar")
}
Text("Auto dismiss after 2 seconds")
}
.charcoalSnackBar(
isPresenting: $isPresenting4,
text: "ブックマークしました",
thumbnailImage: Image("SnackbarDemo", bundle: Bundle.module),
dismissAfter: 2,
action: {
Button {
print("Tapped")
} label: {
Text("編集")
}
}
)
}

Section(header: Text("Toasts")) {
Button {
isPresentingToast.toggle()
} label: {
Text("Toast")
}
.charcoalToast(
isPresenting: $isPresentingToast,
text: "テキストメッセージ"
)

VStack(alignment: .leading) {
Button {
isPresentingToast2.toggle()
} label: {
Text("Toast")
}
Text("with Action")
}
.charcoalToast(
isPresenting: $isPresentingToast2,
screenEdge: .top,
text: "テキストメッセージ",
action: {
Button {
isPresentingToast2 = false
} label: {
Image(charocalIcon: .remove16)
.renderingMode(.template)
}
}
)

VStack(alignment: .leading) {
Button {
isPresentingToast3.toggle()
} label: {
Text("Toast(Error Appearance)")
}
Text("with Custom Animation")
}
.charcoalToast(
isPresenting: $isPresentingToast3,
text: "ブックマークしました",
appearance: .error,
animationConfiguration: CharcoalToastAnimationConfiguration(enablePositionAnimation: false, animation: .easeInOut),
action: {
Button {
isPresentingToast3 = false
} label: {
Image(charocalIcon: .remove16)
.renderingMode(.template)
}
}
)

VStack(alignment: .leading) {
Button {
isPresentingToast4.toggle()
} label: {
Text("Toast(Error Appearance)")
}
Text("Auto dismiss after 2 seconds")
}
.charcoalToast(
isPresenting: $isPresentingToast4,
text: "ブックマークしました",
dismissAfter: 2,
appearance: .error
)
}
}.navigationBarTitle("Toasts")
}
}

#Preview {
ToastsView().charcoalOverlayContainer()
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,15 @@
import SwiftUI

struct CharcoalIdentifiableOverlayView: View {
struct CharcoalIdentifiableOverlayView: View, Identifiable {
typealias IDValue = UUID

/// The unique ID of the overlay.
let id: IDValue
var contentView: AnyView
@Binding var isPresenting: Bool

/// The content to display in the overlay.
let contentView: AnyView

var body: some View {
ZStack {
if isPresenting {
Color.clear
.contentShape(Rectangle())
.simultaneousGesture(
TapGesture()
.onEnded { _ in
isPresenting = false
}
)
.simultaneousGesture(
DragGesture()
.onChanged { _ in
isPresenting = false
}
)
contentView
}
}.animation(.easeInOut(duration: 0.2), value: isPresenting)
contentView
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,38 @@ struct CharcoalOverlayContainerModifier: ViewModifier {
}
}

typealias CharcoalPopupView = Equatable & View

struct CharcoalOverlayContainerChild<SubContent: CharcoalPopupView>: ViewModifier {
struct CharcoalOverlayUpdaterContainer<SubContent: CharcoalPopupProtocol>: ViewModifier {
@EnvironmentObject var viewManager: CharcoalContainerManager

@Binding var isPresenting: Bool

var view: SubContent
let view: SubContent

let viewID: UUID

func createOverlayView(view: SubContent) -> CharcoalIdentifiableOverlayView {
return CharcoalIdentifiableOverlayView(id: viewID, contentView: AnyView(view), isPresenting: $isPresenting)
return CharcoalIdentifiableOverlayView(id: viewID, contentView: AnyView(view))
}

func updateView(view: SubContent) {
viewManager.addView(view: createOverlayView(view: view))
}

func body(content: Content) -> some View {
content
.onChange(of: isPresenting) { newValue in
if newValue {
let newView = createOverlayView(view: view)
viewManager.addView(view: newView)
updateView(view: view)
}
}
.onChange(of: view) { newValue in
if isPresenting {
let newView = createOverlayView(view: newValue)
viewManager.addView(view: newView)
updateView(view: newValue)
}
}
.onAppear {
// onAppear is needed if the overlay is presented by default
let newView = createOverlayView(view: view)
viewManager.addView(view: newView)
updateView(view: view)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import SwiftUI

typealias CharcoalPopupProtocol = Equatable & Identifiable & View

public enum CharcoalPopupViewEdge {
case top
case bottom

var alignment: Alignment {
switch self {
case .top:
return .top
case .bottom:
return .bottom
}
}

var direction: CGFloat {
switch self {
case .top:
return 1
case .bottom:
return -1
}
}
}
Loading

0 comments on commit b238c37

Please sign in to comment.