From 38280a79070f55139b8379578e76239457736ae7 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 30 Jan 2024 14:35:38 +0900 Subject: [PATCH 001/199] Add Tooltip view --- .../CharcoalSwiftUISample/ContentView.swift | 3 + .../CharcoalSwiftUISample/TooltipsView.swift | 43 +++++++++++ .../Components/CharcoalTooltip.swift | 54 ++++++++++++++ .../Overlay/ChacoalOverlayManager.swift | 30 ++++++++ .../CharcoalOverlayContainerModifier.swift | 74 +++++++++++++++++++ 5 files changed, 204 insertions(+) create mode 100644 CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift create mode 100644 Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift create mode 100644 Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift create mode 100644 Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift index 524ffba52..73af7d4bc 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift @@ -43,6 +43,9 @@ public struct ContentView: View { NavigationLink(destination: ModalsView()) { Text("Modal") } + NavigationLink(destination: TooltipsView()) { + Text("Tooltips") + } } .navigationBarTitle("Charcoal") } diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift new file mode 100644 index 000000000..66acc9c08 --- /dev/null +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift @@ -0,0 +1,43 @@ +import Charcoal +import SwiftUI + +public struct TooltipsView: View { + @State var isOverlayShow = false + @State var isPresented = false + @State var isBigPresented = false + @State var isPresentedTransparent = false + + @State var isPresentedTooltipGlobal = false + @State var isPresentedTooltip = false + + public var body: some View { + List { + Button(action: { + isPresented.toggle() + }, label: { + VStack(alignment: .leading) { + Text("Regular") + } + }) + + Button(action: { + isBigPresented.toggle() + }, label: { + VStack(alignment: .leading) { + Text("Bigger") + } + }) + + Button(action: { + isPresentedTransparent.toggle() + }, label: { + VStack(alignment: .leading) { + Text("Transparent") + } + }) + + } + .charcoalOverlayContainer() + .navigationBarTitle("Spinners") + } +} diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift new file mode 100644 index 000000000..5c618dcd7 --- /dev/null +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -0,0 +1,54 @@ +import SwiftUI + +public struct CharcoalTooltip: CharcoalPopupView { + let text: String + let targetFrame: CGRect + + public init(text: String, targetFrame: CGRect) { + self.text = text + self.targetFrame = targetFrame + } + + private var animation: Animation { + .easeOut(duration: 1) + .repeatForever(autoreverses: false) + } + + public var body: some View { + ZStack(alignment: .topLeading) { + Color.clear + ZStack { + Text(text).foregroundColor(Color.white).padding() + } + .background(Color(CharcoalAsset.ColorPaletteGenerated.brand.color)) + .cornerRadius(8, corners: .allCorners) + .shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 0) + .offset(CGSize(width: targetFrame.maxX - (targetFrame.width / 2.0), height: targetFrame.maxY)) + } + } +} + +public struct CharcoalTooltipModifier: ViewModifier { + /// Presentation `Binding` + @Binding var isPresenting: Bool + + var text: String + + public func body(content: Content) -> some View { + content + .overlay(GeometryReader(content: { proxy in + let frame = proxy.frame(in: .global) + EmptyView() + .modifier(CharcoalOverlayContainerChild(isPresenting: $isPresenting, view: CharcoalTooltip(text: text, targetFrame: frame))) + })) + } +} + +public extension View { + func charcoalTooltipGlobal( + isPresenting: Binding, + text: String + ) -> some View { + return modifier(CharcoalTooltipModifier(isPresenting: isPresenting, text: text)) + } +} diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift new file mode 100644 index 000000000..10982465c --- /dev/null +++ b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift @@ -0,0 +1,30 @@ +import Foundation +import SwiftUI +import Combine + +public class CharcoalContainerManager: ObservableObject { + static let share = CharcoalContainerManager() + + @Published var overlayView: AnyView? + + @Published var isPresenting: Bool = false + + public func addView(view: SubContent) { + self.overlayView = AnyView(view) + } + + public func removeView() { + self.overlayView = nil + } +} + +struct CharcoalContainerManagerKey: EnvironmentKey { + static var defaultValue = CharcoalContainerManager.share +} + +public extension EnvironmentValues { + var overlayContainerManager: CharcoalContainerManager { + get { self[CharcoalContainerManagerKey.self] } + set { self[CharcoalContainerManagerKey.self] = newValue } + } +} diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift new file mode 100644 index 000000000..a4ab67b37 --- /dev/null +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -0,0 +1,74 @@ +import SwiftUI + +struct CharcoalOverlayContainerModifier: ViewModifier { + @Environment(\.overlayContainerManager) var manager + + func body(content: Content) -> some View { + content + .overlay( + CharcoalOverlayContainer(viewManager: self.manager).ignoresSafeArea() + ) + } +} + +typealias CharcoalPopupView = View & Equatable + +struct CharcoalOverlayContainerChild: ViewModifier { + @Environment(\.overlayContainerManager) var manager + @Binding var isPresenting: Bool + + var view: SubContent + + func body(content: Content) -> some View { + content + .onChange(of: isPresenting) { newValue in + if (newValue) { + manager.addView(view: view) + manager.isPresenting = true + } else { + manager.isPresenting = false + manager.removeView() + } + } + .onChange(of: view) { newValue in + if isPresenting { + manager.addView(view: view) + } + } + } +} + +public extension View { + func charcoalOverlayContainer() -> some View { + modifier(CharcoalOverlayContainerModifier()) + } +} + +struct CharcoalOverlayContainerChildModifier: ViewModifier { + @Environment(\.overlayContainerManager) var manager + + func body(content: Content) -> some View { + content + .overlay( + CharcoalOverlayContainer(viewManager: self.manager) + ) + } +} + +struct CharcoalOverlayContainer: View { + + @ObservedObject var viewManager: CharcoalContainerManager + + var body: some View { + ZStack { + Color.clear.allowsHitTesting(false) + if let view = viewManager.overlayView { + if viewManager.isPresenting { + view + } + } + } + .animation(.spring, value: viewManager.isPresenting) + .onDisappear { viewManager.removeView() } + } +} From 6bff03f3a3e09bde188a9c6988dfde5b934bc63d Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 30 Jan 2024 15:29:44 +0900 Subject: [PATCH 002/199] Refactor Tooltip style --- .../CharcoalSwiftUISample/TooltipsView.swift | 23 +++---------------- .../Components/CharcoalTooltip.swift | 23 ++++++++++++++----- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift index 66acc9c08..13b844877 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift @@ -16,28 +16,11 @@ public struct TooltipsView: View { isPresented.toggle() }, label: { VStack(alignment: .leading) { - Text("Regular") + Image(charocalIcon: .question16) } - }) - - Button(action: { - isBigPresented.toggle() - }, label: { - VStack(alignment: .leading) { - Text("Bigger") - } - }) - - Button(action: { - isPresentedTransparent.toggle() - }, label: { - VStack(alignment: .leading) { - Text("Transparent") - } - }) - + }).charcoalTooltip(isPresenting: $isPresented, text: "Tooltip created by Charcoal") } .charcoalOverlayContainer() - .navigationBarTitle("Spinners") + .navigationBarTitle("Tooltips") } } diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index 5c618dcd7..26612c40c 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -3,10 +3,12 @@ import SwiftUI public struct CharcoalTooltip: CharcoalPopupView { let text: String let targetFrame: CGRect + let maxWidth: CGFloat - public init(text: String, targetFrame: CGRect) { + public init(text: String, targetFrame: CGRect, maxWidth: CGFloat = 184) { self.text = text self.targetFrame = targetFrame + self.maxWidth = maxWidth } private var animation: Animation { @@ -18,11 +20,16 @@ public struct CharcoalTooltip: CharcoalPopupView { ZStack(alignment: .topLeading) { Color.clear ZStack { - Text(text).foregroundColor(Color.white).padding() + Text(text) + .charcoalTypography12Regular() + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) + .padding(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12)) } - .background(Color(CharcoalAsset.ColorPaletteGenerated.brand.color)) - .cornerRadius(8, corners: .allCorners) - .shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 0) + .frame(maxWidth: maxWidth) + .background(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) + .cornerRadius(4, corners: .allCorners) .offset(CGSize(width: targetFrame.maxX - (targetFrame.width / 2.0), height: targetFrame.maxY)) } } @@ -45,10 +52,14 @@ public struct CharcoalTooltipModifier: ViewModifier { } public extension View { - func charcoalTooltipGlobal( + func charcoalTooltip( isPresenting: Binding, text: String ) -> some View { return modifier(CharcoalTooltipModifier(isPresenting: isPresenting, text: text)) } } + +#Preview { + CharcoalTooltip(text: "Hellow World This is a tooltip and here is testing it's multiple line feature", targetFrame: CGRect(x: 0, y: 100, width: 100, height: 100)) +} From c5e0daa91e00f8b17c23c7b6539d7c0c5fb28ca9 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 31 Jan 2024 14:23:19 +0900 Subject: [PATCH 003/199] Can adjust tooltip position --- .../Components/CharcoalTooltip.swift | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index 26612c40c..66daaf5a1 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -1,9 +1,15 @@ import SwiftUI public struct CharcoalTooltip: CharcoalPopupView { + public static func == (lhs: CharcoalTooltip, rhs: CharcoalTooltip) -> Bool { + return lhs.text == rhs.text && lhs.targetFrame == rhs.targetFrame && lhs.maxWidth == rhs.maxWidth + } + let text: String let targetFrame: CGRect let maxWidth: CGFloat + + @State private var tooltipSize: CGSize = .zero public init(text: String, targetFrame: CGRect, maxWidth: CGFloat = 184) { self.text = text @@ -19,7 +25,7 @@ public struct CharcoalTooltip: CharcoalPopupView { public var body: some View { ZStack(alignment: .topLeading) { Color.clear - ZStack { + VStack { Text(text) .charcoalTypography12Regular() .multilineTextAlignment(.center) @@ -30,11 +36,28 @@ public struct CharcoalTooltip: CharcoalPopupView { .frame(maxWidth: maxWidth) .background(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) .cornerRadius(4, corners: .allCorners) - .offset(CGSize(width: targetFrame.maxX - (targetFrame.width / 2.0), height: targetFrame.maxY)) + .overlay( + GeometryReader(content: { geometry in + Color.clear.preference(key: TooltipSizeKey.self, value: geometry.size) + }) + ) + .onPreferenceChange(TooltipSizeKey.self, perform: { value in + tooltipSize = value + }) + .offset(CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY)) + .animation(.none, value: tooltipSize) + .animation(.none, value: targetFrame) } } } +struct TooltipSizeKey: PreferenceKey { + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + value = nextValue() + } + static var defaultValue: CGSize = .zero +} + public struct CharcoalTooltipModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool @@ -60,6 +83,31 @@ public extension View { } } +private struct TooltipsPreviewView: View { + @State var isPresenting = false + @State var isPresenting2 = false + + var body: some View { + ZStack(alignment: .topLeading) { + Color.clear + Button { + isPresenting.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalTooltip(isPresenting: $isPresenting, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + + Button { + isPresenting2.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalTooltip(isPresenting: $isPresenting2, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + .offset(CGSize(width: 100.0, height: 100.0)) + }.charcoalOverlayContainer() + } +} + #Preview { - CharcoalTooltip(text: "Hellow World This is a tooltip and here is testing it's multiple line feature", targetFrame: CGRect(x: 0, y: 100, width: 100, height: 100)) + TooltipsPreviewView() } From cbd58afbcbc4e14609b5f2048d1fcf108080d30f Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 31 Jan 2024 15:13:43 +0900 Subject: [PATCH 004/199] Add tooltip playground for testing --- .../Components/CharcoalTooltip.swift | 120 +++++++++++++----- 1 file changed, 85 insertions(+), 35 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index 66daaf5a1..1c8038073 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -9,22 +9,45 @@ public struct CharcoalTooltip: CharcoalPopupView { let targetFrame: CGRect let maxWidth: CGFloat + let spacingToTarget: CGFloat = 5 + @State private var tooltipSize: CGSize = .zero - + + private var offset: CGSize { + CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY) + } + public init(text: String, targetFrame: CGRect, maxWidth: CGFloat = 184) { self.text = text self.targetFrame = targetFrame self.maxWidth = maxWidth } - + private var animation: Animation { .easeOut(duration: 1) - .repeatForever(autoreverses: false) + .repeatForever(autoreverses: false) } - + + func tooltipX(canvasGeometrySize: CGSize) -> CGFloat { +// print("targetFrameMidX \(targetFrame.midX) tooltipWidth \(tooltipSize.width)") + let minX = targetFrame.midX - (tooltipSize.width / 2.0) +// print("tooltipX \(max(0, minX))") + let edgeLeft = max(0, minX) + let edgeRight = min(edgeLeft, canvasGeometrySize.width - tooltipSize.width) +// print("edgeLeft \(edgeLeft) edgeRight \(edgeRight)") + return min(edgeLeft, edgeRight) + } + + func tooltipY(canvasGeometrySize: CGSize) -> CGFloat { + print("targetFrameMidX \(targetFrame.midX) tooltipHeight \(tooltipSize.height)") + let minX = targetFrame.maxY + spacingToTarget + let edgeBottom = canvasGeometrySize.height - tooltipSize.height - targetFrame.height - spacingToTarget + print("edgeBottom \(edgeBottom)") + return min(minX, edgeBottom) + } + public var body: some View { - ZStack(alignment: .topLeading) { - Color.clear + GeometryReader(content: { canvasGeometry in VStack { Text(text) .charcoalTypography12Regular() @@ -37,17 +60,22 @@ public struct CharcoalTooltip: CharcoalPopupView { .background(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) .cornerRadius(4, corners: .allCorners) .overlay( - GeometryReader(content: { geometry in - Color.clear.preference(key: TooltipSizeKey.self, value: geometry.size) + GeometryReader(content: { tooltipGeometry in + Color.clear.preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) }) ) .onPreferenceChange(TooltipSizeKey.self, perform: { value in - tooltipSize = value + DispatchQueue.main.async { + tooltipSize = value + } + print("canvasGeometry \(canvasGeometry.size) tooltipSize \(tooltipSize)") }) - .offset(CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY)) + .offset(CGSize( + width: tooltipX(canvasGeometrySize: canvasGeometry.size), + height: tooltipY(canvasGeometrySize: canvasGeometry.size))) .animation(.none, value: tooltipSize) .animation(.none, value: targetFrame) - } + }) } } @@ -63,7 +91,7 @@ public struct CharcoalTooltipModifier: ViewModifier { @Binding var isPresenting: Bool var text: String - + public func body(content: Content) -> some View { content .overlay(GeometryReader(content: { proxy in @@ -84,30 +112,52 @@ public extension View { } private struct TooltipsPreviewView: View { - @State var isPresenting = false - @State var isPresenting2 = false - - var body: some View { - ZStack(alignment: .topLeading) { - Color.clear - Button { - isPresenting.toggle() - } label: { - Image(charocalIcon: .question24) - } - .charcoalTooltip(isPresenting: $isPresenting, text: "Hello World This is a tooltip and here is testing it's multiple line feature") - - Button { - isPresenting2.toggle() - } label: { - Image(charocalIcon: .question24) - } - .charcoalTooltip(isPresenting: $isPresenting2, text: "Hello World This is a tooltip and here is testing it's multiple line feature") - .offset(CGSize(width: 100.0, height: 100.0)) - }.charcoalOverlayContainer() - } + @State var isPresenting = false + @State var isPresenting2 = false + @State var isPresenting3 = false + @State var isPresenting4 = false + + var body: some View { + ZStack(alignment: .topLeading) { + GeometryReader(content: { geometry in + Button { + isPresenting.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalTooltip(isPresenting: $isPresenting, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + .offset(CGSize(width: 20.0, height: 20.0)) + + Button { + isPresenting2.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalTooltip(isPresenting: $isPresenting2, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + .offset(CGSize(width: 100.0, height: 100.0)) + + Button { + isPresenting3.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalTooltip(isPresenting: $isPresenting3, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + .offset(CGSize(width: geometry.size.width - 30, height: 100.0)) + + Button { + isPresenting4.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalTooltip(isPresenting: $isPresenting4, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + .offset(CGSize(width: geometry.size.width - 40, height: geometry.size.height - 40)) + }) + } + .ignoresSafeArea() + .charcoalOverlayContainer() + } } #Preview { - TooltipsPreviewView() + TooltipsPreviewView() } From 7d138119e92a9bdc1fc3c283d0638ee408306df2 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 31 Jan 2024 17:53:12 +0900 Subject: [PATCH 005/199] Refine layout tips logic --- .../Components/CharcoalTooltip.swift | 98 ++++++++++--------- .../CharcoalOverlayContainerModifier.swift | 2 +- 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index 1c8038073..7a03343ef 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -1,10 +1,6 @@ import SwiftUI public struct CharcoalTooltip: CharcoalPopupView { - public static func == (lhs: CharcoalTooltip, rhs: CharcoalTooltip) -> Bool { - return lhs.text == rhs.text && lhs.targetFrame == rhs.targetFrame && lhs.maxWidth == rhs.maxWidth - } - let text: String let targetFrame: CGRect let maxWidth: CGFloat @@ -29,20 +25,20 @@ public struct CharcoalTooltip: CharcoalPopupView { } func tooltipX(canvasGeometrySize: CGSize) -> CGFloat { -// print("targetFrameMidX \(targetFrame.midX) tooltipWidth \(tooltipSize.width)") let minX = targetFrame.midX - (tooltipSize.width / 2.0) -// print("tooltipX \(max(0, minX))") let edgeLeft = max(0, minX) let edgeRight = min(edgeLeft, canvasGeometrySize.width - tooltipSize.width) -// print("edgeLeft \(edgeLeft) edgeRight \(edgeRight)") return min(edgeLeft, edgeRight) } func tooltipY(canvasGeometrySize: CGSize) -> CGFloat { - print("targetFrameMidX \(targetFrame.midX) tooltipHeight \(tooltipSize.height)") let minX = targetFrame.maxY + spacingToTarget - let edgeBottom = canvasGeometrySize.height - tooltipSize.height - targetFrame.height - spacingToTarget - print("edgeBottom \(edgeBottom)") + var edgeBottom = targetFrame.maxY + spacingToTarget + targetFrame.height + + if edgeBottom >= canvasGeometrySize.height { + edgeBottom = targetFrame.minY - tooltipSize.height - spacingToTarget + } + return min(minX, edgeBottom) } @@ -65,10 +61,7 @@ public struct CharcoalTooltip: CharcoalPopupView { }) ) .onPreferenceChange(TooltipSizeKey.self, perform: { value in - DispatchQueue.main.async { - tooltipSize = value - } - print("canvasGeometry \(canvasGeometry.size) tooltipSize \(tooltipSize)") + tooltipSize = value }) .offset(CGSize( width: tooltipX(canvasGeometrySize: canvasGeometry.size), @@ -77,6 +70,10 @@ public struct CharcoalTooltip: CharcoalPopupView { .animation(.none, value: targetFrame) }) } + + public static func == (lhs: CharcoalTooltip, rhs: CharcoalTooltip) -> Bool { + return lhs.text == rhs.text && lhs.targetFrame == rhs.targetFrame && lhs.maxWidth == rhs.maxWidth && lhs.tooltipSize == rhs.tooltipSize + } } struct TooltipSizeKey: PreferenceKey { @@ -118,41 +115,46 @@ private struct TooltipsPreviewView: View { @State var isPresenting4 = false var body: some View { - ZStack(alignment: .topLeading) { - GeometryReader(content: { geometry in - Button { - isPresenting.toggle() - } label: { - Image(charocalIcon: .question24) + GeometryReader(content: { geometry in + ScrollView { + ZStack(alignment: .topLeading) { + Color.clear + + Button { + isPresenting.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalTooltip(isPresenting: $isPresenting, text: "Hello World ") + .offset(CGSize(width: 20.0, height: 20.0)) + + Button { + isPresenting2.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalTooltip(isPresenting: $isPresenting2, text: "Hello World This is a tooltip") + .offset(CGSize(width: 100.0, height: 100.0)) + + Button { + isPresenting3.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalTooltip(isPresenting: $isPresenting3, text: "here is testing it's multiple line feature") + .offset(CGSize(width: geometry.size.width - 30, height: 100.0)) + + Button { + isPresenting4.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalTooltip(isPresenting: $isPresenting4, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + .offset(CGSize(width: geometry.size.width - 40, height: geometry.size.height - 40)) + } - .charcoalTooltip(isPresenting: $isPresenting, text: "Hello World This is a tooltip and here is testing it's multiple line feature") - .offset(CGSize(width: 20.0, height: 20.0)) - - Button { - isPresenting2.toggle() - } label: { - Image(charocalIcon: .question24) - } - .charcoalTooltip(isPresenting: $isPresenting2, text: "Hello World This is a tooltip and here is testing it's multiple line feature") - .offset(CGSize(width: 100.0, height: 100.0)) - - Button { - isPresenting3.toggle() - } label: { - Image(charocalIcon: .question24) - } - .charcoalTooltip(isPresenting: $isPresenting3, text: "Hello World This is a tooltip and here is testing it's multiple line feature") - .offset(CGSize(width: geometry.size.width - 30, height: 100.0)) - - Button { - isPresenting4.toggle() - } label: { - Image(charocalIcon: .question24) - } - .charcoalTooltip(isPresenting: $isPresenting4, text: "Hello World This is a tooltip and here is testing it's multiple line feature") - .offset(CGSize(width: geometry.size.width - 40, height: geometry.size.height - 40)) - }) - } + } + }) .ignoresSafeArea() .charcoalOverlayContainer() } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index a4ab67b37..37614e4f0 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -68,7 +68,7 @@ struct CharcoalOverlayContainer: View { } } } - .animation(.spring, value: viewManager.isPresenting) + .animation(.easeInOut(duration: 0.2), value: viewManager.isPresenting) .onDisappear { viewManager.removeView() } } } From 874a5b998de200bb49b79de7984ba26e6a29bbea Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 31 Jan 2024 18:29:20 +0900 Subject: [PATCH 006/199] Refine layout logic --- .../Components/CharcoalTooltip.swift | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index 7a03343ef..f069b9d2c 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -26,14 +26,21 @@ public struct CharcoalTooltip: CharcoalPopupView { func tooltipX(canvasGeometrySize: CGSize) -> CGFloat { let minX = targetFrame.midX - (tooltipSize.width / 2.0) - let edgeLeft = max(0, minX) - let edgeRight = min(edgeLeft, canvasGeometrySize.width - tooltipSize.width) - return min(edgeLeft, edgeRight) + + var edgeLeft = minX + + if (edgeLeft + tooltipSize.width >= canvasGeometrySize.width) { + edgeLeft = canvasGeometrySize.width - tooltipSize.width + } else if (edgeLeft < 0) { + edgeLeft = 0 + } + + return edgeLeft } func tooltipY(canvasGeometrySize: CGSize) -> CGFloat { let minX = targetFrame.maxY + spacingToTarget - var edgeBottom = targetFrame.maxY + spacingToTarget + targetFrame.height + var edgeBottom = targetFrame.maxY + spacingToTarget + targetFrame.height if edgeBottom >= canvasGeometrySize.height { edgeBottom = targetFrame.minY - tooltipSize.height - spacingToTarget @@ -44,17 +51,17 @@ public struct CharcoalTooltip: CharcoalPopupView { public var body: some View { GeometryReader(content: { canvasGeometry in - VStack { - Text(text) - .charcoalTypography12Regular() - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) - .padding(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12)) - } + Text(text) + .charcoalTypography12Regular() + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) + .padding(EdgeInsets(top: 2, leading: 12, bottom: 2, trailing: 12)) + .background(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color) .cornerRadius(4, corners: .allCorners)) .frame(maxWidth: maxWidth) - .background(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) - .cornerRadius(4, corners: .allCorners) + .offset(CGSize( + width: tooltipX(canvasGeometrySize: canvasGeometry.size), + height: tooltipY(canvasGeometrySize: canvasGeometry.size))) .overlay( GeometryReader(content: { tooltipGeometry in Color.clear.preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) @@ -63,9 +70,6 @@ public struct CharcoalTooltip: CharcoalPopupView { .onPreferenceChange(TooltipSizeKey.self, perform: { value in tooltipSize = value }) - .offset(CGSize( - width: tooltipX(canvasGeometrySize: canvasGeometry.size), - height: tooltipY(canvasGeometrySize: canvasGeometry.size))) .animation(.none, value: tooltipSize) .animation(.none, value: targetFrame) }) @@ -126,7 +130,7 @@ private struct TooltipsPreviewView: View { Image(charocalIcon: .question24) } .charcoalTooltip(isPresenting: $isPresenting, text: "Hello World ") - .offset(CGSize(width: 20.0, height: 20.0)) + .offset(CGSize(width: 20.0, height: 80.0)) Button { isPresenting2.toggle() From eecaff84e92c7ac4a6be8f2dc4dcabe2d81d745f Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 31 Jan 2024 18:43:12 +0900 Subject: [PATCH 007/199] Update CharcoalTooltip.swift --- .../Components/CharcoalTooltip.swift | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index f069b9d2c..1fd867025 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -49,6 +49,8 @@ public struct CharcoalTooltip: CharcoalPopupView { return min(minX, edgeBottom) } + @State private var adaptiveMaxWidth: CGFloat? + public var body: some View { GeometryReader(content: { canvasGeometry in Text(text) @@ -57,18 +59,18 @@ public struct CharcoalTooltip: CharcoalPopupView { .fixedSize(horizontal: false, vertical: true) .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) .padding(EdgeInsets(top: 2, leading: 12, bottom: 2, trailing: 12)) - .background(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color) .cornerRadius(4, corners: .allCorners)) - .frame(maxWidth: maxWidth) + .background(GeometryReader(content: { tooltipGeometry in + Color(CharcoalAsset.ColorPaletteGenerated.surface8.color) .cornerRadius(4, corners: .allCorners).preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) + })) + .frame(maxWidth: adaptiveMaxWidth) .offset(CGSize( width: tooltipX(canvasGeometrySize: canvasGeometry.size), height: tooltipY(canvasGeometrySize: canvasGeometry.size))) - .overlay( - GeometryReader(content: { tooltipGeometry in - Color.clear.preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) - }) - ) .onPreferenceChange(TooltipSizeKey.self, perform: { value in tooltipSize = value + if (adaptiveMaxWidth == nil) { + adaptiveMaxWidth = tooltipSize.width < maxWidth ? nil : maxWidth + } }) .animation(.none, value: tooltipSize) .animation(.none, value: targetFrame) From 0ea9f6abdfa4e701795194eeff15386a87e1e981 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 6 Feb 2024 14:13:25 +0900 Subject: [PATCH 008/199] Refine tooltip spacing --- Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index 1fd867025..b5c9313c0 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -58,9 +58,10 @@ public struct CharcoalTooltip: CharcoalPopupView { .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) - .padding(EdgeInsets(top: 2, leading: 12, bottom: 2, trailing: 12)) + .padding(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12)) .background(GeometryReader(content: { tooltipGeometry in - Color(CharcoalAsset.ColorPaletteGenerated.surface8.color) .cornerRadius(4, corners: .allCorners).preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) + Color(CharcoalAsset.ColorPaletteGenerated.surface8.color) .cornerRadius(4, corners: .allCorners) + .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) })) .frame(maxWidth: adaptiveMaxWidth) .offset(CGSize( @@ -98,9 +99,8 @@ public struct CharcoalTooltipModifier: ViewModifier { public func body(content: Content) -> some View { content .overlay(GeometryReader(content: { proxy in - let frame = proxy.frame(in: .global) EmptyView() - .modifier(CharcoalOverlayContainerChild(isPresenting: $isPresenting, view: CharcoalTooltip(text: text, targetFrame: frame))) + .modifier(CharcoalOverlayContainerChild(isPresenting: $isPresenting, view: CharcoalTooltip(text: text, targetFrame: proxy.frame(in: .global)))) })) } } From 085626f110672f0c0f4371294e6c21b36cecce00 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 6 Feb 2024 15:07:01 +0900 Subject: [PATCH 009/199] Add CharcoalIdentifiableOverlayView --- .../Components/CharcoalTooltip.swift | 6 ++- .../Overlay/ChacoalOverlayManager.swift | 37 ++++++++++++++++--- .../CharcoalOverlayContainerModifier.swift | 36 +++++++++++------- 3 files changed, 57 insertions(+), 22 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index b5c9313c0..2a2bc5c41 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -96,11 +96,13 @@ public struct CharcoalTooltipModifier: ViewModifier { var text: String + var viewID = UUID() + public func body(content: Content) -> some View { content .overlay(GeometryReader(content: { proxy in EmptyView() - .modifier(CharcoalOverlayContainerChild(isPresenting: $isPresenting, view: CharcoalTooltip(text: text, targetFrame: proxy.frame(in: .global)))) + .modifier(CharcoalOverlayContainerChild(isPresenting: $isPresenting, view: CharcoalTooltip(text: text, targetFrame: proxy.frame(in: .global)), viewID: viewID)) })) } } @@ -115,7 +117,7 @@ public extension View { } private struct TooltipsPreviewView: View { - @State var isPresenting = false + @State var isPresenting = true @State var isPresenting2 = false @State var isPresenting3 = false @State var isPresenting4 = false diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift index 10982465c..1c7f57e6e 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift @@ -2,19 +2,44 @@ import Foundation import SwiftUI import Combine +struct CharcoalIdentifiableOverlayView: View { + typealias IDValue = UUID + let id: IDValue + var contentView: AnyView + @Binding var isPresenting: Bool + + var body: some View { + VStack { + if isPresenting { + contentView + } + }.animation(.easeInOut(duration: 0.2), value: isPresenting) + } +} + public class CharcoalContainerManager: ObservableObject { static let share = CharcoalContainerManager() - @Published var overlayView: AnyView? + @Published var overlayViews: [CharcoalIdentifiableOverlayView] = [] - @Published var isPresenting: Bool = false + func addView(view: CharcoalIdentifiableOverlayView) { + if let index = self.overlayViews.firstIndex(where: { $0.id == view.id }) { + self.overlayViews[index] = view + } else { + self.overlayViews.append(view) + } + } + + func getView(id: CharcoalIdentifiableOverlayView.IDValue) -> CharcoalIdentifiableOverlayView? { + return self.overlayViews.first(where: { $0.id == id }) + } - public func addView(view: SubContent) { - self.overlayView = AnyView(view) + func removeView(id: CharcoalIdentifiableOverlayView.IDValue) { + self.overlayViews.removeAll(where: { $0.id == id }) } - public func removeView() { - self.overlayView = nil + func clear() { + self.overlayViews.removeAll() } } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 37614e4f0..2479b6d19 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -19,21 +19,31 @@ struct CharcoalOverlayContainerChild: ViewModifie var view: SubContent + let viewID: UUID + + func createOverlayView(view: SubContent) -> CharcoalIdentifiableOverlayView { + return CharcoalIdentifiableOverlayView(id: viewID, contentView: AnyView(view), isPresenting: $isPresenting) + } + + init(isPresenting: Binding, view: SubContent, viewID: UUID) { + _isPresenting = isPresenting + self.view = view + self.viewID = viewID + + manager.addView(view: createOverlayView(view: view)) + } + func body(content: Content) -> some View { content .onChange(of: isPresenting) { newValue in - if (newValue) { - manager.addView(view: view) - manager.isPresenting = true + if newValue { + manager.addView(view: createOverlayView(view: view)) } else { - manager.isPresenting = false - manager.removeView() + manager.removeView(id: viewID) } } .onChange(of: view) { newValue in - if isPresenting { - manager.addView(view: view) - } + manager.addView(view: createOverlayView(view: view)) } } } @@ -62,13 +72,11 @@ struct CharcoalOverlayContainer: View { var body: some View { ZStack { Color.clear.allowsHitTesting(false) - if let view = viewManager.overlayView { - if viewManager.isPresenting { - view - } + + ForEach(viewManager.overlayViews, id: \.id) { overlayView in + overlayView } } - .animation(.easeInOut(duration: 0.2), value: viewManager.isPresenting) - .onDisappear { viewManager.removeView() } + .onDisappear { viewManager.clear() } } } From b12beac465a870f80881b2f2d40cdddfa510490a Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 6 Feb 2024 15:14:03 +0900 Subject: [PATCH 010/199] Use Actor to prevent Data Race --- .../Overlay/ChacoalOverlayManager.swift | 16 +++++------ .../CharcoalOverlayContainerModifier.swift | 27 +++++++++++++++---- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift index 1c7f57e6e..bb3a5ec7c 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift @@ -17,28 +17,28 @@ struct CharcoalIdentifiableOverlayView: View { } } +@globalActor actor CharcoalContainerActor { + static let shared = CharcoalContainerActor() +} + public class CharcoalContainerManager: ObservableObject { static let share = CharcoalContainerManager() @Published var overlayViews: [CharcoalIdentifiableOverlayView] = [] - func addView(view: CharcoalIdentifiableOverlayView) { + @CharcoalContainerActor func addView(view: CharcoalIdentifiableOverlayView) { if let index = self.overlayViews.firstIndex(where: { $0.id == view.id }) { self.overlayViews[index] = view } else { self.overlayViews.append(view) } } - - func getView(id: CharcoalIdentifiableOverlayView.IDValue) -> CharcoalIdentifiableOverlayView? { - return self.overlayViews.first(where: { $0.id == id }) - } - - func removeView(id: CharcoalIdentifiableOverlayView.IDValue) { + + @CharcoalContainerActor func removeView(id: CharcoalIdentifiableOverlayView.IDValue) { self.overlayViews.removeAll(where: { $0.id == id }) } - func clear() { + @CharcoalContainerActor func clear() { self.overlayViews.removeAll() } } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 2479b6d19..91e59aaa6 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -30,21 +30,34 @@ struct CharcoalOverlayContainerChild: ViewModifie self.view = view self.viewID = viewID - manager.addView(view: createOverlayView(view: view)) + let newView = createOverlayView(view: view) + let manager = manager + Task { + await manager.addView(view: newView) + } } func body(content: Content) -> some View { content .onChange(of: isPresenting) { newValue in if newValue { - manager.addView(view: createOverlayView(view: view)) + let newView = createOverlayView(view: view) + Task { + await manager.addView(view: newView) + } } else { - manager.removeView(id: viewID) + Task { + await manager.removeView(id: viewID) + } } } .onChange(of: view) { newValue in - manager.addView(view: createOverlayView(view: view)) + let newView = createOverlayView(view: view) + Task { + await manager.addView(view: newView) + } } + } } @@ -77,6 +90,10 @@ struct CharcoalOverlayContainer: View { overlayView } } - .onDisappear { viewManager.clear() } + .onDisappear { + Task { + await viewManager.clear() + } + } } } From d2c39e50b823f7d0a268f1a291e3c8bf19197d33 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 6 Feb 2024 15:23:38 +0900 Subject: [PATCH 011/199] Remove CharcoalIdentifiableOverlayView out --- .../Overlay/ChacoalOverlayManager.swift | 20 ++----------------- .../CharcoalIdentifiableOverlayView.swift | 16 +++++++++++++++ .../CharcoalOverlayContainerModifier.swift | 4 ---- 3 files changed, 18 insertions(+), 22 deletions(-) create mode 100644 Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift index bb3a5ec7c..59c31e0b7 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift @@ -2,21 +2,6 @@ import Foundation import SwiftUI import Combine -struct CharcoalIdentifiableOverlayView: View { - typealias IDValue = UUID - let id: IDValue - var contentView: AnyView - @Binding var isPresenting: Bool - - var body: some View { - VStack { - if isPresenting { - contentView - } - }.animation(.easeInOut(duration: 0.2), value: isPresenting) - } -} - @globalActor actor CharcoalContainerActor { static let shared = CharcoalContainerActor() } @@ -28,10 +13,9 @@ public class CharcoalContainerManager: ObservableObject { @CharcoalContainerActor func addView(view: CharcoalIdentifiableOverlayView) { if let index = self.overlayViews.firstIndex(where: { $0.id == view.id }) { - self.overlayViews[index] = view - } else { - self.overlayViews.append(view) + self.overlayViews.remove(at: index) // Make sure we don't have duplicate views and the latest view is on top of the Stack } + self.overlayViews.append(view) } @CharcoalContainerActor func removeView(id: CharcoalIdentifiableOverlayView.IDValue) { diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift new file mode 100644 index 000000000..5bf56cf51 --- /dev/null +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -0,0 +1,16 @@ +import SwiftUI + +struct CharcoalIdentifiableOverlayView: View { + typealias IDValue = UUID + let id: IDValue + var contentView: AnyView + @Binding var isPresenting: Bool + + var body: some View { + VStack { + if isPresenting { + contentView + } + }.animation(.easeInOut(duration: 0.2), value: isPresenting) + } +} diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 91e59aaa6..990c6fe77 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -45,10 +45,6 @@ struct CharcoalOverlayContainerChild: ViewModifie Task { await manager.addView(view: newView) } - } else { - Task { - await manager.removeView(id: viewID) - } } } .onChange(of: view) { newValue in From 83cb45fadfbf3faa806e687d248e4df8bd923d88 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 6 Feb 2024 15:25:02 +0900 Subject: [PATCH 012/199] Clean code --- .../Overlay/CharcoalOverlayContainerModifier.swift | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 990c6fe77..6a86cab2e 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -63,17 +63,6 @@ public extension View { } } -struct CharcoalOverlayContainerChildModifier: ViewModifier { - @Environment(\.overlayContainerManager) var manager - - func body(content: Content) -> some View { - content - .overlay( - CharcoalOverlayContainer(viewManager: self.manager) - ) - } -} - struct CharcoalOverlayContainer: View { @ObservedObject var viewManager: CharcoalContainerManager From 1a5cd1baa315cbaf4f423836b4f47f4370b2fc57 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 6 Feb 2024 16:53:52 +0900 Subject: [PATCH 013/199] Only update view when it is isPresenting --- .../Overlay/CharcoalOverlayContainerModifier.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 6a86cab2e..2b51f6ec7 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -48,9 +48,11 @@ struct CharcoalOverlayContainerChild: ViewModifie } } .onChange(of: view) { newValue in - let newView = createOverlayView(view: view) - Task { - await manager.addView(view: newView) + if isPresenting { + let newView = createOverlayView(view: view) + Task { + await manager.addView(view: newView) + } } } From 747da2a979e64d670b0073692b3a0e2d7dd6d2ca Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 6 Feb 2024 17:26:41 +0900 Subject: [PATCH 014/199] Clean access control --- .../Components/CharcoalTooltip.swift | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index 2a2bc5c41..77792a10e 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -1,25 +1,25 @@ import SwiftUI -public struct CharcoalTooltip: CharcoalPopupView { +struct CharcoalTooltip: CharcoalPopupView { let text: String let targetFrame: CGRect let maxWidth: CGFloat let spacingToTarget: CGFloat = 5 - @State private var tooltipSize: CGSize = .zero + @State var tooltipSize: CGSize = .zero - private var offset: CGSize { + var offset: CGSize { CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY) } - public init(text: String, targetFrame: CGRect, maxWidth: CGFloat = 184) { + init(text: String, targetFrame: CGRect, maxWidth: CGFloat = 184) { self.text = text self.targetFrame = targetFrame self.maxWidth = maxWidth } - private var animation: Animation { + var animation: Animation { .easeOut(duration: 1) .repeatForever(autoreverses: false) } @@ -49,9 +49,9 @@ public struct CharcoalTooltip: CharcoalPopupView { return min(minX, edgeBottom) } - @State private var adaptiveMaxWidth: CGFloat? + @State var adaptiveMaxWidth: CGFloat? - public var body: some View { + var body: some View { GeometryReader(content: { canvasGeometry in Text(text) .charcoalTypography12Regular() @@ -78,7 +78,7 @@ public struct CharcoalTooltip: CharcoalPopupView { }) } - public static func == (lhs: CharcoalTooltip, rhs: CharcoalTooltip) -> Bool { + static func == (lhs: CharcoalTooltip, rhs: CharcoalTooltip) -> Bool { return lhs.text == rhs.text && lhs.targetFrame == rhs.targetFrame && lhs.maxWidth == rhs.maxWidth && lhs.tooltipSize == rhs.tooltipSize } } @@ -90,7 +90,7 @@ struct TooltipSizeKey: PreferenceKey { static var defaultValue: CGSize = .zero } -public struct CharcoalTooltipModifier: ViewModifier { +struct CharcoalTooltipModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool @@ -98,7 +98,7 @@ public struct CharcoalTooltipModifier: ViewModifier { var viewID = UUID() - public func body(content: Content) -> some View { + func body(content: Content) -> some View { content .overlay(GeometryReader(content: { proxy in EmptyView() @@ -133,7 +133,7 @@ private struct TooltipsPreviewView: View { } label: { Image(charocalIcon: .question24) } - .charcoalTooltip(isPresenting: $isPresenting, text: "Hello World ") + .charcoalTooltip(isPresenting: $isPresenting, text: "Hello World") .offset(CGSize(width: 20.0, height: 80.0)) Button { From 61fd8503bf9e194a24e8370117da3864e1522426 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 6 Feb 2024 17:37:59 +0900 Subject: [PATCH 015/199] Add TooltipsView --- .../CharcoalSwiftUISample/TooltipsView.swift | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift index 13b844877..7860b3f20 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift @@ -2,25 +2,62 @@ import Charcoal import SwiftUI public struct TooltipsView: View { - @State var isOverlayShow = false + @State var isPresented = false - @State var isBigPresented = false - @State var isPresentedTransparent = false - @State var isPresentedTooltipGlobal = false - @State var isPresentedTooltip = false + @State var isPresented2 = false + + @State var isPresented3 = false + + @State var isPresented4 = false public var body: some View { - List { - Button(action: { - isPresented.toggle() - }, label: { - VStack(alignment: .leading) { - Image(charocalIcon: .question16) + VStack { + List { + HStack() { + Text("Help") + Button(action: { + isPresented.toggle() + }, label: { + Image(charocalIcon: .question16) + }).charcoalTooltip(isPresenting: $isPresented, text: "Tooltip created by Charcoal") + } + + HStack() { + Text("Help (Multiple Line)") + Button(action: { + isPresented2.toggle() + }, label: { + Image(charocalIcon: .question16) + }).charcoalTooltip(isPresenting: $isPresented2, text: "Tooltip created by Charcoal and here is testing it's multiple line feature") } - }).charcoalTooltip(isPresenting: $isPresented, text: "Tooltip created by Charcoal") + + HStack() { + Text("Help (Auto-Positioning-Trailing)") + Spacer() + Button(action: { + isPresented4.toggle() + }, label: { + Image(charocalIcon: .question16) + }).charcoalTooltip(isPresenting: $isPresented4, text: "Tooltip created by Charcoal and here is testing Auto-Positioning") + } + + } + Spacer() + HStack() { + Text("Help (Auto-Positioning-Bottom)") + Button(action: { + isPresented3.toggle() + }, label: { + Image(charocalIcon: .question16) + }).charcoalTooltip(isPresenting: $isPresented3, text: "Tooltip created by Charcoal and here is testing Auto-Positioning") + } } .charcoalOverlayContainer() .navigationBarTitle("Tooltips") } } + +#Preview { + TooltipsView() +} From 7883c8a2643f7997861b89f925add3a99afd5dad Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 6 Feb 2024 17:52:43 +0900 Subject: [PATCH 016/199] Fix tooltipY layout logic --- Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index 77792a10e..088a03a2e 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -41,8 +41,7 @@ struct CharcoalTooltip: CharcoalPopupView { func tooltipY(canvasGeometrySize: CGSize) -> CGFloat { let minX = targetFrame.maxY + spacingToTarget var edgeBottom = targetFrame.maxY + spacingToTarget + targetFrame.height - - if edgeBottom >= canvasGeometrySize.height { + if edgeBottom + tooltipSize.height >= canvasGeometrySize.height { edgeBottom = targetFrame.minY - tooltipSize.height - spacingToTarget } @@ -163,7 +162,6 @@ private struct TooltipsPreviewView: View { } } }) - .ignoresSafeArea() .charcoalOverlayContainer() } } From a0cbea0eb91eca06b22b110818cf6eb2506a82b3 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 6 Feb 2024 18:03:16 +0900 Subject: [PATCH 017/199] Use main actor and remove CharcoalContainerManagerKey --- .../CharcoalSwiftUISample/TooltipsView.swift | 2 +- .../Overlay/ChacoalOverlayManager.swift | 23 +++------------- .../CharcoalOverlayContainerModifier.swift | 27 +++++++------------ 3 files changed, 15 insertions(+), 37 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift index 7860b3f20..608925cbe 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift @@ -15,7 +15,7 @@ public struct TooltipsView: View { VStack { List { HStack() { - Text("Help") + Text("Help") Button(action: { isPresented.toggle() }, label: { diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift index 59c31e0b7..bf2d368ef 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift @@ -2,38 +2,23 @@ import Foundation import SwiftUI import Combine -@globalActor actor CharcoalContainerActor { - static let shared = CharcoalContainerActor() -} - -public class CharcoalContainerManager: ObservableObject { +@MainActor public class CharcoalContainerManager: ObservableObject { static let share = CharcoalContainerManager() @Published var overlayViews: [CharcoalIdentifiableOverlayView] = [] - @CharcoalContainerActor func addView(view: CharcoalIdentifiableOverlayView) { + func addView(view: CharcoalIdentifiableOverlayView) { if let index = self.overlayViews.firstIndex(where: { $0.id == view.id }) { self.overlayViews.remove(at: index) // Make sure we don't have duplicate views and the latest view is on top of the Stack } self.overlayViews.append(view) } - @CharcoalContainerActor func removeView(id: CharcoalIdentifiableOverlayView.IDValue) { + func removeView(id: CharcoalIdentifiableOverlayView.IDValue) { self.overlayViews.removeAll(where: { $0.id == id }) } - @CharcoalContainerActor func clear() { + func clear() { self.overlayViews.removeAll() } } - -struct CharcoalContainerManagerKey: EnvironmentKey { - static var defaultValue = CharcoalContainerManager.share -} - -public extension EnvironmentValues { - var overlayContainerManager: CharcoalContainerManager { - get { self[CharcoalContainerManagerKey.self] } - set { self[CharcoalContainerManagerKey.self] = newValue } - } -} diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 2b51f6ec7..9690af0bc 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -1,12 +1,10 @@ import SwiftUI struct CharcoalOverlayContainerModifier: ViewModifier { - @Environment(\.overlayContainerManager) var manager - func body(content: Content) -> some View { content .overlay( - CharcoalOverlayContainer(viewManager: self.manager).ignoresSafeArea() + CharcoalOverlayContainer().ignoresSafeArea() ) } } @@ -14,7 +12,8 @@ struct CharcoalOverlayContainerModifier: ViewModifier { typealias CharcoalPopupView = View & Equatable struct CharcoalOverlayContainerChild: ViewModifier { - @Environment(\.overlayContainerManager) var manager + + var viewManager = CharcoalContainerManager.share @Binding var isPresenting: Bool var view: SubContent @@ -31,9 +30,9 @@ struct CharcoalOverlayContainerChild: ViewModifie self.viewID = viewID let newView = createOverlayView(view: view) - let manager = manager + let viewManager = viewManager Task { - await manager.addView(view: newView) + await viewManager.addView(view: newView) } } @@ -42,20 +41,16 @@ struct CharcoalOverlayContainerChild: ViewModifie .onChange(of: isPresenting) { newValue in if newValue { let newView = createOverlayView(view: view) - Task { - await manager.addView(view: newView) - } + viewManager.addView(view: newView) } } .onChange(of: view) { newValue in if isPresenting { let newView = createOverlayView(view: view) - Task { - await manager.addView(view: newView) - } + viewManager.addView(view: newView) } } - + } } @@ -67,7 +62,7 @@ public extension View { struct CharcoalOverlayContainer: View { - @ObservedObject var viewManager: CharcoalContainerManager + @ObservedObject var viewManager = CharcoalContainerManager.share var body: some View { ZStack { @@ -78,9 +73,7 @@ struct CharcoalOverlayContainer: View { } } .onDisappear { - Task { - await viewManager.clear() - } + viewManager.clear() } } } From bbb6b90a667f8afa29ba7ef3fa42399f4e19811f Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 6 Feb 2024 18:07:56 +0900 Subject: [PATCH 018/199] Fix access control on CharcoalContainerManager --- .../Components/Overlay/ChacoalOverlayManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift index bf2d368ef..7f6997403 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift @@ -2,7 +2,7 @@ import Foundation import SwiftUI import Combine -@MainActor public class CharcoalContainerManager: ObservableObject { +@MainActor class CharcoalContainerManager: ObservableObject { static let share = CharcoalContainerManager() @Published var overlayViews: [CharcoalIdentifiableOverlayView] = [] From f5b77b3b43d9d2e6d2ee4c01ce388adaa175f8eb Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 6 Feb 2024 18:20:49 +0900 Subject: [PATCH 019/199] Make viewID as @State --- .../Components/CharcoalTooltip.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index 088a03a2e..9d8f0c7f7 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -95,7 +95,7 @@ struct CharcoalTooltipModifier: ViewModifier { var text: String - var viewID = UUID() + @State var viewID = UUID() func body(content: Content) -> some View { content @@ -121,12 +121,24 @@ private struct TooltipsPreviewView: View { @State var isPresenting3 = false @State var isPresenting4 = false + @State var textOfLabel = "Hello" + var body: some View { GeometryReader(content: { geometry in ScrollView { ZStack(alignment: .topLeading) { Color.clear + VStack { + Text(textOfLabel) + + Button { + textOfLabel = "Changed" + } label: { + Text("Change Label") + } + } + Button { isPresenting.toggle() } label: { From 5020edf63b7086d8a6be5a39c765bcda3a5ffc1d Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 8 Feb 2024 13:05:15 +0900 Subject: [PATCH 020/199] Use EnviromentObject to create CharcoalContainerManager for each container --- .../Components/CharcoalTooltip.swift | 3 ++- .../Overlay/ChacoalOverlayManager.swift | 10 +++++--- .../CharcoalOverlayContainerModifier.swift | 24 ++++++++----------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index 9d8f0c7f7..cce51bf50 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -100,7 +100,7 @@ struct CharcoalTooltipModifier: ViewModifier { func body(content: Content) -> some View { content .overlay(GeometryReader(content: { proxy in - EmptyView() + Color.clear .modifier(CharcoalOverlayContainerChild(isPresenting: $isPresenting, view: CharcoalTooltip(text: text, targetFrame: proxy.frame(in: .global)), viewID: viewID)) })) } @@ -175,6 +175,7 @@ private struct TooltipsPreviewView: View { } }) .charcoalOverlayContainer() + } } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift index 7f6997403..4c201ba38 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift @@ -2,14 +2,14 @@ import Foundation import SwiftUI import Combine -@MainActor class CharcoalContainerManager: ObservableObject { - static let share = CharcoalContainerManager() +class CharcoalContainerManager: ObservableObject { @Published var overlayViews: [CharcoalIdentifiableOverlayView] = [] func addView(view: CharcoalIdentifiableOverlayView) { if let index = self.overlayViews.firstIndex(where: { $0.id == view.id }) { - self.overlayViews.remove(at: index) // Make sure we don't have duplicate views and the latest view is on top of the Stack + // Make sure we don't have duplicate views and the latest view's zIndex is on top of the Stack + self.overlayViews.remove(at: index) } self.overlayViews.append(view) } @@ -21,4 +21,8 @@ import Combine func clear() { self.overlayViews.removeAll() } + + deinit { + clear() + } } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 9690af0bc..a8ff36db3 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -1,19 +1,22 @@ import SwiftUI struct CharcoalOverlayContainerModifier: ViewModifier { + @StateObject var viewManager = CharcoalContainerManager() + func body(content: Content) -> some View { content .overlay( CharcoalOverlayContainer().ignoresSafeArea() ) + .environmentObject(viewManager) } } typealias CharcoalPopupView = View & Equatable struct CharcoalOverlayContainerChild: ViewModifier { + @EnvironmentObject var viewManager: CharcoalContainerManager - var viewManager = CharcoalContainerManager.share @Binding var isPresenting: Bool var view: SubContent @@ -24,18 +27,6 @@ struct CharcoalOverlayContainerChild: ViewModifie return CharcoalIdentifiableOverlayView(id: viewID, contentView: AnyView(view), isPresenting: $isPresenting) } - init(isPresenting: Binding, view: SubContent, viewID: UUID) { - _isPresenting = isPresenting - self.view = view - self.viewID = viewID - - let newView = createOverlayView(view: view) - let viewManager = viewManager - Task { - await viewManager.addView(view: newView) - } - } - func body(content: Content) -> some View { content .onChange(of: isPresenting) { newValue in @@ -50,6 +41,11 @@ struct CharcoalOverlayContainerChild: ViewModifie viewManager.addView(view: newView) } } + .onAppear { + // onAppear is needed if the overlay is presented by default + let newView = createOverlayView(view: view) + viewManager.addView(view: newView) + } } } @@ -62,7 +58,7 @@ public extension View { struct CharcoalOverlayContainer: View { - @ObservedObject var viewManager = CharcoalContainerManager.share + @EnvironmentObject var viewManager: CharcoalContainerManager var body: some View { ZStack { From 793a6354e0ea3e2691b47196e46286ae431de713 Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 8 Feb 2024 13:25:47 +0900 Subject: [PATCH 021/199] Use ObservedObject on CharcoalContainerManager --- .../Components/Overlay/ChacoalOverlayManager.swift | 4 ---- .../Overlay/CharcoalOverlayContainerModifier.swift | 5 +---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift index 4c201ba38..f610581cb 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift @@ -21,8 +21,4 @@ class CharcoalContainerManager: ObservableObject { func clear() { self.overlayViews.removeAll() } - - deinit { - clear() - } } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index a8ff36db3..8666a93fe 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -1,7 +1,7 @@ import SwiftUI struct CharcoalOverlayContainerModifier: ViewModifier { - @StateObject var viewManager = CharcoalContainerManager() + @ObservedObject var viewManager = CharcoalContainerManager() func body(content: Content) -> some View { content @@ -68,8 +68,5 @@ struct CharcoalOverlayContainer: View { overlayView } } - .onDisappear { - viewManager.clear() - } } } From 131c2dd624c7932e3913d2de1d74a7fcd7be4431 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 12 Feb 2024 13:14:34 +0900 Subject: [PATCH 022/199] Add use charcoal button as demo trigger --- .../Components/CharcoalTooltip.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index cce51bf50..79bbdd697 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -5,7 +5,9 @@ struct CharcoalTooltip: CharcoalPopupView { let targetFrame: CGRect let maxWidth: CGFloat - let spacingToTarget: CGFloat = 5 + let spacingToTarget: CGFloat = 4 + + let spacingToScreen: CGFloat = 16 @State var tooltipSize: CGSize = .zero @@ -30,9 +32,9 @@ struct CharcoalTooltip: CharcoalPopupView { var edgeLeft = minX if (edgeLeft + tooltipSize.width >= canvasGeometrySize.width) { - edgeLeft = canvasGeometrySize.width - tooltipSize.width + edgeLeft = canvasGeometrySize.width - tooltipSize.width - spacingToScreen } else if (edgeLeft < 0) { - edgeLeft = 0 + edgeLeft = spacingToScreen } return edgeLeft @@ -150,18 +152,20 @@ private struct TooltipsPreviewView: View { Button { isPresenting2.toggle() } label: { - Image(charocalIcon: .question24) + Text("Help") } + .charcoalDefaultButton() .charcoalTooltip(isPresenting: $isPresenting2, text: "Hello World This is a tooltip") - .offset(CGSize(width: 100.0, height: 100.0)) + .offset(CGSize(width: 100.0, height: 150.0)) Button { isPresenting3.toggle() } label: { - Image(charocalIcon: .question24) + Text("Right") } + .charcoalPrimaryButton(size: .medium) .charcoalTooltip(isPresenting: $isPresenting3, text: "here is testing it's multiple line feature") - .offset(CGSize(width: geometry.size.width - 30, height: 100.0)) + .offset(CGSize(width: geometry.size.width - 100, height: 100.0)) Button { isPresenting4.toggle() From 3b4e47b6340495cca839a0966658867ecef888b3 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 12 Feb 2024 13:54:17 +0900 Subject: [PATCH 023/199] Add arrow logic on tooltip --- .../Components/CharcoalTooltip.swift | 84 ++++++++++++++----- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index 79bbdd697..9e56da89e 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -2,14 +2,24 @@ import SwiftUI struct CharcoalTooltip: CharcoalPopupView { let text: String + let targetFrame: CGRect + let maxWidth: CGFloat + let arrowHeight: CGFloat = 3 + let spacingToTarget: CGFloat = 4 let spacingToScreen: CGFloat = 16 - @State var tooltipSize: CGSize = .zero + @State private var tooltipSize: CGSize = .zero + + /// The actuall width control parameter of tooltip + /// + /// This will be set only when text size is greater than maxWidth to prevent SwiftUI might + /// layout a unexpected width for the tooltip + @State private var adaptiveMaxWidth: CGFloat? var offset: CGSize { CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY) @@ -41,17 +51,15 @@ struct CharcoalTooltip: CharcoalPopupView { } func tooltipY(canvasGeometrySize: CGSize) -> CGFloat { - let minX = targetFrame.maxY + spacingToTarget + let minX = targetFrame.maxY + spacingToTarget + arrowHeight var edgeBottom = targetFrame.maxY + spacingToTarget + targetFrame.height if edgeBottom + tooltipSize.height >= canvasGeometrySize.height { - edgeBottom = targetFrame.minY - tooltipSize.height - spacingToTarget + edgeBottom = targetFrame.minY - tooltipSize.height - spacingToTarget - arrowHeight } return min(minX, edgeBottom) } - @State var adaptiveMaxWidth: CGFloat? - var body: some View { GeometryReader(content: { canvasGeometry in Text(text) @@ -60,22 +68,26 @@ struct CharcoalTooltip: CharcoalPopupView { .fixedSize(horizontal: false, vertical: true) .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) .padding(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12)) - .background(GeometryReader(content: { tooltipGeometry in - Color(CharcoalAsset.ColorPaletteGenerated.surface8.color) .cornerRadius(4, corners: .allCorners) - .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) - })) - .frame(maxWidth: adaptiveMaxWidth) - .offset(CGSize( - width: tooltipX(canvasGeometrySize: canvasGeometry.size), - height: tooltipY(canvasGeometrySize: canvasGeometry.size))) - .onPreferenceChange(TooltipSizeKey.self, perform: { value in - tooltipSize = value - if (adaptiveMaxWidth == nil) { - adaptiveMaxWidth = tooltipSize.width < maxWidth ? nil : maxWidth - } - }) - .animation(.none, value: tooltipSize) - .animation(.none, value: targetFrame) + .background(GeometryReader(content: { tooltipGeometry in + BubbleShape( + frameInGlobal: tooltipGeometry.frame(in: .global), + targetFrame: targetFrame, + arrowHeight: arrowHeight) + .fill(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) + .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) + })) + .frame(maxWidth: adaptiveMaxWidth) + .offset(CGSize( + width: tooltipX(canvasGeometrySize: canvasGeometry.size), + height: tooltipY(canvasGeometrySize: canvasGeometry.size))) + .onPreferenceChange(TooltipSizeKey.self, perform: { value in + tooltipSize = value + if (adaptiveMaxWidth == nil) { + adaptiveMaxWidth = tooltipSize.width < maxWidth ? nil : maxWidth + } + }) + .animation(.none, value: tooltipSize) + .animation(.none, value: targetFrame) }) } @@ -84,6 +96,36 @@ struct CharcoalTooltip: CharcoalPopupView { } } +struct BubbleShape: Shape { + let frameInGlobal: CGRect + let targetFrame: CGRect + let arrowHeight: CGFloat + + func path(in rect: CGRect) -> Path { + let diffX = frameInGlobal.origin.x - rect.origin.x + let targetRelativeX = targetFrame.midX - diffX + let diffY = frameInGlobal.origin.y - rect.origin.y + let targetRelativeY = targetFrame.midY - diffY + var arrowY = rect.minY - arrowHeight + var arrowBaseY = rect.minY + if (targetRelativeY > rect.minY) { + arrowY = rect.maxY + arrowHeight + arrowBaseY = rect.maxY + } + + var bubblePath = RoundedRectangle(cornerRadius: 4).path(in: rect) + let arrowPath = Path { path in + path.move(to: CGPoint(x: targetRelativeX + 5, y: arrowBaseY)) + path.addLine(to: CGPoint(x: targetRelativeX - 5, y: arrowBaseY)) + path.addLine(to: CGPoint(x: targetRelativeX, y: arrowY)) + path.closeSubpath() + } + + bubblePath.addPath(arrowPath) + return bubblePath + } +} + struct TooltipSizeKey: PreferenceKey { static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() From d902eedab404a256b1487b5b490bb4c0693a337d Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 12 Feb 2024 14:12:47 +0900 Subject: [PATCH 024/199] Refine arrow logic --- .../Components/CharcoalTooltip.swift | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index 9e56da89e..2928b19a4 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -7,6 +7,8 @@ struct CharcoalTooltip: CharcoalPopupView { let maxWidth: CGFloat + let cornerRadius: CGFloat = 4 + let arrowHeight: CGFloat = 3 let spacingToTarget: CGFloat = 4 @@ -72,7 +74,9 @@ struct CharcoalTooltip: CharcoalPopupView { BubbleShape( frameInGlobal: tooltipGeometry.frame(in: .global), targetFrame: targetFrame, - arrowHeight: arrowHeight) + arrowHeight: arrowHeight, + cornerRadius: cornerRadius + ) .fill(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) })) @@ -100,24 +104,34 @@ struct BubbleShape: Shape { let frameInGlobal: CGRect let targetFrame: CGRect let arrowHeight: CGFloat + let cornerRadius: CGFloat func path(in rect: CGRect) -> Path { let diffX = frameInGlobal.origin.x - rect.origin.x - let targetRelativeX = targetFrame.midX - diffX + let targetLocalX = targetFrame.midX - diffX + let diffY = frameInGlobal.origin.y - rect.origin.y - let targetRelativeY = targetFrame.midY - diffY + let targetLocalY = targetFrame.midY - diffY + var arrowY = rect.minY - arrowHeight - var arrowBaseY = rect.minY - if (targetRelativeY > rect.minY) { + var arrowBaseY = rect.minY + 1 + + var arrowMaxX = min(targetLocalX + 5, rect.maxX) + + var arrowMinX = max(targetLocalX - 5 , rect.minX) + + if (targetLocalY > rect.minY) { arrowY = rect.maxY + arrowHeight - arrowBaseY = rect.maxY + arrowBaseY = rect.maxY - 1 + arrowMaxX = max(targetLocalX - 5 , rect.minX) + arrowMinX = min(targetLocalX + 5, rect.maxX) } - var bubblePath = RoundedRectangle(cornerRadius: 4).path(in: rect) + var bubblePath = RoundedRectangle(cornerRadius: cornerRadius).path(in: rect) let arrowPath = Path { path in - path.move(to: CGPoint(x: targetRelativeX + 5, y: arrowBaseY)) - path.addLine(to: CGPoint(x: targetRelativeX - 5, y: arrowBaseY)) - path.addLine(to: CGPoint(x: targetRelativeX, y: arrowY)) + path.move(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) + path.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) + path.addLine(to: CGPoint(x: targetLocalX, y: arrowY)) path.closeSubpath() } @@ -161,9 +175,11 @@ public extension View { private struct TooltipsPreviewView: View { @State var isPresenting = true - @State var isPresenting2 = false - @State var isPresenting3 = false - @State var isPresenting4 = false + @State var isPresenting2 = true + @State var isPresenting3 = true + @State var isPresenting4 = true + @State var isPresenting5 = true + @State var isPresenting6 = true @State var textOfLabel = "Hello" @@ -217,6 +233,23 @@ private struct TooltipsPreviewView: View { .charcoalTooltip(isPresenting: $isPresenting4, text: "Hello World This is a tooltip and here is testing it's multiple line feature") .offset(CGSize(width: geometry.size.width - 40, height: geometry.size.height - 40)) + Button { + isPresenting5.toggle() + } label: { + Text("Bottom") + } + .charcoalPrimaryButton(size: .medium) + .charcoalTooltip(isPresenting: $isPresenting5, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + .offset(CGSize(width: geometry.size.width - 240, height: geometry.size.height - 40)) + + Button { + isPresenting6.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalTooltip(isPresenting: $isPresenting6, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + .offset(CGSize(width: geometry.size.width - 380, height: geometry.size.height - 240)) + } } }) From 9ae7e055b5a59dd24659722ea612f21375e3d89b Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 12 Feb 2024 14:42:10 +0900 Subject: [PATCH 025/199] Refine arrow layout logic --- .../Components/CharcoalTooltip.swift | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift index 2928b19a4..9940b10f8 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift @@ -105,6 +105,7 @@ struct BubbleShape: Shape { let targetFrame: CGRect let arrowHeight: CGFloat let cornerRadius: CGFloat + let arrowWidth: CGFloat = 5 func path(in rect: CGRect) -> Path { let diffX = frameInGlobal.origin.x - rect.origin.x @@ -114,24 +115,29 @@ struct BubbleShape: Shape { let targetLocalY = targetFrame.midY - diffY var arrowY = rect.minY - arrowHeight - var arrowBaseY = rect.minY + 1 + var arrowBaseY = rect.minY - var arrowMaxX = min(targetLocalX + 5, rect.maxX) + let minX = rect.minX + cornerRadius + arrowWidth + let maxX = rect.maxX - cornerRadius - arrowWidth - var arrowMinX = max(targetLocalX - 5 , rect.minX) + let arrowMidX = min(max(minX, targetLocalX), maxX) + + var arrowMaxX = arrowMidX + arrowWidth + + var arrowMinX = arrowMidX - arrowWidth if (targetLocalY > rect.minY) { arrowY = rect.maxY + arrowHeight - arrowBaseY = rect.maxY - 1 - arrowMaxX = max(targetLocalX - 5 , rect.minX) - arrowMinX = min(targetLocalX + 5, rect.maxX) + arrowBaseY = rect.maxY + arrowMaxX = arrowMidX - arrowWidth + arrowMinX = arrowMidX + arrowWidth } var bubblePath = RoundedRectangle(cornerRadius: cornerRadius).path(in: rect) let arrowPath = Path { path in path.move(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) path.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) - path.addLine(to: CGPoint(x: targetLocalX, y: arrowY)) + path.addLine(to: CGPoint(x: arrowMidX, y: arrowY)) path.closeSubpath() } @@ -193,7 +199,7 @@ private struct TooltipsPreviewView: View { Text(textOfLabel) Button { - textOfLabel = "Changed" + textOfLabel = ["Changed", "Hello"].randomElement()! } label: { Text("Change Label") } @@ -231,7 +237,7 @@ private struct TooltipsPreviewView: View { Image(charocalIcon: .question24) } .charcoalTooltip(isPresenting: $isPresenting4, text: "Hello World This is a tooltip and here is testing it's multiple line feature") - .offset(CGSize(width: geometry.size.width - 40, height: geometry.size.height - 40)) + .offset(CGSize(width: geometry.size.width - 30, height: geometry.size.height - 40)) Button { isPresenting5.toggle() From 968feb9a6d7e33150eb32697e27ad8615b0e4b9d Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 12 Feb 2024 14:42:36 +0900 Subject: [PATCH 026/199] Use StateObject to prevent unexpected reinit --- .../Components/Overlay/CharcoalOverlayContainerModifier.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 8666a93fe..277596b12 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -1,7 +1,7 @@ import SwiftUI struct CharcoalOverlayContainerModifier: ViewModifier { - @ObservedObject var viewManager = CharcoalContainerManager() + @StateObject var viewManager = CharcoalContainerManager() func body(content: Content) -> some View { content @@ -67,6 +67,8 @@ struct CharcoalOverlayContainer: View { ForEach(viewManager.overlayViews, id: \.id) { overlayView in overlayView } + }.onDisappear { + viewManager.clear() } } } From 1539dacd04e6a5b21a062bc15afedf4df1a57819 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 12 Feb 2024 15:26:42 +0900 Subject: [PATCH 027/199] Refactor TooltipBubbleShape --- .../{ => Tooltip}/CharcoalTooltip.swift | 71 +++++-------------- .../Tooltip/TooltipBubbleShape.swift | 47 ++++++++++++ 2 files changed, 64 insertions(+), 54 deletions(-) rename Sources/CharcoalSwiftUI/Components/{ => Tooltip}/CharcoalTooltip.swift (81%) create mode 100644 Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift diff --git a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift similarity index 81% rename from Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift rename to Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 9940b10f8..bcfd8b6d6 100644 --- a/Sources/CharcoalSwiftUI/Components/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -1,18 +1,25 @@ import SwiftUI struct CharcoalTooltip: CharcoalPopupView { + /// The text of the tooltip let text: String + /// The frame of the target view which the arrow will point to let targetFrame: CGRect + /// The maximum width of the tooltip let maxWidth: CGFloat + /// The corner radius of the tooltip let cornerRadius: CGFloat = 4 + /// The height of the arrow let arrowHeight: CGFloat = 3 + /// The spacing between the tooltip and the target view let spacingToTarget: CGFloat = 4 + /// The spacing between the tooltip and the screen edge let spacingToScreen: CGFloat = 16 @State private var tooltipSize: CGSize = .zero @@ -33,11 +40,6 @@ struct CharcoalTooltip: CharcoalPopupView { self.maxWidth = maxWidth } - var animation: Animation { - .easeOut(duration: 1) - .repeatForever(autoreverses: false) - } - func tooltipX(canvasGeometrySize: CGSize) -> CGFloat { let minX = targetFrame.midX - (tooltipSize.width / 2.0) @@ -71,9 +73,11 @@ struct CharcoalTooltip: CharcoalPopupView { .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) .padding(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12)) .background(GeometryReader(content: { tooltipGeometry in - BubbleShape( - frameInGlobal: tooltipGeometry.frame(in: .global), - targetFrame: targetFrame, + let tooltipOrigin = tooltipGeometry.frame(in: .global).origin + TooltipBubbleShape( + targetPoint: + CGPoint(x: targetFrame.midX - tooltipOrigin.x, + y: targetFrame.maxY - tooltipOrigin.y), arrowHeight: arrowHeight, cornerRadius: cornerRadius ) @@ -87,6 +91,9 @@ struct CharcoalTooltip: CharcoalPopupView { .onPreferenceChange(TooltipSizeKey.self, perform: { value in tooltipSize = value if (adaptiveMaxWidth == nil) { + // Set adaptiveMaxWidth only when the text size is greater than maxWidth + // This is a workaround to `.frame(maxWidth: ).fixedSize()` problem + // SwiftUI might give the view a greater width if it is smaller than maxWidth adaptiveMaxWidth = tooltipSize.width < maxWidth ? nil : maxWidth } }) @@ -100,52 +107,6 @@ struct CharcoalTooltip: CharcoalPopupView { } } -struct BubbleShape: Shape { - let frameInGlobal: CGRect - let targetFrame: CGRect - let arrowHeight: CGFloat - let cornerRadius: CGFloat - let arrowWidth: CGFloat = 5 - - func path(in rect: CGRect) -> Path { - let diffX = frameInGlobal.origin.x - rect.origin.x - let targetLocalX = targetFrame.midX - diffX - - let diffY = frameInGlobal.origin.y - rect.origin.y - let targetLocalY = targetFrame.midY - diffY - - var arrowY = rect.minY - arrowHeight - var arrowBaseY = rect.minY - - let minX = rect.minX + cornerRadius + arrowWidth - let maxX = rect.maxX - cornerRadius - arrowWidth - - let arrowMidX = min(max(minX, targetLocalX), maxX) - - var arrowMaxX = arrowMidX + arrowWidth - - var arrowMinX = arrowMidX - arrowWidth - - if (targetLocalY > rect.minY) { - arrowY = rect.maxY + arrowHeight - arrowBaseY = rect.maxY - arrowMaxX = arrowMidX - arrowWidth - arrowMinX = arrowMidX + arrowWidth - } - - var bubblePath = RoundedRectangle(cornerRadius: cornerRadius).path(in: rect) - let arrowPath = Path { path in - path.move(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) - path.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) - path.addLine(to: CGPoint(x: arrowMidX, y: arrowY)) - path.closeSubpath() - } - - bubblePath.addPath(arrowPath) - return bubblePath - } -} - struct TooltipSizeKey: PreferenceKey { static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() @@ -157,8 +118,10 @@ struct CharcoalTooltipModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool + /// Text to be displayed in the tooltip var text: String + /// Assign a unique ID to the view @State var viewID = UUID() func body(content: Content) -> some View { diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift new file mode 100644 index 000000000..6b6529a2a --- /dev/null +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift @@ -0,0 +1,47 @@ +import SwiftUI + +struct TooltipBubbleShape: Shape { + /// The target point which + let targetPoint: CGPoint + /// The height of the arrow + let arrowHeight: CGFloat + /// The corner radius of the tooltip + let cornerRadius: CGFloat + /// The width of the arrow + let arrowWidth: CGFloat = 5 + + func path(in rect: CGRect) -> Path { + var arrowY = rect.minY - arrowHeight + var arrowBaseY = rect.minY + + // The minimum and maximum x position of the arrow + let minX = rect.minX + cornerRadius + arrowWidth + let maxX = rect.maxX - cornerRadius - arrowWidth + + // The x position of the arrow + let arrowMidX = min(max(minX, targetPoint.x), maxX) + + var arrowMaxX = arrowMidX + arrowWidth + + var arrowMinX = arrowMidX - arrowWidth + + // Check if the arrow should be on top of the tooltip + if (targetPoint.y > rect.minY) { + arrowY = rect.maxY + arrowHeight + arrowBaseY = rect.maxY + arrowMaxX = arrowMidX - arrowWidth + arrowMinX = arrowMidX + arrowWidth + } + + var bubblePath = RoundedRectangle(cornerRadius: cornerRadius).path(in: rect) + let arrowPath = Path { path in + path.move(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) + path.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) + path.addLine(to: CGPoint(x: arrowMidX, y: arrowY)) + path.closeSubpath() + } + + bubblePath.addPath(arrowPath) + return bubblePath + } +} From ca43fec41bf24e1893530abe8efbc259f4a7d2a3 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 12 Feb 2024 15:36:24 +0900 Subject: [PATCH 028/199] Fix edge layout logic --- .../CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index bcfd8b6d6..2b54a5c10 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -47,7 +47,7 @@ struct CharcoalTooltip: CharcoalPopupView { if (edgeLeft + tooltipSize.width >= canvasGeometrySize.width) { edgeLeft = canvasGeometrySize.width - tooltipSize.width - spacingToScreen - } else if (edgeLeft < 0) { + } else if (edgeLeft < spacingToScreen) { edgeLeft = spacingToScreen } From 506345d62786c926a07048edd253a398a0e79f08 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 12 Feb 2024 16:01:07 +0900 Subject: [PATCH 029/199] Add comment --- .../Components/Tooltip/CharcoalTooltip.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 2b54a5c10..a5c817ef9 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -75,7 +75,7 @@ struct CharcoalTooltip: CharcoalPopupView { .background(GeometryReader(content: { tooltipGeometry in let tooltipOrigin = tooltipGeometry.frame(in: .global).origin TooltipBubbleShape( - targetPoint: + targetPoint: CGPoint(x: targetFrame.midX - tooltipOrigin.x, y: targetFrame.maxY - tooltipOrigin.y), arrowHeight: arrowHeight, @@ -134,6 +134,18 @@ struct CharcoalTooltipModifier: ViewModifier { } public extension View { + /** + Add a tooltip to the view + + - Parameters: + - isPresenting: A binding to whether the Tooltip is presented. + - text: The text to be displayed in the tooltip. + + # Example # + ```swift + Text("Hello").charcoalTooltip(isPresenting: $isPresenting, text: "This is a tooltip") + ``` + */ func charcoalTooltip( isPresenting: Binding, text: String From 08b32ddb372c3fe78cfed5c5cab821f101c8100a Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 12 Feb 2024 16:07:49 +0900 Subject: [PATCH 030/199] Format code --- .../CharcoalSwiftUISample/TooltipsView.swift | 24 ++--- .../Overlay/ChacoalOverlayManager.swift | 17 ++- .../CharcoalIdentifiableOverlayView.swift | 2 +- .../CharcoalOverlayContainerModifier.swift | 22 ++-- .../Components/Tooltip/CharcoalTooltip.swift | 100 +++++++++--------- .../Tooltip/TooltipBubbleShape.swift | 20 ++-- 6 files changed, 91 insertions(+), 94 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift index 608925cbe..a838fbdfb 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift @@ -2,28 +2,27 @@ import Charcoal import SwiftUI public struct TooltipsView: View { - @State var isPresented = false - + @State var isPresented2 = false - + @State var isPresented3 = false - + @State var isPresented4 = false - + public var body: some View { VStack { List { - HStack() { - Text("Help") + HStack { + Text("Help") Button(action: { isPresented.toggle() }, label: { Image(charocalIcon: .question16) }).charcoalTooltip(isPresenting: $isPresented, text: "Tooltip created by Charcoal") } - - HStack() { + + HStack { Text("Help (Multiple Line)") Button(action: { isPresented2.toggle() @@ -31,8 +30,8 @@ public struct TooltipsView: View { Image(charocalIcon: .question16) }).charcoalTooltip(isPresenting: $isPresented2, text: "Tooltip created by Charcoal and here is testing it's multiple line feature") } - - HStack() { + + HStack { Text("Help (Auto-Positioning-Trailing)") Spacer() Button(action: { @@ -41,10 +40,9 @@ public struct TooltipsView: View { Image(charocalIcon: .question16) }).charcoalTooltip(isPresenting: $isPresented4, text: "Tooltip created by Charcoal and here is testing Auto-Positioning") } - } Spacer() - HStack() { + HStack { Text("Help (Auto-Positioning-Bottom)") Button(action: { isPresented3.toggle() diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift index f610581cb..c5ecc2800 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/ChacoalOverlayManager.swift @@ -1,24 +1,23 @@ +import Combine import Foundation import SwiftUI -import Combine class CharcoalContainerManager: ObservableObject { - @Published var overlayViews: [CharcoalIdentifiableOverlayView] = [] - + func addView(view: CharcoalIdentifiableOverlayView) { - if let index = self.overlayViews.firstIndex(where: { $0.id == view.id }) { + if let index = overlayViews.firstIndex(where: { $0.id == view.id }) { // Make sure we don't have duplicate views and the latest view's zIndex is on top of the Stack - self.overlayViews.remove(at: index) + overlayViews.remove(at: index) } - self.overlayViews.append(view) + overlayViews.append(view) } func removeView(id: CharcoalIdentifiableOverlayView.IDValue) { - self.overlayViews.removeAll(where: { $0.id == id }) + overlayViews.removeAll(where: { $0.id == id }) } - + func clear() { - self.overlayViews.removeAll() + overlayViews.removeAll() } } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 5bf56cf51..3aefcea43 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -5,7 +5,7 @@ struct CharcoalIdentifiableOverlayView: View { let id: IDValue var contentView: AnyView @Binding var isPresenting: Bool - + var body: some View { VStack { if isPresenting { diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 277596b12..f2cfc8c3d 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -2,7 +2,7 @@ import SwiftUI struct CharcoalOverlayContainerModifier: ViewModifier { @StateObject var viewManager = CharcoalContainerManager() - + func body(content: Content) -> some View { content .overlay( @@ -12,21 +12,21 @@ struct CharcoalOverlayContainerModifier: ViewModifier { } } -typealias CharcoalPopupView = View & Equatable +typealias CharcoalPopupView = Equatable & View struct CharcoalOverlayContainerChild: ViewModifier { @EnvironmentObject var viewManager: CharcoalContainerManager - + @Binding var isPresenting: Bool - + var view: SubContent - + let viewID: UUID - + func createOverlayView(view: SubContent) -> CharcoalIdentifiableOverlayView { return CharcoalIdentifiableOverlayView(id: viewID, contentView: AnyView(view), isPresenting: $isPresenting) } - + func body(content: Content) -> some View { content .onChange(of: isPresenting) { newValue in @@ -35,7 +35,7 @@ struct CharcoalOverlayContainerChild: ViewModifie viewManager.addView(view: newView) } } - .onChange(of: view) { newValue in + .onChange(of: view) { _ in if isPresenting { let newView = createOverlayView(view: view) viewManager.addView(view: newView) @@ -46,7 +46,6 @@ struct CharcoalOverlayContainerChild: ViewModifie let newView = createOverlayView(view: view) viewManager.addView(view: newView) } - } } @@ -57,13 +56,12 @@ public extension View { } struct CharcoalOverlayContainer: View { - @EnvironmentObject var viewManager: CharcoalContainerManager - + var body: some View { ZStack { Color.clear.allowsHitTesting(false) - + ForEach(viewManager.overlayViews, id: \.id) { overlayView in overlayView } diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index a5c817ef9..0d1fbf219 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -3,67 +3,67 @@ import SwiftUI struct CharcoalTooltip: CharcoalPopupView { /// The text of the tooltip let text: String - + /// The frame of the target view which the arrow will point to let targetFrame: CGRect - + /// The maximum width of the tooltip let maxWidth: CGFloat - + /// The corner radius of the tooltip let cornerRadius: CGFloat = 4 - + /// The height of the arrow let arrowHeight: CGFloat = 3 - + /// The spacing between the tooltip and the target view let spacingToTarget: CGFloat = 4 - + /// The spacing between the tooltip and the screen edge let spacingToScreen: CGFloat = 16 - + @State private var tooltipSize: CGSize = .zero - + /// The actuall width control parameter of tooltip /// /// This will be set only when text size is greater than maxWidth to prevent SwiftUI might /// layout a unexpected width for the tooltip @State private var adaptiveMaxWidth: CGFloat? - + var offset: CGSize { CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY) } - + init(text: String, targetFrame: CGRect, maxWidth: CGFloat = 184) { self.text = text self.targetFrame = targetFrame self.maxWidth = maxWidth } - + func tooltipX(canvasGeometrySize: CGSize) -> CGFloat { let minX = targetFrame.midX - (tooltipSize.width / 2.0) - + var edgeLeft = minX - - if (edgeLeft + tooltipSize.width >= canvasGeometrySize.width) { + + if edgeLeft + tooltipSize.width >= canvasGeometrySize.width { edgeLeft = canvasGeometrySize.width - tooltipSize.width - spacingToScreen - } else if (edgeLeft < spacingToScreen) { + } else if edgeLeft < spacingToScreen { edgeLeft = spacingToScreen } - + return edgeLeft } - + func tooltipY(canvasGeometrySize: CGSize) -> CGFloat { let minX = targetFrame.maxY + spacingToTarget + arrowHeight var edgeBottom = targetFrame.maxY + spacingToTarget + targetFrame.height if edgeBottom + tooltipSize.height >= canvasGeometrySize.height { edgeBottom = targetFrame.minY - tooltipSize.height - spacingToTarget - arrowHeight } - + return min(minX, edgeBottom) } - + var body: some View { GeometryReader(content: { canvasGeometry in Text(text) @@ -76,21 +76,24 @@ struct CharcoalTooltip: CharcoalPopupView { let tooltipOrigin = tooltipGeometry.frame(in: .global).origin TooltipBubbleShape( targetPoint: - CGPoint(x: targetFrame.midX - tooltipOrigin.x, - y: targetFrame.maxY - tooltipOrigin.y), + CGPoint( + x: targetFrame.midX - tooltipOrigin.x, + y: targetFrame.maxY - tooltipOrigin.y + ), arrowHeight: arrowHeight, cornerRadius: cornerRadius ) - .fill(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) - .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) + .fill(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) + .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) })) .frame(maxWidth: adaptiveMaxWidth) .offset(CGSize( width: tooltipX(canvasGeometrySize: canvasGeometry.size), - height: tooltipY(canvasGeometrySize: canvasGeometry.size))) + height: tooltipY(canvasGeometrySize: canvasGeometry.size) + )) .onPreferenceChange(TooltipSizeKey.self, perform: { value in tooltipSize = value - if (adaptiveMaxWidth == nil) { + if adaptiveMaxWidth == nil { // Set adaptiveMaxWidth only when the text size is greater than maxWidth // This is a workaround to `.frame(maxWidth: ).fixedSize()` problem // SwiftUI might give the view a greater width if it is smaller than maxWidth @@ -101,7 +104,7 @@ struct CharcoalTooltip: CharcoalPopupView { .animation(.none, value: targetFrame) }) } - + static func == (lhs: CharcoalTooltip, rhs: CharcoalTooltip) -> Bool { return lhs.text == rhs.text && lhs.targetFrame == rhs.targetFrame && lhs.maxWidth == rhs.maxWidth && lhs.tooltipSize == rhs.tooltipSize } @@ -111,19 +114,20 @@ struct TooltipSizeKey: PreferenceKey { static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() } + static var defaultValue: CGSize = .zero } struct CharcoalTooltipModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool - + /// Text to be displayed in the tooltip var text: String - + /// Assign a unique ID to the view @State var viewID = UUID() - + func body(content: Content) -> some View { content .overlay(GeometryReader(content: { proxy in @@ -136,11 +140,11 @@ struct CharcoalTooltipModifier: ViewModifier { public extension View { /** Add a tooltip to the view - + - Parameters: - isPresenting: A binding to whether the Tooltip is presented. - text: The text to be displayed in the tooltip. - + # Example # ```swift Text("Hello").charcoalTooltip(isPresenting: $isPresenting, text: "This is a tooltip") @@ -161,34 +165,34 @@ private struct TooltipsPreviewView: View { @State var isPresenting4 = true @State var isPresenting5 = true @State var isPresenting6 = true - + @State var textOfLabel = "Hello" - + var body: some View { GeometryReader(content: { geometry in ScrollView { ZStack(alignment: .topLeading) { Color.clear - + VStack { Text(textOfLabel) - + Button { textOfLabel = ["Changed", "Hello"].randomElement()! } label: { Text("Change Label") } } - - Button { + + Button { isPresenting.toggle() } label: { Image(charocalIcon: .question24) } .charcoalTooltip(isPresenting: $isPresenting, text: "Hello World") .offset(CGSize(width: 20.0, height: 80.0)) - - Button { + + Button { isPresenting2.toggle() } label: { Text("Help") @@ -196,8 +200,8 @@ private struct TooltipsPreviewView: View { .charcoalDefaultButton() .charcoalTooltip(isPresenting: $isPresenting2, text: "Hello World This is a tooltip") .offset(CGSize(width: 100.0, height: 150.0)) - - Button { + + Button { isPresenting3.toggle() } label: { Text("Right") @@ -205,16 +209,16 @@ private struct TooltipsPreviewView: View { .charcoalPrimaryButton(size: .medium) .charcoalTooltip(isPresenting: $isPresenting3, text: "here is testing it's multiple line feature") .offset(CGSize(width: geometry.size.width - 100, height: 100.0)) - - Button { + + Button { isPresenting4.toggle() } label: { Image(charocalIcon: .question24) } .charcoalTooltip(isPresenting: $isPresenting4, text: "Hello World This is a tooltip and here is testing it's multiple line feature") .offset(CGSize(width: geometry.size.width - 30, height: geometry.size.height - 40)) - - Button { + + Button { isPresenting5.toggle() } label: { Text("Bottom") @@ -222,20 +226,18 @@ private struct TooltipsPreviewView: View { .charcoalPrimaryButton(size: .medium) .charcoalTooltip(isPresenting: $isPresenting5, text: "Hello World This is a tooltip and here is testing it's multiple line feature") .offset(CGSize(width: geometry.size.width - 240, height: geometry.size.height - 40)) - - Button { + + Button { isPresenting6.toggle() } label: { Image(charocalIcon: .question24) } .charcoalTooltip(isPresenting: $isPresenting6, text: "Hello World This is a tooltip and here is testing it's multiple line feature") .offset(CGSize(width: geometry.size.width - 380, height: geometry.size.height - 240)) - } } }) .charcoalOverlayContainer() - } } diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift index 6b6529a2a..f5ef234f2 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift @@ -1,7 +1,7 @@ import SwiftUI struct TooltipBubbleShape: Shape { - /// The target point which + /// The target point which let targetPoint: CGPoint /// The height of the arrow let arrowHeight: CGFloat @@ -9,30 +9,30 @@ struct TooltipBubbleShape: Shape { let cornerRadius: CGFloat /// The width of the arrow let arrowWidth: CGFloat = 5 - + func path(in rect: CGRect) -> Path { var arrowY = rect.minY - arrowHeight var arrowBaseY = rect.minY - + // The minimum and maximum x position of the arrow let minX = rect.minX + cornerRadius + arrowWidth let maxX = rect.maxX - cornerRadius - arrowWidth - + // The x position of the arrow let arrowMidX = min(max(minX, targetPoint.x), maxX) - + var arrowMaxX = arrowMidX + arrowWidth - + var arrowMinX = arrowMidX - arrowWidth - + // Check if the arrow should be on top of the tooltip - if (targetPoint.y > rect.minY) { + if targetPoint.y > rect.minY { arrowY = rect.maxY + arrowHeight arrowBaseY = rect.maxY arrowMaxX = arrowMidX - arrowWidth arrowMinX = arrowMidX + arrowWidth } - + var bubblePath = RoundedRectangle(cornerRadius: cornerRadius).path(in: rect) let arrowPath = Path { path in path.move(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) @@ -40,7 +40,7 @@ struct TooltipBubbleShape: Shape { path.addLine(to: CGPoint(x: arrowMidX, y: arrowY)) path.closeSubpath() } - + bubblePath.addPath(arrowPath) return bubblePath } From 2de961e432b18432079e4629dc168f8230a6d990 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 12 Feb 2024 17:00:33 +0900 Subject: [PATCH 031/199] Use new approach to remove adaptiveMaxWidth --- .../Components/Tooltip/CharcoalTooltip.swift | 73 ++++++++----------- 1 file changed, 31 insertions(+), 42 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 0d1fbf219..863e4401f 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -24,12 +24,6 @@ struct CharcoalTooltip: CharcoalPopupView { @State private var tooltipSize: CGSize = .zero - /// The actuall width control parameter of tooltip - /// - /// This will be set only when text size is greater than maxWidth to prevent SwiftUI might - /// layout a unexpected width for the tooltip - @State private var adaptiveMaxWidth: CGFloat? - var offset: CGSize { CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY) } @@ -66,42 +60,37 @@ struct CharcoalTooltip: CharcoalPopupView { var body: some View { GeometryReader(content: { canvasGeometry in - Text(text) - .charcoalTypography12Regular() - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) - .padding(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12)) - .background(GeometryReader(content: { tooltipGeometry in - let tooltipOrigin = tooltipGeometry.frame(in: .global).origin - TooltipBubbleShape( - targetPoint: - CGPoint( - x: targetFrame.midX - tooltipOrigin.x, - y: targetFrame.maxY - tooltipOrigin.y - ), - arrowHeight: arrowHeight, - cornerRadius: cornerRadius - ) - .fill(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) - .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) - })) - .frame(maxWidth: adaptiveMaxWidth) - .offset(CGSize( - width: tooltipX(canvasGeometrySize: canvasGeometry.size), - height: tooltipY(canvasGeometrySize: canvasGeometry.size) - )) - .onPreferenceChange(TooltipSizeKey.self, perform: { value in - tooltipSize = value - if adaptiveMaxWidth == nil { - // Set adaptiveMaxWidth only when the text size is greater than maxWidth - // This is a workaround to `.frame(maxWidth: ).fixedSize()` problem - // SwiftUI might give the view a greater width if it is smaller than maxWidth - adaptiveMaxWidth = tooltipSize.width < maxWidth ? nil : maxWidth - } - }) - .animation(.none, value: tooltipSize) - .animation(.none, value: targetFrame) + VStack { + Text(text) + .charcoalTypography12Regular() + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) + .padding(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12)) + .background(GeometryReader(content: { tooltipGeometry in + let tooltipOrigin = tooltipGeometry.frame(in: .global).origin + TooltipBubbleShape( + targetPoint: + CGPoint( + x: targetFrame.midX - tooltipOrigin.x, + y: targetFrame.maxY - tooltipOrigin.y + ), + arrowHeight: arrowHeight, + cornerRadius: cornerRadius + ) + .fill(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) + .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) + })) + .offset(CGSize( + width: tooltipX(canvasGeometrySize: canvasGeometry.size), + height: tooltipY(canvasGeometrySize: canvasGeometry.size) + )) + .onPreferenceChange(TooltipSizeKey.self, perform: { value in + tooltipSize = value + }) + .animation(.none, value: tooltipSize) + .animation(.none, value: targetFrame) + }.frame(minWidth: 0, maxWidth: maxWidth, alignment: .leading) }) } From 64586f50c779bb2bda10c15d89f538bcedc4b41a Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 12 Feb 2024 17:04:19 +0900 Subject: [PATCH 032/199] Fix the tip bubble's position latency --- .../Components/Overlay/CharcoalOverlayContainerModifier.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index f2cfc8c3d..e2d480b48 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -35,9 +35,9 @@ struct CharcoalOverlayContainerChild: ViewModifie viewManager.addView(view: newView) } } - .onChange(of: view) { _ in + .onChange(of: view) { newValue in if isPresenting { - let newView = createOverlayView(view: view) + let newView = createOverlayView(view: newValue) viewManager.addView(view: newView) } } From abdd34582932d963f6ce73b808bc02d2f33892c2 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 20 Feb 2024 16:19:25 +0900 Subject: [PATCH 033/199] Add dismiss when interaction --- .../CharcoalIdentifiableOverlayView.swift | 20 +++++++++++++++++-- .../CharcoalOverlayContainerModifier.swift | 4 ++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 3aefcea43..60257732f 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -1,16 +1,32 @@ import SwiftUI struct CharcoalIdentifiableOverlayView: View { + typealias IDValue = UUID let id: IDValue var contentView: AnyView @Binding var isPresenting: Bool - + var body: some View { - VStack { + 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) + } } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index e2d480b48..d86329890 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -60,8 +60,8 @@ struct CharcoalOverlayContainer: View { var body: some View { ZStack { - Color.clear.allowsHitTesting(false) - + Color.clear + ForEach(viewManager.overlayViews, id: \.id) { overlayView in overlayView } From e38ca0e4cff95e206278f33259326287f7421e3f Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 20 Feb 2024 16:19:48 +0900 Subject: [PATCH 034/199] Reformat --- .../Components/Overlay/CharcoalIdentifiableOverlayView.swift | 4 +--- .../Components/Overlay/CharcoalOverlayContainerModifier.swift | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 60257732f..fb6c82b77 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -1,12 +1,11 @@ import SwiftUI struct CharcoalIdentifiableOverlayView: View { - typealias IDValue = UUID let id: IDValue var contentView: AnyView @Binding var isPresenting: Bool - + var body: some View { ZStack { if isPresenting { @@ -27,6 +26,5 @@ struct CharcoalIdentifiableOverlayView: View { contentView } }.animation(.easeInOut(duration: 0.2), value: isPresenting) - } } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index d86329890..af234516d 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -61,7 +61,7 @@ struct CharcoalOverlayContainer: View { var body: some View { ZStack { Color.clear - + ForEach(viewManager.overlayViews, id: \.id) { overlayView in overlayView } From 720bb561b6d626d5a1a8fe7aae1f2ec6c5ca8114 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 20 Feb 2024 18:35:10 +0900 Subject: [PATCH 035/199] Add initial Snackbar --- .../Components/Toast/CharcoalSnackBar.swift | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift new file mode 100644 index 000000000..2dac2103d --- /dev/null +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -0,0 +1,133 @@ +import SwiftUI + +struct CharcoalSnackBar: CharcoalPopupView { + /// The text of the tooltip + let text: String + + /// The maximum width of the tooltip + let maxWidth: CGFloat + + /// The corner radius of the tooltip + let cornerRadius: CGFloat = 32 + + /// The spacing between the tooltip and the screen edge + let spacingToScreen: CGFloat = 16 + + @State private var tooltipSize: CGSize = .zero + + var offset: CGSize { + CGSize(width: 0, height: 0) + } + + init(text: String, maxWidth: CGFloat = 312) { + self.text = text + self.maxWidth = maxWidth + } + + var body: some View { + ZStack(alignment: .center) { + HStack(spacing: 0) { + Image(charocalIcon: .addImage24) + .resizable() + .scaledToFill() + .frame(width: 64, height: 64) + HStack(spacing: 16) { + Text(text) + .charcoalTypography14Bold(isSingleLine: true) + .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text1.color)) + Button { + + } label: { + Text("hello") + }.charcoalDefaultButton(size: .small) + } + .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) + } + .cornerRadius(cornerRadius, corners: .allCorners) + .clipped() + .background( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1) + ) + + }.frame(minWidth: 0, maxWidth: maxWidth, alignment: .leading) + } + + static func == (lhs: CharcoalSnackBar, rhs: CharcoalSnackBar) -> Bool { + return lhs.text == rhs.text && lhs.maxWidth == rhs.maxWidth && lhs.tooltipSize == rhs.tooltipSize + } +} + + +struct CharcoalSnackBarModifier: ViewModifier { + /// Presentation `Binding` + @Binding var isPresenting: Bool + + /// Text to be displayed in the tooltip + var text: String + + /// Assign a unique ID to the view + @State var viewID = UUID() + + func body(content: Content) -> some View { + content + .overlay(GeometryReader(content: { proxy in + Color.clear + .modifier(CharcoalOverlayContainerChild(isPresenting: $isPresenting, view: CharcoalSnackBar(text: text), viewID: viewID)) + })) + } +} + +public extension View { + /** + Add a tooltip to the view + + - Parameters: + - isPresenting: A binding to whether the Tooltip is presented. + - text: The text to be displayed in the tooltip. + + # Example # + ```swift + Text("Hello").charcoalTooltip(isPresenting: $isPresenting, text: "This is a tooltip") + ``` + */ + func charcoalSnackBar( + isPresenting: Binding, + text: String + ) -> some View { + return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, text: text)) + } +} + +private struct SnackBarsPreviewView: View { + @State var isPresenting = true + @State var isPresenting6 = true + + @State var textOfLabel = "Hello" + + var body: some View { + GeometryReader(content: { geometry in + ScrollView { + ZStack(alignment: .topLeading) { + Color.clear + + VStack { + Text(textOfLabel) + + Button { + textOfLabel = ["Changed", "Hello"].randomElement()! + } label: { + Text("Change Label") + } + } + } + } + .charcoalSnackBar(isPresenting: $isPresenting6, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + }) + .charcoalOverlayContainer() + } +} + +#Preview { + SnackBarsPreviewView() +} From bf66953ad8eec96c23c0fa976c40c824ff5679a1 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 13:33:45 +0900 Subject: [PATCH 036/199] Add thumbnail image --- .../Components/Toast/CharcoalSnackBar.swift | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 2dac2103d..491401440 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -1,6 +1,7 @@ import SwiftUI struct CharcoalSnackBar: CharcoalPopupView { + let thumbnailImage: UIImage? /// The text of the tooltip let text: String @@ -19,18 +20,21 @@ struct CharcoalSnackBar: CharcoalPopupView { CGSize(width: 0, height: 0) } - init(text: String, maxWidth: CGFloat = 312) { + init(text: String, maxWidth: CGFloat = 312, thumbnailImage: UIImage?) { self.text = text self.maxWidth = maxWidth + self.thumbnailImage = thumbnailImage } var body: some View { - ZStack(alignment: .center) { + ZStack(alignment: .bottom) { HStack(spacing: 0) { - Image(charocalIcon: .addImage24) - .resizable() - .scaledToFill() - .frame(width: 64, height: 64) + if let thumbnailImage = thumbnailImage { + Image(uiImage: thumbnailImage) + .resizable() + .scaledToFill() + .frame(width: 64, height: 64) + } HStack(spacing: 16) { Text(text) .charcoalTypography14Bold(isSingleLine: true) @@ -65,6 +69,8 @@ struct CharcoalSnackBarModifier: ViewModifier { /// Text to be displayed in the tooltip var text: String + + let thumbnailImage: UIImage? /// Assign a unique ID to the view @State var viewID = UUID() @@ -73,7 +79,7 @@ struct CharcoalSnackBarModifier: ViewModifier { content .overlay(GeometryReader(content: { proxy in Color.clear - .modifier(CharcoalOverlayContainerChild(isPresenting: $isPresenting, view: CharcoalSnackBar(text: text), viewID: viewID)) + .modifier(CharcoalOverlayContainerChild(isPresenting: $isPresenting, view: CharcoalSnackBar(text: text, thumbnailImage: thumbnailImage), viewID: viewID)) })) } } @@ -93,9 +99,10 @@ public extension View { */ func charcoalSnackBar( isPresenting: Binding, - text: String + text: String, + thumbnailImage: UIImage? = nil ) -> some View { - return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, text: text)) + return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, text: text, thumbnailImage: thumbnailImage)) } } @@ -122,7 +129,10 @@ private struct SnackBarsPreviewView: View { } } } - .charcoalSnackBar(isPresenting: $isPresenting6, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + .charcoalSnackBar(isPresenting: $isPresenting6, + text: "Hello World This is a tooltip and here is testing it's multiple line feature", + thumbnailImage: CharcoalAsset.Images.addPeople24.image + ) }) .charcoalOverlayContainer() } From 238768c7d981029a7f8b43a40759b03e651764cd Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 14:17:33 +0900 Subject: [PATCH 037/199] Add support for thumbnailImage and action --- .../Components/Toast/CharcoalSnackBar.swift | 137 +++++++++++------- 1 file changed, 82 insertions(+), 55 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 491401440..d86e84d43 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -1,33 +1,38 @@ import SwiftUI -struct CharcoalSnackBar: CharcoalPopupView { - let thumbnailImage: UIImage? - /// The text of the tooltip +struct CharcoalSnackBar: CharcoalPopupView { + /// The text of the snackbar let text: String - - /// The maximum width of the tooltip + + /// The thumbnail image of the snackbar + let thumbnailImage: UIImage? + + /// The maximum width of the snackbar let maxWidth: CGFloat - - /// The corner radius of the tooltip + + /// The corner radius of the snackbar let cornerRadius: CGFloat = 32 - - /// The spacing between the tooltip and the screen edge + + /// The spacing between the snackbar and the screen edge let spacingToScreen: CGFloat = 16 - + + /// The content of the action view + let action: CharcoalSnackBarActionContent? + @State private var tooltipSize: CGSize = .zero - - var offset: CGSize { - CGSize(width: 0, height: 0) - } - - init(text: String, maxWidth: CGFloat = 312, thumbnailImage: UIImage?) { + + init(text: String, + maxWidth: CGFloat = 312, + thumbnailImage: UIImage?, + action: CharcoalSnackBarActionContent?) { self.text = text self.maxWidth = maxWidth self.thumbnailImage = thumbnailImage + self.action = action } - + var body: some View { - ZStack(alignment: .bottom) { + ZStack(alignment: .center) { HStack(spacing: 0) { if let thumbnailImage = thumbnailImage { Image(uiImage: thumbnailImage) @@ -39,11 +44,11 @@ struct CharcoalSnackBar: CharcoalPopupView { Text(text) .charcoalTypography14Bold(isSingleLine: true) .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text1.color)) - Button { - - } label: { - Text("hello") - }.charcoalDefaultButton(size: .small) + + if let action = action { + action + .charcoalDefaultButton(size: .small) + } } .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) } @@ -51,35 +56,47 @@ struct CharcoalSnackBar: CharcoalPopupView { .clipped() .background( RoundedRectangle(cornerRadius: cornerRadius) - .stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1) + .stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1) ) - - }.frame(minWidth: 0, maxWidth: maxWidth, alignment: .leading) + + }.frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } - + static func == (lhs: CharcoalSnackBar, rhs: CharcoalSnackBar) -> Bool { return lhs.text == rhs.text && lhs.maxWidth == rhs.maxWidth && lhs.tooltipSize == rhs.tooltipSize } } -struct CharcoalSnackBarModifier: ViewModifier { +struct CharcoalSnackBarModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool - - /// Text to be displayed in the tooltip - var text: String + /// Text to be displayed in the snackbar + let text: String + + /// The thumbnail image to be displayed in the snackbar let thumbnailImage: UIImage? - + + /// The action to be displayed in the snackbar + let action: CharcoalSnackBarActionContent? + /// Assign a unique ID to the view @State var viewID = UUID() - + func body(content: Content) -> some View { content .overlay(GeometryReader(content: { proxy in Color.clear - .modifier(CharcoalOverlayContainerChild(isPresenting: $isPresenting, view: CharcoalSnackBar(text: text, thumbnailImage: thumbnailImage), viewID: viewID)) + .modifier( + CharcoalOverlayContainerChild( + isPresenting: $isPresenting, + view: CharcoalSnackBar( + text: text, + thumbnailImage: thumbnailImage, + action: action + ), + viewID: viewID)) })) } } @@ -87,51 +104,61 @@ struct CharcoalSnackBarModifier: ViewModifier { public extension View { /** Add a tooltip to the view - + - Parameters: - isPresenting: A binding to whether the Tooltip is presented. - - text: The text to be displayed in the tooltip. - + - text: The text to be displayed in the snackbar. + - thumbnailImage: The thumbnail image to be displayed in the snackbar. + - action: The action to be displayed in the snackbar. + # Example # ```swift - Text("Hello").charcoalTooltip(isPresenting: $isPresenting, text: "This is a tooltip") + Text("Hello").charcoalSnackBar(isPresenting: $isPresenting, text: "Hello") ``` */ func charcoalSnackBar( isPresenting: Binding, text: String, - thumbnailImage: UIImage? = nil + thumbnailImage: UIImage? = nil, + action: @escaping () -> some View = {EmptyView()} ) -> some View { - return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, text: text, thumbnailImage: thumbnailImage)) + return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, text: text, thumbnailImage: thumbnailImage, action: action())) + } +} + +private extension UIColor { + func imageWithColor(width: Int, height: Int) -> UIImage { + let size = CGSize(width: width, height: height) + return UIGraphicsImageRenderer(size: size).image { rendererContext in + self.setFill() + rendererContext.fill(CGRect(origin: .zero, size: size)) + } } } private struct SnackBarsPreviewView: View { @State var isPresenting = true @State var isPresenting6 = true - + @State var textOfLabel = "Hello" - + var body: some View { GeometryReader(content: { geometry in ScrollView { ZStack(alignment: .topLeading) { Color.clear - - VStack { - Text(textOfLabel) - - Button { - textOfLabel = ["Changed", "Hello"].randomElement()! - } label: { - Text("Change Label") - } - } } } - .charcoalSnackBar(isPresenting: $isPresenting6, - text: "Hello World This is a tooltip and here is testing it's multiple line feature", - thumbnailImage: CharcoalAsset.Images.addPeople24.image + .charcoalSnackBar( + isPresenting: $isPresenting6, + text: "ブックマークしました", + thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), + action: { + Button { + print("Tapped") + } label: { + Text("編集") + }} ) }) .charcoalOverlayContainer() From 7aa797f15cc5cedb639733adc86ec3618e2895ca Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 14:27:04 +0900 Subject: [PATCH 038/199] Clean code --- .../Components/Toast/CharcoalSnackBar.swift | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index d86e84d43..44def9a6d 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -14,7 +14,7 @@ struct CharcoalSnackBar: CharcoalPopupView let cornerRadius: CGFloat = 32 /// The spacing between the snackbar and the screen edge - let spacingToScreen: CGFloat = 16 + let bottomSpacing: CGFloat /// The content of the action view let action: CharcoalSnackBarActionContent? @@ -23,16 +23,19 @@ struct CharcoalSnackBar: CharcoalPopupView init(text: String, maxWidth: CGFloat = 312, + bottomSpacing: CGFloat, thumbnailImage: UIImage?, action: CharcoalSnackBarActionContent?) { self.text = text self.maxWidth = maxWidth self.thumbnailImage = thumbnailImage self.action = action + self.bottomSpacing = bottomSpacing } var body: some View { - ZStack(alignment: .center) { + ZStack(alignment: .bottom) { + Color.clear HStack(spacing: 0) { if let thumbnailImage = thumbnailImage { Image(uiImage: thumbnailImage) @@ -58,6 +61,7 @@ struct CharcoalSnackBar: CharcoalPopupView RoundedRectangle(cornerRadius: cornerRadius) .stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1) ) + .offset(CGSize(width: 0, height: -bottomSpacing)) }.frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } @@ -72,6 +76,9 @@ struct CharcoalSnackBarModifier: ViewModifi /// Presentation `Binding` @Binding var isPresenting: Bool + /// The spacing between the snackbar and the screen edge + let bottomSpacing: CGFloat + /// Text to be displayed in the snackbar let text: String @@ -93,6 +100,7 @@ struct CharcoalSnackBarModifier: ViewModifi isPresenting: $isPresenting, view: CharcoalSnackBar( text: text, + bottomSpacing: bottomSpacing, thumbnailImage: thumbnailImage, action: action ), @@ -118,11 +126,12 @@ public extension View { */ func charcoalSnackBar( isPresenting: Binding, + bottomSpacing: CGFloat = 96, text: String, thumbnailImage: UIImage? = nil, action: @escaping () -> some View = {EmptyView()} ) -> some View { - return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, text: text, thumbnailImage: thumbnailImage, action: action())) + return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, bottomSpacing: bottomSpacing, text: text, thumbnailImage: thumbnailImage, action: action())) } } @@ -138,19 +147,21 @@ private extension UIColor { private struct SnackBarsPreviewView: View { @State var isPresenting = true - @State var isPresenting6 = true @State var textOfLabel = "Hello" var body: some View { - GeometryReader(content: { geometry in - ScrollView { - ZStack(alignment: .topLeading) { - Color.clear + ZStack { + Color.clear + ZStack() { + Button { + isPresenting.toggle() + } label: { + Text("Toggle SnackBar") } } .charcoalSnackBar( - isPresenting: $isPresenting6, + isPresenting: $isPresenting, text: "ブックマークしました", thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), action: { @@ -160,7 +171,7 @@ private struct SnackBarsPreviewView: View { Text("編集") }} ) - }) + } .charcoalOverlayContainer() } } From 79d6caa305d2869b4c430468eed7253a9c8ca530 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 14:27:59 +0900 Subject: [PATCH 039/199] Reformat code --- .../Components/Toast/CharcoalSnackBar.swift | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 44def9a6d..9c432cb15 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -3,36 +3,38 @@ import SwiftUI struct CharcoalSnackBar: CharcoalPopupView { /// The text of the snackbar let text: String - + /// The thumbnail image of the snackbar let thumbnailImage: UIImage? - + /// The maximum width of the snackbar let maxWidth: CGFloat - + /// The corner radius of the snackbar let cornerRadius: CGFloat = 32 - + /// The spacing between the snackbar and the screen edge let bottomSpacing: CGFloat - + /// The content of the action view let action: CharcoalSnackBarActionContent? - + @State private var tooltipSize: CGSize = .zero - - init(text: String, - maxWidth: CGFloat = 312, - bottomSpacing: CGFloat, - thumbnailImage: UIImage?, - action: CharcoalSnackBarActionContent?) { + + init( + text: String, + maxWidth: CGFloat = 312, + bottomSpacing: CGFloat, + thumbnailImage: UIImage?, + action: CharcoalSnackBarActionContent? + ) { self.text = text self.maxWidth = maxWidth self.thumbnailImage = thumbnailImage self.action = action self.bottomSpacing = bottomSpacing } - + var body: some View { ZStack(alignment: .bottom) { Color.clear @@ -47,7 +49,7 @@ struct CharcoalSnackBar: CharcoalPopupView Text(text) .charcoalTypography14Bold(isSingleLine: true) .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text1.color)) - + if let action = action { action .charcoalDefaultButton(size: .small) @@ -62,38 +64,37 @@ struct CharcoalSnackBar: CharcoalPopupView .stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1) ) .offset(CGSize(width: 0, height: -bottomSpacing)) - + }.frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } - + static func == (lhs: CharcoalSnackBar, rhs: CharcoalSnackBar) -> Bool { return lhs.text == rhs.text && lhs.maxWidth == rhs.maxWidth && lhs.tooltipSize == rhs.tooltipSize } } - struct CharcoalSnackBarModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool - + /// The spacing between the snackbar and the screen edge let bottomSpacing: CGFloat - + /// Text to be displayed in the snackbar let text: String - + /// The thumbnail image to be displayed in the snackbar let thumbnailImage: UIImage? - + /// The action to be displayed in the snackbar let action: CharcoalSnackBarActionContent? - + /// Assign a unique ID to the view @State var viewID = UUID() - + func body(content: Content) -> some View { content - .overlay(GeometryReader(content: { proxy in + .overlay(GeometryReader(content: { _ in Color.clear .modifier( CharcoalOverlayContainerChild( @@ -104,7 +105,8 @@ struct CharcoalSnackBarModifier: ViewModifi thumbnailImage: thumbnailImage, action: action ), - viewID: viewID)) + viewID: viewID + )) })) } } @@ -112,13 +114,13 @@ struct CharcoalSnackBarModifier: ViewModifi public extension View { /** Add a tooltip to the view - + - Parameters: - isPresenting: A binding to whether the Tooltip is presented. - text: The text to be displayed in the snackbar. - thumbnailImage: The thumbnail image to be displayed in the snackbar. - action: The action to be displayed in the snackbar. - + # Example # ```swift Text("Hello").charcoalSnackBar(isPresenting: $isPresenting, text: "Hello") @@ -129,7 +131,7 @@ public extension View { bottomSpacing: CGFloat = 96, text: String, thumbnailImage: UIImage? = nil, - action: @escaping () -> some View = {EmptyView()} + action: @escaping () -> some View = { EmptyView() } ) -> some View { return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, bottomSpacing: bottomSpacing, text: text, thumbnailImage: thumbnailImage, action: action())) } @@ -147,13 +149,13 @@ private extension UIColor { private struct SnackBarsPreviewView: View { @State var isPresenting = true - + @State var textOfLabel = "Hello" - + var body: some View { ZStack { Color.clear - ZStack() { + ZStack { Button { isPresenting.toggle() } label: { @@ -169,7 +171,8 @@ private struct SnackBarsPreviewView: View { print("Tapped") } label: { Text("編集") - }} + } + } ) } .charcoalOverlayContainer() From ac539d626f1deff654e7c5538538143a3a69ab52 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 14:28:41 +0900 Subject: [PATCH 040/199] Rename ActionContent --- .../Components/Toast/CharcoalSnackBar.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 9c432cb15..abc607f8b 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalSnackBar: CharcoalPopupView { +struct CharcoalSnackBar: CharcoalPopupView { /// The text of the snackbar let text: String @@ -17,7 +17,7 @@ struct CharcoalSnackBar: CharcoalPopupView let bottomSpacing: CGFloat /// The content of the action view - let action: CharcoalSnackBarActionContent? + let action: ActionContent? @State private var tooltipSize: CGSize = .zero @@ -26,7 +26,7 @@ struct CharcoalSnackBar: CharcoalPopupView maxWidth: CGFloat = 312, bottomSpacing: CGFloat, thumbnailImage: UIImage?, - action: CharcoalSnackBarActionContent? + action: ActionContent? ) { self.text = text self.maxWidth = maxWidth @@ -73,7 +73,7 @@ struct CharcoalSnackBar: CharcoalPopupView } } -struct CharcoalSnackBarModifier: ViewModifier { +struct CharcoalSnackBarModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool @@ -87,7 +87,7 @@ struct CharcoalSnackBarModifier: ViewModifi let thumbnailImage: UIImage? /// The action to be displayed in the snackbar - let action: CharcoalSnackBarActionContent? + let action: ActionContent? /// Assign a unique ID to the view @State var viewID = UUID() From c593130474c84edbdf02df6fc951cf00f340f729 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 14:32:18 +0900 Subject: [PATCH 041/199] Replace thumbnailImage type --- .../Components/Toast/CharcoalSnackBar.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index abc607f8b..2d34a92ad 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -5,7 +5,7 @@ struct CharcoalSnackBar: CharcoalPopupView { let text: String /// The thumbnail image of the snackbar - let thumbnailImage: UIImage? + let thumbnailImage: Image? /// The maximum width of the snackbar let maxWidth: CGFloat @@ -25,7 +25,7 @@ struct CharcoalSnackBar: CharcoalPopupView { text: String, maxWidth: CGFloat = 312, bottomSpacing: CGFloat, - thumbnailImage: UIImage?, + thumbnailImage: Image?, action: ActionContent? ) { self.text = text @@ -40,7 +40,7 @@ struct CharcoalSnackBar: CharcoalPopupView { Color.clear HStack(spacing: 0) { if let thumbnailImage = thumbnailImage { - Image(uiImage: thumbnailImage) + thumbnailImage .resizable() .scaledToFill() .frame(width: 64, height: 64) @@ -84,7 +84,7 @@ struct CharcoalSnackBarModifier: ViewModifier { let text: String /// The thumbnail image to be displayed in the snackbar - let thumbnailImage: UIImage? + let thumbnailImage: Image? /// The action to be displayed in the snackbar let action: ActionContent? @@ -130,7 +130,7 @@ public extension View { isPresenting: Binding, bottomSpacing: CGFloat = 96, text: String, - thumbnailImage: UIImage? = nil, + thumbnailImage: Image? = nil, action: @escaping () -> some View = { EmptyView() } ) -> some View { return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, bottomSpacing: bottomSpacing, text: text, thumbnailImage: thumbnailImage, action: action())) @@ -165,7 +165,7 @@ private struct SnackBarsPreviewView: View { .charcoalSnackBar( isPresenting: $isPresenting, text: "ブックマークしました", - thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), + thumbnailImage: Image(uiImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64)), action: { Button { print("Tapped") From 701198da32a5c5c3059c5928dbc5c9a8ba505570 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 15:03:30 +0900 Subject: [PATCH 042/199] Add dismissOnTouchOutside control --- .../CharcoalIdentifiableOverlayView.swift | 7 +++-- .../CharcoalOverlayContainerModifier.swift | 6 +++-- .../Components/Toast/CharcoalSnackBar.swift | 26 ++++++++++++++++++- .../Components/Tooltip/CharcoalTooltip.swift | 7 ++++- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift index fb6c82b77..c11216a4c 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -3,12 +3,13 @@ import SwiftUI struct CharcoalIdentifiableOverlayView: View { typealias IDValue = UUID let id: IDValue - var contentView: AnyView + let contentView: AnyView + let dismissOnTouchOutside: Bool @Binding var isPresenting: Bool var body: some View { ZStack { - if isPresenting { + if dismissOnTouchOutside && isPresenting { Color.clear .contentShape(Rectangle()) .simultaneousGesture( @@ -23,6 +24,8 @@ struct CharcoalIdentifiableOverlayView: View { isPresenting = false } ) + } + if isPresenting { contentView } }.animation(.easeInOut(duration: 0.2), value: isPresenting) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index af234516d..7c9a8bdf6 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -18,13 +18,15 @@ struct CharcoalOverlayContainerChild: ViewModifie @EnvironmentObject var viewManager: CharcoalContainerManager @Binding var isPresenting: Bool + + let dismissOnTouchOutside: 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), dismissOnTouchOutside: dismissOnTouchOutside, isPresenting: $isPresenting) } func body(content: Content) -> some View { diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 2d34a92ad..fe72e8a38 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -69,7 +69,7 @@ struct CharcoalSnackBar: CharcoalPopupView { } static func == (lhs: CharcoalSnackBar, rhs: CharcoalSnackBar) -> Bool { - return lhs.text == rhs.text && lhs.maxWidth == rhs.maxWidth && lhs.tooltipSize == rhs.tooltipSize + return lhs.text == rhs.text && lhs.maxWidth == rhs.maxWidth && lhs.thumbnailImage == rhs.thumbnailImage } } @@ -99,6 +99,7 @@ struct CharcoalSnackBarModifier: ViewModifier { .modifier( CharcoalOverlayContainerChild( isPresenting: $isPresenting, + dismissOnTouchOutside: false, view: CharcoalSnackBar( text: text, bottomSpacing: bottomSpacing, @@ -149,6 +150,10 @@ private extension UIColor { private struct SnackBarsPreviewView: View { @State var isPresenting = true + + @State var isPresenting2 = true + + @State var isPresenting3 = true @State var textOfLabel = "Hello" @@ -158,6 +163,8 @@ private struct SnackBarsPreviewView: View { ZStack { Button { isPresenting.toggle() + isPresenting2.toggle() + isPresenting3.toggle() } label: { Text("Toggle SnackBar") } @@ -174,6 +181,23 @@ private struct SnackBarsPreviewView: View { } } ) + .charcoalSnackBar( + isPresenting: $isPresenting2, + bottomSpacing: 192, + text: "ブックマークしました", + action: { + Button { + print("Tapped") + } label: { + Text("編集") + } + } + ) + .charcoalSnackBar( + isPresenting: $isPresenting3, + bottomSpacing: 275, + text: "ブックマークしました" + ) } .charcoalOverlayContainer() } diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 863e4401f..ef6eecb63 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -121,7 +121,12 @@ struct CharcoalTooltipModifier: ViewModifier { content .overlay(GeometryReader(content: { proxy in Color.clear - .modifier(CharcoalOverlayContainerChild(isPresenting: $isPresenting, view: CharcoalTooltip(text: text, targetFrame: proxy.frame(in: .global)), viewID: viewID)) + .modifier(CharcoalOverlayContainerChild( + isPresenting: $isPresenting, + dismissOnTouchOutside: true, view: CharcoalTooltip( + text: text, + targetFrame: proxy.frame(in: .global)), + viewID: viewID)) })) } } From 6db7a741ebea98db57047fb34e43cdc39b7a95df Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 15:04:22 +0900 Subject: [PATCH 043/199] Add comment on CharcoalIdentifiableOverlayView --- .../Overlay/CharcoalIdentifiableOverlayView.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift index c11216a4c..211086089 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -2,9 +2,17 @@ import SwiftUI struct CharcoalIdentifiableOverlayView: View { typealias IDValue = UUID + + /// The unique ID of the overlay. let id: IDValue + + /// The content to display in the overlay. let contentView: AnyView + + /// If true, the overlay will be dismissed when the user taps outside of the overlay. let dismissOnTouchOutside: Bool + + /// A binding to whether the overlay is presented. @Binding var isPresenting: Bool var body: some View { From 63da0c1515c93348e7c96ed8be19b253e86fa28d Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 15:27:43 +0900 Subject: [PATCH 044/199] Update CharcoalTooltip.swift --- .../CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index ef6eecb63..a875d9834 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -123,8 +123,9 @@ struct CharcoalTooltipModifier: ViewModifier { Color.clear .modifier(CharcoalOverlayContainerChild( isPresenting: $isPresenting, - dismissOnTouchOutside: true, view: CharcoalTooltip( - text: text, + dismissOnTouchOutside: true, + view: CharcoalTooltip( + text: text, targetFrame: proxy.frame(in: .global)), viewID: viewID)) })) From b58fb42241223595772a3d645e222659e53a9f1b Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 16:48:09 +0900 Subject: [PATCH 045/199] Add SnackBar demo --- .../CharcoalSwiftUISample/ContentView.swift | 3 + .../Media.xcassets/Contents.json | 6 ++ .../759298c7073930520f161ef50e70e873.jpeg | Bin 0 -> 100922 bytes .../SnackbarDemo.imageset/Contents.json | 12 +++ .../CharcoalSwiftUISample/ToastsView.swift | 70 ++++++++++++++++++ .../Components/Toast/CharcoalSnackBar.swift | 9 +-- 6 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/Contents.json create mode 100644 CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/759298c7073930520f161ef50e70e873.jpeg create mode 100644 CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/Contents.json create mode 100644 CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift index 73af7d4bc..35de5e231 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift @@ -46,6 +46,9 @@ public struct ContentView: View { NavigationLink(destination: TooltipsView()) { Text("Tooltips") } + NavigationLink(destination: ToastsView()) { + Text("Toasts") + } } .navigationBarTitle("Charcoal") } diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/Contents.json b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/759298c7073930520f161ef50e70e873.jpeg b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/759298c7073930520f161ef50e70e873.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..e107ffecbdbca70a2b10fbf4495e04ce2afc77e8 GIT binary patch literal 100922 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 R2$cOyzuEoEN+xf@iu^+DC6He-VU6MLB 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: CharcoalPopupView { } .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) } - .cornerRadius(cornerRadius, corners: .allCorners) - .clipped() .background( - RoundedRectangle(cornerRadius: cornerRadius) - .stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1) + Color(CharcoalAsset.ColorPaletteGenerated.background1.color) ) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1)) .offset(CGSize(width: 0, height: -bottomSpacing)) }.frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) @@ -132,7 +131,7 @@ public extension View { bottomSpacing: CGFloat = 96, text: String, thumbnailImage: Image? = nil, - action: @escaping () -> some View = { EmptyView() } + @ViewBuilder action: @escaping () -> some View = { EmptyView() } ) -> some View { return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, bottomSpacing: bottomSpacing, text: text, thumbnailImage: thumbnailImage, action: action())) } From 75c92eec154a0ee1df2f0ec338272c6f68cef716 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 17:28:57 +0900 Subject: [PATCH 046/199] Replace thumbnail with charcoal logo --- .../759298c7073930520f161ef50e70e873.jpeg | Bin 100922 -> 0 bytes .../SnackbarDemo.imageset/Contents.json | 2 +- ...03\203\343\203\210 2024-02-21 17.28.10.png" | Bin 0 -> 27570 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/759298c7073930520f161ef50e70e873.jpeg create mode 100644 "CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210 2024-02-21 17.28.10.png" diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/759298c7073930520f161ef50e70e873.jpeg b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/759298c7073930520f161ef50e70e873.jpeg deleted file mode 100644 index e107ffecbdbca70a2b10fbf4495e04ce2afc77e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 100922 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 R2$cOyzuEoEN+xf@iu^+DC6He-VU6MLB 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 A`Tzg` literal 0 HcmV?d00001 From 0bf8e8e1dbe67a09077cf10e812b6d60b6017dd6 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 17:51:44 +0900 Subject: [PATCH 047/199] Use @ViewBuilder --- .../Sources/CharcoalSwiftUISample/ToastsView.swift | 1 + .../Components/Toast/CharcoalSnackBar.swift | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift index 025523418..7e0812da9 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift @@ -2,6 +2,7 @@ import Charcoal import SwiftUI public struct ToastsView: View { + @State var text = "Hello" @State var isPresenting = false @State var isPresenting2 = false @State var isPresenting3 = false diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index d0c49f184..93aefad0f 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -26,12 +26,12 @@ struct CharcoalSnackBar: CharcoalPopupView { maxWidth: CGFloat = 312, bottomSpacing: CGFloat, thumbnailImage: Image?, - action: ActionContent? + @ViewBuilder action: () -> ActionContent? ) { self.text = text self.maxWidth = maxWidth self.thumbnailImage = thumbnailImage - self.action = action + self.action = action() self.bottomSpacing = bottomSpacing } @@ -86,7 +86,7 @@ struct CharcoalSnackBarModifier: ViewModifier { let thumbnailImage: Image? /// The action to be displayed in the snackbar - let action: ActionContent? + @ViewBuilder let action: () -> ActionContent? /// Assign a unique ID to the view @State var viewID = UUID() @@ -133,7 +133,7 @@ public extension View { thumbnailImage: Image? = nil, @ViewBuilder action: @escaping () -> some View = { EmptyView() } ) -> some View { - return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, bottomSpacing: bottomSpacing, text: text, thumbnailImage: thumbnailImage, action: action())) + return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, bottomSpacing: bottomSpacing, text: text, thumbnailImage: thumbnailImage, action: action)) } } From 5e534bb02af05feeb6864017044933bc3f3b3033 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 18:18:51 +0900 Subject: [PATCH 048/199] Clean Code --- .../Components/Toast/CharcoalSnackBar.swift | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 93aefad0f..33aa58c94 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -93,21 +93,19 @@ struct CharcoalSnackBarModifier: ViewModifier { func body(content: Content) -> some View { content - .overlay(GeometryReader(content: { _ in - Color.clear - .modifier( - CharcoalOverlayContainerChild( - isPresenting: $isPresenting, - dismissOnTouchOutside: false, - view: CharcoalSnackBar( - text: text, - bottomSpacing: bottomSpacing, - thumbnailImage: thumbnailImage, - action: action - ), - viewID: viewID - )) - })) + .overlay(Color.clear + .modifier( + CharcoalOverlayContainerChild( + isPresenting: $isPresenting, + dismissOnTouchOutside: false, + view: CharcoalSnackBar( + text: text, + bottomSpacing: bottomSpacing, + thumbnailImage: thumbnailImage, + action: action + ), + viewID: viewID + ))) } } From d9fbca4b47afe04b314b2f78fef8dc9d9ebc7268 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 18:40:45 +0900 Subject: [PATCH 049/199] Made code more readable --- .../CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 33aa58c94..d03bb5d9e 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -124,13 +124,13 @@ public extension View { Text("Hello").charcoalSnackBar(isPresenting: $isPresenting, text: "Hello") ``` */ - func charcoalSnackBar( + func charcoalSnackBar( isPresenting: Binding, bottomSpacing: CGFloat = 96, text: String, thumbnailImage: Image? = nil, - @ViewBuilder action: @escaping () -> some View = { EmptyView() } - ) -> some View { + @ViewBuilder action: @escaping () -> Content = { EmptyView() } + ) -> some View where Content: View { return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, bottomSpacing: bottomSpacing, text: text, thumbnailImage: thumbnailImage, action: action)) } } From bd664eb58978c3db963e8a88d20c624d6f3cf712 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 21 Feb 2024 18:44:49 +0900 Subject: [PATCH 050/199] Update ToastsView.swift --- .../Sources/CharcoalSwiftUISample/ToastsView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift index 7e0812da9..025523418 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift @@ -2,7 +2,6 @@ import Charcoal import SwiftUI public struct ToastsView: View { - @State var text = "Hello" @State var isPresenting = false @State var isPresenting2 = false @State var isPresenting3 = false From 0ead7e91618afbff343d6e4589f1d2365f6e309c Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 14:28:45 +0900 Subject: [PATCH 051/199] Add auto dismiss logic --- .../CharcoalIdentifiableOverlayView.swift | 12 +++++++++++- .../CharcoalOverlayContainerModifier.swift | 5 ++++- .../Components/Toast/CharcoalSnackBar.swift | 10 ++++++++-- .../Components/Tooltip/CharcoalTooltip.swift | 18 +++++++++++++----- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 211086089..ef44478f3 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -14,6 +14,9 @@ struct CharcoalIdentifiableOverlayView: View { /// A binding to whether the overlay is presented. @Binding var isPresenting: Bool + + /// If true, the overlay will be dismissed when the user taps outside of the overlay. + let dismissAfter: TimeInterval? var body: some View { ZStack { @@ -34,8 +37,15 @@ struct CharcoalIdentifiableOverlayView: View { ) } if isPresenting { - contentView + contentView.onAppear { + if let dismissAfter = dismissAfter { + DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { + isPresenting = false + } + } + } } }.animation(.easeInOut(duration: 0.2), value: isPresenting) + } } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 7c9a8bdf6..74f49863b 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -24,9 +24,12 @@ struct CharcoalOverlayContainerChild: ViewModifie let view: SubContent let viewID: UUID + + /// If true, the overlay will be dismissed when the user taps outside of the overlay. + let dismissAfter: TimeInterval? func createOverlayView(view: SubContent) -> CharcoalIdentifiableOverlayView { - return CharcoalIdentifiableOverlayView(id: viewID, contentView: AnyView(view), dismissOnTouchOutside: dismissOnTouchOutside, isPresenting: $isPresenting) + return CharcoalIdentifiableOverlayView(id: viewID, contentView: AnyView(view), dismissOnTouchOutside: dismissOnTouchOutside, isPresenting: $isPresenting, dismissAfter: dismissAfter) } func body(content: Content) -> some View { diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index d03bb5d9e..012c1b502 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -90,6 +90,9 @@ struct CharcoalSnackBarModifier: ViewModifier { /// Assign a unique ID to the view @State var viewID = UUID() + + /// If true, the overlay will be dismissed when the user taps outside of the overlay. + let dismissAfter: TimeInterval? func body(content: Content) -> some View { content @@ -104,7 +107,8 @@ struct CharcoalSnackBarModifier: ViewModifier { thumbnailImage: thumbnailImage, action: action ), - viewID: viewID + viewID: viewID, + dismissAfter: dismissAfter ))) } } @@ -129,9 +133,10 @@ public extension View { bottomSpacing: CGFloat = 96, text: String, thumbnailImage: Image? = nil, + dismissAfter: TimeInterval? = nil, @ViewBuilder action: @escaping () -> Content = { EmptyView() } ) -> some View where Content: View { - return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, bottomSpacing: bottomSpacing, text: text, thumbnailImage: thumbnailImage, action: action)) + return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, bottomSpacing: bottomSpacing, text: text, thumbnailImage: thumbnailImage, action: action, dismissAfter: dismissAfter)) } } @@ -182,6 +187,7 @@ private struct SnackBarsPreviewView: View { isPresenting: $isPresenting2, bottomSpacing: 192, text: "ブックマークしました", + dismissAfter: 2, action: { Button { print("Tapped") diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index a875d9834..b540d95db 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -116,6 +116,9 @@ struct CharcoalTooltipModifier: ViewModifier { /// Assign a unique ID to the view @State var viewID = UUID() + + /// If true, the overlay will be dismissed when the user taps outside of the overlay. + let dismissAfter: TimeInterval? func body(content: Content) -> some View { content @@ -123,11 +126,12 @@ struct CharcoalTooltipModifier: ViewModifier { Color.clear .modifier(CharcoalOverlayContainerChild( isPresenting: $isPresenting, - dismissOnTouchOutside: true, + dismissOnTouchOutside: true, view: CharcoalTooltip( text: text, targetFrame: proxy.frame(in: .global)), - viewID: viewID)) + viewID: viewID, + dismissAfter: dismissAfter)) })) } } @@ -147,9 +151,10 @@ public extension View { */ func charcoalTooltip( isPresenting: Binding, - text: String + text: String, + dismissAfter: TimeInterval? = nil ) -> some View { - return modifier(CharcoalTooltipModifier(isPresenting: isPresenting, text: text)) + return modifier(CharcoalTooltipModifier(isPresenting: isPresenting, text: text, dismissAfter: dismissAfter)) } } @@ -219,7 +224,10 @@ private struct TooltipsPreviewView: View { Text("Bottom") } .charcoalPrimaryButton(size: .medium) - .charcoalTooltip(isPresenting: $isPresenting5, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + .charcoalTooltip( + isPresenting: $isPresenting5, + text: "Hello World This is a tooltip and here is testing it's multiple line feature", + dismissAfter: 3) .offset(CGSize(width: geometry.size.width - 240, height: geometry.size.height - 40)) Button { From e1ee7c239bd14514f6ae973deae75bb44c3e85e6 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 14:31:27 +0900 Subject: [PATCH 052/199] Fix dismiss comment --- .../Components/Overlay/CharcoalIdentifiableOverlayView.swift | 2 +- .../Components/Overlay/CharcoalOverlayContainerModifier.swift | 2 +- Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift | 2 +- .../CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift index ef44478f3..7795a7574 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -15,7 +15,7 @@ struct CharcoalIdentifiableOverlayView: View { /// A binding to whether the overlay is presented. @Binding var isPresenting: Bool - /// If true, the overlay will be dismissed when the user taps outside of the overlay. + /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? var body: some View { diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 74f49863b..ae3794572 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -25,7 +25,7 @@ struct CharcoalOverlayContainerChild: ViewModifie let viewID: UUID - /// If true, the overlay will be dismissed when the user taps outside of the overlay. + /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? func createOverlayView(view: SubContent) -> CharcoalIdentifiableOverlayView { diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 012c1b502..eec40d4f0 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -91,7 +91,7 @@ struct CharcoalSnackBarModifier: ViewModifier { /// Assign a unique ID to the view @State var viewID = UUID() - /// If true, the overlay will be dismissed when the user taps outside of the overlay. + /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? func body(content: Content) -> some View { diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index b540d95db..df5c676ee 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -117,7 +117,7 @@ struct CharcoalTooltipModifier: ViewModifier { /// Assign a unique ID to the view @State var viewID = UUID() - /// If true, the overlay will be dismissed when the user taps outside of the overlay. + /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? func body(content: Content) -> some View { From 388452de355bdc2c4fe44caba19732168847831c Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 14:38:35 +0900 Subject: [PATCH 053/199] Add Identifiable to CharcoalIdentifiableOverlayView --- .../Components/Overlay/CharcoalIdentifiableOverlayView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 7795a7574..a1ca51e42 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalIdentifiableOverlayView: View { +struct CharcoalIdentifiableOverlayView: View, Identifiable { typealias IDValue = UUID /// The unique ID of the overlay. From 7a84acf1785f977bb611300522e54950bdc36109 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 14:51:22 +0900 Subject: [PATCH 054/199] Make all CharcoalPopupView identifiable --- .../CharcoalIdentifiableOverlayView.swift | 11 ++++++----- .../CharcoalOverlayContainerModifier.swift | 2 +- .../Components/Toast/CharcoalSnackBar.swift | 7 +++++++ .../Components/Tooltip/CharcoalTooltip.swift | 17 +++++++++++++---- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift index a1ca51e42..841651a44 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -37,13 +37,14 @@ struct CharcoalIdentifiableOverlayView: View, Identifiable { ) } if isPresenting { - contentView.onAppear { - if let dismissAfter = dismissAfter { - DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { - isPresenting = false + contentView + .onAppear { + if let dismissAfter = dismissAfter { + DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { + isPresenting = false + } } } - } } }.animation(.easeInOut(duration: 0.2), value: isPresenting) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index ae3794572..415661cf1 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -12,7 +12,7 @@ struct CharcoalOverlayContainerModifier: ViewModifier { } } -typealias CharcoalPopupView = Equatable & View +typealias CharcoalPopupView = Equatable & View & Identifiable struct CharcoalOverlayContainerChild: ViewModifier { @EnvironmentObject var viewManager: CharcoalContainerManager diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index eec40d4f0..cdf09fb13 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -1,6 +1,10 @@ import SwiftUI struct CharcoalSnackBar: CharcoalPopupView { + typealias IDValue = UUID + + /// The unique ID of the overlay. + let id: IDValue /// The text of the snackbar let text: String @@ -22,12 +26,14 @@ struct CharcoalSnackBar: CharcoalPopupView { @State private var tooltipSize: CGSize = .zero init( + id: IDValue, text: String, maxWidth: CGFloat = 312, bottomSpacing: CGFloat, thumbnailImage: Image?, @ViewBuilder action: () -> ActionContent? ) { + self.id = id self.text = text self.maxWidth = maxWidth self.thumbnailImage = thumbnailImage @@ -102,6 +108,7 @@ struct CharcoalSnackBarModifier: ViewModifier { isPresenting: $isPresenting, dismissOnTouchOutside: false, view: CharcoalSnackBar( + id: viewID, text: text, bottomSpacing: bottomSpacing, thumbnailImage: thumbnailImage, diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index df5c676ee..af1c60f0c 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -1,6 +1,10 @@ import SwiftUI struct CharcoalTooltip: CharcoalPopupView { + typealias IDValue = UUID + + /// The unique ID of the overlay. + let id: IDValue /// The text of the tooltip let text: String @@ -28,10 +32,14 @@ struct CharcoalTooltip: CharcoalPopupView { CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY) } - init(text: String, targetFrame: CGRect, maxWidth: CGFloat = 184) { - self.text = text - self.targetFrame = targetFrame - self.maxWidth = maxWidth + init(id: IDValue, + text: String, + targetFrame: CGRect, + maxWidth: CGFloat = 184) { + self.id = id + self.text = text + self.targetFrame = targetFrame + self.maxWidth = maxWidth } func tooltipX(canvasGeometrySize: CGSize) -> CGFloat { @@ -128,6 +136,7 @@ struct CharcoalTooltipModifier: ViewModifier { isPresenting: $isPresenting, dismissOnTouchOutside: true, view: CharcoalTooltip( + id: viewID, text: text, targetFrame: proxy.frame(in: .global)), viewID: viewID, From e9744a8dd68a5ac604eb41ff837e3ff3f24831f6 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 15:14:01 +0900 Subject: [PATCH 055/199] Move all control logic into CharcoalPopupView --- .../CharcoalIdentifiableOverlayView.swift | 39 +----- .../CharcoalOverlayContainerModifier.swift | 7 +- .../Components/Toast/CharcoalSnackBar.swift | 102 ++++++++++---- .../Components/Tooltip/CharcoalTooltip.swift | 130 ++++++++++++------ .../Extensions/ConditionalViewModifier.swift | 16 +++ 5 files changed, 178 insertions(+), 116 deletions(-) create mode 100644 Sources/CharcoalSwiftUI/Extensions/ConditionalViewModifier.swift diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 841651a44..f14abb480 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -9,44 +9,7 @@ struct CharcoalIdentifiableOverlayView: View, Identifiable { /// The content to display in the overlay. let contentView: AnyView - /// If true, the overlay will be dismissed when the user taps outside of the overlay. - let dismissOnTouchOutside: Bool - - /// A binding to whether the overlay is presented. - @Binding var isPresenting: Bool - - /// The overlay will be dismissed after a certain time interval. - let dismissAfter: TimeInterval? - var body: some View { - ZStack { - if dismissOnTouchOutside && isPresenting { - Color.clear - .contentShape(Rectangle()) - .simultaneousGesture( - TapGesture() - .onEnded { _ in - isPresenting = false - } - ) - .simultaneousGesture( - DragGesture() - .onChanged { _ in - isPresenting = false - } - ) - } - if isPresenting { - contentView - .onAppear { - if let dismissAfter = dismissAfter { - DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { - isPresenting = false - } - } - } - } - }.animation(.easeInOut(duration: 0.2), value: isPresenting) - + contentView } } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 415661cf1..2667aa442 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -18,18 +18,13 @@ struct CharcoalOverlayContainerChild: ViewModifie @EnvironmentObject var viewManager: CharcoalContainerManager @Binding var isPresenting: Bool - - let dismissOnTouchOutside: Bool let view: SubContent let viewID: UUID - - /// The overlay will be dismissed after a certain time interval. - let dismissAfter: TimeInterval? func createOverlayView(view: SubContent) -> CharcoalIdentifiableOverlayView { - return CharcoalIdentifiableOverlayView(id: viewID, contentView: AnyView(view), dismissOnTouchOutside: dismissOnTouchOutside, isPresenting: $isPresenting, dismissAfter: dismissAfter) + return CharcoalIdentifiableOverlayView(id: viewID, contentView: AnyView(view)) } func body(content: Content) -> some View { diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index cdf09fb13..2bd70322f 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -24,6 +24,15 @@ struct CharcoalSnackBar: CharcoalPopupView { let action: ActionContent? @State private var tooltipSize: CGSize = .zero + + /// A binding to whether the overlay is presented. + @Binding var isPresenting: Bool + + /// If true, the overlay will be dismissed when the user taps outside of the overlay. + let dismissOnTouchOutside: Bool + + /// The overlay will be dismissed after a certain time interval. + let dismissAfter: TimeInterval? init( id: IDValue, @@ -31,7 +40,10 @@ struct CharcoalSnackBar: CharcoalPopupView { maxWidth: CGFloat = 312, bottomSpacing: CGFloat, thumbnailImage: Image?, - @ViewBuilder action: () -> ActionContent? + @ViewBuilder action: () -> ActionContent?, + isPresenting: Binding, + dismissOnTouchOutside: Bool = true, + dismissAfter: TimeInterval? = nil ) { self.id = id self.text = text @@ -39,42 +51,71 @@ struct CharcoalSnackBar: CharcoalPopupView { self.thumbnailImage = thumbnailImage self.action = action() self.bottomSpacing = bottomSpacing + _isPresenting = isPresenting + self.dismissOnTouchOutside = dismissOnTouchOutside + self.dismissAfter = dismissAfter } var body: some View { ZStack(alignment: .bottom) { Color.clear - HStack(spacing: 0) { - if let thumbnailImage = thumbnailImage { - thumbnailImage - .resizable() - .scaledToFill() - .frame(width: 64, height: 64) + .if(dismissOnTouchOutside && isPresenting) { view in + view.contentShape(Rectangle()) + .simultaneousGesture( + TapGesture() + .onEnded { _ in + isPresenting = false + } + ) + .simultaneousGesture( + DragGesture() + .onChanged { _ in + isPresenting = false + } + ) + } + if isPresenting { + HStack(spacing: 0) { + if let thumbnailImage = thumbnailImage { + thumbnailImage + .resizable() + .scaledToFill() + .frame(width: 64, height: 64) + } + HStack(spacing: 16) { + Text(text) + .charcoalTypography14Bold(isSingleLine: true) + .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text1.color)) + + if let action = action { + action + .charcoalDefaultButton(size: .small) + } + } + .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) } - HStack(spacing: 16) { - Text(text) - .charcoalTypography14Bold(isSingleLine: true) - .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text1.color)) - - if let action = action { - action - .charcoalDefaultButton(size: .small) + .background( + Color(CharcoalAsset.ColorPaletteGenerated.background1.color) + ) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1)) + .offset(CGSize(width: 0, height: -bottomSpacing)) + .onAppear { + if let dismissAfter = dismissAfter { + DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { + isPresenting = false + } } } - .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) - } - .background( - Color(CharcoalAsset.ColorPaletteGenerated.background1.color) - ) - .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) - .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1)) - .offset(CGSize(width: 0, height: -bottomSpacing)) - }.frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) + } + } + .animation(.easeInOut(duration: 0.2), value: isPresenting) + .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } static func == (lhs: CharcoalSnackBar, rhs: CharcoalSnackBar) -> Bool { - return lhs.text == rhs.text && lhs.maxWidth == rhs.maxWidth && lhs.thumbnailImage == rhs.thumbnailImage + return lhs.text == rhs.text && lhs.maxWidth == rhs.maxWidth && lhs.thumbnailImage == rhs.thumbnailImage && lhs.isPresenting == rhs.isPresenting } } @@ -106,16 +147,18 @@ struct CharcoalSnackBarModifier: ViewModifier { .modifier( CharcoalOverlayContainerChild( isPresenting: $isPresenting, - dismissOnTouchOutside: false, + view: CharcoalSnackBar( id: viewID, text: text, bottomSpacing: bottomSpacing, thumbnailImage: thumbnailImage, - action: action + action: action, + isPresenting: $isPresenting, + dismissOnTouchOutside: false, + dismissAfter: dismissAfter ), - viewID: viewID, - dismissAfter: dismissAfter + viewID: viewID ))) } } @@ -172,7 +215,6 @@ private struct SnackBarsPreviewView: View { ZStack { Button { isPresenting.toggle() - isPresenting2.toggle() isPresenting3.toggle() } label: { Text("Toggle SnackBar") diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index af1c60f0c..731687c4c 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -27,6 +27,15 @@ struct CharcoalTooltip: CharcoalPopupView { let spacingToScreen: CGFloat = 16 @State private var tooltipSize: CGSize = .zero + + /// A binding to whether the overlay is presented. + @Binding var isPresenting: Bool + + /// If true, the overlay will be dismissed when the user taps outside of the overlay. + let dismissOnTouchOutside: Bool + + /// The overlay will be dismissed after a certain time interval. + let dismissAfter: TimeInterval? var offset: CGSize { CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY) @@ -35,11 +44,18 @@ struct CharcoalTooltip: CharcoalPopupView { init(id: IDValue, text: String, targetFrame: CGRect, - maxWidth: CGFloat = 184) { - self.id = id - self.text = text - self.targetFrame = targetFrame - self.maxWidth = maxWidth + maxWidth: CGFloat = 184, + isPresenting: Binding, + dismissOnTouchOutside: Bool = true, + dismissAfter: TimeInterval? = nil + ) { + self.id = id + self.text = text + self.targetFrame = targetFrame + self.maxWidth = maxWidth + _isPresenting = isPresenting + self.dismissOnTouchOutside = dismissOnTouchOutside + self.dismissAfter = dismissAfter } func tooltipX(canvasGeometrySize: CGSize) -> CGFloat { @@ -67,39 +83,68 @@ struct CharcoalTooltip: CharcoalPopupView { } var body: some View { - GeometryReader(content: { canvasGeometry in - VStack { - Text(text) - .charcoalTypography12Regular() - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) - .padding(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12)) - .background(GeometryReader(content: { tooltipGeometry in - let tooltipOrigin = tooltipGeometry.frame(in: .global).origin - TooltipBubbleShape( - targetPoint: - CGPoint( - x: targetFrame.midX - tooltipOrigin.x, - y: targetFrame.maxY - tooltipOrigin.y - ), - arrowHeight: arrowHeight, - cornerRadius: cornerRadius + ZStack { + Color.clear + .if(dismissOnTouchOutside && isPresenting) { view in + view.contentShape(Rectangle()) + .simultaneousGesture( + TapGesture() + .onEnded { _ in + isPresenting = false + } ) - .fill(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) - .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) - })) - .offset(CGSize( - width: tooltipX(canvasGeometrySize: canvasGeometry.size), - height: tooltipY(canvasGeometrySize: canvasGeometry.size) - )) - .onPreferenceChange(TooltipSizeKey.self, perform: { value in - tooltipSize = value - }) - .animation(.none, value: tooltipSize) - .animation(.none, value: targetFrame) - }.frame(minWidth: 0, maxWidth: maxWidth, alignment: .leading) - }) + .simultaneousGesture( + DragGesture() + .onChanged { _ in + isPresenting = false + } + ) + } + if isPresenting { + GeometryReader(content: { canvasGeometry in + VStack { + Text(text) + .charcoalTypography12Regular() + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) + .padding(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12)) + .background(GeometryReader(content: { tooltipGeometry in + let tooltipOrigin = tooltipGeometry.frame(in: .global).origin + TooltipBubbleShape( + targetPoint: + CGPoint( + x: targetFrame.midX - tooltipOrigin.x, + y: targetFrame.maxY - tooltipOrigin.y + ), + arrowHeight: arrowHeight, + cornerRadius: cornerRadius + ) + .fill(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) + .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) + })) + .offset(CGSize( + width: tooltipX(canvasGeometrySize: canvasGeometry.size), + height: tooltipY(canvasGeometrySize: canvasGeometry.size) + )) + .onPreferenceChange(TooltipSizeKey.self, perform: { value in + tooltipSize = value + }) + .animation(.none, value: tooltipSize) + .animation(.none, value: targetFrame) + } + .frame(minWidth: 0, maxWidth: maxWidth, alignment: .leading) + }) + .onAppear { + if let dismissAfter = dismissAfter { + DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { + isPresenting = false + } + } + } + } + } + .animation(.easeInOut(duration: 0.2), value: isPresenting) } static func == (lhs: CharcoalTooltip, rhs: CharcoalTooltip) -> Bool { @@ -134,13 +179,14 @@ struct CharcoalTooltipModifier: ViewModifier { Color.clear .modifier(CharcoalOverlayContainerChild( isPresenting: $isPresenting, - dismissOnTouchOutside: true, view: CharcoalTooltip( id: viewID, text: text, - targetFrame: proxy.frame(in: .global)), - viewID: viewID, - dismissAfter: dismissAfter)) + targetFrame: proxy.frame(in: .global), + isPresenting: $isPresenting, + dismissAfter: dismissAfter + ), + viewID: viewID)) })) } } @@ -236,7 +282,7 @@ private struct TooltipsPreviewView: View { .charcoalTooltip( isPresenting: $isPresenting5, text: "Hello World This is a tooltip and here is testing it's multiple line feature", - dismissAfter: 3) + dismissAfter: 2) .offset(CGSize(width: geometry.size.width - 240, height: geometry.size.height - 40)) Button { diff --git a/Sources/CharcoalSwiftUI/Extensions/ConditionalViewModifier.swift b/Sources/CharcoalSwiftUI/Extensions/ConditionalViewModifier.swift new file mode 100644 index 000000000..f6b2eb5c4 --- /dev/null +++ b/Sources/CharcoalSwiftUI/Extensions/ConditionalViewModifier.swift @@ -0,0 +1,16 @@ +import SwiftUI + +extension View { + /// Applies the given transform if the given condition evaluates to `true`. + /// - Parameters: + /// - condition: The condition to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. + @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } +} From 0b09383c855e5aba8bdfb7b5ff5412bed44a1d3e Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 15:14:18 +0900 Subject: [PATCH 056/199] Reformat --- .../CharcoalSwiftUISample/ToastsView.swift | 4 +-- .../CharcoalIdentifiableOverlayView.swift | 6 ++-- .../CharcoalOverlayContainerModifier.swift | 2 +- .../Components/Toast/CharcoalSnackBar.swift | 19 +++++------ .../Components/Tooltip/CharcoalTooltip.swift | 33 ++++++++++--------- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift index 025523418..feace6c95 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift @@ -18,7 +18,7 @@ public struct ToastsView: View { isPresenting: $isPresenting, text: "ブックマークしました" ) - + VStack(alignment: .leading) { Button { isPresenting2.toggle() @@ -38,7 +38,7 @@ public struct ToastsView: View { } } ) - + VStack(alignment: .leading) { Button { isPresenting3.toggle() diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift index f14abb480..7c71af704 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -2,13 +2,13 @@ import SwiftUI struct CharcoalIdentifiableOverlayView: View, Identifiable { typealias IDValue = UUID - + /// The unique ID of the overlay. let id: IDValue - + /// The content to display in the overlay. let contentView: AnyView - + var body: some View { contentView } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 2667aa442..3dbfd7f63 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -12,7 +12,7 @@ struct CharcoalOverlayContainerModifier: ViewModifier { } } -typealias CharcoalPopupView = Equatable & View & Identifiable +typealias CharcoalPopupView = Equatable & Identifiable & View struct CharcoalOverlayContainerChild: ViewModifier { @EnvironmentObject var viewManager: CharcoalContainerManager diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 2bd70322f..5b1d3675b 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -24,13 +24,13 @@ struct CharcoalSnackBar: CharcoalPopupView { let action: ActionContent? @State private var tooltipSize: CGSize = .zero - + /// A binding to whether the overlay is presented. @Binding var isPresenting: Bool - + /// If true, the overlay will be dismissed when the user taps outside of the overlay. let dismissOnTouchOutside: Bool - + /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? @@ -40,7 +40,7 @@ struct CharcoalSnackBar: CharcoalPopupView { maxWidth: CGFloat = 312, bottomSpacing: CGFloat, thumbnailImage: Image?, - @ViewBuilder action: () -> ActionContent?, + @ViewBuilder action: () -> ActionContent?, isPresenting: Binding, dismissOnTouchOutside: Bool = true, dismissAfter: TimeInterval? = nil @@ -60,7 +60,7 @@ struct CharcoalSnackBar: CharcoalPopupView { ZStack(alignment: .bottom) { Color.clear .if(dismissOnTouchOutside && isPresenting) { view in - view.contentShape(Rectangle()) + view.contentShape(Rectangle()) .simultaneousGesture( TapGesture() .onEnded { _ in @@ -107,7 +107,6 @@ struct CharcoalSnackBar: CharcoalPopupView { } } } - } } .animation(.easeInOut(duration: 0.2), value: isPresenting) @@ -133,11 +132,11 @@ struct CharcoalSnackBarModifier: ViewModifier { let thumbnailImage: Image? /// The action to be displayed in the snackbar - @ViewBuilder let action: () -> ActionContent? + @ViewBuilder let action: () -> ActionContent? /// Assign a unique ID to the view @State var viewID = UUID() - + /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? @@ -202,9 +201,9 @@ private extension UIColor { private struct SnackBarsPreviewView: View { @State var isPresenting = true - + @State var isPresenting2 = true - + @State var isPresenting3 = true @State var textOfLabel = "Hello" diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 731687c4c..c214e6021 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -2,7 +2,7 @@ import SwiftUI struct CharcoalTooltip: CharcoalPopupView { typealias IDValue = UUID - + /// The unique ID of the overlay. let id: IDValue /// The text of the tooltip @@ -27,13 +27,13 @@ struct CharcoalTooltip: CharcoalPopupView { let spacingToScreen: CGFloat = 16 @State private var tooltipSize: CGSize = .zero - + /// A binding to whether the overlay is presented. @Binding var isPresenting: Bool - + /// If true, the overlay will be dismissed when the user taps outside of the overlay. let dismissOnTouchOutside: Bool - + /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? @@ -41,13 +41,14 @@ struct CharcoalTooltip: CharcoalPopupView { CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY) } - init(id: IDValue, - text: String, - targetFrame: CGRect, - maxWidth: CGFloat = 184, - isPresenting: Binding, - dismissOnTouchOutside: Bool = true, - dismissAfter: TimeInterval? = nil + init( + id: IDValue, + text: String, + targetFrame: CGRect, + maxWidth: CGFloat = 184, + isPresenting: Binding, + dismissOnTouchOutside: Bool = true, + dismissAfter: TimeInterval? = nil ) { self.id = id self.text = text @@ -86,7 +87,7 @@ struct CharcoalTooltip: CharcoalPopupView { ZStack { Color.clear .if(dismissOnTouchOutside && isPresenting) { view in - view.contentShape(Rectangle()) + view.contentShape(Rectangle()) .simultaneousGesture( TapGesture() .onEnded { _ in @@ -169,7 +170,7 @@ struct CharcoalTooltipModifier: ViewModifier { /// Assign a unique ID to the view @State var viewID = UUID() - + /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? @@ -186,7 +187,8 @@ struct CharcoalTooltipModifier: ViewModifier { isPresenting: $isPresenting, dismissAfter: dismissAfter ), - viewID: viewID)) + viewID: viewID + )) })) } } @@ -282,7 +284,8 @@ private struct TooltipsPreviewView: View { .charcoalTooltip( isPresenting: $isPresenting5, text: "Hello World This is a tooltip and here is testing it's multiple line feature", - dismissAfter: 2) + dismissAfter: 2 + ) .offset(CGSize(width: geometry.size.width - 240, height: geometry.size.height - 40)) Button { From 92bcc5a13a1f08d3a289eb5019b420e5dc7af872 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 15:24:36 +0900 Subject: [PATCH 057/199] Refine CharcoalOverlayContainerChild logic of updating view --- .../Overlay/CharcoalOverlayContainerModifier.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 3dbfd7f63..6e30d4afe 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -26,25 +26,26 @@ struct CharcoalOverlayContainerChild: ViewModifie func createOverlayView(view: SubContent) -> CharcoalIdentifiableOverlayView { 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) } } } From 53e02b6b05c9ff12d9602e8492034f0ad6f9aff9 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 16:01:16 +0900 Subject: [PATCH 058/199] Rename to CharcoalOverlayUpdaterContainer --- .../Components/Overlay/CharcoalOverlayContainerModifier.swift | 2 +- Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift | 2 +- .../CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 6e30d4afe..a01ba9e00 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -14,7 +14,7 @@ struct CharcoalOverlayContainerModifier: ViewModifier { typealias CharcoalPopupView = Equatable & Identifiable & View -struct CharcoalOverlayContainerChild: ViewModifier { +struct CharcoalOverlayUpdaterContainer: ViewModifier { @EnvironmentObject var viewManager: CharcoalContainerManager @Binding var isPresenting: Bool diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 5b1d3675b..451293018 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -144,7 +144,7 @@ struct CharcoalSnackBarModifier: ViewModifier { content .overlay(Color.clear .modifier( - CharcoalOverlayContainerChild( + CharcoalOverlayUpdaterContainer( isPresenting: $isPresenting, view: CharcoalSnackBar( diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index c214e6021..891853772 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -178,7 +178,7 @@ struct CharcoalTooltipModifier: ViewModifier { content .overlay(GeometryReader(content: { proxy in Color.clear - .modifier(CharcoalOverlayContainerChild( + .modifier(CharcoalOverlayUpdaterContainer( isPresenting: $isPresenting, view: CharcoalTooltip( id: viewID, From e10a885d28fd419efddd4a47210e67a96cf7415e Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 16:32:10 +0900 Subject: [PATCH 059/199] Add CharcoalToast --- .../Components/Toast/CharcoalToast.swift | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift new file mode 100644 index 000000000..78d5bc461 --- /dev/null +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -0,0 +1,265 @@ +import SwiftUI + +struct CharcoalToast: CharcoalPopupView { + typealias IDValue = UUID + + /// The unique ID of the overlay. + let id: IDValue + /// The text of the Toast + let text: String + + /// The maximum width of the Toast + let maxWidth: CGFloat + + /// The corner radius of the Toast + let cornerRadius: CGFloat = 32 + + /// The spacing between the snackbar and the screen edge + let bottomSpacing: CGFloat + + /// The content of the action view + let action: ActionContent? + + @State private var tooltipSize: CGSize = .zero + + /// A binding to whether the overlay is presented. + @Binding var isPresenting: Bool + + /// If true, the overlay will be dismissed when the user taps outside of the overlay. + let dismissOnTouchOutside: Bool + + /// The overlay will be dismissed after a certain time interval. + let dismissAfter: TimeInterval? + + /// The appearance of the Toast + let appearance: CharcoalToastAppearance + + init( + id: IDValue, + text: String, + maxWidth: CGFloat = 312, + bottomSpacing: CGFloat, + @ViewBuilder action: () -> ActionContent?, + isPresenting: Binding, + dismissOnTouchOutside: Bool = true, + dismissAfter: TimeInterval? = nil, + appearance: CharcoalToastAppearance = .success + ) { + self.id = id + self.text = text + self.maxWidth = maxWidth + self.action = action() + self.bottomSpacing = bottomSpacing + _isPresenting = isPresenting + self.dismissOnTouchOutside = dismissOnTouchOutside + self.dismissAfter = dismissAfter + self.appearance = appearance + } + + var body: some View { + ZStack(alignment: .bottom) { + Color.clear + .if(dismissOnTouchOutside && isPresenting) { view in + view.contentShape(Rectangle()) + .simultaneousGesture( + TapGesture() + .onEnded { _ in + isPresenting = false + } + ) + .simultaneousGesture( + DragGesture() + .onChanged { _ in + isPresenting = false + } + ) + } + if isPresenting { + HStack(spacing: 0) { + HStack(spacing: 8) { + Text(text) + .charcoalTypography14Bold(isSingleLine: true) + .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.background1.color)) + + if let action = action { + action + .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.background1.color)) + } + } + .padding(EdgeInsets(top: 8, leading: 24, bottom: 8, trailing: 24)) + } + .background( + appearance.background + ) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.background1.color), lineWidth: 2)) + .offset(CGSize(width: 0, height: -bottomSpacing)) + .onAppear { + if let dismissAfter = dismissAfter { + DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { + isPresenting = false + } + } + } + } + } + .animation(.easeInOut(duration: 0.2), value: isPresenting) + .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) + } + + static func == (lhs: CharcoalToast, rhs: CharcoalToast) -> Bool { + return lhs.text == rhs.text && lhs.maxWidth == rhs.maxWidth && lhs.isPresenting == rhs.isPresenting + } +} + + +public enum CharcoalToastAppearance { + case success + case error + + var background: Color { + switch self { + case .success: + return Color(CharcoalAsset.ColorPaletteGenerated.success.color) + case .error: + return Color(CharcoalAsset.ColorPaletteGenerated.assertive.color) + } + } +} + +struct CharcoalToastModifier: ViewModifier { + /// Presentation `Binding` + @Binding var isPresenting: Bool + + /// The spacing between the snackbar and the screen edge + let bottomSpacing: CGFloat + + /// Text to be displayed in the snackbar + let text: String + + /// The action to be displayed in the snackbar + @ViewBuilder let action: () -> ActionContent? + + /// Assign a unique ID to the view + @State var viewID = UUID() + + /// The overlay will be dismissed after a certain time interval. + let dismissAfter: TimeInterval? + + /// The appearance of the Toast + let appearance: CharcoalToastAppearance + + func body(content: Content) -> some View { + content + .overlay(Color.clear + .modifier( + CharcoalOverlayUpdaterContainer( + isPresenting: $isPresenting, + + view: CharcoalToast( + id: viewID, + text: text, + bottomSpacing: bottomSpacing, + action: action, + isPresenting: $isPresenting, + dismissOnTouchOutside: false, + dismissAfter: dismissAfter, + appearance: appearance + ), + viewID: viewID + ))) + } +} + +public extension View { + /** + Add a tooltip to the view + + - Parameters: + - isPresenting: A binding to whether the Tooltip is presented. + - text: The text to be displayed in the snackbar. + - action: The action to be displayed in the snackbar. + + # Example # + ```swift + Text("Hello").charcoalSnackBar(isPresenting: $isPresenting, text: "Hello") + ``` + */ + func charcoalToast( + isPresenting: Binding, + bottomSpacing: CGFloat = 96, + text: String, + dismissAfter: TimeInterval? = nil, + appearance: CharcoalToastAppearance = .success, + @ViewBuilder action: @escaping () -> Content = { EmptyView() } + ) -> some View where Content: View { + return modifier( + CharcoalToastModifier( + isPresenting: isPresenting, + bottomSpacing: bottomSpacing, + text: text, + action: action, + dismissAfter: dismissAfter, + appearance: appearance) + ) + } +} + +private struct ToastsPreviewView: View { + @State var isPresenting = true + + @State var isPresenting2 = true + + @State var isPresenting3 = true + + @State var textOfLabel = "Hello" + + var body: some View { + ZStack { + Color.clear + ZStack { + Button { + isPresenting.toggle() + isPresenting3.toggle() + } label: { + Text("Toggle SnackBar") + } + } + .charcoalToast( + isPresenting: $isPresenting, + text: "ブックマークしました", + action: { + Button { + print("Tapped") + } label: { + Image(charocalIcon: .remove16) + .renderingMode(.template) + } + } + ) + .charcoalToast( + isPresenting: $isPresenting2, + bottomSpacing: 192, + text: "ブックマークしました", + appearance: .error, + action: { + Button { + print("Tapped") + } label: { + Text("編集") + } + } + ) + .charcoalToast( + isPresenting: $isPresenting3, + bottomSpacing: 275, + text: "ブックマークしました" + ) + } + .charcoalOverlayContainer() + } +} + +#Preview { + ToastsPreviewView() +} From c39ad76222597497da5618b7df7d6e57aa8f7a86 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 16:33:36 +0900 Subject: [PATCH 060/199] Refine toast control --- .../Components/Toast/CharcoalToast.swift | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index 78d5bc461..1aa802aec 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -230,7 +230,7 @@ private struct ToastsPreviewView: View { text: "ブックマークしました", action: { Button { - print("Tapped") + isPresenting = false } label: { Image(charocalIcon: .remove16) .renderingMode(.template) @@ -241,14 +241,7 @@ private struct ToastsPreviewView: View { isPresenting: $isPresenting2, bottomSpacing: 192, text: "ブックマークしました", - appearance: .error, - action: { - Button { - print("Tapped") - } label: { - Text("編集") - } - } + appearance: .error ) .charcoalToast( isPresenting: $isPresenting3, From fce72707b8e9faba62ee46db9cf7f8fb40def3ff Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 16:50:38 +0900 Subject: [PATCH 061/199] Refine screen edge of toast --- .../Components/Toast/CharcoalSnackBar.swift | 39 ++++++++---- .../Components/Toast/CharcoalToast.swift | 60 ++++++++++++++----- 2 files changed, 74 insertions(+), 25 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 451293018..67c44bea6 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -17,8 +17,10 @@ struct CharcoalSnackBar: CharcoalPopupView { /// The corner radius of the snackbar let cornerRadius: CGFloat = 32 + let screenEdge: CharcoalPopupViewEdge + /// The spacing between the snackbar and the screen edge - let bottomSpacing: CGFloat + let screenEdgeSpacing: CGFloat /// The content of the action view let action: ActionContent? @@ -38,7 +40,8 @@ struct CharcoalSnackBar: CharcoalPopupView { id: IDValue, text: String, maxWidth: CGFloat = 312, - bottomSpacing: CGFloat, + screenEdge: CharcoalPopupViewEdge = .bottom, + screenEdgeSpacing: CGFloat, thumbnailImage: Image?, @ViewBuilder action: () -> ActionContent?, isPresenting: Binding, @@ -50,14 +53,15 @@ struct CharcoalSnackBar: CharcoalPopupView { self.maxWidth = maxWidth self.thumbnailImage = thumbnailImage self.action = action() - self.bottomSpacing = bottomSpacing + self.screenEdgeSpacing = screenEdgeSpacing _isPresenting = isPresenting self.dismissOnTouchOutside = dismissOnTouchOutside self.dismissAfter = dismissAfter + self.screenEdge = screenEdge } var body: some View { - ZStack(alignment: .bottom) { + ZStack(alignment: screenEdge.alignment) { Color.clear .if(dismissOnTouchOutside && isPresenting) { view in view.contentShape(Rectangle()) @@ -99,7 +103,7 @@ struct CharcoalSnackBar: CharcoalPopupView { ) .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1)) - .offset(CGSize(width: 0, height: -bottomSpacing)) + .offset(CGSize(width: 0, height: screenEdge.offset * screenEdgeSpacing)) .onAppear { if let dismissAfter = dismissAfter { DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { @@ -121,9 +125,11 @@ struct CharcoalSnackBar: CharcoalPopupView { struct CharcoalSnackBarModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool + + let screenEdge: CharcoalPopupViewEdge /// The spacing between the snackbar and the screen edge - let bottomSpacing: CGFloat + let screenEdgeSpacing: CGFloat /// Text to be displayed in the snackbar let text: String @@ -150,7 +156,8 @@ struct CharcoalSnackBarModifier: ViewModifier { view: CharcoalSnackBar( id: viewID, text: text, - bottomSpacing: bottomSpacing, + screenEdge: screenEdge, + screenEdgeSpacing: screenEdgeSpacing, thumbnailImage: thumbnailImage, action: action, isPresenting: $isPresenting, @@ -179,13 +186,23 @@ public extension View { */ func charcoalSnackBar( isPresenting: Binding, - bottomSpacing: CGFloat = 96, + screenEdge: CharcoalPopupViewEdge = .bottom, + screenEdgeSpacing: CGFloat = 96, text: String, thumbnailImage: Image? = nil, dismissAfter: TimeInterval? = nil, @ViewBuilder action: @escaping () -> Content = { EmptyView() } ) -> some View where Content: View { - return modifier(CharcoalSnackBarModifier(isPresenting: isPresenting, bottomSpacing: bottomSpacing, text: text, thumbnailImage: thumbnailImage, action: action, dismissAfter: dismissAfter)) + return modifier( + CharcoalSnackBarModifier( + isPresenting: isPresenting, + screenEdge: screenEdge, + screenEdgeSpacing: screenEdgeSpacing, + text: text, + thumbnailImage: thumbnailImage, + action: action, + dismissAfter: dismissAfter) + ) } } @@ -233,7 +250,7 @@ private struct SnackBarsPreviewView: View { ) .charcoalSnackBar( isPresenting: $isPresenting2, - bottomSpacing: 192, + screenEdgeSpacing: 192, text: "ブックマークしました", dismissAfter: 2, action: { @@ -246,7 +263,7 @@ private struct SnackBarsPreviewView: View { ) .charcoalSnackBar( isPresenting: $isPresenting3, - bottomSpacing: 275, + screenEdgeSpacing: 275, text: "ブックマークしました" ) } diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index 1aa802aec..212cf2432 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -13,9 +13,11 @@ struct CharcoalToast: CharcoalPopupView { /// The corner radius of the Toast let cornerRadius: CGFloat = 32 + + let screenEdge: CharcoalPopupViewEdge /// The spacing between the snackbar and the screen edge - let bottomSpacing: CGFloat + let screenEdgeSpacing: CGFloat /// The content of the action view let action: ActionContent? @@ -38,7 +40,8 @@ struct CharcoalToast: CharcoalPopupView { id: IDValue, text: String, maxWidth: CGFloat = 312, - bottomSpacing: CGFloat, + screenEdge: CharcoalPopupViewEdge = .bottom, + screenEdgeSpacing: CGFloat, @ViewBuilder action: () -> ActionContent?, isPresenting: Binding, dismissOnTouchOutside: Bool = true, @@ -49,15 +52,16 @@ struct CharcoalToast: CharcoalPopupView { self.text = text self.maxWidth = maxWidth self.action = action() - self.bottomSpacing = bottomSpacing + self.screenEdgeSpacing = screenEdgeSpacing _isPresenting = isPresenting self.dismissOnTouchOutside = dismissOnTouchOutside self.dismissAfter = dismissAfter self.appearance = appearance + self.screenEdge = screenEdge } var body: some View { - ZStack(alignment: .bottom) { + ZStack(alignment: screenEdge.alignment) { Color.clear .if(dismissOnTouchOutside && isPresenting) { view in view.contentShape(Rectangle()) @@ -93,7 +97,7 @@ struct CharcoalToast: CharcoalPopupView { ) .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.background1.color), lineWidth: 2)) - .offset(CGSize(width: 0, height: -bottomSpacing)) + .offset(CGSize(width: 0, height: screenEdge.offset*screenEdgeSpacing)) .onAppear { if let dismissAfter = dismissAfter { DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { @@ -112,6 +116,29 @@ struct CharcoalToast: CharcoalPopupView { } } +public enum CharcoalPopupViewEdge { + case top + case bottom + + var alignment: Alignment { + switch self { + case .top: + return .top + case .bottom: + return .bottom + } + } + + var offset: CGFloat { + switch self { + case .top: + return 1 + case .bottom: + return -1 + } + } +} + public enum CharcoalToastAppearance { case success @@ -131,8 +158,9 @@ struct CharcoalToastModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool + let screenEdge: CharcoalPopupViewEdge /// The spacing between the snackbar and the screen edge - let bottomSpacing: CGFloat + let screenEdgeSpacing: CGFloat /// Text to be displayed in the snackbar let text: String @@ -159,7 +187,8 @@ struct CharcoalToastModifier: ViewModifier { view: CharcoalToast( id: viewID, text: text, - bottomSpacing: bottomSpacing, + screenEdge: screenEdge, + screenEdgeSpacing: screenEdgeSpacing, action: action, isPresenting: $isPresenting, dismissOnTouchOutside: false, @@ -187,7 +216,8 @@ public extension View { */ func charcoalToast( isPresenting: Binding, - bottomSpacing: CGFloat = 96, + screenEdge: CharcoalPopupViewEdge = .bottom, + screenEdgeSpacing: CGFloat = 96, text: String, dismissAfter: TimeInterval? = nil, appearance: CharcoalToastAppearance = .success, @@ -196,7 +226,8 @@ public extension View { return modifier( CharcoalToastModifier( isPresenting: isPresenting, - bottomSpacing: bottomSpacing, + screenEdge: screenEdge, + screenEdgeSpacing: screenEdgeSpacing, text: text, action: action, dismissAfter: dismissAfter, @@ -227,7 +258,8 @@ private struct ToastsPreviewView: View { } .charcoalToast( isPresenting: $isPresenting, - text: "ブックマークしました", + screenEdge: .top, + text: "テキストメッセージ", action: { Button { isPresenting = false @@ -239,14 +271,14 @@ private struct ToastsPreviewView: View { ) .charcoalToast( isPresenting: $isPresenting2, - bottomSpacing: 192, - text: "ブックマークしました", + screenEdgeSpacing: 192, + text: "テキストメッセージ", appearance: .error ) .charcoalToast( isPresenting: $isPresenting3, - bottomSpacing: 275, - text: "ブックマークしました" + screenEdgeSpacing: 275, + text: "テキストメッセージ" ) } .charcoalOverlayContainer() From bdba4d345d72e9638a57ece2506a9e41d064b0d3 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 16:54:37 +0900 Subject: [PATCH 062/199] Refine comments --- .../Components/Toast/CharcoalSnackBar.swift | 7 ++++++- .../CharcoalSwiftUI/Components/Toast/CharcoalToast.swift | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 67c44bea6..68c7fdd1e 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -17,6 +17,7 @@ struct CharcoalSnackBar: CharcoalPopupView { /// The corner radius of the snackbar let cornerRadius: CGFloat = 32 + /// The edge of the screen where the snackbar will be presented let screenEdge: CharcoalPopupViewEdge /// The spacing between the snackbar and the screen edge @@ -126,6 +127,7 @@ struct CharcoalSnackBarModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool + /// The edge of the screen where the snackbar will be presented let screenEdge: CharcoalPopupViewEdge /// The spacing between the snackbar and the screen edge @@ -177,8 +179,11 @@ public extension View { - isPresenting: A binding to whether the Tooltip is presented. - text: The text to be displayed in the snackbar. - thumbnailImage: The thumbnail image to be displayed in the snackbar. + - dismissAfter: The overlay will be dismissed after a certain time interval. + - screenEdge: The edge of the screen where the snackbar will be presented + - screenEdgeSpacing: The spacing between the snackbar and the screen edge - action: The action to be displayed in the snackbar. - + # Example # ```swift Text("Hello").charcoalSnackBar(isPresenting: $isPresenting, text: "Hello") diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index 212cf2432..db26e93b5 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -14,6 +14,7 @@ struct CharcoalToast: CharcoalPopupView { /// The corner radius of the Toast let cornerRadius: CGFloat = 32 + /// The edge of the screen where the Toast will be presented let screenEdge: CharcoalPopupViewEdge /// The spacing between the snackbar and the screen edge @@ -206,12 +207,16 @@ public extension View { - Parameters: - isPresenting: A binding to whether the Tooltip is presented. + - dismissAfter: The overlay will be dismissed after a certain time interval. + - screenEdge: The edge of the screen where the snackbar will be presented + - screenEdgeSpacing: The spacing between the snackbar and the screen edge - text: The text to be displayed in the snackbar. - action: The action to be displayed in the snackbar. + - appearance: The appearance of the Toast # Example # ```swift - Text("Hello").charcoalSnackBar(isPresenting: $isPresenting, text: "Hello") + Text("Hello").charcoalToast(isPresenting: $isPresenting, text: "Hello") ``` */ func charcoalToast( From 70417bab6ffc8d72a8287ca6d73a3b046486495f Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 16:56:53 +0900 Subject: [PATCH 063/199] Rename CharcoalPopupProtocol --- .../CharcoalOverlayContainerModifier.swift | 4 +-- .../Overlay/CharcoalPopupProtocol.swift | 26 +++++++++++++++++++ .../Components/Toast/CharcoalSnackBar.swift | 2 +- .../Components/Toast/CharcoalToast.swift | 26 +------------------ .../Components/Tooltip/CharcoalTooltip.swift | 2 +- 5 files changed, 30 insertions(+), 30 deletions(-) create mode 100644 Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index a01ba9e00..5cfae69ab 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -12,9 +12,7 @@ struct CharcoalOverlayContainerModifier: ViewModifier { } } -typealias CharcoalPopupView = Equatable & Identifiable & View - -struct CharcoalOverlayUpdaterContainer: ViewModifier { +struct CharcoalOverlayUpdaterContainer: ViewModifier { @EnvironmentObject var viewManager: CharcoalContainerManager @Binding var isPresenting: Bool diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift new file mode 100644 index 000000000..8bce44e57 --- /dev/null +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift @@ -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 offset: CGFloat { + switch self { + case .top: + return 1 + case .bottom: + return -1 + } + } +} diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 68c7fdd1e..9fffe8657 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalSnackBar: CharcoalPopupView { +struct CharcoalSnackBar: CharcoalPopupProtocol { typealias IDValue = UUID /// The unique ID of the overlay. diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index db26e93b5..e4d58053d 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalToast: CharcoalPopupView { +struct CharcoalToast: CharcoalPopupProtocol { typealias IDValue = UUID /// The unique ID of the overlay. @@ -117,30 +117,6 @@ struct CharcoalToast: CharcoalPopupView { } } -public enum CharcoalPopupViewEdge { - case top - case bottom - - var alignment: Alignment { - switch self { - case .top: - return .top - case .bottom: - return .bottom - } - } - - var offset: CGFloat { - switch self { - case .top: - return 1 - case .bottom: - return -1 - } - } -} - - public enum CharcoalToastAppearance { case success case error diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 891853772..8fe02d741 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalTooltip: CharcoalPopupView { +struct CharcoalTooltip: CharcoalPopupProtocol { typealias IDValue = UUID /// The unique ID of the overlay. From 87a129c7c00e37d398015f3d7a1670324ad79def Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 17:21:03 +0900 Subject: [PATCH 064/199] Refine isActuallyPresenting logic --- .../Components/Toast/CharcoalToast.swift | 69 +++++++++++++------ 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index e4d58053d..0a6b21f6b 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -36,6 +36,8 @@ struct CharcoalToast: CharcoalPopupProtocol { /// The appearance of the Toast let appearance: CharcoalToastAppearance + + @State var isActuallyPresenting: Bool = false init( id: IDValue, @@ -79,35 +81,49 @@ struct CharcoalToast: CharcoalPopupProtocol { } ) } - if isPresenting { - HStack(spacing: 0) { - HStack(spacing: 8) { - Text(text) - .charcoalTypography14Bold(isSingleLine: true) - .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.background1.color)) + HStack(spacing: 0) { + HStack(spacing: 8) { + Text(text) + .charcoalTypography14Bold(isSingleLine: true) + .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.background1.color)) - if let action = action { - action - .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.background1.color)) - } + if let action = action { + action + .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.background1.color)) } - .padding(EdgeInsets(top: 8, leading: 24, bottom: 8, trailing: 24)) } - .background( - appearance.background - ) - .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) - .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.background1.color), lineWidth: 2)) - .offset(CGSize(width: 0, height: screenEdge.offset*screenEdgeSpacing)) - .onAppear { - if let dismissAfter = dismissAfter { - DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { - isPresenting = false - } + .padding(EdgeInsets(top: 8, leading: 24, bottom: 8, trailing: 24)) + } + .background( + appearance.background + ) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.background1.color), lineWidth: 2)) + .offset(CGSize(width: 0, height: isActuallyPresenting ? screenEdge.offset*screenEdgeSpacing : -screenEdge.offset*(tooltipSize.height))) + .opacity(isActuallyPresenting ? 1 : 0) + .overlay( + GeometryReader(content: { geometry in + Color.clear.preference(key: PopupViewSizeKey.self, value: geometry.size) + }) + ) + .onPreferenceChange(PopupViewSizeKey.self, perform: { value in + tooltipSize = value + }) + .onChange(of: isActuallyPresenting) { newValue in + if let dismissAfter = dismissAfter, newValue { + DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter + 0.5) { + isPresenting = false } } } } + .onChange(of: isPresenting, perform: { newValue in + isActuallyPresenting = isPresenting + }) + .onAppear { + isActuallyPresenting = isPresenting + } + .animation(.spring(), value: isActuallyPresenting) .animation(.easeInOut(duration: 0.2), value: isPresenting) .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } @@ -117,6 +133,14 @@ struct CharcoalToast: CharcoalPopupProtocol { } } +struct PopupViewSizeKey: PreferenceKey { + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + value = nextValue() + } + + static var defaultValue: CGSize = .zero +} + public enum CharcoalToastAppearance { case success case error @@ -254,6 +278,7 @@ private struct ToastsPreviewView: View { isPresenting: $isPresenting2, screenEdgeSpacing: 192, text: "テキストメッセージ", + dismissAfter: 2, appearance: .error ) .charcoalToast( From 6cf98c9fbb265532a0843281d1678265e251e244 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 17:21:40 +0900 Subject: [PATCH 065/199] Clean animation --- Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index 0a6b21f6b..eaf4a70ca 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -124,7 +124,6 @@ struct CharcoalToast: CharcoalPopupProtocol { isActuallyPresenting = isPresenting } .animation(.spring(), value: isActuallyPresenting) - .animation(.easeInOut(duration: 0.2), value: isPresenting) .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } From c16d318a106bd6c275f0259326f269e7cbf05aca Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 17:34:46 +0900 Subject: [PATCH 066/199] Add animation configuration --- .../Components/Toast/CharcoalToast.swift | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index eaf4a70ca..5f93cea95 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -38,6 +38,8 @@ struct CharcoalToast: CharcoalPopupProtocol { let appearance: CharcoalToastAppearance @State var isActuallyPresenting: Bool = false + + let animationConfiguration: CharcoalToastAnimationConfiguration init( id: IDValue, @@ -49,7 +51,8 @@ struct CharcoalToast: CharcoalPopupProtocol { isPresenting: Binding, dismissOnTouchOutside: Bool = true, dismissAfter: TimeInterval? = nil, - appearance: CharcoalToastAppearance = .success + appearance: CharcoalToastAppearance = .success, + animationConfiguration: CharcoalToastAnimationConfiguration ) { self.id = id self.text = text @@ -61,6 +64,7 @@ struct CharcoalToast: CharcoalPopupProtocol { self.dismissAfter = dismissAfter self.appearance = appearance self.screenEdge = screenEdge + self.animationConfiguration = animationConfiguration } var body: some View { @@ -99,7 +103,7 @@ struct CharcoalToast: CharcoalPopupProtocol { ) .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.background1.color), lineWidth: 2)) - .offset(CGSize(width: 0, height: isActuallyPresenting ? screenEdge.offset*screenEdgeSpacing : -screenEdge.offset*(tooltipSize.height))) + .offset(CGSize(width: 0, height: animationConfiguration.enablePositionAnimation ? (isActuallyPresenting ? screenEdge.offset*screenEdgeSpacing : -screenEdge.offset*(tooltipSize.height)) : screenEdge.offset*screenEdgeSpacing)) .opacity(isActuallyPresenting ? 1 : 0) .overlay( GeometryReader(content: { geometry in @@ -123,7 +127,7 @@ struct CharcoalToast: CharcoalPopupProtocol { .onAppear { isActuallyPresenting = isPresenting } - .animation(.spring(), value: isActuallyPresenting) + .animation(animationConfiguration.animation, value: isActuallyPresenting) .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } @@ -154,6 +158,13 @@ public enum CharcoalToastAppearance { } } +public struct CharcoalToastAnimationConfiguration { + public let enablePositionAnimation: Bool + public let animation: Animation + + public static let `default` = CharcoalToastAnimationConfiguration(enablePositionAnimation: true, animation: .spring()) +} + struct CharcoalToastModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool @@ -176,6 +187,8 @@ struct CharcoalToastModifier: ViewModifier { /// The appearance of the Toast let appearance: CharcoalToastAppearance + + let animationConfiguration: CharcoalToastAnimationConfiguration func body(content: Content) -> some View { content @@ -193,7 +206,8 @@ struct CharcoalToastModifier: ViewModifier { isPresenting: $isPresenting, dismissOnTouchOutside: false, dismissAfter: dismissAfter, - appearance: appearance + appearance: appearance, + animationConfiguration: animationConfiguration ), viewID: viewID ))) @@ -225,6 +239,7 @@ public extension View { text: String, dismissAfter: TimeInterval? = nil, appearance: CharcoalToastAppearance = .success, + animationConfiguration: CharcoalToastAnimationConfiguration = .default, @ViewBuilder action: @escaping () -> Content = { EmptyView() } ) -> some View where Content: View { return modifier( @@ -235,7 +250,7 @@ public extension View { text: text, action: action, dismissAfter: dismissAfter, - appearance: appearance) + appearance: appearance, animationConfiguration: animationConfiguration) ) } } @@ -278,12 +293,21 @@ private struct ToastsPreviewView: View { screenEdgeSpacing: 192, text: "テキストメッセージ", dismissAfter: 2, - appearance: .error + appearance: .error, + action: { + Button { + isPresenting2 = false + } label: { + Image(charocalIcon: .remove16) + .renderingMode(.template) + } + } ) .charcoalToast( isPresenting: $isPresenting3, screenEdgeSpacing: 275, - text: "テキストメッセージ" + text: "テキストメッセージ", + animationConfiguration: .init(enablePositionAnimation: false, animation: .easeInOut(duration: 0.2)) ) } .charcoalOverlayContainer() From edc2ea6dabd9f5ddef9748cdf0c5d89b776d4f5d Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 27 Feb 2024 17:57:29 +0900 Subject: [PATCH 067/199] Add custom animation --- .../CharcoalSwiftUISample/ContentView.swift | 1 + .../CharcoalSwiftUISample/ToastsView.swift | 157 +++++++++++++----- .../CharcoalSwiftUISample/TooltipsView.swift | 1 - .../Components/Toast/CharcoalToast.swift | 5 + 4 files changed, 123 insertions(+), 41 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift index 35de5e231..175475fab 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift @@ -52,6 +52,7 @@ public struct ContentView: View { } .navigationBarTitle("Charcoal") } + .charcoalOverlayContainer() .preferredColorScheme(isDarkModeOn ? .dark : .light) } } diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift index feace6c95..9ebd3a2ef 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift @@ -6,65 +6,142 @@ public struct ToastsView: View { @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 { - Button { - isPresenting.toggle() - } label: { - Text("SnackBar") - } - .charcoalSnackBar( - isPresenting: $isPresenting, - text: "ブックマークしました" - ) - - VStack(alignment: .leading) { + Section(header: Text("SnackBar")) { Button { - isPresenting2.toggle() + isPresenting.toggle() } label: { Text("SnackBar") } - Text("with Action") - } - .charcoalSnackBar( - isPresenting: $isPresenting2, - text: "ブックマークしました", - action: { + .charcoalSnackBar( + isPresenting: $isPresenting, + text: "ブックマークしました" + ) + + VStack(alignment: .leading) { Button { - print("Tapped") + isPresenting2.toggle() } label: { - Text("編集") + Text("SnackBar") } + Text("with Action") } - ) - - VStack(alignment: .leading) { + .charcoalSnackBar( + isPresenting: $isPresenting2, + 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("編集") + } + } + ) + } + + Section(header: Text("Toasts")) { Button { - isPresenting3.toggle() + isPresentingToast.toggle() } label: { - Text("SnackBar") + Text("Toast") } - Text("with Action and Thumbnail") - } - .charcoalSnackBar( - isPresenting: $isPresenting3, - text: "ブックマークしました", - thumbnailImage: Image("SnackbarDemo", bundle: Bundle.module), - action: { + .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 { - print("Tapped") + isPresentingToast3.toggle() } label: { - Text("編集") + Text("Toast(Error Appearance)") } + Text("with Custom Animation") } - ) - } - .charcoalOverlayContainer() - .navigationBarTitle("Toasts") + .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() + ToastsView().charcoalOverlayContainer() } diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift index a838fbdfb..416008dc6 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift @@ -51,7 +51,6 @@ public struct TooltipsView: View { }).charcoalTooltip(isPresenting: $isPresented3, text: "Tooltip created by Charcoal and here is testing Auto-Positioning") } } - .charcoalOverlayContainer() .navigationBarTitle("Tooltips") } } diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index 5f93cea95..49a0ee59a 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -162,6 +162,11 @@ public struct CharcoalToastAnimationConfiguration { public let enablePositionAnimation: Bool public let animation: Animation + public init(enablePositionAnimation: Bool, animation: Animation) { + self.enablePositionAnimation = enablePositionAnimation + self.animation = animation + } + public static let `default` = CharcoalToastAnimationConfiguration(enablePositionAnimation: true, animation: .spring()) } From c939bc9428a9003fe6740ffed7db306402832fcf Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 28 Feb 2024 13:58:13 +0900 Subject: [PATCH 068/199] Add CharcoalToastProtocol --- .../Components/Toast/CharcoalSnackBar.swift | 38 ++++--------------- .../Components/Toast/CharcoalToast.swift | 30 +-------------- .../Toast/CharcoalToastProtocol.swift | 19 ++++++++++ 3 files changed, 29 insertions(+), 58 deletions(-) create mode 100644 Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 9fffe8657..a64b1e213 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -1,26 +1,22 @@ import SwiftUI -struct CharcoalSnackBar: CharcoalPopupProtocol { +struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToastProtocol { + typealias IDValue = UUID - /// The unique ID of the overlay. let id: IDValue - /// The text of the snackbar + let text: String /// The thumbnail image of the snackbar let thumbnailImage: Image? - /// The maximum width of the snackbar let maxWidth: CGFloat - /// The corner radius of the snackbar let cornerRadius: CGFloat = 32 - /// The edge of the screen where the snackbar will be presented let screenEdge: CharcoalPopupViewEdge - /// The spacing between the snackbar and the screen edge let screenEdgeSpacing: CGFloat /// The content of the action view @@ -31,11 +27,9 @@ struct CharcoalSnackBar: CharcoalPopupProtocol { /// A binding to whether the overlay is presented. @Binding var isPresenting: Bool - /// If true, the overlay will be dismissed when the user taps outside of the overlay. - let dismissOnTouchOutside: Bool - - /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? + + var animationConfiguration: CharcoalToastAnimationConfiguration init( id: IDValue, @@ -46,8 +40,8 @@ struct CharcoalSnackBar: CharcoalPopupProtocol { thumbnailImage: Image?, @ViewBuilder action: () -> ActionContent?, isPresenting: Binding, - dismissOnTouchOutside: Bool = true, - dismissAfter: TimeInterval? = nil + dismissAfter: TimeInterval? = nil, + animationConfiguration: CharcoalToastAnimationConfiguration = .default ) { self.id = id self.text = text @@ -56,29 +50,14 @@ struct CharcoalSnackBar: CharcoalPopupProtocol { self.action = action() self.screenEdgeSpacing = screenEdgeSpacing _isPresenting = isPresenting - self.dismissOnTouchOutside = dismissOnTouchOutside self.dismissAfter = dismissAfter self.screenEdge = screenEdge + self.animationConfiguration = animationConfiguration } var body: some View { ZStack(alignment: screenEdge.alignment) { Color.clear - .if(dismissOnTouchOutside && isPresenting) { view in - view.contentShape(Rectangle()) - .simultaneousGesture( - TapGesture() - .onEnded { _ in - isPresenting = false - } - ) - .simultaneousGesture( - DragGesture() - .onChanged { _ in - isPresenting = false - } - ) - } if isPresenting { HStack(spacing: 0) { if let thumbnailImage = thumbnailImage { @@ -163,7 +142,6 @@ struct CharcoalSnackBarModifier: ViewModifier { thumbnailImage: thumbnailImage, action: action, isPresenting: $isPresenting, - dismissOnTouchOutside: false, dismissAfter: dismissAfter ), viewID: viewID diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index 49a0ee59a..38cdc1676 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -1,23 +1,19 @@ import SwiftUI -struct CharcoalToast: CharcoalPopupProtocol { +struct CharcoalToast: CharcoalPopupProtocol, CharcoalToastProtocol { typealias IDValue = UUID - /// The unique ID of the overlay. let id: IDValue - /// The text of the Toast + let text: String - /// The maximum width of the Toast let maxWidth: CGFloat /// The corner radius of the Toast let cornerRadius: CGFloat = 32 - /// The edge of the screen where the Toast will be presented let screenEdge: CharcoalPopupViewEdge - /// The spacing between the snackbar and the screen edge let screenEdgeSpacing: CGFloat /// The content of the action view @@ -28,10 +24,6 @@ struct CharcoalToast: CharcoalPopupProtocol { /// A binding to whether the overlay is presented. @Binding var isPresenting: Bool - /// If true, the overlay will be dismissed when the user taps outside of the overlay. - let dismissOnTouchOutside: Bool - - /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? /// The appearance of the Toast @@ -49,7 +41,6 @@ struct CharcoalToast: CharcoalPopupProtocol { screenEdgeSpacing: CGFloat, @ViewBuilder action: () -> ActionContent?, isPresenting: Binding, - dismissOnTouchOutside: Bool = true, dismissAfter: TimeInterval? = nil, appearance: CharcoalToastAppearance = .success, animationConfiguration: CharcoalToastAnimationConfiguration @@ -60,7 +51,6 @@ struct CharcoalToast: CharcoalPopupProtocol { self.action = action() self.screenEdgeSpacing = screenEdgeSpacing _isPresenting = isPresenting - self.dismissOnTouchOutside = dismissOnTouchOutside self.dismissAfter = dismissAfter self.appearance = appearance self.screenEdge = screenEdge @@ -70,21 +60,6 @@ struct CharcoalToast: CharcoalPopupProtocol { var body: some View { ZStack(alignment: screenEdge.alignment) { Color.clear - .if(dismissOnTouchOutside && isPresenting) { view in - view.contentShape(Rectangle()) - .simultaneousGesture( - TapGesture() - .onEnded { _ in - isPresenting = false - } - ) - .simultaneousGesture( - DragGesture() - .onChanged { _ in - isPresenting = false - } - ) - } HStack(spacing: 0) { HStack(spacing: 8) { Text(text) @@ -209,7 +184,6 @@ struct CharcoalToastModifier: ViewModifier { screenEdgeSpacing: screenEdgeSpacing, action: action, isPresenting: $isPresenting, - dismissOnTouchOutside: false, dismissAfter: dismissAfter, appearance: appearance, animationConfiguration: animationConfiguration diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift new file mode 100644 index 000000000..883369735 --- /dev/null +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift @@ -0,0 +1,19 @@ +import SwiftUI + +protocol CharcoalToastProtocol { + associatedtype ActionContent: View + /// The text of the toast + var text: String { get } + /// The maximum width of the toast + var maxWidth: CGFloat { get } + /// The configuration of the toast animation + var animationConfiguration: CharcoalToastAnimationConfiguration { get } + /// The toast will be dismissed after a certain time interval. + var dismissAfter: TimeInterval? { get } + /// The edge of the screen where the toast will be presented + var screenEdge: CharcoalPopupViewEdge { get } + /// The spacing between the toast and the screen edge + var screenEdgeSpacing: CGFloat { get } + /// The content of the action view + var action: ActionContent? { get } +} From b2ca18d306f4b98e368a62f5908bdb64ba506e1a Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 28 Feb 2024 14:03:19 +0900 Subject: [PATCH 069/199] Makes CharcoalSnackBar adapt CharcoalToastProtocol --- .../Components/Toast/CharcoalSnackBar.swift | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index a64b1e213..6511add5e 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -29,6 +29,8 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa let dismissAfter: TimeInterval? + @State var isActuallyPresenting: Bool = false + var animationConfiguration: CharcoalToastAnimationConfiguration init( @@ -58,42 +60,47 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa var body: some View { ZStack(alignment: screenEdge.alignment) { Color.clear - if isPresenting { - HStack(spacing: 0) { - if let thumbnailImage = thumbnailImage { - thumbnailImage - .resizable() - .scaledToFill() - .frame(width: 64, height: 64) - } - HStack(spacing: 16) { - Text(text) - .charcoalTypography14Bold(isSingleLine: true) - .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text1.color)) - - if let action = action { - action - .charcoalDefaultButton(size: .small) - } + HStack(spacing: 0) { + if let thumbnailImage = thumbnailImage { + thumbnailImage + .resizable() + .scaledToFill() + .frame(width: 64, height: 64) + } + HStack(spacing: 16) { + Text(text) + .charcoalTypography14Bold(isSingleLine: true) + .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text1.color)) + + if let action = action { + action + .charcoalDefaultButton(size: .small) } - .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) } - .background( - Color(CharcoalAsset.ColorPaletteGenerated.background1.color) - ) - .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) - .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1)) - .offset(CGSize(width: 0, height: screenEdge.offset * screenEdgeSpacing)) - .onAppear { - if let dismissAfter = dismissAfter { - DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { - isPresenting = false - } + .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) + } + .background( + Color(CharcoalAsset.ColorPaletteGenerated.background1.color) + ) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1)) + .offset(CGSize(width: 0, height: animationConfiguration.enablePositionAnimation ? (isActuallyPresenting ? screenEdge.offset*screenEdgeSpacing : -screenEdge.offset*(tooltipSize.height)) : screenEdge.offset*screenEdgeSpacing)) + .opacity(isActuallyPresenting ? 1 : 0) + .onChange(of: isActuallyPresenting) { newValue in + if let dismissAfter = dismissAfter, newValue { + DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter + 0.5) { + isPresenting = false } } } } - .animation(.easeInOut(duration: 0.2), value: isPresenting) + .onChange(of: isPresenting, perform: { newValue in + isActuallyPresenting = isPresenting + }) + .onAppear { + isActuallyPresenting = isPresenting + } + .animation(animationConfiguration.animation, value: isActuallyPresenting) .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } @@ -221,6 +228,7 @@ private struct SnackBarsPreviewView: View { } .charcoalSnackBar( isPresenting: $isPresenting, + screenEdge: .top, text: "ブックマークしました", thumbnailImage: Image(uiImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64)), action: { From d829f8febf0b238e3c51c2503cc39d40b7186405 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 28 Feb 2024 14:06:00 +0900 Subject: [PATCH 070/199] Remove time delay --- Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift | 2 +- Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 6511add5e..99c1bdf59 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -88,7 +88,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa .opacity(isActuallyPresenting ? 1 : 0) .onChange(of: isActuallyPresenting) { newValue in if let dismissAfter = dismissAfter, newValue { - DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter + 0.5) { + DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { isPresenting = false } } diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index 38cdc1676..edcb845c2 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -90,7 +90,7 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalToastP }) .onChange(of: isActuallyPresenting) { newValue in if let dismissAfter = dismissAfter, newValue { - DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter + 0.5) { + DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { isPresenting = false } } From db5466f75642ed52132d13f16891163235f23121 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 28 Feb 2024 14:08:47 +0900 Subject: [PATCH 071/199] Refine SnackBar Animation logic --- .../Sources/CharcoalSwiftUISample/ToastsView.swift | 1 + .../Components/Toast/CharcoalSnackBar.swift | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift index 9ebd3a2ef..a6317d743 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift @@ -55,6 +55,7 @@ public struct ToastsView: View { } .charcoalSnackBar( isPresenting: $isPresenting3, + screenEdge: .top, text: "ブックマークしました", thumbnailImage: Image("SnackbarDemo", bundle: Bundle.module), action: { diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 99c1bdf59..5428588ec 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -84,6 +84,14 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa ) .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1)) + .overlay( + GeometryReader(content: { geometry in + Color.clear.preference(key: PopupViewSizeKey.self, value: geometry.size) + }) + ) + .onPreferenceChange(PopupViewSizeKey.self, perform: { value in + tooltipSize = value + }) .offset(CGSize(width: 0, height: animationConfiguration.enablePositionAnimation ? (isActuallyPresenting ? screenEdge.offset*screenEdgeSpacing : -screenEdge.offset*(tooltipSize.height)) : screenEdge.offset*screenEdgeSpacing)) .opacity(isActuallyPresenting ? 1 : 0) .onChange(of: isActuallyPresenting) { newValue in From 5b39c79014d1af10ae903297b2878702d52940d8 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 28 Feb 2024 14:40:26 +0900 Subject: [PATCH 072/199] Add CharcoalToastAnimationModifier --- .../Components/Toast/CharcoalSnackBar.swift | 45 +++++------ .../Components/Toast/CharcoalToast.swift | 43 ++++------- .../CharcoalToastAnimationModifier.swift | 74 +++++++++++++++++++ .../Toast/CharcoalToastProtocol.swift | 26 ++++++- 4 files changed, 129 insertions(+), 59 deletions(-) create mode 100644 Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 5428588ec..2eed9eb6c 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToastProtocol { +struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalAnimatedToastProtocol { typealias IDValue = UUID @@ -12,8 +12,12 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa let thumbnailImage: Image? let maxWidth: CGFloat - /// The corner radius of the snackbar + let cornerRadius: CGFloat = 32 + + let borderColor: Color + + let borderLineWidth: CGFloat = 1 let screenEdge: CharcoalPopupViewEdge @@ -55,6 +59,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa self.dismissAfter = dismissAfter self.screenEdge = screenEdge self.animationConfiguration = animationConfiguration + self.borderColor = Color(CharcoalAsset.ColorPaletteGenerated.border.color) } var body: some View { @@ -82,33 +87,17 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa .background( Color(CharcoalAsset.ColorPaletteGenerated.background1.color) ) - .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) - .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.border.color), lineWidth: 1)) - .overlay( - GeometryReader(content: { geometry in - Color.clear.preference(key: PopupViewSizeKey.self, value: geometry.size) - }) - ) - .onPreferenceChange(PopupViewSizeKey.self, perform: { value in - tooltipSize = value - }) - .offset(CGSize(width: 0, height: animationConfiguration.enablePositionAnimation ? (isActuallyPresenting ? screenEdge.offset*screenEdgeSpacing : -screenEdge.offset*(tooltipSize.height)) : screenEdge.offset*screenEdgeSpacing)) - .opacity(isActuallyPresenting ? 1 : 0) - .onChange(of: isActuallyPresenting) { newValue in - if let dismissAfter = dismissAfter, newValue { - DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { - isPresenting = false - } - } - } - } - .onChange(of: isPresenting, perform: { newValue in - isActuallyPresenting = isPresenting - }) - .onAppear { - isActuallyPresenting = isPresenting + .charcoalAnimatedToast( + isPresenting: $isPresenting, + isActuallyPresenting: $isActuallyPresenting, + tooltipSize: $tooltipSize, + cornerRadius: cornerRadius, + borderColor: borderColor, + borderLineWidth: borderLineWidth, + screenEdge: screenEdge, + screenEdgeSpacing: screenEdgeSpacing, + dismissAfter: dismissAfter) } - .animation(animationConfiguration.animation, value: isActuallyPresenting) .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index edcb845c2..75bf0bace 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalToast: CharcoalPopupProtocol, CharcoalToastProtocol { +struct CharcoalToast: CharcoalPopupProtocol, CharcoalAnimatedToastProtocol { typealias IDValue = UUID let id: IDValue @@ -12,6 +12,10 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalToastP /// The corner radius of the Toast let cornerRadius: CGFloat = 32 + let borderColor: Color + + let borderLineWidth: CGFloat = 2 + let screenEdge: CharcoalPopupViewEdge let screenEdgeSpacing: CGFloat @@ -55,6 +59,7 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalToastP self.appearance = appearance self.screenEdge = screenEdge self.animationConfiguration = animationConfiguration + self.borderColor = Color(CharcoalAsset.ColorPaletteGenerated.background1.color) } var body: some View { @@ -76,33 +81,17 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalToastP .background( appearance.background ) - .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) - .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(Color(CharcoalAsset.ColorPaletteGenerated.background1.color), lineWidth: 2)) - .offset(CGSize(width: 0, height: animationConfiguration.enablePositionAnimation ? (isActuallyPresenting ? screenEdge.offset*screenEdgeSpacing : -screenEdge.offset*(tooltipSize.height)) : screenEdge.offset*screenEdgeSpacing)) - .opacity(isActuallyPresenting ? 1 : 0) - .overlay( - GeometryReader(content: { geometry in - Color.clear.preference(key: PopupViewSizeKey.self, value: geometry.size) - }) - ) - .onPreferenceChange(PopupViewSizeKey.self, perform: { value in - tooltipSize = value - }) - .onChange(of: isActuallyPresenting) { newValue in - if let dismissAfter = dismissAfter, newValue { - DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { - isPresenting = false - } - } - } - } - .onChange(of: isPresenting, perform: { newValue in - isActuallyPresenting = isPresenting - }) - .onAppear { - isActuallyPresenting = isPresenting + .charcoalAnimatedToast( + isPresenting: $isPresenting, + isActuallyPresenting: $isActuallyPresenting, + tooltipSize: $tooltipSize, + cornerRadius: cornerRadius, + borderColor: borderColor, + borderLineWidth: borderLineWidth, + screenEdge: screenEdge, + screenEdgeSpacing: screenEdgeSpacing, + dismissAfter: dismissAfter) } - .animation(animationConfiguration.animation, value: isActuallyPresenting) .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift new file mode 100644 index 000000000..f70266bff --- /dev/null +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift @@ -0,0 +1,74 @@ +import SwiftUI + +struct CharcoalToastAnimationModifier: ViewModifier, CharcoalToastBaseProtocol, CharcoalToastAnimationProtocol { + var text: String + + var maxWidth: CGFloat + + @Binding var isPresenting: Bool + + let cornerRadius: CGFloat + + let borderColor: Color + + let borderLineWidth: CGFloat + + let screenEdge: CharcoalPopupViewEdge + + let screenEdgeSpacing: CGFloat + + @Binding var tooltipSize: CGSize + + @Binding var isActuallyPresenting: Bool + + let animationConfiguration: CharcoalToastAnimationConfiguration + + let dismissAfter: TimeInterval? + + func body(content: Content) -> some View { + content + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(borderColor, lineWidth: borderLineWidth)) + .overlay( + GeometryReader(content: { geometry in + Color.clear.preference(key: PopupViewSizeKey.self, value: geometry.size) + }) + ) + .offset(CGSize(width: 0, height: animationConfiguration.enablePositionAnimation ? (isActuallyPresenting ? screenEdge.offset*screenEdgeSpacing : -screenEdge.offset*(tooltipSize.height)) : screenEdge.offset*screenEdgeSpacing)) + .opacity(isActuallyPresenting ? 1 : 0) + .onPreferenceChange(PopupViewSizeKey.self, perform: { value in + tooltipSize = value + }) + .onChange(of: isActuallyPresenting) { newValue in + if let dismissAfter = dismissAfter, newValue { + DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { + isPresenting = false + } + } + } + .animation(animationConfiguration.animation, value: isActuallyPresenting) + .onChange(of: isPresenting, perform: { newValue in + isActuallyPresenting = isPresenting + }) + .onAppear { + isActuallyPresenting = isPresenting + } + } +} + + +public extension View { + func charcoalAnimatedToast( + isPresenting: Binding, + isActuallyPresenting: Binding, + tooltipSize: Binding, + cornerRadius: CGFloat, + borderColor: Color, + borderLineWidth: CGFloat, + screenEdge: CharcoalPopupViewEdge, + screenEdgeSpacing: CGFloat, + dismissAfter: TimeInterval? = nil, + animationConfiguration: CharcoalToastAnimationConfiguration = .default) -> some View { + modifier(CharcoalToastAnimationModifier(text: "", maxWidth: 0, isPresenting: isPresenting, cornerRadius: cornerRadius, borderColor: borderColor, borderLineWidth: borderLineWidth , screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, tooltipSize: tooltipSize, isActuallyPresenting: isActuallyPresenting, animationConfiguration: animationConfiguration, dismissAfter: dismissAfter)) + } +} diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift index 883369735..6da7c890b 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift @@ -1,19 +1,37 @@ import SwiftUI -protocol CharcoalToastProtocol { - associatedtype ActionContent: View +typealias CharcoalToastProtocol = CharcoalToastBaseProtocol & CharcoalToastActionProtocol + +typealias CharcoalAnimatedToastProtocol = CharcoalToastProtocol & CharcoalToastAnimationProtocol + +protocol CharcoalToastBaseProtocol { /// The text of the toast var text: String { get } /// The maximum width of the toast var maxWidth: CGFloat { get } - /// The configuration of the toast animation - var animationConfiguration: CharcoalToastAnimationConfiguration { get } /// The toast will be dismissed after a certain time interval. var dismissAfter: TimeInterval? { get } /// The edge of the screen where the toast will be presented var screenEdge: CharcoalPopupViewEdge { get } /// The spacing between the toast and the screen edge var screenEdgeSpacing: CGFloat { get } + /// If the toast is currently being presented + var isPresenting: Bool { get set } + /// Radius of the toast corners + var cornerRadius: CGFloat { get } + /// Color of the toast border + var borderColor: Color { get } + /// Width of the toast border line + var borderLineWidth: CGFloat { get } +} + +protocol CharcoalToastActionProtocol { + associatedtype ActionContent: View /// The content of the action view var action: ActionContent? { get } } + +protocol CharcoalToastAnimationProtocol { + /// The configuration of the toast animation + var animationConfiguration: CharcoalToastAnimationConfiguration { get } +} From a0e0992d2593dd099427a93c20a89cad0fd37255 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 28 Feb 2024 14:41:51 +0900 Subject: [PATCH 073/199] Reformat code --- .../CharcoalSwiftUISample/ToastsView.swift | 16 ++++----- .../CharcoalOverlayContainerModifier.swift | 4 +-- .../Overlay/CharcoalPopupProtocol.swift | 4 +-- .../Components/Toast/CharcoalSnackBar.swift | 27 ++++++++------- .../Components/Toast/CharcoalToast.swift | 34 ++++++++++--------- .../CharcoalToastAnimationModifier.swift | 30 ++++++++-------- .../Toast/CharcoalToastProtocol.swift | 4 +-- 7 files changed, 61 insertions(+), 58 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift index a6317d743..324c99912 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift @@ -6,12 +6,12 @@ public struct ToastsView: View { @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")) { @@ -24,7 +24,7 @@ public struct ToastsView: View { isPresenting: $isPresenting, text: "ブックマークしました" ) - + VStack(alignment: .leading) { Button { isPresenting2.toggle() @@ -44,7 +44,7 @@ public struct ToastsView: View { } } ) - + VStack(alignment: .leading) { Button { isPresenting3.toggle() @@ -67,7 +67,7 @@ public struct ToastsView: View { } ) } - + Section(header: Text("Toasts")) { Button { isPresentingToast.toggle() @@ -78,7 +78,7 @@ public struct ToastsView: View { isPresenting: $isPresentingToast, text: "テキストメッセージ" ) - + VStack(alignment: .leading) { Button { isPresentingToast2.toggle() @@ -100,7 +100,7 @@ public struct ToastsView: View { } } ) - + VStack(alignment: .leading) { Button { isPresentingToast3.toggle() @@ -123,7 +123,7 @@ public struct ToastsView: View { } } ) - + VStack(alignment: .leading) { Button { isPresentingToast4.toggle() diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 5cfae69ab..07e3be725 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -24,8 +24,8 @@ struct CharcoalOverlayUpdaterContainer: ViewM func createOverlayView(view: SubContent) -> CharcoalIdentifiableOverlayView { return CharcoalIdentifiableOverlayView(id: viewID, contentView: AnyView(view)) } - - func updateView(view: SubContent) { + + func updateView(view: SubContent) { viewManager.addView(view: createOverlayView(view: view)) } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift index 8bce44e57..f782efab1 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift @@ -5,7 +5,7 @@ typealias CharcoalPopupProtocol = Equatable & Identifiable & View public enum CharcoalPopupViewEdge { case top case bottom - + var alignment: Alignment { switch self { case .top: @@ -14,7 +14,7 @@ public enum CharcoalPopupViewEdge { return .bottom } } - + var offset: CGFloat { switch self { case .top: diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 2eed9eb6c..db1ea32e7 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -1,22 +1,21 @@ import SwiftUI struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalAnimatedToastProtocol { - typealias IDValue = UUID let id: IDValue - + let text: String /// The thumbnail image of the snackbar let thumbnailImage: Image? let maxWidth: CGFloat - + let cornerRadius: CGFloat = 32 - + let borderColor: Color - + let borderLineWidth: CGFloat = 1 let screenEdge: CharcoalPopupViewEdge @@ -32,9 +31,9 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalAni @Binding var isPresenting: Bool let dismissAfter: TimeInterval? - + @State var isActuallyPresenting: Bool = false - + var animationConfiguration: CharcoalToastAnimationConfiguration init( @@ -59,7 +58,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalAni self.dismissAfter = dismissAfter self.screenEdge = screenEdge self.animationConfiguration = animationConfiguration - self.borderColor = Color(CharcoalAsset.ColorPaletteGenerated.border.color) + borderColor = Color(CharcoalAsset.ColorPaletteGenerated.border.color) } var body: some View { @@ -96,7 +95,8 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalAni borderLineWidth: borderLineWidth, screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, - dismissAfter: dismissAfter) + dismissAfter: dismissAfter + ) } .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } @@ -109,7 +109,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalAni struct CharcoalSnackBarModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool - + /// The edge of the screen where the snackbar will be presented let screenEdge: CharcoalPopupViewEdge @@ -165,7 +165,7 @@ public extension View { - screenEdge: The edge of the screen where the snackbar will be presented - screenEdgeSpacing: The spacing between the snackbar and the screen edge - action: The action to be displayed in the snackbar. - + # Example # ```swift Text("Hello").charcoalSnackBar(isPresenting: $isPresenting, text: "Hello") @@ -188,7 +188,8 @@ public extension View { text: text, thumbnailImage: thumbnailImage, action: action, - dismissAfter: dismissAfter) + dismissAfter: dismissAfter + ) ) } } @@ -225,7 +226,7 @@ private struct SnackBarsPreviewView: View { } .charcoalSnackBar( isPresenting: $isPresenting, - screenEdge: .top, + screenEdge: .top, text: "ブックマークしました", thumbnailImage: Image(uiImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64)), action: { diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index 75bf0bace..b33a77fe8 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -6,16 +6,16 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalAnimat let id: IDValue let text: String - + let maxWidth: CGFloat /// The corner radius of the Toast let cornerRadius: CGFloat = 32 - + let borderColor: Color - + let borderLineWidth: CGFloat = 2 - + let screenEdge: CharcoalPopupViewEdge let screenEdgeSpacing: CGFloat @@ -29,12 +29,12 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalAnimat @Binding var isPresenting: Bool let dismissAfter: TimeInterval? - + /// The appearance of the Toast let appearance: CharcoalToastAppearance - + @State var isActuallyPresenting: Bool = false - + let animationConfiguration: CharcoalToastAnimationConfiguration init( @@ -59,7 +59,7 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalAnimat self.appearance = appearance self.screenEdge = screenEdge self.animationConfiguration = animationConfiguration - self.borderColor = Color(CharcoalAsset.ColorPaletteGenerated.background1.color) + borderColor = Color(CharcoalAsset.ColorPaletteGenerated.background1.color) } var body: some View { @@ -90,13 +90,14 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalAnimat borderLineWidth: borderLineWidth, screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, - dismissAfter: dismissAfter) + dismissAfter: dismissAfter + ) } .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } static func == (lhs: CharcoalToast, rhs: CharcoalToast) -> Bool { - return lhs.text == rhs.text && lhs.maxWidth == rhs.maxWidth && lhs.isPresenting == rhs.isPresenting + return lhs.text == rhs.text && lhs.maxWidth == rhs.maxWidth && lhs.isPresenting == rhs.isPresenting } } @@ -111,7 +112,7 @@ struct PopupViewSizeKey: PreferenceKey { public enum CharcoalToastAppearance { case success case error - + var background: Color { switch self { case .success: @@ -125,12 +126,12 @@ public enum CharcoalToastAppearance { public struct CharcoalToastAnimationConfiguration { public let enablePositionAnimation: Bool public let animation: Animation - + public init(enablePositionAnimation: Bool, animation: Animation) { self.enablePositionAnimation = enablePositionAnimation self.animation = animation } - + public static let `default` = CharcoalToastAnimationConfiguration(enablePositionAnimation: true, animation: .spring()) } @@ -153,10 +154,10 @@ struct CharcoalToastModifier: ViewModifier { /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? - + /// The appearance of the Toast let appearance: CharcoalToastAppearance - + let animationConfiguration: CharcoalToastAnimationConfiguration func body(content: Content) -> some View { @@ -218,7 +219,8 @@ public extension View { text: text, action: action, dismissAfter: dismissAfter, - appearance: appearance, animationConfiguration: animationConfiguration) + appearance: appearance, animationConfiguration: animationConfiguration + ) ) } } diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift index f70266bff..e0265dfe5 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift @@ -2,27 +2,27 @@ import SwiftUI struct CharcoalToastAnimationModifier: ViewModifier, CharcoalToastBaseProtocol, CharcoalToastAnimationProtocol { var text: String - + var maxWidth: CGFloat - + @Binding var isPresenting: Bool - + let cornerRadius: CGFloat - + let borderColor: Color - + let borderLineWidth: CGFloat - + let screenEdge: CharcoalPopupViewEdge let screenEdgeSpacing: CGFloat - + @Binding var tooltipSize: CGSize - + @Binding var isActuallyPresenting: Bool - + let animationConfiguration: CharcoalToastAnimationConfiguration - + let dismissAfter: TimeInterval? func body(content: Content) -> some View { @@ -34,7 +34,7 @@ struct CharcoalToastAnimationModifier: ViewModifier, CharcoalToastBaseProtocol, Color.clear.preference(key: PopupViewSizeKey.self, value: geometry.size) }) ) - .offset(CGSize(width: 0, height: animationConfiguration.enablePositionAnimation ? (isActuallyPresenting ? screenEdge.offset*screenEdgeSpacing : -screenEdge.offset*(tooltipSize.height)) : screenEdge.offset*screenEdgeSpacing)) + .offset(CGSize(width: 0, height: animationConfiguration.enablePositionAnimation ? (isActuallyPresenting ? screenEdge.offset * screenEdgeSpacing : -screenEdge.offset * (tooltipSize.height)) : screenEdge.offset * screenEdgeSpacing)) .opacity(isActuallyPresenting ? 1 : 0) .onPreferenceChange(PopupViewSizeKey.self, perform: { value in tooltipSize = value @@ -47,7 +47,7 @@ struct CharcoalToastAnimationModifier: ViewModifier, CharcoalToastBaseProtocol, } } .animation(animationConfiguration.animation, value: isActuallyPresenting) - .onChange(of: isPresenting, perform: { newValue in + .onChange(of: isPresenting, perform: { _ in isActuallyPresenting = isPresenting }) .onAppear { @@ -56,7 +56,6 @@ struct CharcoalToastAnimationModifier: ViewModifier, CharcoalToastBaseProtocol, } } - public extension View { func charcoalAnimatedToast( isPresenting: Binding, @@ -68,7 +67,8 @@ public extension View { screenEdge: CharcoalPopupViewEdge, screenEdgeSpacing: CGFloat, dismissAfter: TimeInterval? = nil, - animationConfiguration: CharcoalToastAnimationConfiguration = .default) -> some View { - modifier(CharcoalToastAnimationModifier(text: "", maxWidth: 0, isPresenting: isPresenting, cornerRadius: cornerRadius, borderColor: borderColor, borderLineWidth: borderLineWidth , screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, tooltipSize: tooltipSize, isActuallyPresenting: isActuallyPresenting, animationConfiguration: animationConfiguration, dismissAfter: dismissAfter)) + animationConfiguration: CharcoalToastAnimationConfiguration = .default + ) -> some View { + modifier(CharcoalToastAnimationModifier(text: "", maxWidth: 0, isPresenting: isPresenting, cornerRadius: cornerRadius, borderColor: borderColor, borderLineWidth: borderLineWidth, screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, tooltipSize: tooltipSize, isActuallyPresenting: isActuallyPresenting, animationConfiguration: animationConfiguration, dismissAfter: dismissAfter)) } } diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift index 6da7c890b..5c9bff452 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift @@ -1,8 +1,8 @@ import SwiftUI -typealias CharcoalToastProtocol = CharcoalToastBaseProtocol & CharcoalToastActionProtocol +typealias CharcoalToastProtocol = CharcoalToastActionProtocol & CharcoalToastBaseProtocol -typealias CharcoalAnimatedToastProtocol = CharcoalToastProtocol & CharcoalToastAnimationProtocol +typealias CharcoalAnimatedToastProtocol = CharcoalToastAnimationProtocol & CharcoalToastProtocol protocol CharcoalToastBaseProtocol { /// The text of the toast From 8412cd033b53608f86079c7361f1b6d97e4d1fb4 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 28 Feb 2024 14:43:16 +0900 Subject: [PATCH 074/199] Update CharcoalPopupViewEdge of direction --- .../Components/Overlay/CharcoalPopupProtocol.swift | 2 +- .../Components/Toast/CharcoalToastAnimationModifier.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift index f782efab1..fae2d331c 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift @@ -15,7 +15,7 @@ public enum CharcoalPopupViewEdge { } } - var offset: CGFloat { + var direction: CGFloat { switch self { case .top: return 1 diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift index e0265dfe5..db334526f 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift @@ -34,7 +34,7 @@ struct CharcoalToastAnimationModifier: ViewModifier, CharcoalToastBaseProtocol, Color.clear.preference(key: PopupViewSizeKey.self, value: geometry.size) }) ) - .offset(CGSize(width: 0, height: animationConfiguration.enablePositionAnimation ? (isActuallyPresenting ? screenEdge.offset * screenEdgeSpacing : -screenEdge.offset * (tooltipSize.height)) : screenEdge.offset * screenEdgeSpacing)) + .offset(CGSize(width: 0, height: animationConfiguration.enablePositionAnimation ? (isActuallyPresenting ? screenEdge.direction * screenEdgeSpacing : -screenEdge.direction * (tooltipSize.height)) : screenEdge.direction * screenEdgeSpacing)) .opacity(isActuallyPresenting ? 1 : 0) .onPreferenceChange(PopupViewSizeKey.self, perform: { value in tooltipSize = value From 932ffaee02a64dbeb792f2bb4bb0c251355952fb Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 28 Feb 2024 14:51:18 +0900 Subject: [PATCH 075/199] Refine demo --- .../CharcoalSwiftUISample/ToastsView.swift | 24 ++++++++++++++++++- .../CharcoalToastAnimationModifier.swift | 4 ++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift index 324c99912..10925e750 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift @@ -35,6 +35,7 @@ public struct ToastsView: View { } .charcoalSnackBar( isPresenting: $isPresenting2, + screenEdge: .top, text: "ブックマークしました", action: { Button { @@ -55,7 +56,6 @@ public struct ToastsView: View { } .charcoalSnackBar( isPresenting: $isPresenting3, - screenEdge: .top, text: "ブックマークしました", thumbnailImage: Image("SnackbarDemo", bundle: Bundle.module), action: { @@ -66,6 +66,28 @@ public struct ToastsView: View { } } ) + + 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")) { diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift index db334526f..95efd6285 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift @@ -47,8 +47,8 @@ struct CharcoalToastAnimationModifier: ViewModifier, CharcoalToastBaseProtocol, } } .animation(animationConfiguration.animation, value: isActuallyPresenting) - .onChange(of: isPresenting, perform: { _ in - isActuallyPresenting = isPresenting + .onChange(of: isPresenting, perform: { newValue in + isActuallyPresenting = newValue }) .onAppear { isActuallyPresenting = isPresenting From 333503b4bf6c5e4219d5ab0634e527c5fcefc076 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 28 Feb 2024 14:53:05 +0900 Subject: [PATCH 076/199] Fix missing animation --- .../CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift | 5 +++-- Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift | 5 +++-- .../Components/Toast/CharcoalToastAnimationModifier.swift | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index db1ea32e7..ea3083ca5 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -58,7 +58,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalAni self.dismissAfter = dismissAfter self.screenEdge = screenEdge self.animationConfiguration = animationConfiguration - borderColor = Color(CharcoalAsset.ColorPaletteGenerated.border.color) + self.borderColor = Color(CharcoalAsset.ColorPaletteGenerated.border.color) } var body: some View { @@ -95,7 +95,8 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalAni borderLineWidth: borderLineWidth, screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, - dismissAfter: dismissAfter + dismissAfter: dismissAfter, + animationConfiguration: animationConfiguration ) } .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index b33a77fe8..ac627c8e6 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -59,7 +59,7 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalAnimat self.appearance = appearance self.screenEdge = screenEdge self.animationConfiguration = animationConfiguration - borderColor = Color(CharcoalAsset.ColorPaletteGenerated.background1.color) + self.borderColor = Color(CharcoalAsset.ColorPaletteGenerated.background1.color) } var body: some View { @@ -90,7 +90,8 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalAnimat borderLineWidth: borderLineWidth, screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, - dismissAfter: dismissAfter + dismissAfter: dismissAfter, + animationConfiguration: animationConfiguration ) } .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift index 95efd6285..9620f1b05 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift @@ -67,7 +67,7 @@ public extension View { screenEdge: CharcoalPopupViewEdge, screenEdgeSpacing: CGFloat, dismissAfter: TimeInterval? = nil, - animationConfiguration: CharcoalToastAnimationConfiguration = .default + animationConfiguration: CharcoalToastAnimationConfiguration ) -> some View { modifier(CharcoalToastAnimationModifier(text: "", maxWidth: 0, isPresenting: isPresenting, cornerRadius: cornerRadius, borderColor: borderColor, borderLineWidth: borderLineWidth, screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, tooltipSize: tooltipSize, isActuallyPresenting: isActuallyPresenting, animationConfiguration: animationConfiguration, dismissAfter: dismissAfter)) } From fe42a48ace93891ea52aa0429feff649d1c2b8d6 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 28 Feb 2024 15:54:48 +0900 Subject: [PATCH 077/199] Rename charcoalAnimatedToast to charcoalAnimatableToast --- .../CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift | 2 +- .../CharcoalSwiftUI/Components/Toast/CharcoalToast.swift | 2 +- ...Modifier.swift => CharcoalToastAnimatableModifier.swift} | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename Sources/CharcoalSwiftUI/Components/Toast/{CharcoalToastAnimationModifier.swift => CharcoalToastAnimatableModifier.swift} (81%) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index ea3083ca5..0dd2afe04 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -86,7 +86,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalAni .background( Color(CharcoalAsset.ColorPaletteGenerated.background1.color) ) - .charcoalAnimatedToast( + .charcoalAnimatableToast( isPresenting: $isPresenting, isActuallyPresenting: $isActuallyPresenting, tooltipSize: $tooltipSize, diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index ac627c8e6..a9f5ec2a8 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -81,7 +81,7 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalAnimat .background( appearance.background ) - .charcoalAnimatedToast( + .charcoalAnimatableToast( isPresenting: $isPresenting, isActuallyPresenting: $isActuallyPresenting, tooltipSize: $tooltipSize, diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift similarity index 81% rename from Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift rename to Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift index 9620f1b05..aae019c3f 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimationModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalToastAnimationModifier: ViewModifier, CharcoalToastBaseProtocol, CharcoalToastAnimationProtocol { +struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBaseProtocol, CharcoalToastAnimationProtocol { var text: String var maxWidth: CGFloat @@ -57,7 +57,7 @@ struct CharcoalToastAnimationModifier: ViewModifier, CharcoalToastBaseProtocol, } public extension View { - func charcoalAnimatedToast( + func charcoalAnimatableToast( isPresenting: Binding, isActuallyPresenting: Binding, tooltipSize: Binding, @@ -69,6 +69,6 @@ public extension View { dismissAfter: TimeInterval? = nil, animationConfiguration: CharcoalToastAnimationConfiguration ) -> some View { - modifier(CharcoalToastAnimationModifier(text: "", maxWidth: 0, isPresenting: isPresenting, cornerRadius: cornerRadius, borderColor: borderColor, borderLineWidth: borderLineWidth, screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, tooltipSize: tooltipSize, isActuallyPresenting: isActuallyPresenting, animationConfiguration: animationConfiguration, dismissAfter: dismissAfter)) + modifier(CharcoalToastAnimatableModifier(text: "", maxWidth: 0, isPresenting: isPresenting, cornerRadius: cornerRadius, borderColor: borderColor, borderLineWidth: borderLineWidth, screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, tooltipSize: tooltipSize, isActuallyPresenting: isActuallyPresenting, animationConfiguration: animationConfiguration, dismissAfter: dismissAfter)) } } From ffeec3f50cf20060bc3156dbc9189b62ad07b9c1 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 28 Feb 2024 15:55:18 +0900 Subject: [PATCH 078/199] Rename CharcoalAnimatableToastProtocol --- Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift | 2 +- Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift | 2 +- .../Components/Toast/CharcoalToastProtocol.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 0dd2afe04..27c1fee72 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalAnimatedToastProtocol { +struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalAnimatableToastProtocol { typealias IDValue = UUID let id: IDValue diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index a9f5ec2a8..241118a34 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalToast: CharcoalPopupProtocol, CharcoalAnimatedToastProtocol { +struct CharcoalToast: CharcoalPopupProtocol, CharcoalAnimatableToastProtocol { typealias IDValue = UUID let id: IDValue diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift index 5c9bff452..f15b27b5a 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift @@ -2,7 +2,7 @@ import SwiftUI typealias CharcoalToastProtocol = CharcoalToastActionProtocol & CharcoalToastBaseProtocol -typealias CharcoalAnimatedToastProtocol = CharcoalToastAnimationProtocol & CharcoalToastProtocol +typealias CharcoalAnimatableToastProtocol = CharcoalToastAnimationProtocol & CharcoalToastProtocol protocol CharcoalToastBaseProtocol { /// The text of the toast From be2a909489ede336a7b4ae4147bb4e9348569fa9 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 28 Feb 2024 15:56:39 +0900 Subject: [PATCH 079/199] Rename for clean --- .../Components/Toast/CharcoalSnackBar.swift | 2 +- .../CharcoalSwiftUI/Components/Toast/CharcoalToast.swift | 2 +- .../Toast/CharcoalToastAnimatableModifier.swift | 2 +- .../Components/Toast/CharcoalToastProtocol.swift | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 27c1fee72..7fce35424 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalAnimatableToastProtocol { +struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToastAnimatable { typealias IDValue = UUID let id: IDValue diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index 241118a34..dea8481bf 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalToast: CharcoalPopupProtocol, CharcoalAnimatableToastProtocol { +struct CharcoalToast: CharcoalPopupProtocol, CharcoalToastAnimatable { typealias IDValue = UUID let id: IDValue diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift index aae019c3f..6d226564d 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBaseProtocol, CharcoalToastAnimationProtocol { +struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBase, CharcoalToastAnimationProtocol { var text: String var maxWidth: CGFloat diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift index f15b27b5a..7f48889b2 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift @@ -1,10 +1,10 @@ import SwiftUI -typealias CharcoalToastProtocol = CharcoalToastActionProtocol & CharcoalToastBaseProtocol +typealias CharcoalToastProtocol = CharcoalToastAction & CharcoalToastBase -typealias CharcoalAnimatableToastProtocol = CharcoalToastAnimationProtocol & CharcoalToastProtocol +typealias CharcoalToastAnimatable = CharcoalToastAnimationProtocol & CharcoalToastProtocol -protocol CharcoalToastBaseProtocol { +protocol CharcoalToastBase { /// The text of the toast var text: String { get } /// The maximum width of the toast @@ -25,7 +25,7 @@ protocol CharcoalToastBaseProtocol { var borderLineWidth: CGFloat { get } } -protocol CharcoalToastActionProtocol { +protocol CharcoalToastAction { associatedtype ActionContent: View /// The content of the action view var action: ActionContent? { get } From 009312607f393ad0e1326f3e667a7c2052aae38f Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 28 Feb 2024 16:00:15 +0900 Subject: [PATCH 080/199] Simplify protocols --- .../Components/Toast/CharcoalSnackBar.swift | 2 +- .../Components/Toast/CharcoalToast.swift | 2 +- .../Toast/CharcoalToastAnimatableModifier.swift | 2 +- .../Components/Toast/CharcoalToastProtocol.swift | 12 +++--------- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 7fce35424..6a5a55fb7 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToastAnimatable { +struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToastBase, CharcoalToastActionable { typealias IDValue = UUID let id: IDValue diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index dea8481bf..a7f54a239 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalToast: CharcoalPopupProtocol, CharcoalToastAnimatable { +struct CharcoalToast: CharcoalPopupProtocol, CharcoalToastBase, CharcoalToastActionable { typealias IDValue = UUID let id: IDValue diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift index 6d226564d..881c444a6 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBase, CharcoalToastAnimationProtocol { +struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBase { var text: String var maxWidth: CGFloat diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift index 7f48889b2..b7715dec4 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift @@ -1,9 +1,5 @@ import SwiftUI -typealias CharcoalToastProtocol = CharcoalToastAction & CharcoalToastBase - -typealias CharcoalToastAnimatable = CharcoalToastAnimationProtocol & CharcoalToastProtocol - protocol CharcoalToastBase { /// The text of the toast var text: String { get } @@ -23,15 +19,13 @@ protocol CharcoalToastBase { var borderColor: Color { get } /// Width of the toast border line var borderLineWidth: CGFloat { get } + /// The configuration of the toast animation + var animationConfiguration: CharcoalToastAnimationConfiguration { get } } -protocol CharcoalToastAction { +protocol CharcoalToastActionable { associatedtype ActionContent: View /// The content of the action view var action: ActionContent? { get } } -protocol CharcoalToastAnimationProtocol { - /// The configuration of the toast animation - var animationConfiguration: CharcoalToastAnimationConfiguration { get } -} From 18f4adec34ce1a8dca9ea201e3227755b5093668 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 5 Mar 2024 15:07:42 +0900 Subject: [PATCH 081/199] Add drag control --- .../Components/Toast/CharcoalSnackBar.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 6a5a55fb7..3cd24083c 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -35,6 +35,10 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa @State var isActuallyPresenting: Bool = false var animationConfiguration: CharcoalToastAnimationConfiguration + + @State private var offset = CGSize.zero + + @State private var dragVelocity = CGSize.zero init( id: IDValue, @@ -98,6 +102,44 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa dismissAfter: dismissAfter, animationConfiguration: animationConfiguration ) + .offset(y: offset.height) + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { gesture in + let translationInDirection = gesture.translation.height*screenEdge.direction + dragVelocity = gesture.velocity + if (translationInDirection < 0) { + offset = CGSize(width: 0, + height: gesture.translation.height) + } else { + // the less the faster resistance + let limit: CGFloat = 100 + let yOff = gesture.translation.height + let dist = sqrt(yOff*yOff) + let factor = 1 / (dist / limit + 1) + + offset = CGSize(width: 0, + height: gesture.translation.height * factor) + } + } + .onEnded { _ in + let movingVelocityInDirection = dragVelocity.height * screenEdge.direction + let offsetInDirection = offset.height * screenEdge.direction + + if offsetInDirection < -50 || movingVelocityInDirection < -100 { + // remove the card + isPresenting = false + let animation = Animation.interpolatingSpring(initialVelocity: movingVelocityInDirection) + withAnimation(animation) { + offset = .zero + } + } else { + withAnimation(.bouncy) { + offset = .zero + } + } + } + ) } .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } From 3d1b63c68843c1e2fea7225a3333451df09e17c6 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 5 Mar 2024 15:42:45 +0900 Subject: [PATCH 082/199] Add Dismiss timer control logic --- .../Components/Toast/CharcoalSnackBar.swift | 6 ++++++ .../CharcoalToastAnimatableModifier.swift | 21 +++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 3cd24083c..f1c20fa36 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -39,6 +39,8 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa @State private var offset = CGSize.zero @State private var dragVelocity = CGSize.zero + + @State private var isDragging = false init( id: IDValue, @@ -93,6 +95,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa .charcoalAnimatableToast( isPresenting: $isPresenting, isActuallyPresenting: $isActuallyPresenting, + isDragging: $isDragging, tooltipSize: $tooltipSize, cornerRadius: cornerRadius, borderColor: borderColor, @@ -108,6 +111,8 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa .onChanged { gesture in let translationInDirection = gesture.translation.height*screenEdge.direction dragVelocity = gesture.velocity + isDragging = true + print("isDraging \(isDragging)") if (translationInDirection < 0) { offset = CGSize(width: 0, height: gesture.translation.height) @@ -123,6 +128,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa } } .onEnded { _ in + isDragging = false let movingVelocityInDirection = dragVelocity.height * screenEdge.direction let offsetInDirection = offset.height * screenEdge.direction diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift index 881c444a6..8f1223eb8 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift @@ -1,4 +1,5 @@ import SwiftUI +import Combine struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBase { var text: String @@ -24,6 +25,10 @@ struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBase { let animationConfiguration: CharcoalToastAnimationConfiguration let dismissAfter: TimeInterval? + + @Binding var isDragging: Bool + + @State var timer: Timer? func body(content: Content) -> some View { content @@ -41,11 +46,18 @@ struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBase { }) .onChange(of: isActuallyPresenting) { newValue in if let dismissAfter = dismissAfter, newValue { - DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { - isPresenting = false - } + timer = Timer.scheduledTimer(withTimeInterval: dismissAfter, repeats: false, block: { timer in + if !isDragging { + isPresenting = false + } + }) } } + .onChange(of: isDragging, perform: { newValue in + if (isDragging) { + timer?.invalidate() + } + }) .animation(animationConfiguration.animation, value: isActuallyPresenting) .onChange(of: isPresenting, perform: { newValue in isActuallyPresenting = newValue @@ -60,6 +72,7 @@ public extension View { func charcoalAnimatableToast( isPresenting: Binding, isActuallyPresenting: Binding, + isDragging: Binding = Binding.constant(false), tooltipSize: Binding, cornerRadius: CGFloat, borderColor: Color, @@ -69,6 +82,6 @@ public extension View { dismissAfter: TimeInterval? = nil, animationConfiguration: CharcoalToastAnimationConfiguration ) -> some View { - modifier(CharcoalToastAnimatableModifier(text: "", maxWidth: 0, isPresenting: isPresenting, cornerRadius: cornerRadius, borderColor: borderColor, borderLineWidth: borderLineWidth, screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, tooltipSize: tooltipSize, isActuallyPresenting: isActuallyPresenting, animationConfiguration: animationConfiguration, dismissAfter: dismissAfter)) + modifier(CharcoalToastAnimatableModifier(text: "", maxWidth: 0, isPresenting: isPresenting, cornerRadius: cornerRadius, borderColor: borderColor, borderLineWidth: borderLineWidth, screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, tooltipSize: tooltipSize, isActuallyPresenting: isActuallyPresenting, animationConfiguration: animationConfiguration, dismissAfter: dismissAfter, isDragging: isDragging)) } } From 48485f2896d16723347b8eb8c1f4b49c10dfa7c1 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 5 Mar 2024 15:48:58 +0900 Subject: [PATCH 083/199] Refine drag damping logic --- .../CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index f1c20fa36..eae39ffcf 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -111,14 +111,14 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa .onChanged { gesture in let translationInDirection = gesture.translation.height*screenEdge.direction dragVelocity = gesture.velocity + isDragging = true - print("isDraging \(isDragging)") if (translationInDirection < 0) { offset = CGSize(width: 0, height: gesture.translation.height) } else { // the less the faster resistance - let limit: CGFloat = 100 + let limit: CGFloat = 60 let yOff = gesture.translation.height let dist = sqrt(yOff*yOff) let factor = 1 / (dist / limit + 1) @@ -135,7 +135,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa if offsetInDirection < -50 || movingVelocityInDirection < -100 { // remove the card isPresenting = false - let animation = Animation.interpolatingSpring(initialVelocity: movingVelocityInDirection) + let animation = Animation.interpolatingSpring(initialVelocity: dragVelocity.height) withAnimation(animation) { offset = .zero } From 023bfd955fd786ee4d0b6bb90a0186c5d11cc2d4 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 6 Mar 2024 14:32:45 +0900 Subject: [PATCH 084/199] Add CharcoalToastDraggable --- .../Components/Toast/CharcoalSnackBar.swift | 9 +++++---- .../Components/Toast/CharcoalToastProtocol.swift | 7 +++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index eae39ffcf..3f9d2373a 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -1,6 +1,6 @@ import SwiftUI -struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToastBase, CharcoalToastActionable { +struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToastBase, CharcoalToastActionable, CharcoalToastDraggable { typealias IDValue = UUID let id: IDValue @@ -36,11 +36,11 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa var animationConfiguration: CharcoalToastAnimationConfiguration - @State private var offset = CGSize.zero + @State internal var offset = CGSize.zero - @State private var dragVelocity = CGSize.zero + @State internal var dragVelocity = CGSize.zero - @State private var isDragging = false + @State internal var isDragging = false init( id: IDValue, @@ -276,6 +276,7 @@ private struct SnackBarsPreviewView: View { .charcoalSnackBar( isPresenting: $isPresenting, screenEdge: .top, + screenEdgeSpacing: 96, text: "ブックマークしました", thumbnailImage: Image(uiImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64)), action: { diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift index b7715dec4..ed831299c 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift @@ -29,3 +29,10 @@ protocol CharcoalToastActionable { var action: ActionContent? { get } } +protocol CharcoalToastDraggable { + var offset: CGSize {get set} + + var dragVelocity: CGSize {get set} + + var isDragging: Bool {get set} +} From e3b8f46ba6964d92624a7ebd6a2240655fc04602 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 6 Mar 2024 14:51:17 +0900 Subject: [PATCH 085/199] Use CharcoalToastDraggableModifier on CharcoalSnackBar --- .../Components/Toast/CharcoalSnackBar.swift | 229 ++++++++---------- .../CharcoalToastAnimatableModifier.swift | 2 +- .../CharcoalToastDraggableModifier.swift | 72 ++++++ 3 files changed, 179 insertions(+), 124 deletions(-) create mode 100644 Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastDraggableModifier.swift diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 3f9d2373a..f644eddaf 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -2,38 +2,38 @@ import SwiftUI struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToastBase, CharcoalToastActionable, CharcoalToastDraggable { typealias IDValue = UUID - + let id: IDValue - + let text: String - + /// The thumbnail image of the snackbar let thumbnailImage: Image? - + let maxWidth: CGFloat - + let cornerRadius: CGFloat = 32 - + let borderColor: Color - + let borderLineWidth: CGFloat = 1 - + let screenEdge: CharcoalPopupViewEdge - + let screenEdgeSpacing: CGFloat - + /// The content of the action view let action: ActionContent? - + @State private var tooltipSize: CGSize = .zero - + /// A binding to whether the overlay is presented. @Binding var isPresenting: Bool - + let dismissAfter: TimeInterval? - + @State var isActuallyPresenting: Bool = false - + var animationConfiguration: CharcoalToastAnimationConfiguration @State internal var offset = CGSize.zero @@ -41,7 +41,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa @State internal var dragVelocity = CGSize.zero @State internal var isDragging = false - + init( id: IDValue, text: String, @@ -66,7 +66,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa self.animationConfiguration = animationConfiguration self.borderColor = Color(CharcoalAsset.ColorPaletteGenerated.border.color) } - + var body: some View { ZStack(alignment: screenEdge.alignment) { Color.clear @@ -81,7 +81,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa Text(text) .charcoalTypography14Bold(isSingleLine: true) .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text1.color)) - + if let action = action { action .charcoalDefaultButton(size: .small) @@ -105,51 +105,12 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa dismissAfter: dismissAfter, animationConfiguration: animationConfiguration ) - .offset(y: offset.height) - .gesture( - DragGesture(minimumDistance: 0) - .onChanged { gesture in - let translationInDirection = gesture.translation.height*screenEdge.direction - dragVelocity = gesture.velocity - - isDragging = true - if (translationInDirection < 0) { - offset = CGSize(width: 0, - height: gesture.translation.height) - } else { - // the less the faster resistance - let limit: CGFloat = 60 - let yOff = gesture.translation.height - let dist = sqrt(yOff*yOff) - let factor = 1 / (dist / limit + 1) - - offset = CGSize(width: 0, - height: gesture.translation.height * factor) - } - } - .onEnded { _ in - isDragging = false - let movingVelocityInDirection = dragVelocity.height * screenEdge.direction - let offsetInDirection = offset.height * screenEdge.direction - - if offsetInDirection < -50 || movingVelocityInDirection < -100 { - // remove the card - isPresenting = false - let animation = Animation.interpolatingSpring(initialVelocity: dragVelocity.height) - withAnimation(animation) { - offset = .zero - } - } else { - withAnimation(.bouncy) { - offset = .zero - } - } - } - ) + .charcoalDraggableToast(screenEdge: screenEdge, isPresenting: $isPresenting, offset: $offset, dragVelocity: $dragVelocity, isDragging: $isDragging) + } .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } - + static func == (lhs: CharcoalSnackBar, rhs: CharcoalSnackBar) -> Bool { return lhs.text == rhs.text && lhs.maxWidth == rhs.maxWidth && lhs.thumbnailImage == rhs.thumbnailImage && lhs.isPresenting == rhs.isPresenting } @@ -158,35 +119,35 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa struct CharcoalSnackBarModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool - + /// The edge of the screen where the snackbar will be presented let screenEdge: CharcoalPopupViewEdge - + /// The spacing between the snackbar and the screen edge let screenEdgeSpacing: CGFloat - + /// Text to be displayed in the snackbar let text: String - + /// The thumbnail image to be displayed in the snackbar let thumbnailImage: Image? - + /// The action to be displayed in the snackbar @ViewBuilder let action: () -> ActionContent? - + /// Assign a unique ID to the view @State var viewID = UUID() - + /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? - + func body(content: Content) -> some View { content .overlay(Color.clear .modifier( CharcoalOverlayUpdaterContainer( isPresenting: $isPresenting, - + view: CharcoalSnackBar( id: viewID, text: text, @@ -205,16 +166,16 @@ struct CharcoalSnackBarModifier: ViewModifier { public extension View { /** Add a tooltip to the view - + - Parameters: - - isPresenting: A binding to whether the Tooltip is presented. - - text: The text to be displayed in the snackbar. - - thumbnailImage: The thumbnail image to be displayed in the snackbar. - - dismissAfter: The overlay will be dismissed after a certain time interval. - - screenEdge: The edge of the screen where the snackbar will be presented - - screenEdgeSpacing: The spacing between the snackbar and the screen edge - - action: The action to be displayed in the snackbar. - + - isPresenting: A binding to whether the Tooltip is presented. + - text: The text to be displayed in the snackbar. + - thumbnailImage: The thumbnail image to be displayed in the snackbar. + - dismissAfter: The overlay will be dismissed after a certain time interval. + - screenEdge: The edge of the screen where the snackbar will be presented + - screenEdgeSpacing: The spacing between the snackbar and the screen edge + - action: The action to be displayed in the snackbar. + # Example # ```swift Text("Hello").charcoalSnackBar(isPresenting: $isPresenting, text: "Hello") @@ -223,7 +184,7 @@ public extension View { func charcoalSnackBar( isPresenting: Binding, screenEdge: CharcoalPopupViewEdge = .bottom, - screenEdgeSpacing: CGFloat = 96, + screenEdgeSpacing: CGFloat = 120, text: String, thumbnailImage: Image? = nil, dismissAfter: TimeInterval? = nil, @@ -255,57 +216,79 @@ private extension UIColor { private struct SnackBarsPreviewView: View { @State var isPresenting = true - + @State var isPresenting2 = true - + @State var isPresenting3 = true - + @State var textOfLabel = "Hello" - + var body: some View { - ZStack { - Color.clear - ZStack { - Button { - isPresenting.toggle() - isPresenting3.toggle() - } label: { - Text("Toggle SnackBar") - } - } - .charcoalSnackBar( - isPresenting: $isPresenting, - screenEdge: .top, - screenEdgeSpacing: 96, - text: "ブックマークしました", - thumbnailImage: Image(uiImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64)), - action: { - Button { - print("Tapped") - } label: { - Text("編集") + + NavigationView(content: { + TabView { + ZStack { + ZStack { + Button { + isPresenting.toggle() + isPresenting3.toggle() + } label: { + Text("Toggle SnackBar") + } } + .charcoalSnackBar( + isPresenting: $isPresenting, + screenEdge: .top, + text: "ブックマークしました", + thumbnailImage: Image(uiImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64)), + action: { + Button { + print("Tapped") + } label: { + Text("編集") + } + } + ) + .charcoalSnackBar( + isPresenting: $isPresenting2, + screenEdge: .bottom, + text: "ブックマークしました", + dismissAfter: 2, + action: { + Button { + print("Tapped") + } label: { + Text("編集") + } + } + ) + .charcoalSnackBar( + isPresenting: $isPresenting3, + screenEdgeSpacing: 275, + text: "ブックマークしました" + ) } - ) - .charcoalSnackBar( - isPresenting: $isPresenting2, - screenEdgeSpacing: 192, - text: "ブックマークしました", - dismissAfter: 2, - action: { - Button { - print("Tapped") - } label: { - Text("編集") - } + + .tabItem { + Image(systemName: "1.circle") + Text("First") } - ) - .charcoalSnackBar( - isPresenting: $isPresenting3, - screenEdgeSpacing: 275, - text: "ブックマークしました" - ) - } + + Text("Second Tab") + .tabItem { + Image(systemName: "2.circle") + Text("Second") + } + + + Text("Third Tab") + .tabItem { + Image(systemName: "3.circle") + Text("Third") + } + } + .navigationTitle("Snackbar") + }) .charcoalOverlayContainer() } } diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift index 8f1223eb8..093a80710 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift @@ -68,7 +68,7 @@ struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBase { } } -public extension View { +extension View { func charcoalAnimatableToast( isPresenting: Binding, isActuallyPresenting: Binding, diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastDraggableModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastDraggableModifier.swift new file mode 100644 index 000000000..c3076d550 --- /dev/null +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastDraggableModifier.swift @@ -0,0 +1,72 @@ +import SwiftUI + +struct CharcoalToastDraggableModifier: ViewModifier, CharcoalToastDraggable { + let screenEdge: CharcoalPopupViewEdge + + @Binding var isPresenting: Bool + + @Binding var offset: CGSize + + @Binding var dragVelocity: CGSize + + @Binding var isDragging: Bool + + func body(content: Content) -> some View { + content + .offset(y: offset.height) + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { gesture in + let translationInDirection = gesture.translation.height*screenEdge.direction + dragVelocity = gesture.velocity + + isDragging = true + if (translationInDirection < 0) { + offset = CGSize(width: 0, + height: gesture.translation.height) + } else { + // the less the faster resistance + let limit: CGFloat = 60 + let yOff = gesture.translation.height + let dist = sqrt(yOff*yOff) + let factor = 1 / (dist / limit + 1) + + offset = CGSize(width: 0, + height: gesture.translation.height * factor) + } + } + .onEnded { _ in + isDragging = false + let movingVelocityInDirection = dragVelocity.height * screenEdge.direction + let offsetInDirection = offset.height * screenEdge.direction + + if offsetInDirection < -50 || movingVelocityInDirection < -100 { + // remove the card + isPresenting = false + let animation = Animation.interpolatingSpring(initialVelocity: dragVelocity.height) + withAnimation(animation) { + offset = .zero + } + } else { + withAnimation(.bouncy) { + offset = .zero + } + } + } + ) + } +} + +extension View { + func charcoalDraggableToast(screenEdge: CharcoalPopupViewEdge, + isPresenting: Binding, + offset: Binding, + dragVelocity: Binding, + isDragging: Binding) -> some View { + self.modifier(CharcoalToastDraggableModifier(screenEdge: screenEdge, + isPresenting: isPresenting, + offset: offset, + dragVelocity: dragVelocity, + isDragging: isDragging)) + } +} From 82b5c891907da3f997a6926269e8b551c972e829 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 6 Mar 2024 14:54:41 +0900 Subject: [PATCH 086/199] Format code --- .../CharcoalSwiftUISample/ToastsView.swift | 2 +- .../Components/Toast/CharcoalSnackBar.swift | 91 +++++++++---------- .../Components/Toast/CharcoalToast.swift | 2 +- .../CharcoalToastAnimatableModifier.swift | 12 +-- .../CharcoalToastDraggableModifier.swift | 58 +++++++----- .../Toast/CharcoalToastProtocol.swift | 10 +- .../Modal/CharcoalModalView.swift | 1 + 7 files changed, 91 insertions(+), 85 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift index 10925e750..9b1fef4b4 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift @@ -66,7 +66,7 @@ public struct ToastsView: View { } } ) - + VStack(alignment: .leading) { Button { isPresenting4.toggle() diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index f644eddaf..8fe6bc812 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -2,46 +2,46 @@ import SwiftUI struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToastBase, CharcoalToastActionable, CharcoalToastDraggable { typealias IDValue = UUID - + let id: IDValue - + let text: String - + /// The thumbnail image of the snackbar let thumbnailImage: Image? - + let maxWidth: CGFloat - + let cornerRadius: CGFloat = 32 - + let borderColor: Color - + let borderLineWidth: CGFloat = 1 - + let screenEdge: CharcoalPopupViewEdge - + let screenEdgeSpacing: CGFloat - + /// The content of the action view let action: ActionContent? - + @State private var tooltipSize: CGSize = .zero - + /// A binding to whether the overlay is presented. @Binding var isPresenting: Bool - + let dismissAfter: TimeInterval? - + @State var isActuallyPresenting: Bool = false - + var animationConfiguration: CharcoalToastAnimationConfiguration - - @State internal var offset = CGSize.zero - - @State internal var dragVelocity = CGSize.zero - - @State internal var isDragging = false - + + @State var offset = CGSize.zero + + @State var dragVelocity = CGSize.zero + + @State var isDragging = false + init( id: IDValue, text: String, @@ -64,9 +64,9 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa self.dismissAfter = dismissAfter self.screenEdge = screenEdge self.animationConfiguration = animationConfiguration - self.borderColor = Color(CharcoalAsset.ColorPaletteGenerated.border.color) + borderColor = Color(CharcoalAsset.ColorPaletteGenerated.border.color) } - + var body: some View { ZStack(alignment: screenEdge.alignment) { Color.clear @@ -81,7 +81,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa Text(text) .charcoalTypography14Bold(isSingleLine: true) .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text1.color)) - + if let action = action { action .charcoalDefaultButton(size: .small) @@ -106,11 +106,10 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa animationConfiguration: animationConfiguration ) .charcoalDraggableToast(screenEdge: screenEdge, isPresenting: $isPresenting, offset: $offset, dragVelocity: $dragVelocity, isDragging: $isDragging) - } .frame(minWidth: 0, maxWidth: maxWidth, alignment: .center) } - + static func == (lhs: CharcoalSnackBar, rhs: CharcoalSnackBar) -> Bool { return lhs.text == rhs.text && lhs.maxWidth == rhs.maxWidth && lhs.thumbnailImage == rhs.thumbnailImage && lhs.isPresenting == rhs.isPresenting } @@ -119,35 +118,35 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa struct CharcoalSnackBarModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool - + /// The edge of the screen where the snackbar will be presented let screenEdge: CharcoalPopupViewEdge - + /// The spacing between the snackbar and the screen edge let screenEdgeSpacing: CGFloat - + /// Text to be displayed in the snackbar let text: String - + /// The thumbnail image to be displayed in the snackbar let thumbnailImage: Image? - + /// The action to be displayed in the snackbar @ViewBuilder let action: () -> ActionContent? - + /// Assign a unique ID to the view @State var viewID = UUID() - + /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? - + func body(content: Content) -> some View { content .overlay(Color.clear .modifier( CharcoalOverlayUpdaterContainer( isPresenting: $isPresenting, - + view: CharcoalSnackBar( id: viewID, text: text, @@ -166,7 +165,7 @@ struct CharcoalSnackBarModifier: ViewModifier { public extension View { /** Add a tooltip to the view - + - Parameters: - isPresenting: A binding to whether the Tooltip is presented. - text: The text to be displayed in the snackbar. @@ -175,7 +174,7 @@ public extension View { - screenEdge: The edge of the screen where the snackbar will be presented - screenEdgeSpacing: The spacing between the snackbar and the screen edge - action: The action to be displayed in the snackbar. - + # Example # ```swift Text("Hello").charcoalSnackBar(isPresenting: $isPresenting, text: "Hello") @@ -216,15 +215,14 @@ private extension UIColor { private struct SnackBarsPreviewView: View { @State var isPresenting = true - + @State var isPresenting2 = true - + @State var isPresenting3 = true - + @State var textOfLabel = "Hello" - + var body: some View { - NavigationView(content: { TabView { ZStack { @@ -268,19 +266,18 @@ private struct SnackBarsPreviewView: View { text: "ブックマークしました" ) } - + .tabItem { Image(systemName: "1.circle") Text("First") } - + Text("Second Tab") .tabItem { Image(systemName: "2.circle") Text("Second") } - - + Text("Third Tab") .tabItem { Image(systemName: "3.circle") diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index a7f54a239..ed9998106 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -59,7 +59,7 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalToastB self.appearance = appearance self.screenEdge = screenEdge self.animationConfiguration = animationConfiguration - self.borderColor = Color(CharcoalAsset.ColorPaletteGenerated.background1.color) + borderColor = Color(CharcoalAsset.ColorPaletteGenerated.background1.color) } var body: some View { diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift index 093a80710..3f5c4c053 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift @@ -1,5 +1,5 @@ -import SwiftUI import Combine +import SwiftUI struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBase { var text: String @@ -25,9 +25,9 @@ struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBase { let animationConfiguration: CharcoalToastAnimationConfiguration let dismissAfter: TimeInterval? - + @Binding var isDragging: Bool - + @State var timer: Timer? func body(content: Content) -> some View { @@ -46,15 +46,15 @@ struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBase { }) .onChange(of: isActuallyPresenting) { newValue in if let dismissAfter = dismissAfter, newValue { - timer = Timer.scheduledTimer(withTimeInterval: dismissAfter, repeats: false, block: { timer in + timer = Timer.scheduledTimer(withTimeInterval: dismissAfter, repeats: false, block: { _ in if !isDragging { isPresenting = false } }) } } - .onChange(of: isDragging, perform: { newValue in - if (isDragging) { + .onChange(of: isDragging, perform: { _ in + if isDragging { timer?.invalidate() } }) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastDraggableModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastDraggableModifier.swift index c3076d550..f883d53fe 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastDraggableModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastDraggableModifier.swift @@ -2,44 +2,48 @@ import SwiftUI struct CharcoalToastDraggableModifier: ViewModifier, CharcoalToastDraggable { let screenEdge: CharcoalPopupViewEdge - + @Binding var isPresenting: Bool - + @Binding var offset: CGSize - + @Binding var dragVelocity: CGSize - + @Binding var isDragging: Bool - + func body(content: Content) -> some View { content .offset(y: offset.height) .gesture( DragGesture(minimumDistance: 0) .onChanged { gesture in - let translationInDirection = gesture.translation.height*screenEdge.direction + let translationInDirection = gesture.translation.height * screenEdge.direction dragVelocity = gesture.velocity - + isDragging = true - if (translationInDirection < 0) { - offset = CGSize(width: 0, - height: gesture.translation.height) + if translationInDirection < 0 { + offset = CGSize( + width: 0, + height: gesture.translation.height + ) } else { // the less the faster resistance let limit: CGFloat = 60 let yOff = gesture.translation.height - let dist = sqrt(yOff*yOff) + let dist = sqrt(yOff * yOff) let factor = 1 / (dist / limit + 1) - - offset = CGSize(width: 0, - height: gesture.translation.height * factor) + + offset = CGSize( + width: 0, + height: gesture.translation.height * factor + ) } } .onEnded { _ in isDragging = false let movingVelocityInDirection = dragVelocity.height * screenEdge.direction let offsetInDirection = offset.height * screenEdge.direction - + if offsetInDirection < -50 || movingVelocityInDirection < -100 { // remove the card isPresenting = false @@ -58,15 +62,19 @@ struct CharcoalToastDraggableModifier: ViewModifier, CharcoalToastDraggable { } extension View { - func charcoalDraggableToast(screenEdge: CharcoalPopupViewEdge, - isPresenting: Binding, - offset: Binding, - dragVelocity: Binding, - isDragging: Binding) -> some View { - self.modifier(CharcoalToastDraggableModifier(screenEdge: screenEdge, - isPresenting: isPresenting, - offset: offset, - dragVelocity: dragVelocity, - isDragging: isDragging)) + func charcoalDraggableToast( + screenEdge: CharcoalPopupViewEdge, + isPresenting: Binding, + offset: Binding, + dragVelocity: Binding, + isDragging: Binding + ) -> some View { + modifier(CharcoalToastDraggableModifier( + screenEdge: screenEdge, + isPresenting: isPresenting, + offset: offset, + dragVelocity: dragVelocity, + isDragging: isDragging + )) } } diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift index ed831299c..3048000f0 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastProtocol.swift @@ -30,9 +30,9 @@ protocol CharcoalToastActionable { } protocol CharcoalToastDraggable { - var offset: CGSize {get set} - - var dragVelocity: CGSize {get set} - - var isDragging: Bool {get set} + var offset: CGSize { get set } + + var dragVelocity: CGSize { get set } + + var isDragging: Bool { get set } } diff --git a/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift b/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift index 5ed444d7e..321cc0dd0 100644 --- a/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift +++ b/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift @@ -1,4 +1,5 @@ import SwiftUI + struct CharcoalModalView: View { /// The title of the modal view. var title: String? From 44c462f9584a130205f806b8bef5f1d68fe23dac Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 12 Mar 2024 16:00:56 +0900 Subject: [PATCH 087/199] Add init structure --- .../Components/Balloon/CharcoalBalloon.swift | 316 ++++++++++++++++++ .../Components/Tooltip/CharcoalTooltip.swift | 3 +- .../Tooltip/TooltipBubbleShape.swift | 2 +- 3 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift new file mode 100644 index 000000000..2deaf3479 --- /dev/null +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -0,0 +1,316 @@ +import SwiftUI + +struct CharcoalBalloon: CharcoalPopupProtocol { + typealias IDValue = UUID + + /// The unique ID of the overlay. + let id: IDValue + /// The text of the tooltip + let text: String + + /// The frame of the target view which the arrow will point to + let targetFrame: CGRect + + /// The maximum width of the tooltip + let maxWidth: CGFloat + + /// The corner radius of the tooltip + let cornerRadius: CGFloat = 8 + + /// The height of the arrow + let arrowHeight: CGFloat = 4 + + /// The spacing between the tooltip and the target view + let spacingToTarget: CGFloat = 3.5 + + /// The spacing between the tooltip and the screen edge + let spacingToScreen: CGFloat = 16 + + @State private var tooltipSize: CGSize = .zero + + /// A binding to whether the overlay is presented. + @Binding var isPresenting: Bool + + /// If true, the overlay will be dismissed when the user taps outside of the overlay. + let dismissOnTouchOutside: Bool + + /// The overlay will be dismissed after a certain time interval. + let dismissAfter: TimeInterval? + + var offset: CGSize { + CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY) + } + + init( + id: IDValue, + text: String, + targetFrame: CGRect, + maxWidth: CGFloat = 184, + isPresenting: Binding, + dismissOnTouchOutside: Bool = false, + dismissAfter: TimeInterval? = nil + ) { + self.id = id + self.text = text + self.targetFrame = targetFrame + self.maxWidth = maxWidth + _isPresenting = isPresenting + self.dismissOnTouchOutside = dismissOnTouchOutside + self.dismissAfter = dismissAfter + } + + func tooltipX(canvasGeometrySize: CGSize) -> CGFloat { + let minX = targetFrame.midX - (tooltipSize.width / 2.0) + + var edgeLeft = minX + + if edgeLeft + tooltipSize.width >= canvasGeometrySize.width { + edgeLeft = canvasGeometrySize.width - tooltipSize.width - spacingToScreen + } else if edgeLeft < spacingToScreen { + edgeLeft = spacingToScreen + } + + return edgeLeft + } + + func tooltipY(canvasGeometrySize: CGSize) -> CGFloat { + let minX = targetFrame.maxY + spacingToTarget + arrowHeight + var edgeBottom = targetFrame.maxY + spacingToTarget + targetFrame.height + if edgeBottom + tooltipSize.height >= canvasGeometrySize.height { + edgeBottom = targetFrame.minY - tooltipSize.height - spacingToTarget - arrowHeight + } + + return min(minX, edgeBottom) + } + + var body: some View { + ZStack { + Color.clear + .if(dismissOnTouchOutside && isPresenting) { view in + view.contentShape(Rectangle()) + .simultaneousGesture( + TapGesture() + .onEnded { _ in + isPresenting = false + } + ) + .simultaneousGesture( + DragGesture() + .onChanged { _ in + isPresenting = false + } + ) + } + if isPresenting { + GeometryReader(content: { canvasGeometry in + VStack { + HStack(alignment: .firstTextBaseline, spacing: 5) { + Text(text) + .charcoalTypography14Bold() + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) + + Image(charocalIcon: .remove16) + .renderingMode(.template) + .foregroundColor(Color.white) + .frame(width: 16+6) + .background(Circle() + .fill(Color.black.opacity(0.35)) + .frame(width: 16+6, height: 16+6)) + .overlay( + EmptyView().frame(width: 45, height: 45) + .contentShape(Rectangle()).onTapGesture { + isPresenting = false + }) + } + .padding(EdgeInsets(top: 7, leading: 9, bottom: 7, trailing: 9)) + .background(GeometryReader(content: { tooltipGeometry in + let tooltipOrigin = tooltipGeometry.frame(in: .global).origin + TooltipBubbleShape( + targetPoint: + CGPoint( + x: targetFrame.midX - tooltipOrigin.x, + y: targetFrame.maxY - tooltipOrigin.y + ), + arrowHeight: arrowHeight, + cornerRadius: cornerRadius, + arrowWidth: 8 + ) + .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color)) + .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) + })) + .offset(CGSize( + width: tooltipX(canvasGeometrySize: canvasGeometry.size), + height: tooltipY(canvasGeometrySize: canvasGeometry.size) + )) + .onPreferenceChange(TooltipSizeKey.self, perform: { value in + tooltipSize = value + }) + .animation(.none, value: tooltipSize) + .animation(.none, value: targetFrame) + } + .frame(minWidth: 0, maxWidth: maxWidth, alignment: .leading) + }) + .onAppear { + if let dismissAfter = dismissAfter { + DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { + isPresenting = false + } + } + } + } + } + .animation(.easeInOut(duration: 0.2), value: isPresenting) + } + + static func == (lhs: CharcoalBalloon, rhs: CharcoalBalloon) -> Bool { + return lhs.text == rhs.text && lhs.targetFrame == rhs.targetFrame && lhs.maxWidth == rhs.maxWidth && lhs.tooltipSize == rhs.tooltipSize + } +} + + +struct CharcoalBalloonModifier: ViewModifier { + /// Presentation `Binding` + @Binding var isPresenting: Bool + + /// Text to be displayed in the tooltip + var text: String + + /// Assign a unique ID to the view + @State var viewID = UUID() + + /// The overlay will be dismissed after a certain time interval. + let dismissAfter: TimeInterval? + + func body(content: Content) -> some View { + content + .overlay(GeometryReader(content: { proxy in + Color.clear + .modifier(CharcoalOverlayUpdaterContainer( + isPresenting: $isPresenting, + view: CharcoalBalloon( + id: viewID, + text: text, + targetFrame: proxy.frame(in: .global), + isPresenting: $isPresenting, + dismissAfter: dismissAfter + ), + viewID: viewID + )) + })) + } +} + +public extension View { + /** + Add a tooltip to the view + + - Parameters: + - isPresenting: A binding to whether the Tooltip is presented. + - text: The text to be displayed in the tooltip. + + # Example # + ```swift + Text("Hello").charcoalTooltip(isPresenting: $isPresenting, text: "This is a tooltip") + ``` + */ + func charcoalBalloon( + isPresenting: Binding, + text: String, + dismissAfter: TimeInterval? = nil + ) -> some View { + return modifier(CharcoalBalloonModifier(isPresenting: isPresenting, text: text, dismissAfter: dismissAfter)) + } +} + +private struct BalloonsPreviewView: View { + @State var isPresenting = true + @State var isPresenting2 = true + @State var isPresenting3 = true + @State var isPresenting4 = true + @State var isPresenting5 = true + @State var isPresenting6 = true + + @State var textOfLabel = "Hello" + + var body: some View { + GeometryReader(content: { geometry in + ScrollView { + ZStack(alignment: .topLeading) { + Color.clear + + VStack { + Text(textOfLabel) + + Button { + textOfLabel = ["Changed", "Hello"].randomElement()! + } label: { + Text("Change Label") + } + } + + Button { + isPresenting.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalBalloon(isPresenting: $isPresenting, text: "Hello World") + .offset(CGSize(width: 20.0, height: 80.0)) + + Button { + isPresenting2.toggle() + } label: { + Text("Help") + } + .charcoalDefaultButton() + .charcoalBalloon(isPresenting: $isPresenting2, text: "Hello World This is a tooltip") + .offset(CGSize(width: 100.0, height: 150.0)) + + Button { + isPresenting3.toggle() + } label: { + Text("Right") + } + .charcoalPrimaryButton(size: .medium) + .charcoalBalloon(isPresenting: $isPresenting3, text: "here is testing it's multiple line feature") + .offset(CGSize(width: geometry.size.width - 100, height: 100.0)) + + Button { + isPresenting4.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalBalloon(isPresenting: $isPresenting4, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + .offset(CGSize(width: geometry.size.width - 30, height: geometry.size.height - 40)) + + Button { + isPresenting5.toggle() + } label: { + Text("Bottom") + } + .charcoalPrimaryButton(size: .medium) + .charcoalBalloon( + isPresenting: $isPresenting5, + text: "Hello World This is a tooltip and here is testing it's multiple line feature", + dismissAfter: 2 + ) + .offset(CGSize(width: geometry.size.width - 240, height: geometry.size.height - 40)) + + Button { + isPresenting6.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalBalloon(isPresenting: $isPresenting6, text: "Hello World This is a tooltip and here is testing it's multiple line feature") + .offset(CGSize(width: geometry.size.width - 380, height: geometry.size.height - 240)) + } + } + }) + .charcoalOverlayContainer() + } +} + +#Preview { + BalloonsPreviewView() +} diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 8fe02d741..8f895adad 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -119,7 +119,8 @@ struct CharcoalTooltip: CharcoalPopupProtocol { y: targetFrame.maxY - tooltipOrigin.y ), arrowHeight: arrowHeight, - cornerRadius: cornerRadius + cornerRadius: cornerRadius, + arrowWidth: 5 ) .fill(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift index f5ef234f2..ce8f2ba74 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift @@ -8,7 +8,7 @@ struct TooltipBubbleShape: Shape { /// The corner radius of the tooltip let cornerRadius: CGFloat /// The width of the arrow - let arrowWidth: CGFloat = 5 + let arrowWidth: CGFloat func path(in rect: CGRect) -> Path { var arrowY = rect.minY - arrowHeight From 8bbe79e67b23d7ade3343ef4418d7dfd25799694 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 12 Mar 2024 16:32:49 +0900 Subject: [PATCH 088/199] Add action button --- .../Components/Balloon/CharcoalBalloon.swift | 87 ++++++++++++------- .../Tooltip/TooltipBubbleShape.swift | 13 +++ 2 files changed, 70 insertions(+), 30 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index 2deaf3479..a833f1300 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -1,6 +1,7 @@ import SwiftUI -struct CharcoalBalloon: CharcoalPopupProtocol { +struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToastActionable { + typealias IDValue = UUID /// The unique ID of the overlay. @@ -15,7 +16,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol { let maxWidth: CGFloat /// The corner radius of the tooltip - let cornerRadius: CGFloat = 8 + let cornerRadius: CGFloat = 16 /// The height of the arrow let arrowHeight: CGFloat = 4 @@ -36,6 +37,8 @@ struct CharcoalBalloon: CharcoalPopupProtocol { /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? + + let action: ActionContent? var offset: CGSize { CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY) @@ -45,10 +48,11 @@ struct CharcoalBalloon: CharcoalPopupProtocol { id: IDValue, text: String, targetFrame: CGRect, - maxWidth: CGFloat = 184, + maxWidth: CGFloat = 240, isPresenting: Binding, dismissOnTouchOutside: Bool = false, - dismissAfter: TimeInterval? = nil + dismissAfter: TimeInterval? = nil, + @ViewBuilder action: () -> ActionContent? ) { self.id = id self.text = text @@ -57,6 +61,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol { _isPresenting = isPresenting self.dismissOnTouchOutside = dismissOnTouchOutside self.dismissAfter = dismissAfter + self.action = action() } func tooltipX(canvasGeometrySize: CGSize) -> CGFloat { @@ -103,28 +108,39 @@ struct CharcoalBalloon: CharcoalPopupProtocol { } if isPresenting { GeometryReader(content: { canvasGeometry in - VStack { - HStack(alignment: .firstTextBaseline, spacing: 5) { - Text(text) - .charcoalTypography14Bold() - .multilineTextAlignment(.leading) - .fixedSize(horizontal: false, vertical: true) - .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) + ZStack { + VStack { + HStack(alignment: .firstTextBaseline, spacing: 5) { + Text(text) + .charcoalTypography14Bold() + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) + + Image(charocalIcon: .remove16) + .renderingMode(.template) + .foregroundColor(Color.white) + .frame(width: 16+6) + .background(Circle() + .fill(Color.black.opacity(0.35)) + .frame(width: 16+6, height: 16+6)) + .overlay( + EmptyView().frame(width: 45, height: 45) + .contentShape(Rectangle()).onTapGesture { + isPresenting = false + }) + } - Image(charocalIcon: .remove16) - .renderingMode(.template) - .foregroundColor(Color.white) - .frame(width: 16+6) - .background(Circle() - .fill(Color.black.opacity(0.35)) - .frame(width: 16+6, height: 16+6)) - .overlay( - EmptyView().frame(width: 45, height: 45) - .contentShape(Rectangle()).onTapGesture { - isPresenting = false - }) + if let action = action { + action + .charcoalTypography14Bold() + .padding(EdgeInsets(top: 3, leading: 16, bottom: 3, trailing: 16)) + .background(Capsule() + .fill(Color.black.opacity(0.35))) + .foregroundColor(Color.white) + } } - .padding(EdgeInsets(top: 7, leading: 9, bottom: 7, trailing: 9)) + .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) .background(GeometryReader(content: { tooltipGeometry in let tooltipOrigin = tooltipGeometry.frame(in: .global).origin TooltipBubbleShape( @@ -170,7 +186,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol { } -struct CharcoalBalloonModifier: ViewModifier { +struct CharcoalBalloonModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool @@ -182,6 +198,8 @@ struct CharcoalBalloonModifier: ViewModifier { /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? + + @ViewBuilder let action: () -> ActionContent? func body(content: Content) -> some View { content @@ -194,7 +212,8 @@ struct CharcoalBalloonModifier: ViewModifier { text: text, targetFrame: proxy.frame(in: .global), isPresenting: $isPresenting, - dismissAfter: dismissAfter + dismissAfter: dismissAfter, + action: action ), viewID: viewID )) @@ -215,12 +234,13 @@ public extension View { Text("Hello").charcoalTooltip(isPresenting: $isPresenting, text: "This is a tooltip") ``` */ - func charcoalBalloon( + func charcoalBalloon( isPresenting: Binding, text: String, - dismissAfter: TimeInterval? = nil + dismissAfter: TimeInterval? = nil, + @ViewBuilder action: @escaping () -> Content = { EmptyView() } ) -> some View { - return modifier(CharcoalBalloonModifier(isPresenting: isPresenting, text: text, dismissAfter: dismissAfter)) + return modifier(CharcoalBalloonModifier(isPresenting: isPresenting, text: text, dismissAfter: dismissAfter, action: action)) } } @@ -255,7 +275,14 @@ private struct BalloonsPreviewView: View { } label: { Image(charocalIcon: .question24) } - .charcoalBalloon(isPresenting: $isPresenting, text: "Hello World") + .charcoalBalloon(isPresenting: $isPresenting, + text: "Hello World") { + Button(action: { + + }, label: { + Text("Button") + }) + } .offset(CGSize(width: 20.0, height: 80.0)) Button { diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift index ce8f2ba74..dec173c84 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift @@ -45,3 +45,16 @@ struct TooltipBubbleShape: Shape { return bubblePath } } + +extension Shape { + /// fills and strokes a shape + public func fill( + _ fillContent: S, + stroke : StrokeStyle + ) -> some View { + ZStack { + self.fill(fillContent) + self.stroke(Color.white, lineWidth: 2) + } + } +} From 4fb12f7243c246346b7c29cbab68c39b429de517 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 12 Mar 2024 16:35:11 +0900 Subject: [PATCH 089/199] Replace placeholder with ja text --- .../Components/Balloon/CharcoalBalloon.swift | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index a833f1300..122e394e3 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -276,13 +276,7 @@ private struct BalloonsPreviewView: View { Image(charocalIcon: .question24) } .charcoalBalloon(isPresenting: $isPresenting, - text: "Hello World") { - Button(action: { - - }, label: { - Text("Button") - }) - } + text: "作品中の特定単語について") .offset(CGSize(width: 20.0, height: 80.0)) Button { @@ -291,7 +285,14 @@ private struct BalloonsPreviewView: View { Text("Help") } .charcoalDefaultButton() - .charcoalBalloon(isPresenting: $isPresenting2, text: "Hello World This is a tooltip") + .charcoalBalloon(isPresenting: $isPresenting2, text: "作品中の特定単語について、単語変換をして読めるようになりました") { + Button(action: { + + }, label: { + Text("詳しく") + }) + } + .offset(CGSize(width: 100.0, height: 150.0)) Button { From 17e93768132eed06d80374abc358aa976ff39cd8 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 12 Mar 2024 17:14:38 +0900 Subject: [PATCH 090/199] Use CGPath union for iOS 16 --- .../Components/Balloon/CharcoalBalloon.swift | 4 +- .../Tooltip/TooltipBubbleShape.swift | 49 ++++++++++++++++--- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index 122e394e3..6b578f922 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -151,9 +151,9 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast ), arrowHeight: arrowHeight, cornerRadius: cornerRadius, - arrowWidth: 8 + arrowWidth: 10 ) - .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color)) + .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) })) .offset(CGSize( diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift index dec173c84..54b5aa093 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift @@ -12,7 +12,7 @@ struct TooltipBubbleShape: Shape { func path(in rect: CGRect) -> Path { var arrowY = rect.minY - arrowHeight - var arrowBaseY = rect.minY + var arrowBaseY = rect.minY + 1 // The minimum and maximum x position of the arrow let minX = rect.minX + cornerRadius + arrowWidth @@ -28,7 +28,7 @@ struct TooltipBubbleShape: Shape { // Check if the arrow should be on top of the tooltip if targetPoint.y > rect.minY { arrowY = rect.maxY + arrowHeight - arrowBaseY = rect.maxY + arrowBaseY = rect.maxY - 1 arrowMaxX = arrowMidX - arrowWidth arrowMinX = arrowMidX + arrowWidth } @@ -36,13 +36,20 @@ struct TooltipBubbleShape: Shape { var bubblePath = RoundedRectangle(cornerRadius: cornerRadius).path(in: rect) let arrowPath = Path { path in path.move(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) - path.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) path.addLine(to: CGPoint(x: arrowMidX, y: arrowY)) - path.closeSubpath() + path.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) +// path.closeSubpath() + } + + var combinedPath = bubblePath.cgPath + + if #available(iOS 16.0, *) { + combinedPath = bubblePath.cgPath.union(arrowPath.cgPath) + } else { + // Fallback on earlier versions } - bubblePath.addPath(arrowPath) - return bubblePath + return Path(combinedPath) } } @@ -50,11 +57,37 @@ extension Shape { /// fills and strokes a shape public func fill( _ fillContent: S, - stroke : StrokeStyle + strokeColor: Color, + lineWidth: CGFloat ) -> some View { ZStack { self.fill(fillContent) - self.stroke(Color.white, lineWidth: 2) + self.stroke(strokeColor, lineWidth: 2) } } } + +private struct BubbleShapePreview: View { + var body: some View { + ZStack { + Color.gray + TooltipBubbleShape( + targetPoint: + CGPoint( + x: 0, + y: 0 + ), + arrowHeight: 4, + cornerRadius: 16, + arrowWidth: 8 + ) + .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) + .frame(width: 240, height: 100) + } + } +} + + +#Preview { + BubbleShapePreview() +} From d145b7e18ff4bdff93c3db73cda304437b5e0118 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 12 Mar 2024 17:14:55 +0900 Subject: [PATCH 091/199] Update TooltipBubbleShape.swift --- .../CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift index 54b5aa093..752ad4e57 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift @@ -83,7 +83,7 @@ private struct BubbleShapePreview: View { ) .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) .frame(width: 240, height: 100) - } + }.ignoresSafeArea() } } From 0e475bcb8993a01895ff64d252fbd3de0eaf0bbe Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 12 Mar 2024 17:23:16 +0900 Subject: [PATCH 092/199] Update CharcoalTooltip.swift --- .../CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 8f895adad..b5811d389 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -120,7 +120,7 @@ struct CharcoalTooltip: CharcoalPopupProtocol { ), arrowHeight: arrowHeight, cornerRadius: cornerRadius, - arrowWidth: 5 + arrowWidth: 7 ) .fill(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) From cd032e06e1cc7aa43ac0c1b2ca085980de5a87a5 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 12 Mar 2024 17:34:12 +0900 Subject: [PATCH 093/199] Use GeometryReader on overlay --- .../Components/Balloon/CharcoalBalloon.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index 6b578f922..d53733a42 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -74,6 +74,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast } else if edgeLeft < spacingToScreen { edgeLeft = spacingToScreen } + return edgeLeft } @@ -154,13 +155,16 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast arrowWidth: 10 ) .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) - .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) + })) + .overlay(GeometryReader(content: { tooltipGeometry in + Color.clear.preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) })) .offset(CGSize( width: tooltipX(canvasGeometrySize: canvasGeometry.size), height: tooltipY(canvasGeometrySize: canvasGeometry.size) )) .onPreferenceChange(TooltipSizeKey.self, perform: { value in + print(value) tooltipSize = value }) .animation(.none, value: tooltipSize) From a2b5f14f688ec43192d5c072a5922e84a0865df8 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 12 Mar 2024 18:00:43 +0900 Subject: [PATCH 094/199] Use new path draw logic --- .../Components/Balloon/CharcoalBalloon.swift | 1 + .../Components/Tooltip/CharcoalTooltip.swift | 2 +- .../Tooltip/TooltipBubbleShape.swift | 103 +++++++++++++----- 3 files changed, 75 insertions(+), 31 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index d53733a42..fa772b4df 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -157,6 +157,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) })) .overlay(GeometryReader(content: { tooltipGeometry in + // GeometryReader size is zero in background, so we use overlay instead Color.clear.preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) })) .offset(CGSize( diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index b5811d389..8f895adad 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -120,7 +120,7 @@ struct CharcoalTooltip: CharcoalPopupProtocol { ), arrowHeight: arrowHeight, cornerRadius: cornerRadius, - arrowWidth: 7 + arrowWidth: 5 ) .fill(Color(CharcoalAsset.ColorPaletteGenerated.surface8.color)) .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift index 752ad4e57..0b5d6f6e4 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift @@ -11,9 +11,10 @@ struct TooltipBubbleShape: Shape { let arrowWidth: CGFloat func path(in rect: CGRect) -> Path { + var mordenAPI = false var arrowY = rect.minY - arrowHeight - var arrowBaseY = rect.minY + 1 - + var arrowBaseY = rect.minY + // The minimum and maximum x position of the arrow let minX = rect.minX + cornerRadius + arrowWidth let maxX = rect.maxX - cornerRadius - arrowWidth @@ -28,28 +29,55 @@ struct TooltipBubbleShape: Shape { // Check if the arrow should be on top of the tooltip if targetPoint.y > rect.minY { arrowY = rect.maxY + arrowHeight - arrowBaseY = rect.maxY - 1 + arrowBaseY = rect.maxY arrowMaxX = arrowMidX - arrowWidth arrowMinX = arrowMidX + arrowWidth } - - var bubblePath = RoundedRectangle(cornerRadius: cornerRadius).path(in: rect) - let arrowPath = Path { path in - path.move(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) - path.addLine(to: CGPoint(x: arrowMidX, y: arrowY)) - path.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) -// path.closeSubpath() - } - - var combinedPath = bubblePath.cgPath - if #available(iOS 16.0, *) { - combinedPath = bubblePath.cgPath.union(arrowPath.cgPath) - } else { - // Fallback on earlier versions + // Fallback on earlier versions + // Draw the bubble with the arrow + let width = rect.width + let height = rect.height + let path = Path { p in + p.move(to: .init(x: rect.minX + cornerRadius, y: rect.minY)) + if arrowBaseY == rect.minY { + p.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) + p.addLine(to: CGPoint(x: arrowMidX, y: arrowY)) + p.addLine(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) + } + p.addLine(to: .init(x: rect.maxX - cornerRadius, y: rect.minY)) + p.addArc( + tangent1End: .init(x: rect.maxX, y: rect.minY), + tangent2End: .init(x: rect.maxX, y: rect.minY + cornerRadius), + radius: cornerRadius + ) + p.addLine(to: .init(x: rect.maxX, y: rect.maxY - cornerRadius)) + p.addArc( + tangent1End: .init(x: rect.maxX, y: rect.maxY), + tangent2End: .init(x: rect.maxX - cornerRadius, y: rect.maxY), + radius: cornerRadius + ) + if arrowBaseY == rect.maxY { + p.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) + p.addLine(to: CGPoint(x: arrowMidX, y: arrowY)) + p.addLine(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) + } + p.addLine(to: .init(x: rect.minX + cornerRadius, y: rect.maxY)) + p.addArc( + tangent1End: .init(x: rect.minX, y: rect.maxY), + tangent2End: .init(x: rect.minX, y: rect.maxY - cornerRadius), + radius: cornerRadius + ) + p.addLine(to: .init(x: rect.minX, y: rect.minY + cornerRadius)) + p.addArc( + tangent1End: .init(x: rect.minX, y: rect.minY), + tangent2End: .init(x: rect.minX + cornerRadius, y: rect.minY), + radius: cornerRadius + ) + p.closeSubpath() } - return Path(combinedPath) + return path } } @@ -71,18 +99,33 @@ private struct BubbleShapePreview: View { var body: some View { ZStack { Color.gray - TooltipBubbleShape( - targetPoint: - CGPoint( - x: 0, - y: 0 - ), - arrowHeight: 4, - cornerRadius: 16, - arrowWidth: 8 - ) - .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) - .frame(width: 240, height: 100) + VStack { + TooltipBubbleShape( + targetPoint: + CGPoint( + x: 0, + y: 0 + ), + arrowHeight: 4, + cornerRadius: 16, + arrowWidth: 8 + ) + .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) + .frame(width: 240, height: 100) + + TooltipBubbleShape( + targetPoint: + CGPoint( + x: 200, + y: 200 + ), + arrowHeight: 4, + cornerRadius: 16, + arrowWidth: 8 + ) + .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) + .frame(width: 240, height: 100) + } }.ignoresSafeArea() } } From 9851ca65a5111977ca8e53efd2fc9848b0d5207f Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 12 Mar 2024 18:02:17 +0900 Subject: [PATCH 095/199] Refine arrow width --- .../CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index fa772b4df..a86cf1a57 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -152,7 +152,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast ), arrowHeight: arrowHeight, cornerRadius: cornerRadius, - arrowWidth: 10 + arrowWidth: 9 ) .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) })) From 5cc7b99cec89fe2edc6ab07b63e56f230de8bf52 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 13 Mar 2024 14:56:22 +0900 Subject: [PATCH 096/199] Refine arrow width --- .../CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index a86cf1a57..f5d430df3 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -152,7 +152,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast ), arrowHeight: arrowHeight, cornerRadius: cornerRadius, - arrowWidth: 9 + arrowWidth: 7 ) .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) })) @@ -165,7 +165,6 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast height: tooltipY(canvasGeometrySize: canvasGeometry.size) )) .onPreferenceChange(TooltipSizeKey.self, perform: { value in - print(value) tooltipSize = value }) .animation(.none, value: tooltipSize) From 3835ba933a1bd5fc274287ca7520f65aacb37fb1 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 13 Mar 2024 15:06:04 +0900 Subject: [PATCH 097/199] Clean Code --- .../Components/Balloon/CharcoalBalloon.swift | 8 +------- .../Components/Tooltip/CharcoalTooltip.swift | 4 ---- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index f5d430df3..b6c795dba 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -22,7 +22,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast let arrowHeight: CGFloat = 4 /// The spacing between the tooltip and the target view - let spacingToTarget: CGFloat = 3.5 + let spacingToTarget: CGFloat = 4 /// The spacing between the tooltip and the screen edge let spacingToScreen: CGFloat = 16 @@ -40,10 +40,6 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast let action: ActionContent? - var offset: CGSize { - CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY) - } - init( id: IDValue, text: String, @@ -262,8 +258,6 @@ private struct BalloonsPreviewView: View { GeometryReader(content: { geometry in ScrollView { ZStack(alignment: .topLeading) { - Color.clear - VStack { Text(textOfLabel) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 8f895adad..d465691c4 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -37,10 +37,6 @@ struct CharcoalTooltip: CharcoalPopupProtocol { /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? - var offset: CGSize { - CGSize(width: targetFrame.midX - (tooltipSize.width / 2.0), height: targetFrame.maxY) - } - init( id: IDValue, text: String, From 14f61fc85078cfc3de007860e61fdbdf062f6373 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 13 Mar 2024 15:09:45 +0900 Subject: [PATCH 098/199] Refine preview --- .../CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index b6c795dba..e48add614 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -256,8 +256,10 @@ private struct BalloonsPreviewView: View { var body: some View { GeometryReader(content: { geometry in + Color.gray.opacity(0.1) ScrollView { ZStack(alignment: .topLeading) { + Color.clear VStack { Text(textOfLabel) @@ -333,6 +335,7 @@ private struct BalloonsPreviewView: View { } } }) + .ignoresSafeArea() .charcoalOverlayContainer() } } From ca5e9ca77021fa12e8d8264a11da0dc8dc2ac69d Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 13 Mar 2024 15:18:11 +0900 Subject: [PATCH 099/199] Use timer instead of DispatchQueue --- .../Components/Balloon/CharcoalBalloon.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index e48add614..132cb7f0d 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -39,6 +39,8 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast let dismissAfter: TimeInterval? let action: ActionContent? + + @State var timer: Timer? init( id: IDValue, @@ -170,11 +172,14 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast }) .onAppear { if let dismissAfter = dismissAfter { - DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { + timer = Timer.scheduledTimer(withTimeInterval: dismissAfter, repeats: false, block: { _ in isPresenting = false - } + }) } } + .onDisappear { + timer?.invalidate() + } } } .animation(.easeInOut(duration: 0.2), value: isPresenting) From 597faf36fade25bdffc49b84d9f794da384eabbe Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 13 Mar 2024 19:03:41 +0900 Subject: [PATCH 100/199] Refine layout logic --- .../Components/Balloon/CharcoalBalloon.swift | 94 ++++++++++--- .../Tooltip/TooltipBubbleShape.swift | 133 ++++++++++++++---- 2 files changed, 184 insertions(+), 43 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index 132cb7f0d..e24356066 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -1,5 +1,23 @@ import SwiftUI +enum CharcoalTooltipLayoutPriority: Codable { + case bottom + case top + case right + case left +} + +struct LayoutPriority { + var priority: CharcoalTooltipLayoutPriority + var spaceArea: CGSize +} + +extension CGSize { + var area: CGFloat { + return width * height + } +} + struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToastActionable { typealias IDValue = UUID @@ -61,6 +79,54 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast self.dismissAfter = dismissAfter self.action = action() } + + func intersectionArea(rectA: (x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat), + rectB: (x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat)) -> CGFloat { + let xOverlap = max(0, min(rectA.x2, rectB.x2) - max(rectA.x1, rectB.x1)) + let yOverlap = max(0, min(rectA.y2, rectB.y2) - max(rectA.y1, rectB.y1)) + + return xOverlap * yOverlap // 面积 + } + + func layoutTooltip(canvasGeometrySize: CGSize) -> CGSize { + // Check avaliable area for each direction, compare area size with tooltip size. + // The priorty is Bottom > Top > Right > Left + var priorities: [LayoutPriority] = [] + + // Calculate layout it by sides + let rightWidth = canvasGeometrySize.width - targetFrame.maxX - spacingToScreen - arrowHeight + let rightHeight = canvasGeometrySize.height - targetFrame.height + priorities.append(LayoutPriority(priority: .right, spaceArea: CGSize(width: rightWidth, height: rightHeight))) + + let leftWidth = targetFrame.minX - spacingToScreen - arrowHeight + let leftHeight = canvasGeometrySize.height - targetFrame.height + priorities.append(LayoutPriority(priority: .left, spaceArea: CGSize(width: leftWidth, height: leftHeight))) + + // Calculate layout it by top and bottom + let bottomHeight = canvasGeometrySize.height - targetFrame.maxY - spacingToScreen - spacingToTarget - arrowHeight + let buttonWidth = canvasGeometrySize.width - spacingToScreen * 2 + priorities.append(LayoutPriority(priority: .bottom, spaceArea: CGSize(width: buttonWidth, height: bottomHeight))) + + let topHeight = targetFrame.minY - spacingToScreen - arrowHeight - spacingToTarget + let topWidth = canvasGeometrySize.width - spacingToScreen * 2 + priorities.append(LayoutPriority(priority: .top, spaceArea: CGSize(width: topWidth, height: topHeight))) + + + // Get the ideal layout plan + let layoutPlan = priorities.first(where: { $0.spaceArea.width >= tooltipSize.width && $0.spaceArea.height >= tooltipSize.height }) ?? priorities.sorted(by: { intersectionArea(rectA: (x1: 0, y1: 0, x2: $0.spaceArea.width, y2: $0.spaceArea.height), rectB: (x1: 0, y1: 0, x2: tooltipSize.width, y2: tooltipSize.height)) > intersectionArea(rectA: (x1: 0, y1: 0, x2: $1.spaceArea.width, y2: $1.spaceArea.height), rectB: (x1: 0, y1: 0, x2: tooltipSize.width, y2: tooltipSize.height)) }).first! + + + switch layoutPlan.priority { + case .bottom: + return CGSize(width: tooltipX(canvasGeometrySize: canvasGeometrySize), height: targetFrame.maxY + spacingToTarget + arrowHeight) + case .top: + return CGSize(width: tooltipX(canvasGeometrySize: canvasGeometrySize), height: targetFrame.minY - spacingToTarget - tooltipSize.height - arrowHeight) + case .right: + return CGSize(width: targetFrame.maxX + spacingToTarget + arrowHeight, height: targetFrame.midY - tooltipSize.height / 2.0) + case .left: + return CGSize(width: targetFrame.minX - tooltipSize.width - spacingToTarget - arrowHeight, height: targetFrame.midY - tooltipSize.height / 2.0) + } + } func tooltipX(canvasGeometrySize: CGSize) -> CGFloat { let minX = targetFrame.midX - (tooltipSize.width / 2.0) @@ -146,7 +212,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast targetPoint: CGPoint( x: targetFrame.midX - tooltipOrigin.x, - y: targetFrame.maxY - tooltipOrigin.y + y: targetFrame.midY - tooltipOrigin.y ), arrowHeight: arrowHeight, cornerRadius: cornerRadius, @@ -158,10 +224,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast // GeometryReader size is zero in background, so we use overlay instead Color.clear.preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) })) - .offset(CGSize( - width: tooltipX(canvasGeometrySize: canvasGeometry.size), - height: tooltipY(canvasGeometrySize: canvasGeometry.size) - )) + .offset(layoutTooltip(canvasGeometrySize: canvasGeometry.size)) .onPreferenceChange(TooltipSizeKey.self, perform: { value in tooltipSize = value }) @@ -265,22 +328,22 @@ private struct BalloonsPreviewView: View { ScrollView { ZStack(alignment: .topLeading) { Color.clear - VStack { - Text(textOfLabel) - - Button { - textOfLabel = ["Changed", "Hello"].randomElement()! - } label: { - Text("Change Label") - } - } +// VStack { +// Text(textOfLabel) +// +// Button { +// textOfLabel = ["Changed", "Hello"].randomElement()! +// } label: { +// Text("Change Label") +// } +// } Button { isPresenting.toggle() } label: { Image(charocalIcon: .question24) } - .charcoalBalloon(isPresenting: $isPresenting, + .charcoalBalloon(isPresenting: $isPresenting, text: "作品中の特定単語について") .offset(CGSize(width: 20.0, height: 80.0)) @@ -340,7 +403,6 @@ private struct BalloonsPreviewView: View { } } }) - .ignoresSafeArea() .charcoalOverlayContainer() } } diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift index 0b5d6f6e4..5093e0502 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift @@ -11,36 +11,36 @@ struct TooltipBubbleShape: Shape { let arrowWidth: CGFloat func path(in rect: CGRect) -> Path { - var mordenAPI = false - var arrowY = rect.minY - arrowHeight - var arrowBaseY = rect.minY - // The minimum and maximum x position of the arrow - let minX = rect.minX + cornerRadius + arrowWidth - let maxX = rect.maxX - cornerRadius - arrowWidth - - // The x position of the arrow - let arrowMidX = min(max(minX, targetPoint.x), maxX) - - var arrowMaxX = arrowMidX + arrowWidth - - var arrowMinX = arrowMidX - arrowWidth - - // Check if the arrow should be on top of the tooltip - if targetPoint.y > rect.minY { - arrowY = rect.maxY + arrowHeight - arrowBaseY = rect.maxY - arrowMaxX = arrowMidX - arrowWidth - arrowMinX = arrowMidX + arrowWidth + var layoutDirection: CharcoalTooltipLayoutPriority + + if targetPoint.x < rect.minX && targetPoint.y > rect.minY { + layoutDirection = .left + } else if targetPoint.x > rect.maxX && targetPoint.y > rect.minY { + layoutDirection = .right + } else if targetPoint.y < rect.minY { + layoutDirection = .bottom + } else { + layoutDirection = .top } - // Fallback on earlier versions - // Draw the bubble with the arrow - let width = rect.width - let height = rect.height let path = Path { p in p.move(to: .init(x: rect.minX + cornerRadius, y: rect.minY)) - if arrowBaseY == rect.minY { + if layoutDirection == .bottom { + let arrowY = rect.minY - arrowHeight + let arrowBaseY = rect.minY + + // The minimum and maximum x position of the arrow + let minX = rect.minX + cornerRadius + arrowWidth + let maxX = rect.maxX - cornerRadius - arrowWidth + + // The x position of the arrow + let arrowMidX = min(max(minX, targetPoint.x), maxX) + + let arrowMaxX = arrowMidX + arrowWidth + + let arrowMinX = arrowMidX - arrowWidth + p.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) p.addLine(to: CGPoint(x: arrowMidX, y: arrowY)) p.addLine(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) @@ -51,13 +51,45 @@ struct TooltipBubbleShape: Shape { tangent2End: .init(x: rect.maxX, y: rect.minY + cornerRadius), radius: cornerRadius ) + if layoutDirection == .right { + let arrowX = rect.maxX + arrowHeight + let arrowBaseX = rect.maxX + + // The minimum and maximum x position of the arrow + let minY = rect.minY + cornerRadius + arrowWidth + let maxY = rect.maxY - cornerRadius - arrowWidth + + // The x position of the arrow + let arrowMidY = min(max(minY, targetPoint.y), maxY) + + let arrowMaxY = arrowMidY + arrowWidth + + let arrowMinY = arrowMidY - arrowWidth + + p.addLine(to: CGPoint(x: arrowBaseX, y: arrowMinY)) + p.addLine(to: CGPoint(x: arrowX, y: arrowMidY)) + p.addLine(to: CGPoint(x: arrowBaseX, y: arrowMaxY)) + } p.addLine(to: .init(x: rect.maxX, y: rect.maxY - cornerRadius)) p.addArc( tangent1End: .init(x: rect.maxX, y: rect.maxY), tangent2End: .init(x: rect.maxX - cornerRadius, y: rect.maxY), radius: cornerRadius ) - if arrowBaseY == rect.maxY { + if layoutDirection == .top { + let arrowY = rect.maxY + arrowHeight + let arrowBaseY = rect.maxY + + // The minimum and maximum x position of the arrow + let minX = rect.minX + cornerRadius + arrowWidth + let maxX = rect.maxX - cornerRadius - arrowWidth + + // The x position of the arrow + let arrowMidX = min(max(minX, targetPoint.x), maxX) + + + let arrowMaxX = arrowMidX - arrowWidth + let arrowMinX = arrowMidX + arrowWidth p.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) p.addLine(to: CGPoint(x: arrowMidX, y: arrowY)) p.addLine(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) @@ -68,6 +100,25 @@ struct TooltipBubbleShape: Shape { tangent2End: .init(x: rect.minX, y: rect.maxY - cornerRadius), radius: cornerRadius ) + if layoutDirection == .left { + let arrowX = rect.minX - arrowHeight + let arrowBaseX = rect.minX + + // The minimum and maximum x position of the arrow + let minY = rect.minY + cornerRadius + arrowWidth + let maxY = rect.maxY - cornerRadius - arrowWidth + + // The x position of the arrow + let arrowMidY = min(max(minY, targetPoint.y), maxY) + + let arrowMaxY = arrowMidY + arrowWidth + + let arrowMinY = arrowMidY - arrowWidth + + p.addLine(to: CGPoint(x: arrowBaseX, y: arrowMaxY)) + p.addLine(to: CGPoint(x: arrowX, y: arrowMidY)) + p.addLine(to: CGPoint(x: arrowBaseX, y: arrowMinY)) + } p.addLine(to: .init(x: rect.minX, y: rect.minY + cornerRadius)) p.addArc( tangent1End: .init(x: rect.minX, y: rect.minY), @@ -104,7 +155,7 @@ private struct BubbleShapePreview: View { targetPoint: CGPoint( x: 0, - y: 0 + y: -10 ), arrowHeight: 4, cornerRadius: 16, @@ -125,6 +176,34 @@ private struct BubbleShapePreview: View { ) .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) .frame(width: 240, height: 100) + + + TooltipBubbleShape( + targetPoint: + CGPoint( + x: 300, + y: 50 + ), + arrowHeight: 4, + cornerRadius: 16, + arrowWidth: 8 + ) + .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) + .frame(width: 240, height: 100) + + + TooltipBubbleShape( + targetPoint: + CGPoint( + x: -10, + y: 50 + ), + arrowHeight: 4, + cornerRadius: 16, + arrowWidth: 8 + ) + .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) + .frame(width: 240, height: 100) } }.ignoresSafeArea() } From 996ea884ad0f49abb516974898a39737796868fe Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 13 Mar 2024 19:16:25 +0900 Subject: [PATCH 101/199] Refine arrow logic --- .../Tooltip/TooltipBubbleShape.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift index 5093e0502..dad4bbece 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift @@ -12,21 +12,21 @@ struct TooltipBubbleShape: Shape { func path(in rect: CGRect) -> Path { - var layoutDirection: CharcoalTooltipLayoutPriority + var pointPosition: CharcoalTooltipLayoutPriority if targetPoint.x < rect.minX && targetPoint.y > rect.minY { - layoutDirection = .left + pointPosition = .left } else if targetPoint.x > rect.maxX && targetPoint.y > rect.minY { - layoutDirection = .right + pointPosition = .right } else if targetPoint.y < rect.minY { - layoutDirection = .bottom + pointPosition = .top } else { - layoutDirection = .top + pointPosition = .bottom } let path = Path { p in p.move(to: .init(x: rect.minX + cornerRadius, y: rect.minY)) - if layoutDirection == .bottom { + if pointPosition == .top { let arrowY = rect.minY - arrowHeight let arrowBaseY = rect.minY @@ -51,7 +51,7 @@ struct TooltipBubbleShape: Shape { tangent2End: .init(x: rect.maxX, y: rect.minY + cornerRadius), radius: cornerRadius ) - if layoutDirection == .right { + if pointPosition == .right { let arrowX = rect.maxX + arrowHeight let arrowBaseX = rect.maxX @@ -76,7 +76,7 @@ struct TooltipBubbleShape: Shape { tangent2End: .init(x: rect.maxX - cornerRadius, y: rect.maxY), radius: cornerRadius ) - if layoutDirection == .top { + if pointPosition == .bottom { let arrowY = rect.maxY + arrowHeight let arrowBaseY = rect.maxY @@ -100,7 +100,7 @@ struct TooltipBubbleShape: Shape { tangent2End: .init(x: rect.minX, y: rect.maxY - cornerRadius), radius: cornerRadius ) - if layoutDirection == .left { + if pointPosition == .left { let arrowX = rect.minX - arrowHeight let arrowBaseX = rect.minX From bb8cc9630b568ffff0c3b91b09216b060f4223b9 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 26 Mar 2024 13:53:09 +0900 Subject: [PATCH 102/199] Refine layout logic --- .../Components/Balloon/CharcoalBalloon.swift | 32 ++++++------------- .../Extensions/Rect+Extension.swift | 11 +++++++ 2 files changed, 20 insertions(+), 23 deletions(-) create mode 100644 Sources/CharcoalSwiftUI/Extensions/Rect+Extension.swift diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index e24356066..8c181d99c 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -10,6 +10,10 @@ enum CharcoalTooltipLayoutPriority: Codable { struct LayoutPriority { var priority: CharcoalTooltipLayoutPriority var spaceArea: CGSize + + var rect: CGRect { + return CGRect(x: 0, y: 0, width: spaceArea.width, height: spaceArea.height) + } } extension CGSize { @@ -80,15 +84,8 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast self.action = action() } - func intersectionArea(rectA: (x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat), - rectB: (x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat)) -> CGFloat { - let xOverlap = max(0, min(rectA.x2, rectB.x2) - max(rectA.x1, rectB.x1)) - let yOverlap = max(0, min(rectA.y2, rectB.y2) - max(rectA.y1, rectB.y1)) - - return xOverlap * yOverlap // 面积 - } - - func layoutTooltip(canvasGeometrySize: CGSize) -> CGSize { + /// Calculate the position of the tooltip + func positionOfOverlay(canvasGeometrySize: CGSize) -> CGSize { // Check avaliable area for each direction, compare area size with tooltip size. // The priorty is Bottom > Top > Right > Left var priorities: [LayoutPriority] = [] @@ -111,10 +108,10 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast let topWidth = canvasGeometrySize.width - spacingToScreen * 2 priorities.append(LayoutPriority(priority: .top, spaceArea: CGSize(width: topWidth, height: topHeight))) + let tooltipRect = CGRect(x: 0, y: 0, width: tooltipSize.width, height: tooltipSize.height) // Get the ideal layout plan - let layoutPlan = priorities.first(where: { $0.spaceArea.width >= tooltipSize.width && $0.spaceArea.height >= tooltipSize.height }) ?? priorities.sorted(by: { intersectionArea(rectA: (x1: 0, y1: 0, x2: $0.spaceArea.width, y2: $0.spaceArea.height), rectB: (x1: 0, y1: 0, x2: tooltipSize.width, y2: tooltipSize.height)) > intersectionArea(rectA: (x1: 0, y1: 0, x2: $1.spaceArea.width, y2: $1.spaceArea.height), rectB: (x1: 0, y1: 0, x2: tooltipSize.width, y2: tooltipSize.height)) }).first! - + let layoutPlan = priorities.first(where: { $0.spaceArea.width >= tooltipSize.width && $0.spaceArea.height >= tooltipSize.height }) ?? priorities.sorted(by: { $0.rect.intersectionArea(tooltipRect) > $1.rect.intersectionArea(tooltipRect)}).first! switch layoutPlan.priority { case .bottom: @@ -138,21 +135,10 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast } else if edgeLeft < spacingToScreen { edgeLeft = spacingToScreen } - return edgeLeft } - func tooltipY(canvasGeometrySize: CGSize) -> CGFloat { - let minX = targetFrame.maxY + spacingToTarget + arrowHeight - var edgeBottom = targetFrame.maxY + spacingToTarget + targetFrame.height - if edgeBottom + tooltipSize.height >= canvasGeometrySize.height { - edgeBottom = targetFrame.minY - tooltipSize.height - spacingToTarget - arrowHeight - } - - return min(minX, edgeBottom) - } - var body: some View { ZStack { Color.clear @@ -224,7 +210,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast // GeometryReader size is zero in background, so we use overlay instead Color.clear.preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) })) - .offset(layoutTooltip(canvasGeometrySize: canvasGeometry.size)) + .offset(positionOfOverlay(canvasGeometrySize: canvasGeometry.size)) .onPreferenceChange(TooltipSizeKey.self, perform: { value in tooltipSize = value }) diff --git a/Sources/CharcoalSwiftUI/Extensions/Rect+Extension.swift b/Sources/CharcoalSwiftUI/Extensions/Rect+Extension.swift new file mode 100644 index 000000000..58e414d40 --- /dev/null +++ b/Sources/CharcoalSwiftUI/Extensions/Rect+Extension.swift @@ -0,0 +1,11 @@ +import Foundation + + +extension CGRect { + // Calculate the intersection area of two rectangles + func intersectionArea(_ rect: CGRect) -> CGFloat { + let rect = self.intersection(rect) + + return rect.width * rect.height + } +} From 12bc105b079075c039c5802bad8b95028727761e Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 26 Mar 2024 14:32:35 +0900 Subject: [PATCH 103/199] Add missing charcoalOverlayContainer --- .../CharcoalSwiftUISample/BalloonsView.swift | 61 +++++++++++++++++++ .../CharcoalSwiftUISample/TooltipsView.swift | 2 +- 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift new file mode 100644 index 000000000..08ed81dfb --- /dev/null +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift @@ -0,0 +1,61 @@ +import Charcoal +import SwiftUI + +public struct BalloonsView: View { + @State var isPresented = false + + @State var isPresented2 = false + + @State var isPresented3 = false + + @State var isPresented4 = false + + public var body: some View { + VStack { + List { + HStack { + Text("Help") + Button(action: { + isPresented.toggle() + }, label: { + Image(charocalIcon: .question16) + }).charcoalTooltip(isPresenting: $isPresented, text: "Tooltip created by Charcoal") + } + + HStack { + Text("Help (Multiple Line)") + Button(action: { + isPresented2.toggle() + }, label: { + Image(charocalIcon: .question16) + }).charcoalTooltip(isPresenting: $isPresented2, text: "Tooltip created by Charcoal and here is testing it's multiple line feature") + } + + HStack { + Text("Help (Auto-Positioning-Trailing)") + Spacer() + Button(action: { + isPresented4.toggle() + }, label: { + Image(charocalIcon: .question16) + }).charcoalTooltip(isPresenting: $isPresented4, text: "Tooltip created by Charcoal and here is testing Auto-Positioning") + } + } + Spacer() + HStack { + Text("Help (Auto-Positioning-Bottom)") + Button(action: { + isPresented3.toggle() + }, label: { + Image(charocalIcon: .question16) + }).charcoalTooltip(isPresenting: $isPresented3, text: "Tooltip created by Charcoal and here is testing Auto-Positioning") + } + } + .navigationBarTitle("Tooltips") + } +} + +#Preview { + BalloonsView().charcoalOverlayContainer() +} + diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift index 416008dc6..9ba50e070 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/TooltipsView.swift @@ -56,5 +56,5 @@ public struct TooltipsView: View { } #Preview { - TooltipsView() + TooltipsView().charcoalOverlayContainer() } From 8e0794179e12865c749c2deeef0a13bd7f295430 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 26 Mar 2024 14:41:38 +0900 Subject: [PATCH 104/199] Add Balloon to examples --- .../CharcoalSwiftUISample/BalloonsView.swift | 57 ++++++++++++------- .../CharcoalSwiftUISample/ContentView.swift | 3 + 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift index 08ed81dfb..b687027b5 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift @@ -14,41 +14,54 @@ public struct BalloonsView: View { VStack { List { HStack { - Text("Help") - Button(action: { + Text("Tutorial") + Button { isPresented.toggle() - }, label: { - Image(charocalIcon: .question16) - }).charcoalTooltip(isPresenting: $isPresented, text: "Tooltip created by Charcoal") + } label: { + Image(charocalIcon: .question24) + } + .charcoalBalloon(isPresenting: $isPresented, + text: "作品中の特定単語について") } HStack { - Text("Help (Multiple Line)") - Button(action: { + Text("Tutorial (With Action)") + Button { isPresented2.toggle() - }, label: { - Image(charocalIcon: .question16) - }).charcoalTooltip(isPresenting: $isPresented2, text: "Tooltip created by Charcoal and here is testing it's multiple line feature") + } label: { + Image(charocalIcon: .question24) + } + .charcoalBalloon(isPresenting: $isPresented2, text: "作品中の特定単語について、単語変換をして読めるようになりました") { + Button(action: { + + }, label: { + Text("詳しく") + }) + } } HStack { - Text("Help (Auto-Positioning-Trailing)") + Text("Tutorial (Auto-Positioning-Trailing)") Spacer() - Button(action: { - isPresented4.toggle() - }, label: { - Image(charocalIcon: .question16) - }).charcoalTooltip(isPresenting: $isPresented4, text: "Tooltip created by Charcoal and here is testing Auto-Positioning") + Button { + isPresented3.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalBalloon(isPresenting: $isPresented3, + text: "作品中の特定単語について") } } Spacer() HStack { - Text("Help (Auto-Positioning-Bottom)") - Button(action: { - isPresented3.toggle() - }, label: { - Image(charocalIcon: .question16) - }).charcoalTooltip(isPresenting: $isPresented3, text: "Tooltip created by Charcoal and here is testing Auto-Positioning") + Text("Tutorial") + Button { + isPresented4.toggle() + } label: { + Image(charocalIcon: .question24) + } + .charcoalBalloon(isPresenting: $isPresented4, + text: "Hello World This is a tooltip and here is testing it's multiple line feature") } } .navigationBarTitle("Tooltips") diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift index 175475fab..4fa14d0d7 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift @@ -49,6 +49,9 @@ public struct ContentView: View { NavigationLink(destination: ToastsView()) { Text("Toasts") } + NavigationLink(destination: BalloonsView()) { + Text("Balloons") + } } .navigationBarTitle("Charcoal") } From 47eeb08130b11ecb31148b8c3dc2ac56336458b0 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 26 Mar 2024 14:47:50 +0900 Subject: [PATCH 105/199] Add default tutorials --- .../Sources/CharcoalSwiftUISample/BalloonsView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift index b687027b5..ddc9ba04e 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift @@ -4,11 +4,11 @@ import SwiftUI public struct BalloonsView: View { @State var isPresented = false - @State var isPresented2 = false + @State var isPresented2 = true @State var isPresented3 = false - @State var isPresented4 = false + @State var isPresented4 = true public var body: some View { VStack { @@ -61,7 +61,7 @@ public struct BalloonsView: View { Image(charocalIcon: .question24) } .charcoalBalloon(isPresenting: $isPresented4, - text: "Hello World This is a tooltip and here is testing it's multiple line feature") + text: "作品中の特定単語について、単語変換をして読めるようになりました") } } .navigationBarTitle("Tooltips") From a4060aa14edf3939d0dc45af9abd864caa8a5904 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 26 Mar 2024 16:35:26 +0900 Subject: [PATCH 106/199] Remove overlay when disappear --- .../Sources/CharcoalSwiftUISample/BalloonsView.swift | 4 ++-- .../Components/Overlay/CharcoalOverlayContainerModifier.swift | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift index ddc9ba04e..f63182659 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift @@ -4,11 +4,11 @@ import SwiftUI public struct BalloonsView: View { @State var isPresented = false - @State var isPresented2 = true + @State var isPresented2 = false @State var isPresented3 = false - @State var isPresented4 = true + @State var isPresented4 = false public var body: some View { VStack { diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index 07e3be725..dcd3de9b6 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -45,6 +45,9 @@ struct CharcoalOverlayUpdaterContainer: ViewM // onAppear is needed if the overlay is presented by default updateView(view: view) } + .onDisappear { + viewManager.removeView(id: viewID) + } } } From 42c85e236db59210e80a67d177530d08b9f7ba2f Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 26 Mar 2024 16:41:45 +0900 Subject: [PATCH 107/199] Refine charcoalOverlayContainer place --- .../Sources/CharcoalSwiftUISample/ContentView.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift index 4fa14d0d7..8ff481b27 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift @@ -43,19 +43,18 @@ public struct ContentView: View { NavigationLink(destination: ModalsView()) { Text("Modal") } - NavigationLink(destination: TooltipsView()) { + NavigationLink(destination: TooltipsView().charcoalOverlayContainer()) { Text("Tooltips") } - NavigationLink(destination: ToastsView()) { + NavigationLink(destination: ToastsView().charcoalOverlayContainer()) { Text("Toasts") } - NavigationLink(destination: BalloonsView()) { + NavigationLink(destination: BalloonsView().charcoalOverlayContainer()) { Text("Balloons") } } .navigationBarTitle("Charcoal") } - .charcoalOverlayContainer() .preferredColorScheme(isDarkModeOn ? .dark : .light) } } From 77d0aa6d7bde3eee95f3287ace2fe765e28e25de Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 26 Mar 2024 17:54:15 +0900 Subject: [PATCH 108/199] Refine --- .../Sources/CharcoalSwiftUISample/ToastsView.swift | 4 +++- .../Components/Overlay/CharcoalOverlayContainerModifier.swift | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift index 9b1fef4b4..3e979235f 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ToastsView.swift @@ -166,5 +166,7 @@ public struct ToastsView: View { } #Preview { - ToastsView().charcoalOverlayContainer() + NavigationView { + ToastsView() + }.charcoalOverlayContainer() } diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift index dcd3de9b6..07e3be725 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalOverlayContainerModifier.swift @@ -45,9 +45,6 @@ struct CharcoalOverlayUpdaterContainer: ViewM // onAppear is needed if the overlay is presented by default updateView(view: view) } - .onDisappear { - viewManager.removeView(id: viewID) - } } } From f708b1e971f6b8a6e01cc211faa64ba19a4e142a Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 26 Mar 2024 18:17:21 +0900 Subject: [PATCH 109/199] Update CharcoalSnackBar.swift --- Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 8fe6bc812..5b110bab0 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -183,7 +183,7 @@ public extension View { func charcoalSnackBar( isPresenting: Binding, screenEdge: CharcoalPopupViewEdge = .bottom, - screenEdgeSpacing: CGFloat = 120, + screenEdgeSpacing: CGFloat = 150, text: String, thumbnailImage: Image? = nil, dismissAfter: TimeInterval? = nil, From 4e676671e22ed7ceb034262a4816db8537f887df Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 24 Apr 2024 15:58:48 +0800 Subject: [PATCH 110/199] Init CharcoalTooltipView --- .../Components/Tooltip/CharcoalOverlay.swift | 97 +++++++++++++++++++ .../Tooltip/CharcoalTooltipView.swift | 39 ++++++++ 2 files changed, 136 insertions(+) create mode 100644 Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift create mode 100644 Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift new file mode 100644 index 000000000..d4f59d6cb --- /dev/null +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift @@ -0,0 +1,97 @@ +import UIKit + +/** + Displays a overlay on the screen. + */ +public class CharcoalOverlayView: UIView { + /// The window to display the spinner in. + var mainView: UIView! + /// The background view of the spinner. + var backgroundView: UIView? + /// The container view of the spinner. + var containerView: UIView? + + static let shared = CharcoalOverlayView() +} + +// MARK: - Window + +extension CharcoalOverlayView { + /// Initializes the spinner with the given window. + func setupView(view: UIView?) { + if let view = view { + mainView = view + } else { + if mainView == nil { + let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } + .filter { $0.activationState == .foregroundActive } + .first + mainView = scene?.windows.filter { $0.isKeyWindow }.first ?? + UIApplication.shared.windows.first + } + } + } +} + +// MARK: - Background + +extension CharcoalOverlayView { + private func removeBackground() { + backgroundView?.removeFromSuperview() + backgroundView = nil + } + + private func setupBackground(_ interactionPassthrough: Bool) { + if backgroundView == nil { + let bounds = mainView.bounds + backgroundView = UIView(frame: bounds) + + guard let backgroundView = backgroundView else { + fatalError("Background view is nil.") + } + backgroundView.translatesAutoresizingMaskIntoConstraints = false + mainView.addSubview(backgroundView) + + let constraints: [NSLayoutConstraint] = [ + backgroundView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: 0), + backgroundView.topAnchor.constraint(equalTo: mainView.topAnchor, constant: 0), + backgroundView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: 0), + backgroundView.rightAnchor.constraint(equalTo: mainView.rightAnchor, constant: 0) + ] + + NSLayoutConstraint.activate(constraints) + } + + backgroundView?.isUserInteractionEnabled = interactionPassthrough ? false : true + } +} + +// MARK: - Container + +extension CharcoalOverlayView { + private func removeContainer() { + containerView?.removeFromSuperview() + containerView = nil + } + + private func setupContainer(subview: UIView, transparentBackground: Bool = false) { + if containerView == nil { + containerView = UIView() + containerView?.alpha = 0 + guard let containerView = containerView else { + fatalError("Container view is nil.") + } + + containerView.translatesAutoresizingMaskIntoConstraints = false + mainView.addSubview(containerView) + + let constraints: [NSLayoutConstraint] = [ + containerView.centerXAnchor.constraint(equalTo: mainView.centerXAnchor, constant: 0), + containerView.centerYAnchor.constraint(equalTo: mainView.centerYAnchor, constant: 0) + ] + + NSLayoutConstraint.activate(constraints) + } + } +} + diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift new file mode 100644 index 000000000..e37a077a8 --- /dev/null +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift @@ -0,0 +1,39 @@ +import UIKit + +class CharcoalTooltipView: UIView { + + init(text: String) { + super.init(frame: .zero) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayer() { + } + + private func startAnimating() { + + } + + override var intrinsicContentSize: CGSize { + return CGSize.zero + } +} + +@available(iOS 17.0, *) +#Preview(traits: .sizeThatFitsLayout) { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.distribution = .fill + stackView.alignment = .center + stackView.spacing = 8.0 + + let tooltip = CharcoalTooltipView(text: "Hello") + + stackView.addArrangedSubview(tooltip) + + return stackView +} From 225c31ec6d1f843c25437b87317b7bb5471968b7 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 24 Apr 2024 18:08:35 +0800 Subject: [PATCH 111/199] Init Bubble shape --- .../Tooltip/CharcoalTooltipView.swift | 21 ++- .../Tooltip/TooltipBubbleShape_UIKit.swift | 143 ++++++++++++++++++ 2 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 Sources/CharcoalUIKit/Components/Tooltip/TooltipBubbleShape_UIKit.swift diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift index e37a077a8..e1cd21ff0 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift @@ -1,9 +1,19 @@ import UIKit class CharcoalTooltipView: UIView { + + let bubbleShape: TooltipBubbleShape + + /// The corner radius of the tooltip + let cornerRadius: CGFloat = 4 - init(text: String) { + /// The height of the arrow + let arrowHeight: CGFloat = 3 + + init(text: String, targetFrame: CGRect, maxWidth: CGFloat = 184) { + self.bubbleShape = TooltipBubbleShape(targetPoint: .zero, arrowHeight: 3, bubbleRadius: 4, arrowWidth: 7) super.init(frame: .zero) + self.layer.addSublayer(self.bubbleShape) } @available(*, unavailable) @@ -19,7 +29,12 @@ class CharcoalTooltipView: UIView { } override var intrinsicContentSize: CGSize { - return CGSize.zero + return CGSize(width: 100, height: 50) + } + + override func layoutSubviews() { + super.layoutSubviews() + self.bubbleShape.frame = self.bounds } } @@ -31,7 +46,7 @@ class CharcoalTooltipView: UIView { stackView.alignment = .center stackView.spacing = 8.0 - let tooltip = CharcoalTooltipView(text: "Hello") + let tooltip = CharcoalTooltipView(text: "Hello", targetFrame: CGRect.zero) stackView.addArrangedSubview(tooltip) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/TooltipBubbleShape_UIKit.swift b/Sources/CharcoalUIKit/Components/Tooltip/TooltipBubbleShape_UIKit.swift new file mode 100644 index 000000000..ccbae363b --- /dev/null +++ b/Sources/CharcoalUIKit/Components/Tooltip/TooltipBubbleShape_UIKit.swift @@ -0,0 +1,143 @@ +import UIKit + +enum CharcoalTooltipLayoutPriority: Codable { + case bottom + case top + case right + case left +} + +class TooltipBubbleShape: CAShapeLayer { + var targetPoint: CGPoint + var arrowHeight: CGFloat + var bubbleRadius: CGFloat + var arrowWidth: CGFloat + + init(targetPoint: CGPoint, arrowHeight: CGFloat, bubbleRadius: CGFloat, arrowWidth: CGFloat) { + self.targetPoint = targetPoint + self.arrowHeight = arrowHeight + self.bubbleRadius = bubbleRadius + self.arrowWidth = arrowWidth + super.init() + self.fillColor = CharcoalAsset.ColorPaletteGenerated.surface8.color.cgColor + updatePath() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updatePath() { + let rect = bounds + + var pointPosition: CharcoalTooltipLayoutPriority + + if targetPoint.x < rect.minX && targetPoint.y > rect.minY { + pointPosition = .left + } else if targetPoint.x > rect.maxX && targetPoint.y > rect.minY { + pointPosition = .right + } else if targetPoint.y < rect.minY { + pointPosition = .top + } else { + pointPosition = .bottom + } + + let p = UIBezierPath() + p.move(to: .init(x: rect.minX + bubbleRadius, y: rect.minY)) + if pointPosition == .top { + let arrowY = rect.minY - arrowHeight + let arrowBaseY = rect.minY + + // The minimum and maximum x position of the arrow + let minX = rect.minX + bubbleRadius + arrowWidth + let maxX = rect.maxX - bubbleRadius - arrowWidth + + // The x position of the arrow + let arrowMidX = min(max(minX, targetPoint.x), maxX) + + let arrowMaxX = arrowMidX + arrowWidth + + let arrowMinX = arrowMidX - arrowWidth + + p.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) + p.addLine(to: CGPoint(x: arrowMidX, y: arrowY)) + p.addLine(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) + } + + p.addLine(to: .init(x: rect.maxX - bubbleRadius, y: rect.minY)) + p.addArc(withCenter: .init(x: rect.maxX - bubbleRadius, y: rect.minY + bubbleRadius), radius: bubbleRadius, startAngle: CGFloat.pi * 3 / 2, endAngle: 0, clockwise: true) + + if pointPosition == .right { + let arrowX = rect.maxX + arrowHeight + let arrowBaseX = rect.maxX + + // The minimum and maximum x position of the arrow + let minY = rect.minY + bubbleRadius + arrowWidth + let maxY = rect.maxY - bubbleRadius - arrowWidth + + // The x position of the arrow + let arrowMidY = min(max(minY, targetPoint.y), maxY) + + let arrowMaxY = arrowMidY + arrowWidth + + let arrowMinY = arrowMidY - arrowWidth + + p.addLine(to: CGPoint(x: arrowBaseX, y: arrowMinY)) + p.addLine(to: CGPoint(x: arrowX, y: arrowMidY)) + p.addLine(to: CGPoint(x: arrowBaseX, y: arrowMaxY)) + } + + p.addLine(to: .init(x: rect.maxX, y: rect.maxY - bubbleRadius)) + p.addArc(withCenter: .init(x: rect.maxX - bubbleRadius, y: rect.maxY - bubbleRadius), radius: bubbleRadius, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true) + + if pointPosition == .bottom { + let arrowY = rect.maxY + arrowHeight + let arrowBaseY = rect.maxY + + // The minimum and maximum x position of the arrow + let minX = rect.minX + bubbleRadius + arrowWidth + let maxX = rect.maxX - bubbleRadius - arrowWidth + + // The x position of the arrow + let arrowMidX = min(max(minX, targetPoint.x), maxX) + + + let arrowMaxX = arrowMidX - arrowWidth + let arrowMinX = arrowMidX + arrowWidth + p.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) + p.addLine(to: CGPoint(x: arrowMidX, y: arrowY)) + p.addLine(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) + } + p.addLine(to: .init(x: rect.minX + bubbleRadius, y: rect.maxY)) + p.addArc(withCenter: .init(x: rect.minX + bubbleRadius, y: rect.maxY - bubbleRadius), radius: bubbleRadius, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: true) + if pointPosition == .left { + let arrowX = rect.minX - arrowHeight + let arrowBaseX = rect.minX + + // The minimum and maximum x position of the arrow + let minY = rect.minY + bubbleRadius + arrowWidth + let maxY = rect.maxY - bubbleRadius - arrowWidth + + // The x position of the arrow + let arrowMidY = min(max(minY, targetPoint.y), maxY) + + let arrowMaxY = arrowMidY + arrowWidth + + let arrowMinY = arrowMidY - arrowWidth + + p.addLine(to: CGPoint(x: arrowBaseX, y: arrowMaxY)) + p.addLine(to: CGPoint(x: arrowX, y: arrowMidY)) + p.addLine(to: CGPoint(x: arrowBaseX, y: arrowMinY)) + } + p.addLine(to: .init(x: rect.minX, y: rect.minY + bubbleRadius)) + p.addArc(withCenter: .init(x: rect.minX + bubbleRadius, y: rect.minX + bubbleRadius), radius: bubbleRadius, startAngle: CGFloat.pi, endAngle: CGFloat.pi * 3 / 2, clockwise: true) + p.close() + + self.path = p.cgPath + } + + override func layoutSublayers() { + super.layoutSublayers() + updatePath() // 确保路径更新匹配图层的大小 + } +} From 51eff4a93bb8f78d10a2595391b4ab86ded4879e Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 24 Apr 2024 18:30:48 +0800 Subject: [PATCH 112/199] Refine tooltip preview --- .../Tooltip/CharcoalTooltipView.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift index e1cd21ff0..79f0b1d4b 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift @@ -9,9 +9,12 @@ class CharcoalTooltipView: UIView { /// The height of the arrow let arrowHeight: CGFloat = 3 + + /// The width of the arrow + let arrowWidth: CGFloat = 5 - init(text: String, targetFrame: CGRect, maxWidth: CGFloat = 184) { - self.bubbleShape = TooltipBubbleShape(targetPoint: .zero, arrowHeight: 3, bubbleRadius: 4, arrowWidth: 7) + init(text: String, targetPoint: CGPoint, maxWidth: CGFloat = 184) { + self.bubbleShape = TooltipBubbleShape(targetPoint: targetPoint, arrowHeight: arrowHeight, bubbleRadius: cornerRadius, arrowWidth: arrowWidth) super.init(frame: .zero) self.layer.addSublayer(self.bubbleShape) } @@ -46,9 +49,18 @@ class CharcoalTooltipView: UIView { stackView.alignment = .center stackView.spacing = 8.0 - let tooltip = CharcoalTooltipView(text: "Hello", targetFrame: CGRect.zero) + let tooltip = CharcoalTooltipView(text: "Hello", targetPoint: CGPoint(x: 15, y: -5)) + + let tooltip2 = CharcoalTooltipView(text: "Hello", targetPoint: CGPoint(x: 110, y: 10)) + + let tooltip3 = CharcoalTooltipView(text: "Hello", targetPoint: CGPoint(x: 50, y: 55)) + + let tooltip4 = CharcoalTooltipView(text: "Hello", targetPoint: CGPoint(x: -10, y: 25)) stackView.addArrangedSubview(tooltip) + stackView.addArrangedSubview(tooltip2) + stackView.addArrangedSubview(tooltip3) + stackView.addArrangedSubview(tooltip4) return stackView } From b658ce2ce71f24f3eaa14c6094d6dbd66f077572 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 30 Apr 2024 13:52:14 +0800 Subject: [PATCH 113/199] Rename as Charcoal Bubble Shape --- ...ubbleShape_UIKit.swift => CharcoalBubbleShape_UIKit.swift} | 2 +- .../Components/Tooltip/CharcoalTooltipView.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename Sources/CharcoalUIKit/Components/Tooltip/{TooltipBubbleShape_UIKit.swift => CharcoalBubbleShape_UIKit.swift} (99%) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/TooltipBubbleShape_UIKit.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalBubbleShape_UIKit.swift similarity index 99% rename from Sources/CharcoalUIKit/Components/Tooltip/TooltipBubbleShape_UIKit.swift rename to Sources/CharcoalUIKit/Components/Tooltip/CharcoalBubbleShape_UIKit.swift index ccbae363b..7262277d4 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/TooltipBubbleShape_UIKit.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalBubbleShape_UIKit.swift @@ -7,7 +7,7 @@ enum CharcoalTooltipLayoutPriority: Codable { case left } -class TooltipBubbleShape: CAShapeLayer { +class CharcoalBubbleShape: CAShapeLayer { var targetPoint: CGPoint var arrowHeight: CGFloat var bubbleRadius: CGFloat diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift index 79f0b1d4b..b6981a04f 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift @@ -2,7 +2,7 @@ import UIKit class CharcoalTooltipView: UIView { - let bubbleShape: TooltipBubbleShape + let bubbleShape: CharcoalBubbleShape /// The corner radius of the tooltip let cornerRadius: CGFloat = 4 @@ -14,7 +14,7 @@ class CharcoalTooltipView: UIView { let arrowWidth: CGFloat = 5 init(text: String, targetPoint: CGPoint, maxWidth: CGFloat = 184) { - self.bubbleShape = TooltipBubbleShape(targetPoint: targetPoint, arrowHeight: arrowHeight, bubbleRadius: cornerRadius, arrowWidth: arrowWidth) + self.bubbleShape = CharcoalBubbleShape(targetPoint: targetPoint, arrowHeight: arrowHeight, bubbleRadius: cornerRadius, arrowWidth: arrowWidth) super.init(frame: .zero) self.layer.addSublayer(self.bubbleShape) } From 0073b9e95a8503101d8e4095d90115e89691153d Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 30 Apr 2024 14:30:51 +0800 Subject: [PATCH 114/199] Add Label to tooltip --- .../Tooltip/CharcoalTooltipView.swift | 40 ++++++++++++++++--- .../Extensions/StringExtension.swift | 11 +++++ 2 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 Sources/CharcoalUIKit/Extensions/StringExtension.swift diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift index b6981a04f..2f2ced5ae 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift @@ -2,6 +2,16 @@ import UIKit class CharcoalTooltipView: UIView { + lazy var label: CharcoalTypography12 = { + let label = CharcoalTypography12() + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = CharcoalAsset.ColorPaletteGenerated.text5.color + return label + }() + + let text: String + let bubbleShape: CharcoalBubbleShape /// The corner radius of the tooltip @@ -12,11 +22,22 @@ class CharcoalTooltipView: UIView { /// The width of the arrow let arrowWidth: CGFloat = 5 + + /// The max width of the tooltip + let maxWidth: CGFloat + + let padding = UIEdgeInsets(top: 4, left: 12, bottom: 4, right: 12) + + // Text frame size + private var textFrameSize: CGSize = .zero init(text: String, targetPoint: CGPoint, maxWidth: CGFloat = 184) { self.bubbleShape = CharcoalBubbleShape(targetPoint: targetPoint, arrowHeight: arrowHeight, bubbleRadius: cornerRadius, arrowWidth: arrowWidth) + self.maxWidth = maxWidth + self.text = text super.init(frame: .zero) - self.layer.addSublayer(self.bubbleShape) + self.textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) + self.setupLayer() } @available(*, unavailable) @@ -25,6 +46,11 @@ class CharcoalTooltipView: UIView { } private func setupLayer() { + // Setup Bubble Shape + self.layer.addSublayer(self.bubbleShape) + // Setup Label + addSubview(label) + label.text = text } private func startAnimating() { @@ -32,12 +58,14 @@ class CharcoalTooltipView: UIView { } override var intrinsicContentSize: CGSize { - return CGSize(width: 100, height: 50) + return CGSize(width: padding.left + textFrameSize.width + padding.right, height: padding.top + textFrameSize.height + padding.bottom) } override func layoutSubviews() { super.layoutSubviews() self.bubbleShape.frame = self.bounds + + self.label.frame = CGRect(x: padding.left, y: padding.top, width: textFrameSize.width, height: textFrameSize.height) } } @@ -49,13 +77,13 @@ class CharcoalTooltipView: UIView { stackView.alignment = .center stackView.spacing = 8.0 - let tooltip = CharcoalTooltipView(text: "Hello", targetPoint: CGPoint(x: 15, y: -5)) + let tooltip = CharcoalTooltipView(text: "Hello World", targetPoint: CGPoint(x: 15, y: -5)) - let tooltip2 = CharcoalTooltipView(text: "Hello", targetPoint: CGPoint(x: 110, y: 10)) + let tooltip2 = CharcoalTooltipView(text: "Hello World This is a tooltip", targetPoint: CGPoint(x: 110, y: 10)) - let tooltip3 = CharcoalTooltipView(text: "Hello", targetPoint: CGPoint(x: 50, y: 55)) + let tooltip3 = CharcoalTooltipView(text: "here is testing it's multiple line feature", targetPoint: CGPoint(x: 50, y: 55)) - let tooltip4 = CharcoalTooltipView(text: "Hello", targetPoint: CGPoint(x: -10, y: 25)) + let tooltip4 = CharcoalTooltipView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", targetPoint: CGPoint(x: -10, y: 25)) stackView.addArrangedSubview(tooltip) stackView.addArrangedSubview(tooltip2) diff --git a/Sources/CharcoalUIKit/Extensions/StringExtension.swift b/Sources/CharcoalUIKit/Extensions/StringExtension.swift new file mode 100644 index 000000000..6662659e3 --- /dev/null +++ b/Sources/CharcoalUIKit/Extensions/StringExtension.swift @@ -0,0 +1,11 @@ +import UIKit + +extension String { + func calculateFrame(font: UIFont, maxWidth: CGFloat) -> CGSize { + let size = CGSize(width: maxWidth, height: .greatestFiniteMagnitude) + let options: NSStringDrawingOptions = [.usesLineFragmentOrigin, .usesFontLeading] + let attributes = [NSAttributedString.Key.font: font] + let rect = self.boundingRect(with: size, options: options, attributes: attributes, context: nil) + return rect.size + } +} From 15ffb146c20aa02989d0d80e0594252a20f0f82d Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 30 Apr 2024 15:35:10 +0800 Subject: [PATCH 115/199] Update text frame when traitCollection did change --- .../Components/Tooltip/CharcoalTooltipView.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift index 2f2ced5ae..d07385cdd 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift @@ -26,9 +26,10 @@ class CharcoalTooltipView: UIView { /// The max width of the tooltip let maxWidth: CGFloat + /// Padding around the bubble let padding = UIEdgeInsets(top: 4, left: 12, bottom: 4, right: 12) - // Text frame size + /// Text frame size private var textFrameSize: CGSize = .zero init(text: String, targetPoint: CGPoint, maxWidth: CGFloat = 184) { @@ -56,15 +57,18 @@ class CharcoalTooltipView: UIView { private func startAnimating() { } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + self.textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) + } override var intrinsicContentSize: CGSize { return CGSize(width: padding.left + textFrameSize.width + padding.right, height: padding.top + textFrameSize.height + padding.bottom) } override func layoutSubviews() { - super.layoutSubviews() + super.layoutSubviews() self.bubbleShape.frame = self.bounds - self.label.frame = CGRect(x: padding.left, y: padding.top, width: textFrameSize.width, height: textFrameSize.height) } } From 567de1742df52b78b986f05414ace499048c33c7 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 30 Apr 2024 15:37:29 +0800 Subject: [PATCH 116/199] Update CharcoalTooltipView.swift --- .../Components/Tooltip/CharcoalTooltipView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift index d07385cdd..adbf70e38 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift @@ -59,6 +59,8 @@ class CharcoalTooltipView: UIView { } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + self.textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) } @@ -67,7 +69,7 @@ class CharcoalTooltipView: UIView { } override func layoutSubviews() { - super.layoutSubviews() + super.layoutSubviews() self.bubbleShape.frame = self.bounds self.label.frame = CGRect(x: padding.left, y: padding.top, width: textFrameSize.width, height: textFrameSize.height) } From 6b1ae1059b980030553b418a4fca56d262cc82f0 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 30 Apr 2024 17:39:35 +0800 Subject: [PATCH 117/199] Add CharcoalTooltip --- README.md | 2 +- Sources/Charcoal/Charcoal.docc/Charcoal.md | 2 +- .../Components/Tooltip/CharcoalOverlay.swift | 71 ++++++++++++++----- .../Components/Tooltip/CharcoalTooltip.swift | 39 ++++++++++ 4 files changed, 96 insertions(+), 18 deletions(-) create mode 100644 Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift diff --git a/README.md b/README.md index 5e6850d46..9267d0010 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ let label = CharcoalTypography20() label.isBold = true label.text = "Hello" -let buttton = CharcoalPrimaryMButton() +let button = CharcoalPrimaryMButton() button.setTitle("OK", for: .normal) ``` diff --git a/Sources/Charcoal/Charcoal.docc/Charcoal.md b/Sources/Charcoal/Charcoal.docc/Charcoal.md index 694d6650d..01bd49316 100644 --- a/Sources/Charcoal/Charcoal.docc/Charcoal.md +++ b/Sources/Charcoal/Charcoal.docc/Charcoal.md @@ -30,7 +30,7 @@ let label = CharcoalTypography20() label.isBold = true label.text = "Hello" -let buttton = CharcoalPrimaryMButton() +let button = CharcoalPrimaryMButton() button.setTitle("OK", for: .normal) ``` diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift index d4f59d6cb..2f7e8dafa 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift @@ -1,7 +1,7 @@ import UIKit /** - Displays a overlay on the screen. + Displays a overlay on the screen. */ public class CharcoalOverlayView: UIView { /// The window to display the spinner in. @@ -10,7 +10,7 @@ public class CharcoalOverlayView: UIView { var backgroundView: UIView? /// The container view of the spinner. var containerView: UIView? - + static let shared = CharcoalOverlayView() } @@ -18,7 +18,7 @@ public class CharcoalOverlayView: UIView { extension CharcoalOverlayView { /// Initializes the spinner with the given window. - func setupView(view: UIView?) { + func setupSuperView(view: UIView?) { if let view = view { mainView = view } else { @@ -26,8 +26,10 @@ extension CharcoalOverlayView { let scene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } .filter { $0.activationState == .foregroundActive } .first - mainView = scene?.windows.filter { $0.isKeyWindow }.first ?? + mainView = scene?.windows.filter { $0.isKeyWindow }.first ?? UIApplication.shared.windows.first + + print("shows on main") } } } @@ -40,28 +42,29 @@ extension CharcoalOverlayView { backgroundView?.removeFromSuperview() backgroundView = nil } - + private func setupBackground(_ interactionPassthrough: Bool) { if backgroundView == nil { - let bounds = mainView.bounds - backgroundView = UIView(frame: bounds) - + backgroundView = UIView(frame: .zero) + guard let backgroundView = backgroundView else { fatalError("Background view is nil.") } + backgroundView.translatesAutoresizingMaskIntoConstraints = false + mainView.addSubview(backgroundView) - + let constraints: [NSLayoutConstraint] = [ backgroundView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: 0), backgroundView.topAnchor.constraint(equalTo: mainView.topAnchor, constant: 0), backgroundView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: 0), backgroundView.rightAnchor.constraint(equalTo: mainView.rightAnchor, constant: 0) ] - + NSLayoutConstraint.activate(constraints) } - + backgroundView?.isUserInteractionEnabled = interactionPassthrough ? false : true } } @@ -73,25 +76,61 @@ extension CharcoalOverlayView { containerView?.removeFromSuperview() containerView = nil } - - private func setupContainer(subview: UIView, transparentBackground: Bool = false) { + + private func setupContainer() { if containerView == nil { containerView = UIView() containerView?.alpha = 0 guard let containerView = containerView else { fatalError("Container view is nil.") } - + containerView.translatesAutoresizingMaskIntoConstraints = false mainView.addSubview(containerView) - + let constraints: [NSLayoutConstraint] = [ containerView.centerXAnchor.constraint(equalTo: mainView.centerXAnchor, constant: 0), - containerView.centerYAnchor.constraint(equalTo: mainView.centerYAnchor, constant: 0) + containerView.centerYAnchor.constraint(equalTo: mainView.centerYAnchor, constant: 0), + containerView.heightAnchor.constraint(equalTo: mainView.widthAnchor), + containerView.widthAnchor.constraint(equalTo: mainView.widthAnchor), ] + + NSLayoutConstraint.activate(constraints) + } + } +} + +// MARK: - Show, Dismiss +extension CharcoalOverlayView { + func show( + view: UIView, + transparentBackground: Bool = false, + interactionPassthrough: Bool, + anchorPoint: CGPoint? = nil, + on superView: UIView? = nil + ) { + setupSuperView(view: superView) + setupBackground(interactionPassthrough) + setupContainer() + containerView!.addSubview(view) + + if let anchorPoint = anchorPoint { + let constraints: [NSLayoutConstraint] = [ + view.leadingAnchor.constraint(equalTo: containerView!.leadingAnchor, constant: anchorPoint.x), + view.topAnchor.constraint(equalTo: containerView!.topAnchor, constant: anchorPoint.y), + ] NSLayoutConstraint.activate(constraints) } + + display() + } + + func display() { + print("display") + UIView.animate(withDuration: 0.25, animations: { [weak self] in + self?.containerView?.alpha = 1 + }) } } diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift new file mode 100644 index 000000000..bdf8f5f4b --- /dev/null +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -0,0 +1,39 @@ +import UIKit + +class CharcoalTooltip {} + +extension CharcoalTooltip { + static func show(text: String, anchorView: UIView, on: UIView? = nil) { + let anchorPoint = CGPoint(x: 100, y: 0) + let tooltip = CharcoalTooltipView(text: text, targetPoint: anchorPoint) + + tooltip.translatesAutoresizingMaskIntoConstraints = false + CharcoalOverlayView.shared.show(view: tooltip, interactionPassthrough: false, anchorPoint: anchorPoint, on: on) + } +} + + +@available(iOS 17.0, *) +#Preview(traits: .defaultLayout) { + let view = UIView() + + let button = CharcoalPrimaryMButton() + button.setTitle("OK", for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(button) + + NSLayoutConstraint.activate([ + button.centerXAnchor.constraint(equalTo: view.centerXAnchor), + button.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ]) + +// let tooltip2 = CharcoalTooltipView(text: "Hello World This is a tooltip", targetPoint: CGPoint(x: 110, y: 10)) +// +// let tooltip3 = CharcoalTooltipView(text: "here is testing it's multiple line feature", targetPoint: CGPoint(x: 50, y: 55)) +// +// let tooltip4 = CharcoalTooltipView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", targetPoint: CGPoint(x: -10, y: 25)) + + CharcoalTooltip.show(text: "Hello World", anchorView: button) + + return view +} From ef1684c1589fac622cd4989ca3f6c4570c61cf25 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 30 Apr 2024 18:13:16 +0800 Subject: [PATCH 118/199] Can debug show on method --- .../Components/Tooltip/CharcoalOverlay.swift | 15 +++++------ .../Components/Tooltip/CharcoalTooltip.swift | 25 ++++++++++++++----- .../Tooltip/CharcoalTooltipView.swift | 1 + 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift index 2f7e8dafa..2fa6106fa 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift @@ -79,19 +79,18 @@ extension CharcoalOverlayView { private func setupContainer() { if containerView == nil { - containerView = UIView() + containerView = UIView(frame: .zero) containerView?.alpha = 0 guard let containerView = containerView else { fatalError("Container view is nil.") } - containerView.translatesAutoresizingMaskIntoConstraints = false mainView.addSubview(containerView) let constraints: [NSLayoutConstraint] = [ - containerView.centerXAnchor.constraint(equalTo: mainView.centerXAnchor, constant: 0), - containerView.centerYAnchor.constraint(equalTo: mainView.centerYAnchor, constant: 0), - containerView.heightAnchor.constraint(equalTo: mainView.widthAnchor), + containerView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: 0), + containerView.topAnchor.constraint(equalTo: mainView.topAnchor, constant: 0), + containerView.heightAnchor.constraint(equalTo: mainView.heightAnchor), containerView.widthAnchor.constraint(equalTo: mainView.widthAnchor), ] @@ -107,7 +106,7 @@ extension CharcoalOverlayView { view: UIView, transparentBackground: Bool = false, interactionPassthrough: Bool, - anchorPoint: CGPoint? = nil, + anchorView: UIView? = nil, on superView: UIView? = nil ) { setupSuperView(view: superView) @@ -115,7 +114,9 @@ extension CharcoalOverlayView { setupContainer() containerView!.addSubview(view) - if let anchorPoint = anchorPoint { + if let anchorView = anchorView { + let anchorPoint = anchorView.superview!.convert(anchorView.frame.origin, to: containerView) + print("anchor point \(anchorView.frame) converted \(anchorPoint)") let constraints: [NSLayoutConstraint] = [ view.leadingAnchor.constraint(equalTo: containerView!.leadingAnchor, constant: anchorPoint.x), view.topAnchor.constraint(equalTo: containerView!.topAnchor, constant: anchorPoint.y), diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index bdf8f5f4b..b16da7dca 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -4,18 +4,18 @@ class CharcoalTooltip {} extension CharcoalTooltip { static func show(text: String, anchorView: UIView, on: UIView? = nil) { - let anchorPoint = CGPoint(x: 100, y: 0) - let tooltip = CharcoalTooltipView(text: text, targetPoint: anchorPoint) + let tooltip = CharcoalTooltipView(text: text, targetPoint: CGPoint(x: 0, y: 0)) tooltip.translatesAutoresizingMaskIntoConstraints = false - CharcoalOverlayView.shared.show(view: tooltip, interactionPassthrough: false, anchorPoint: anchorPoint, on: on) + CharcoalOverlayView.shared.show(view: tooltip, interactionPassthrough: false, anchorView: anchorView, on: on) } } @available(iOS 17.0, *) -#Preview(traits: .defaultLayout) { +#Preview() { let view = UIView() + view.backgroundColor = UIColor.lightGray let button = CharcoalPrimaryMButton() button.setTitle("OK", for: .normal) @@ -26,14 +26,27 @@ extension CharcoalTooltip { button.centerXAnchor.constraint(equalTo: view.centerXAnchor), button.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) - + + let debugContainerView = UIView(frame: .zero) + debugContainerView.translatesAutoresizingMaskIntoConstraints = false + debugContainerView.backgroundColor = UIColor.blue.withAlphaComponent(0.1) + view.addSubview(debugContainerView) + + NSLayoutConstraint.activate([ + debugContainerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + debugContainerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + debugContainerView.widthAnchor.constraint(equalTo: view.widthAnchor), + debugContainerView.heightAnchor.constraint(equalTo: view.widthAnchor) + ]) // let tooltip2 = CharcoalTooltipView(text: "Hello World This is a tooltip", targetPoint: CGPoint(x: 110, y: 10)) // // let tooltip3 = CharcoalTooltipView(text: "here is testing it's multiple line feature", targetPoint: CGPoint(x: 50, y: 55)) // // let tooltip4 = CharcoalTooltipView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", targetPoint: CGPoint(x: -10, y: 25)) - CharcoalTooltip.show(text: "Hello World", anchorView: button) + DispatchQueue.main.async { + CharcoalTooltip.show(text: "Hello World", anchorView: button, on: debugContainerView) + } return view } diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift index adbf70e38..2f3479a60 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift @@ -65,6 +65,7 @@ class CharcoalTooltipView: UIView { } override var intrinsicContentSize: CGSize { + self.textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) return CGSize(width: padding.left + textFrameSize.width + padding.right, height: padding.top + textFrameSize.height + padding.bottom) } From c7e60aee36d9e6e8461920d6e5f54d2829688e01 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 30 Apr 2024 19:15:54 +0800 Subject: [PATCH 119/199] Can layout point --- .../Components/Tooltip/CharcoalOverlay.swift | 10 ++++++---- .../Components/Tooltip/CharcoalTooltip.swift | 4 ++-- .../Components/Tooltip/CharcoalTooltipView.swift | 8 ++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift index 2fa6106fa..5b77ee431 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift @@ -115,11 +115,13 @@ extension CharcoalOverlayView { containerView!.addSubview(view) if let anchorView = anchorView { - let anchorPoint = anchorView.superview!.convert(anchorView.frame.origin, to: containerView) - print("anchor point \(anchorView.frame) converted \(anchorPoint)") + let spacing: CGFloat = 4 + let viewSize = view.intrinsicContentSize + let anchorPoint = anchorView.superview!.convert(anchorView.center, to: containerView) + print("anchor point \(anchorView.frame) converted \(anchorPoint) frame \(anchorView.frame)") let constraints: [NSLayoutConstraint] = [ - view.leadingAnchor.constraint(equalTo: containerView!.leadingAnchor, constant: anchorPoint.x), - view.topAnchor.constraint(equalTo: containerView!.topAnchor, constant: anchorPoint.y), + view.leadingAnchor.constraint(equalTo: containerView!.leadingAnchor, constant: anchorPoint.x - viewSize.width / 2.0), + view.topAnchor.constraint(equalTo: containerView!.topAnchor, constant: anchorPoint.y - viewSize.height - anchorView.frame.size.height / 2.0 - spacing), ] NSLayoutConstraint.activate(constraints) } diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index b16da7dca..cc129f5e9 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -5,9 +5,8 @@ class CharcoalTooltip {} extension CharcoalTooltip { static func show(text: String, anchorView: UIView, on: UIView? = nil) { let tooltip = CharcoalTooltipView(text: text, targetPoint: CGPoint(x: 0, y: 0)) - tooltip.translatesAutoresizingMaskIntoConstraints = false - CharcoalOverlayView.shared.show(view: tooltip, interactionPassthrough: false, anchorView: anchorView, on: on) + CharcoalOverlayView.shared.show(view: tooltip, interactionPassthrough: true, anchorView: anchorView, on: on) } } @@ -30,6 +29,7 @@ extension CharcoalTooltip { let debugContainerView = UIView(frame: .zero) debugContainerView.translatesAutoresizingMaskIntoConstraints = false debugContainerView.backgroundColor = UIColor.blue.withAlphaComponent(0.1) + debugContainerView.isUserInteractionEnabled = false view.addSubview(debugContainerView) NSLayoutConstraint.activate([ diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift index 2f3479a60..87bcdd8f4 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift @@ -29,10 +29,13 @@ class CharcoalTooltipView: UIView { /// Padding around the bubble let padding = UIEdgeInsets(top: 4, left: 12, bottom: 4, right: 12) + var targetPoint: CGPoint + /// Text frame size private var textFrameSize: CGSize = .zero init(text: String, targetPoint: CGPoint, maxWidth: CGFloat = 184) { + self.targetPoint = targetPoint self.bubbleShape = CharcoalBubbleShape(targetPoint: targetPoint, arrowHeight: arrowHeight, bubbleRadius: cornerRadius, arrowWidth: arrowWidth) self.maxWidth = maxWidth self.text = text @@ -40,6 +43,11 @@ class CharcoalTooltipView: UIView { self.textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) self.setupLayer() } + + func updateTargetPoint(point:CGPoint) { + self.targetPoint = point + layoutSubviews() + } @available(*, unavailable) required init?(coder: NSCoder) { From 972e3b2f0ede71c4d685ed00d4139309fc4fe343 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 30 Apr 2024 19:33:32 +0800 Subject: [PATCH 120/199] Can redraw target point --- .../Components/Tooltip/CharcoalOverlay.swift | 22 ++++++++++++++++--- .../Components/Tooltip/CharcoalTooltip.swift | 1 + .../Tooltip/CharcoalTooltipView.swift | 13 ++++++----- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift index 5b77ee431..79e19a0fa 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift @@ -118,12 +118,28 @@ extension CharcoalOverlayView { let spacing: CGFloat = 4 let viewSize = view.intrinsicContentSize let anchorPoint = anchorView.superview!.convert(anchorView.center, to: containerView) - print("anchor point \(anchorView.frame) converted \(anchorPoint) frame \(anchorView.frame)") + let targetPoint = containerView!.convert(anchorPoint, to: view) + + let viewLeadingConstant = anchorPoint.x - viewSize.width / 2.0 + let viewTopConstant = anchorPoint.y - viewSize.height - anchorView.frame.size.height / 2.0 - spacing + + if let anchorableView = view as? CharcoalAnchorable { + let newTargetPoint = CGPoint(x: targetPoint.x - viewLeadingConstant, y: targetPoint.y - viewTopConstant) + anchorableView.updateTargetPoint(point: newTargetPoint) + print("Update Anchor \(newTargetPoint)") + } + + print("anchor point \(anchorView.frame) converted \(anchorPoint) frame \(anchorView.frame) target point \(targetPoint)") + let constraints: [NSLayoutConstraint] = [ - view.leadingAnchor.constraint(equalTo: containerView!.leadingAnchor, constant: anchorPoint.x - viewSize.width / 2.0), - view.topAnchor.constraint(equalTo: containerView!.topAnchor, constant: anchorPoint.y - viewSize.height - anchorView.frame.size.height / 2.0 - spacing), + view.leadingAnchor.constraint(equalTo: containerView!.leadingAnchor, constant: viewLeadingConstant), + view.topAnchor.constraint(equalTo: containerView!.topAnchor, constant: viewTopConstant), ] NSLayoutConstraint.activate(constraints) + + + + } display() diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index cc129f5e9..2f017585c 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -5,6 +5,7 @@ class CharcoalTooltip {} extension CharcoalTooltip { static func show(text: String, anchorView: UIView, on: UIView? = nil) { let tooltip = CharcoalTooltipView(text: text, targetPoint: CGPoint(x: 0, y: 0)) + tooltip.translatesAutoresizingMaskIntoConstraints = false CharcoalOverlayView.shared.show(view: tooltip, interactionPassthrough: true, anchorView: anchorView, on: on) } diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift index 87bcdd8f4..fb4e2ff95 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift @@ -1,6 +1,10 @@ import UIKit -class CharcoalTooltipView: UIView { +protocol CharcoalAnchorable { + func updateTargetPoint(point:CGPoint) +} + +class CharcoalTooltipView: UIView, CharcoalAnchorable { lazy var label: CharcoalTypography12 = { let label = CharcoalTypography12() @@ -29,13 +33,10 @@ class CharcoalTooltipView: UIView { /// Padding around the bubble let padding = UIEdgeInsets(top: 4, left: 12, bottom: 4, right: 12) - var targetPoint: CGPoint - /// Text frame size private var textFrameSize: CGSize = .zero init(text: String, targetPoint: CGPoint, maxWidth: CGFloat = 184) { - self.targetPoint = targetPoint self.bubbleShape = CharcoalBubbleShape(targetPoint: targetPoint, arrowHeight: arrowHeight, bubbleRadius: cornerRadius, arrowWidth: arrowWidth) self.maxWidth = maxWidth self.text = text @@ -45,8 +46,8 @@ class CharcoalTooltipView: UIView { } func updateTargetPoint(point:CGPoint) { - self.targetPoint = point - layoutSubviews() + self.bubbleShape.targetPoint = point + self.setNeedsLayout() } @available(*, unavailable) From 1d0fb67c48e758344decbdf6582848ea97ceec78 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 30 Apr 2024 19:33:55 +0800 Subject: [PATCH 121/199] Update CharcoalTooltip.swift --- Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index 2f017585c..4fad8a84f 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -4,7 +4,7 @@ class CharcoalTooltip {} extension CharcoalTooltip { static func show(text: String, anchorView: UIView, on: UIView? = nil) { - let tooltip = CharcoalTooltipView(text: text, targetPoint: CGPoint(x: 0, y: 0)) + let tooltip = CharcoalTooltipView(text: text, targetPoint: .zero) tooltip.translatesAutoresizingMaskIntoConstraints = false CharcoalOverlayView.shared.show(view: tooltip, interactionPassthrough: true, anchorView: anchorView, on: on) From 6f73f32b3ad62b226afd6af7369e0d99f0b6b306 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 14:39:15 +0800 Subject: [PATCH 122/199] Refine tooltip display --- .../Components/Tooltip/CharcoalOverlay.swift | 37 +++++++++++-------- .../Components/Tooltip/CharcoalTooltip.swift | 36 ++++++++++-------- .../Tooltip/CharcoalTooltipView.swift | 1 + 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift index 79e19a0fa..b0cd36e19 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift @@ -88,8 +88,8 @@ extension CharcoalOverlayView { mainView.addSubview(containerView) let constraints: [NSLayoutConstraint] = [ - containerView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: 0), - containerView.topAnchor.constraint(equalTo: mainView.topAnchor, constant: 0), + containerView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor), + containerView.topAnchor.constraint(equalTo: mainView.topAnchor), containerView.heightAnchor.constraint(equalTo: mainView.heightAnchor), containerView.widthAnchor.constraint(equalTo: mainView.widthAnchor), ] @@ -112,34 +112,41 @@ extension CharcoalOverlayView { setupSuperView(view: superView) setupBackground(interactionPassthrough) setupContainer() + + layoutIfNeeded() + containerView!.addSubview(view) - if let anchorView = anchorView { + if let anchorView = anchorView, let anchorableView = view as? CharcoalAnchorable { let spacing: CGFloat = 4 let viewSize = view.intrinsicContentSize let anchorPoint = anchorView.superview!.convert(anchorView.center, to: containerView) - let targetPoint = containerView!.convert(anchorPoint, to: view) + let targetPoint = anchorView.superview!.convert(anchorView.center, to: view) + let anchorViewHalfHeight = anchorView.frame.size.height / 2.0 + let anchroViewMaxY = anchorPoint.y + anchorViewHalfHeight + let anchroViewMinY = anchorPoint.y - anchorViewHalfHeight + print("containerView!.frame.width \(mainView!.frame.width)") - let viewLeadingConstant = anchorPoint.x - viewSize.width / 2.0 - let viewTopConstant = anchorPoint.y - viewSize.height - anchorView.frame.size.height / 2.0 - spacing + let viewLeadingConstant = min(max(16, anchorPoint.x - viewSize.width / 2.0), mainView!.frame.width - viewSize.width - 16) - if let anchorableView = view as? CharcoalAnchorable { - let newTargetPoint = CGPoint(x: targetPoint.x - viewLeadingConstant, y: targetPoint.y - viewTopConstant) - anchorableView.updateTargetPoint(point: newTargetPoint) - print("Update Anchor \(newTargetPoint)") + var viewTopConstant: CGFloat + + if (anchroViewMinY - spacing >= viewSize.height) { + viewTopConstant = anchroViewMinY - spacing - viewSize.height - anchorableView.arrowHeight + } else { + viewTopConstant = anchroViewMaxY + spacing + anchorableView.arrowHeight } - print("anchor point \(anchorView.frame) converted \(anchorPoint) frame \(anchorView.frame) target point \(targetPoint)") + let newTargetPoint = CGPoint(x: targetPoint.x - viewLeadingConstant, y: targetPoint.y - viewTopConstant) + anchorableView.updateTargetPoint(point: newTargetPoint) + + print("anchor point \(anchorPoint) leading \(viewLeadingConstant) top \(viewTopConstant)") let constraints: [NSLayoutConstraint] = [ view.leadingAnchor.constraint(equalTo: containerView!.leadingAnchor, constant: viewLeadingConstant), view.topAnchor.constraint(equalTo: containerView!.topAnchor, constant: viewTopConstant), ] NSLayoutConstraint.activate(constraints) - - - - } display() diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index 4fad8a84f..8b1eddeb4 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -27,26 +27,32 @@ extension CharcoalTooltip { button.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) - let debugContainerView = UIView(frame: .zero) - debugContainerView.translatesAutoresizingMaskIntoConstraints = false - debugContainerView.backgroundColor = UIColor.blue.withAlphaComponent(0.1) - debugContainerView.isUserInteractionEnabled = false - view.addSubview(debugContainerView) + let button2 = CharcoalPrimaryMButton() + button2.setTitle("OK", for: .normal) + button2.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(button2) NSLayoutConstraint.activate([ - debugContainerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - debugContainerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - debugContainerView.widthAnchor.constraint(equalTo: view.widthAnchor), - debugContainerView.heightAnchor.constraint(equalTo: view.widthAnchor) + button2.topAnchor.constraint(equalTo: view.topAnchor), + button2.leadingAnchor.constraint(equalTo: view.leadingAnchor) + ]) + + let button3 = CharcoalPrimaryMButton() + button3.setTitle("OK", for: .normal) + button3.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(button3) + + NSLayoutConstraint.activate([ + button3.bottomAnchor.constraint(equalTo: view.bottomAnchor), + button3.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) -// let tooltip2 = CharcoalTooltipView(text: "Hello World This is a tooltip", targetPoint: CGPoint(x: 110, y: 10)) -// -// let tooltip3 = CharcoalTooltipView(text: "here is testing it's multiple line feature", targetPoint: CGPoint(x: 50, y: 55)) -// -// let tooltip4 = CharcoalTooltipView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", targetPoint: CGPoint(x: -10, y: 25)) DispatchQueue.main.async { - CharcoalTooltip.show(text: "Hello World", anchorView: button, on: debugContainerView) + CharcoalTooltip.show(text: "Hello World", anchorView: button) + + CharcoalTooltip.show(text: "Hello World This is a tooltip", anchorView: button2) + + CharcoalTooltip.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", anchorView: button3) } return view diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift index fb4e2ff95..33fa46441 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift @@ -1,6 +1,7 @@ import UIKit protocol CharcoalAnchorable { + var arrowHeight: CGFloat { get } func updateTargetPoint(point:CGPoint) } From 3efb6f369de0c1230bb1bc87ad59506e8640c917 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 14:51:51 +0800 Subject: [PATCH 123/199] Share the logic --- .../Components/Tooltip/CharcoalOverlay.swift | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift index b0cd36e19..fcb1a72ad 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift @@ -28,8 +28,6 @@ extension CharcoalOverlayView { .first mainView = scene?.windows.filter { $0.isKeyWindow }.first ?? UIApplication.shared.windows.first - - print("shows on main") } } } @@ -118,30 +116,20 @@ extension CharcoalOverlayView { containerView!.addSubview(view) if let anchorView = anchorView, let anchorableView = view as? CharcoalAnchorable { - let spacing: CGFloat = 4 + let spacingToScreen: CGFloat = 16 + let gap: CGFloat = 4 let viewSize = view.intrinsicContentSize - let anchorPoint = anchorView.superview!.convert(anchorView.center, to: containerView) + let anchorPoint = anchorView.superview!.convert(anchorView.frame.origin, to: containerView) let targetPoint = anchorView.superview!.convert(anchorView.center, to: view) - let anchorViewHalfHeight = anchorView.frame.size.height / 2.0 - let anchroViewMaxY = anchorPoint.y + anchorViewHalfHeight - let anchroViewMinY = anchorPoint.y - anchorViewHalfHeight - print("containerView!.frame.width \(mainView!.frame.width)") + let newAnchorRect = CGRect(x: anchorPoint.x, y: anchorPoint.y, width: anchorView.frame.width, height: anchorView.frame.height) - let viewLeadingConstant = min(max(16, anchorPoint.x - viewSize.width / 2.0), mainView!.frame.width - viewSize.width - 16) + let viewLeadingConstant = tooltipX(anchorFrame: newAnchorRect, tooltipSize: viewSize, canvasGeometrySize: mainView.frame.size, spacingToScreen: spacingToScreen) - var viewTopConstant: CGFloat - - if (anchroViewMinY - spacing >= viewSize.height) { - viewTopConstant = anchroViewMinY - spacing - viewSize.height - anchorableView.arrowHeight - } else { - viewTopConstant = anchroViewMaxY + spacing + anchorableView.arrowHeight - } + let viewTopConstant = tooltipY(anchorFrame: newAnchorRect, arrowHeight: anchorableView.arrowHeight, tooltipSize: viewSize, canvasGeometrySize: mainView.frame.size, spacingToTarget: gap) let newTargetPoint = CGPoint(x: targetPoint.x - viewLeadingConstant, y: targetPoint.y - viewTopConstant) anchorableView.updateTargetPoint(point: newTargetPoint) - print("anchor point \(anchorPoint) leading \(viewLeadingConstant) top \(viewTopConstant)") - let constraints: [NSLayoutConstraint] = [ view.leadingAnchor.constraint(equalTo: containerView!.leadingAnchor, constant: viewLeadingConstant), view.topAnchor.constraint(equalTo: containerView!.topAnchor, constant: viewTopConstant), @@ -152,8 +140,31 @@ extension CharcoalOverlayView { display() } + func tooltipX(anchorFrame: CGRect, tooltipSize: CGSize, canvasGeometrySize: CGSize, spacingToScreen: CGFloat) -> CGFloat { + let minX = anchorFrame.midX - (tooltipSize.width / 2.0) + + var edgeLeft = minX + + if edgeLeft + tooltipSize.width >= canvasGeometrySize.width { + edgeLeft = canvasGeometrySize.width - tooltipSize.width - spacingToScreen + } else if edgeLeft < spacingToScreen { + edgeLeft = spacingToScreen + } + + return edgeLeft + } + + func tooltipY(anchorFrame: CGRect, arrowHeight: CGFloat, tooltipSize: CGSize, canvasGeometrySize: CGSize, spacingToTarget: CGFloat) -> CGFloat { + let minX = anchorFrame.maxY + spacingToTarget + arrowHeight + var edgeBottom = anchorFrame.maxY + spacingToTarget + anchorFrame.height + if edgeBottom + tooltipSize.height >= canvasGeometrySize.height { + edgeBottom = anchorFrame.minY - tooltipSize.height - spacingToTarget - arrowHeight + } + + return min(minX, edgeBottom) + } + func display() { - print("display") UIView.animate(withDuration: 0.25, animations: { [weak self] in self?.containerView?.alpha = 1 }) From 87ce7c2a634cf623f1ed963a8b8259bf20e00548 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 14:59:24 +0800 Subject: [PATCH 124/199] Use interaction mode --- .../Components/Tooltip/CharcoalOverlay.swift | 36 ++++++++++++++++--- .../Components/Tooltip/CharcoalTooltip.swift | 2 +- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift index fcb1a72ad..aa1d1363c 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift @@ -1,5 +1,11 @@ import UIKit +enum CharcoalOverlayInteractionMode { +case passThrough +case block +case dimissOnTap +} + /** Displays a overlay on the screen. */ @@ -41,7 +47,7 @@ extension CharcoalOverlayView { backgroundView = nil } - private func setupBackground(_ interactionPassthrough: Bool) { + private func setupBackground(_ interactionMode: CharcoalOverlayInteractionMode) { if backgroundView == nil { backgroundView = UIView(frame: .zero) @@ -63,7 +69,17 @@ extension CharcoalOverlayView { NSLayoutConstraint.activate(constraints) } - backgroundView?.isUserInteractionEnabled = interactionPassthrough ? false : true + switch interactionMode { + case .block: + backgroundView?.isUserInteractionEnabled = true + case .dimissOnTap: + backgroundView?.isUserInteractionEnabled = true + // Add dismiss tap gesture + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismiss)) + backgroundView?.addGestureRecognizer(tapGesture) + case .passThrough: + backgroundView?.isUserInteractionEnabled = false + } } } @@ -79,6 +95,7 @@ extension CharcoalOverlayView { if containerView == nil { containerView = UIView(frame: .zero) containerView?.alpha = 0 + containerView?.isUserInteractionEnabled = false guard let containerView = containerView else { fatalError("Container view is nil.") } @@ -103,12 +120,12 @@ extension CharcoalOverlayView { func show( view: UIView, transparentBackground: Bool = false, - interactionPassthrough: Bool, + interactionMode: CharcoalOverlayInteractionMode = .dimissOnTap, anchorView: UIView? = nil, on superView: UIView? = nil ) { setupSuperView(view: superView) - setupBackground(interactionPassthrough) + setupBackground(interactionMode) setupContainer() layoutIfNeeded() @@ -169,5 +186,16 @@ extension CharcoalOverlayView { self?.containerView?.alpha = 1 }) } + + @objc func dismiss() { + print("dismiss") + UIView.animate(withDuration: 0.25, animations: { [weak self] in + self?.containerView?.alpha = 0 + }) { + [weak self] _ in + self?.removeContainer() + self?.removeBackground() + } + } } diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index 8b1eddeb4..0c5e1b6a3 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -7,7 +7,7 @@ extension CharcoalTooltip { let tooltip = CharcoalTooltipView(text: text, targetPoint: .zero) tooltip.translatesAutoresizingMaskIntoConstraints = false - CharcoalOverlayView.shared.show(view: tooltip, interactionPassthrough: true, anchorView: anchorView, on: on) + CharcoalOverlayView.shared.show(view: tooltip, interactionMode: .dimissOnTap, anchorView: anchorView, on: on) } } From e0ba65ef89605a9e77743e86c0123ab0ca18ef38 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 15:22:26 +0800 Subject: [PATCH 125/199] Use CharcoalOverlayContainerView --- .../Components/Tooltip/CharcoalOverlay.swift | 88 +++++++------------ .../CharcoalOverlayContainerView.swift | 44 ++++++++++ 2 files changed, 75 insertions(+), 57 deletions(-) create mode 100644 Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlayContainerView.swift diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift index aa1d1363c..75caeb7de 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift @@ -12,10 +12,10 @@ case dimissOnTap public class CharcoalOverlayView: UIView { /// The window to display the spinner in. var mainView: UIView! - /// The background view of the spinner. + /// The background view of the overall overlays. var backgroundView: UIView? - /// The container view of the spinner. - var containerView: UIView? + + var overlayContainerViews: [CharcoalOverlayContainerView] = [] static let shared = CharcoalOverlayView() } @@ -47,7 +47,7 @@ extension CharcoalOverlayView { backgroundView = nil } - private func setupBackground(_ interactionMode: CharcoalOverlayInteractionMode) { + private func setupBackground() { if backgroundView == nil { backgroundView = UIView(frame: .zero) @@ -68,49 +68,29 @@ extension CharcoalOverlayView { NSLayoutConstraint.activate(constraints) } - - switch interactionMode { - case .block: - backgroundView?.isUserInteractionEnabled = true - case .dimissOnTap: - backgroundView?.isUserInteractionEnabled = true - // Add dismiss tap gesture - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismiss)) - backgroundView?.addGestureRecognizer(tapGesture) - case .passThrough: - backgroundView?.isUserInteractionEnabled = false - } + } } // MARK: - Container extension CharcoalOverlayView { - private func removeContainer() { - containerView?.removeFromSuperview() - containerView = nil - } - - private func setupContainer() { - if containerView == nil { - containerView = UIView(frame: .zero) - containerView?.alpha = 0 - containerView?.isUserInteractionEnabled = false - guard let containerView = containerView else { - fatalError("Container view is nil.") - } - containerView.translatesAutoresizingMaskIntoConstraints = false - mainView.addSubview(containerView) - - let constraints: [NSLayoutConstraint] = [ - containerView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor), - containerView.topAnchor.constraint(equalTo: mainView.topAnchor), - containerView.heightAnchor.constraint(equalTo: mainView.heightAnchor), - containerView.widthAnchor.constraint(equalTo: mainView.widthAnchor), - ] - - NSLayoutConstraint.activate(constraints) - } + private func setupContainer(_ interactionMode: CharcoalOverlayInteractionMode) -> CharcoalOverlayContainerView { + let containerView = CharcoalOverlayContainerView(interactionMode: interactionMode) + containerView.alpha = 0 + containerView.translatesAutoresizingMaskIntoConstraints = false + mainView.addSubview(containerView) + overlayContainerViews.append(containerView) + let constraints: [NSLayoutConstraint] = [ + containerView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor), + containerView.topAnchor.constraint(equalTo: mainView.topAnchor), + containerView.heightAnchor.constraint(equalTo: mainView.heightAnchor), + containerView.widthAnchor.constraint(equalTo: mainView.widthAnchor), + ] + + NSLayoutConstraint.activate(constraints) + + return containerView } } @@ -125,13 +105,12 @@ extension CharcoalOverlayView { on superView: UIView? = nil ) { setupSuperView(view: superView) - setupBackground(interactionMode) - setupContainer() + setupBackground() + let containerView = setupContainer(interactionMode) + containerView.addSubview(view) layoutIfNeeded() - containerView!.addSubview(view) - if let anchorView = anchorView, let anchorableView = view as? CharcoalAnchorable { let spacingToScreen: CGFloat = 16 let gap: CGFloat = 4 @@ -148,8 +127,8 @@ extension CharcoalOverlayView { anchorableView.updateTargetPoint(point: newTargetPoint) let constraints: [NSLayoutConstraint] = [ - view.leadingAnchor.constraint(equalTo: containerView!.leadingAnchor, constant: viewLeadingConstant), - view.topAnchor.constraint(equalTo: containerView!.topAnchor, constant: viewTopConstant), + view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: viewLeadingConstant), + view.topAnchor.constraint(equalTo: containerView.topAnchor, constant: viewTopConstant), ] NSLayoutConstraint.activate(constraints) } @@ -182,19 +161,14 @@ extension CharcoalOverlayView { } func display() { - UIView.animate(withDuration: 0.25, animations: { [weak self] in - self?.containerView?.alpha = 1 - }) + for containerView in overlayContainerViews { + containerView.display() + } } @objc func dismiss() { - print("dismiss") - UIView.animate(withDuration: 0.25, animations: { [weak self] in - self?.containerView?.alpha = 0 - }) { - [weak self] _ in - self?.removeContainer() - self?.removeBackground() + for containerView in overlayContainerViews { + containerView.dismiss() } } } diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlayContainerView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlayContainerView.swift new file mode 100644 index 000000000..4e9bb9b7b --- /dev/null +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlayContainerView.swift @@ -0,0 +1,44 @@ +import UIKit + +class CharcoalOverlayContainerView: UIView { + + let interactionMode: CharcoalOverlayInteractionMode + + init(interactionMode: CharcoalOverlayInteractionMode) { + self.interactionMode = interactionMode + super.init(frame: .zero) + self.backgroundColor = UIColor.clear + + switch interactionMode { + case .block: + isUserInteractionEnabled = true + case .dimissOnTap: + isUserInteractionEnabled = true + // Add dismiss tap gesture + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismiss)) + addGestureRecognizer(tapGesture) + case .passThrough: + isUserInteractionEnabled = false + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func display() { + UIView.animate(withDuration: 0.25, animations: { [weak self] in + self?.alpha = 1 + }) + } + + @objc func dismiss() { + print("dismiss") + UIView.animate(withDuration: 0.25, animations: { [weak self] in + self?.alpha = 0 + }) { + [weak self] _ in + self?.removeFromSuperview() + } + } +} From 71bb97970628065f5e1d05ae9b308ab8a998d644 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 15:23:45 +0800 Subject: [PATCH 126/199] Update CharcoalOverlay.swift --- .../CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift index 75caeb7de..b404283fd 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift @@ -50,7 +50,7 @@ extension CharcoalOverlayView { private func setupBackground() { if backgroundView == nil { backgroundView = UIView(frame: .zero) - + backgroundView?.isUserInteractionEnabled = false guard let backgroundView = backgroundView else { fatalError("Background view is nil.") } @@ -109,7 +109,7 @@ extension CharcoalOverlayView { let containerView = setupContainer(interactionMode) containerView.addSubview(view) - layoutIfNeeded() +// layoutIfNeeded() if let anchorView = anchorView, let anchorableView = view as? CharcoalAnchorable { let spacingToScreen: CGFloat = 16 From 8c5bf82e7ffe234d859da10e87286ca37762f3d9 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 15:56:26 +0800 Subject: [PATCH 127/199] Refactor to ChacoalOverlayManager --- .../ChacoalOverlayManager.swift} | 18 +++++++++--------- .../CharcoalIdentifiableOverlayView.swift} | 2 +- .../Components/Tooltip/CharcoalTooltip.swift | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) rename Sources/CharcoalUIKit/Components/{Tooltip/CharcoalOverlay.swift => Overlay/ChacoalOverlayManager.swift} (93%) rename Sources/CharcoalUIKit/Components/{Tooltip/CharcoalOverlayContainerView.swift => Overlay/CharcoalIdentifiableOverlayView.swift} (96%) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift similarity index 93% rename from Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift rename to Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift index b404283fd..f83e794e6 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlay.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift @@ -9,20 +9,20 @@ case dimissOnTap /** Displays a overlay on the screen. */ -public class CharcoalOverlayView: UIView { +public class ChacoalOverlayManager: UIView { /// The window to display the spinner in. var mainView: UIView! /// The background view of the overall overlays. var backgroundView: UIView? - var overlayContainerViews: [CharcoalOverlayContainerView] = [] + var overlayContainerViews: [CharcoalIdentifiableOverlayView] = [] - static let shared = CharcoalOverlayView() + static let shared = ChacoalOverlayManager() } // MARK: - Window -extension CharcoalOverlayView { +extension ChacoalOverlayManager { /// Initializes the spinner with the given window. func setupSuperView(view: UIView?) { if let view = view { @@ -41,7 +41,7 @@ extension CharcoalOverlayView { // MARK: - Background -extension CharcoalOverlayView { +extension ChacoalOverlayManager { private func removeBackground() { backgroundView?.removeFromSuperview() backgroundView = nil @@ -74,9 +74,9 @@ extension CharcoalOverlayView { // MARK: - Container -extension CharcoalOverlayView { - private func setupContainer(_ interactionMode: CharcoalOverlayInteractionMode) -> CharcoalOverlayContainerView { - let containerView = CharcoalOverlayContainerView(interactionMode: interactionMode) +extension ChacoalOverlayManager { + private func setupContainer(_ interactionMode: CharcoalOverlayInteractionMode) -> CharcoalIdentifiableOverlayView { + let containerView = CharcoalIdentifiableOverlayView(interactionMode: interactionMode) containerView.alpha = 0 containerView.translatesAutoresizingMaskIntoConstraints = false mainView.addSubview(containerView) @@ -96,7 +96,7 @@ extension CharcoalOverlayView { // MARK: - Show, Dismiss -extension CharcoalOverlayView { +extension ChacoalOverlayManager { func show( view: UIView, transparentBackground: Bool = false, diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlayContainerView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift similarity index 96% rename from Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlayContainerView.swift rename to Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 4e9bb9b7b..f2c1272c0 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalOverlayContainerView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -1,6 +1,6 @@ import UIKit -class CharcoalOverlayContainerView: UIView { +class CharcoalIdentifiableOverlayView: UIView { let interactionMode: CharcoalOverlayInteractionMode diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index 0c5e1b6a3..daba6694a 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -7,7 +7,7 @@ extension CharcoalTooltip { let tooltip = CharcoalTooltipView(text: text, targetPoint: .zero) tooltip.translatesAutoresizingMaskIntoConstraints = false - CharcoalOverlayView.shared.show(view: tooltip, interactionMode: .dimissOnTap, anchorView: anchorView, on: on) + ChacoalOverlayManager.shared.show(view: tooltip, interactionMode: .dimissOnTap, anchorView: anchorView, on: on) } } From b3914884ddd2bfe597dba669559b7b3acfcee7cb Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 16:18:14 +0800 Subject: [PATCH 128/199] Makes CharcoalIdentifiableOverlayView Identifiable --- .../Overlay/ChacoalOverlayManager.swift | 21 ++++++++++++------- .../CharcoalIdentifiableOverlayView.swift | 5 ++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift index f83e794e6..d92a8dd5c 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift @@ -10,13 +10,13 @@ case dimissOnTap Displays a overlay on the screen. */ public class ChacoalOverlayManager: UIView { - /// The window to display the spinner in. + /// The window to display the overlays in. var mainView: UIView! - /// The background view of the overall overlays. + /// The background view of the overlays. var backgroundView: UIView? - + /// The container view array of the overlays. var overlayContainerViews: [CharcoalIdentifiableOverlayView] = [] - + /// Shared instance of the overlay manager. static let shared = ChacoalOverlayManager() } @@ -97,20 +97,19 @@ extension ChacoalOverlayManager { // MARK: - Show, Dismiss extension ChacoalOverlayManager { + @discardableResult func show( view: UIView, transparentBackground: Bool = false, interactionMode: CharcoalOverlayInteractionMode = .dimissOnTap, anchorView: UIView? = nil, on superView: UIView? = nil - ) { + ) -> CharcoalIdentifiableOverlayView.IDValue { setupSuperView(view: superView) setupBackground() let containerView = setupContainer(interactionMode) containerView.addSubview(view) -// layoutIfNeeded() - if let anchorView = anchorView, let anchorableView = view as? CharcoalAnchorable { let spacingToScreen: CGFloat = 16 let gap: CGFloat = 4 @@ -134,6 +133,8 @@ extension ChacoalOverlayManager { } display() + + return containerView.id } func tooltipX(anchorFrame: CGRect, tooltipSize: CGSize, canvasGeometrySize: CGSize, spacingToScreen: CGFloat) -> CGFloat { @@ -171,5 +172,11 @@ extension ChacoalOverlayManager { containerView.dismiss() } } + + /// Dismisses the overlay with the given identifier. + func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { + let containerView = overlayContainerViews.first { $0.id == id } + containerView?.dismiss() + } } diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index f2c1272c0..2016d2a89 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -1,6 +1,9 @@ import UIKit -class CharcoalIdentifiableOverlayView: UIView { +class CharcoalIdentifiableOverlayView: UIView, Identifiable { + typealias IDValue = UUID + + let id = IDValue() let interactionMode: CharcoalOverlayInteractionMode From 9586ce4ab96dfb68efa4c9e7b0a265f18bc357d1 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 16:19:02 +0800 Subject: [PATCH 129/199] Refine layout logic --- .../Overlay/ChacoalOverlayManager.swift | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift index d92a8dd5c..bcf9bf543 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift @@ -137,6 +137,27 @@ extension ChacoalOverlayManager { return containerView.id } + func display() { + for containerView in overlayContainerViews { + containerView.display() + } + } + + @objc func dismiss() { + for containerView in overlayContainerViews { + containerView.dismiss() + } + } + + /// Dismisses the overlay with the given identifier. + func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { + let containerView = overlayContainerViews.first { $0.id == id } + containerView?.dismiss() + } +} + +// MARK: Layout +extension ChacoalOverlayManager { func tooltipX(anchorFrame: CGRect, tooltipSize: CGSize, canvasGeometrySize: CGSize, spacingToScreen: CGFloat) -> CGFloat { let minX = anchorFrame.midX - (tooltipSize.width / 2.0) @@ -160,23 +181,4 @@ extension ChacoalOverlayManager { return min(minX, edgeBottom) } - - func display() { - for containerView in overlayContainerViews { - containerView.display() - } - } - - @objc func dismiss() { - for containerView in overlayContainerViews { - containerView.dismiss() - } - } - - /// Dismisses the overlay with the given identifier. - func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { - let containerView = overlayContainerViews.first { $0.id == id } - containerView?.dismiss() - } } - From a41775875b90d803548f88b2a07a91761a47e2d0 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 16:21:00 +0800 Subject: [PATCH 130/199] Add display(view: CharcoalIdentifiableOverlayView) --- .../Components/Overlay/ChacoalOverlayManager.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift index bcf9bf543..266b80331 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift @@ -132,7 +132,7 @@ extension ChacoalOverlayManager { NSLayoutConstraint.activate(constraints) } - display() + display(view: containerView) return containerView.id } @@ -149,6 +149,11 @@ extension ChacoalOverlayManager { } } + /// Displays the overlay. + func display(view: CharcoalIdentifiableOverlayView) { + view.display() + } + /// Dismisses the overlay with the given identifier. func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { let containerView = overlayContainerViews.first { $0.id == id } From 37cee239db0e6c9ae7fab23f34cf935190faf6a4 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 17:17:35 +0800 Subject: [PATCH 131/199] Add tooltip to uikit example --- .../Views/Tooltips/TooltipTableViewCell.swift | 65 ++++++++ .../Views/Tooltips/Tooltips.swift | 148 ++++++++++++++++++ .../CharcoalIdentifiableOverlayView.swift | 1 - .../Components/Tooltip/CharcoalTooltip.swift | 4 +- 4 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/TooltipTableViewCell.swift create mode 100644 CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/TooltipTableViewCell.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/TooltipTableViewCell.swift new file mode 100644 index 000000000..5f1fc0891 --- /dev/null +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/TooltipTableViewCell.swift @@ -0,0 +1,65 @@ +import UIKit + +class TooltipTableViewCell: UITableViewCell { + + static let identifier = "TooltipCell" + + let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 16, weight: .medium) + label.textColor = UIColor.black + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let leadingImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + let accessoryImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + contentView.addSubview(titleLabel) + contentView.addSubview(leadingImageView) + contentView.addSubview(accessoryImageView) + + NSLayoutConstraint.activate([ + leadingImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), + leadingImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + ]) + + NSLayoutConstraint.activate([ + titleLabel.leadingAnchor.constraint(equalTo: leadingImageView.trailingAnchor, constant: 10), + titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) + ]) + + NSLayoutConstraint.activate([ + accessoryImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), + accessoryImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + } + + override func prepareForReuse() { + super.prepareForReuse() + titleLabel.text = nil + leadingImageView.image = nil + } +} diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift new file mode 100644 index 000000000..cbae25587 --- /dev/null +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift @@ -0,0 +1,148 @@ +import Charcoal +import UIKit + +enum TooltipTitles: String, CaseIterable { + case leading = "Leading" + case trailing = "Trailing" + case bottom = "Bottom" + + var text: String { + switch self { + case .leading: + return "Hello World" + case .trailing: + return "Hello World This is a tooltip with mutiple line" + case .bottom: + return "こんにちは This is a tooltip and here is testing it's multiple line feature" + } + } + + func configCell(cell: TooltipTableViewCell) { + cell.titleLabel.text = rawValue + switch self { + case .leading:å + cell.leadingImageView.image = CharcoalAsset.Images.info24.image + case .trailing: + cell.accessoryImageView.image = CharcoalAsset.Images.info24.image + case .bottom: + break + } + } +} + +public final class TooltipsController: UIViewController { + private lazy var tableView: UITableView = { + let view = UITableView(frame: .zero, style: .insetGrouped) + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + lazy var bottomInfoImage: UIImageView = { + let imageView = UIImageView(image: CharcoalAsset.Images.info16.image) + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private enum Sections: Int, CaseIterable { + case components + + var title: String { + switch self { + case .components: + return "Tooltips" + } + } + + var items: [any CaseIterable] { + switch self { + case .components: + return TooltipTitles.allCases + } + } + } + + private enum SettingsTitles: String, CaseIterable { + case darkMode = "Dark Mode" + case fixedSizeCategory = "Fixed Size Category" + } + + override public func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupUI() + } + + private func setupNavigationBar() { + navigationItem.title = "Charcoal" + } + + private func setupUI() { + view.addSubview(tableView) + + NSLayoutConstraint.activate([ + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + tableView.dataSource = self + tableView.delegate = self + + view.addSubview(bottomInfoImage) + + NSLayoutConstraint.activate([ + bottomInfoImage.centerXAnchor.constraint(equalTo: view.centerXAnchor), + bottomInfoImage.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20) + ]) + } +} + +extension TooltipsController: UITableViewDelegate, UITableViewDataSource { + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let section = Sections.allCases[indexPath.section] + + let cellIdentifier = TooltipTableViewCell.identifier + let cell: TooltipTableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) as? TooltipTableViewCell ?? TooltipTableViewCell(style: .default, reuseIdentifier: cellIdentifier) + + switch section { + case .components: + let titleCase = TooltipTitles.allCases[indexPath.row] + titleCase.configCell(cell: cell) + return cell + } + } + + public func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { + return Sections.allCases[section].items.count + } + + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let cell = tableView.cellForRow(at: indexPath) as! TooltipTableViewCell + tableView.deselectRow(at: indexPath, animated: true) + let titleCase = TooltipTitles.allCases[indexPath.row] + switch titleCase { + case .leading: + CharcoalTooltip.show(text: titleCase.text, anchorView: cell.leadingImageView) + case .trailing: + CharcoalTooltip.show(text: titleCase.text, anchorView: cell.accessoryImageView) + case .bottom: + CharcoalTooltip.show(text: titleCase.text, anchorView: bottomInfoImage) + } + } + + public func numberOfSections(in tableView: UITableView) -> Int { + return Sections.allCases.count + } + + public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return Sections.allCases[section].title + } +} + + +@available(iOS 17.0, *) +#Preview { + let viewController = TooltipsController() + return viewController +} diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 2016d2a89..e2e3a1bfa 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -36,7 +36,6 @@ class CharcoalIdentifiableOverlayView: UIView, Identifiable { } @objc func dismiss() { - print("dismiss") UIView.animate(withDuration: 0.25, animations: { [weak self] in self?.alpha = 0 }) { diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index daba6694a..0dac75643 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -1,8 +1,8 @@ import UIKit -class CharcoalTooltip {} +public class CharcoalTooltip {} -extension CharcoalTooltip { +public extension CharcoalTooltip { static func show(text: String, anchorView: UIView, on: UIView? = nil) { let tooltip = CharcoalTooltipView(text: text, targetPoint: .zero) From 3c62e22d2f3897ee00031d3242edc3ff74f3d975 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 17:18:22 +0800 Subject: [PATCH 132/199] Update Tooltips.swift --- .../Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift index cbae25587..942dcc1db 100644 --- a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift @@ -20,7 +20,7 @@ enum TooltipTitles: String, CaseIterable { func configCell(cell: TooltipTableViewCell) { cell.titleLabel.text = rawValue switch self { - case .leading:å + case .leading: cell.leadingImageView.image = CharcoalAsset.Images.info24.image case .trailing: cell.accessoryImageView.image = CharcoalAsset.Images.info24.image From df2f52a309a9f883029b30343500a65d94c4a5f0 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 17:46:05 +0800 Subject: [PATCH 133/199] Add CharcoalIdentifiableOverlayDelegate --- .../Components/Overlay/ChacoalOverlayManager.swift | 11 +++++++++++ .../Overlay/CharcoalIdentifiableOverlayView.swift | 10 +++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift index 266b80331..4b3674557 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift @@ -187,3 +187,14 @@ extension ChacoalOverlayManager { return min(minX, edgeBottom) } } + +// MARK: - CharcoalIdentifiableOverlayDelegate + +extension ChacoalOverlayManager: CharcoalIdentifiableOverlayDelegate { + func overlayViewDidDismiss(_ overlayView: CharcoalIdentifiableOverlayView) { + overlayContainerViews = overlayContainerViews.filter({ $0.id != overlayView.id}) + if overlayContainerViews.isEmpty { + removeBackground() + } + } +} diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index e2e3a1bfa..dbc8a81ac 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -1,5 +1,9 @@ import UIKit +protocol CharcoalIdentifiableOverlayDelegate: AnyObject { + func overlayViewDidDismiss(_ overlayView: CharcoalIdentifiableOverlayView) +} + class CharcoalIdentifiableOverlayView: UIView, Identifiable { typealias IDValue = UUID @@ -7,6 +11,8 @@ class CharcoalIdentifiableOverlayView: UIView, Identifiable { let interactionMode: CharcoalOverlayInteractionMode + weak var delegate: CharcoalIdentifiableOverlayDelegate? + init(interactionMode: CharcoalOverlayInteractionMode) { self.interactionMode = interactionMode super.init(frame: .zero) @@ -40,7 +46,9 @@ class CharcoalIdentifiableOverlayView: UIView, Identifiable { self?.alpha = 0 }) { [weak self] _ in - self?.removeFromSuperview() + guard let self = self else {return} + self.removeFromSuperview() + self.delegate?.overlayViewDidDismiss(self) } } } From ff1d909f6f7cb4b93a89519a70a4834e3d21cde4 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 17:46:24 +0800 Subject: [PATCH 134/199] Reformat --- .../Views/Tooltips/TooltipTableViewCell.swift | 30 ++++----- .../Views/Tooltips/Tooltips.swift | 15 ++--- .../Modal/CharcoalModalView.swift | 1 + .../Overlay/ChacoalOverlayManager.swift | 54 +++++++-------- .../CharcoalIdentifiableOverlayView.swift | 21 +++--- .../Tooltip/CharcoalBubbleShape_UIKit.swift | 66 +++++++++---------- .../Components/Tooltip/CharcoalTooltip.swift | 21 +++--- .../Tooltip/CharcoalTooltipView.swift | 55 ++++++++-------- .../Extensions/StringExtension.swift | 2 +- 9 files changed, 131 insertions(+), 134 deletions(-) diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/TooltipTableViewCell.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/TooltipTableViewCell.swift index 5f1fc0891..afc89b752 100644 --- a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/TooltipTableViewCell.swift +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/TooltipTableViewCell.swift @@ -1,9 +1,8 @@ import UIKit class TooltipTableViewCell: UITableViewCell { - static let identifier = "TooltipCell" - + let titleLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 16, weight: .medium) @@ -11,52 +10,53 @@ class TooltipTableViewCell: UITableViewCell { label.translatesAutoresizingMaskIntoConstraints = false return label }() - - let leadingImageView: UIImageView = { + + let leadingImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit - imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() - + let accessoryImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() - + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - + contentView.addSubview(titleLabel) contentView.addSubview(leadingImageView) contentView.addSubview(accessoryImageView) - + NSLayoutConstraint.activate([ leadingImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), - leadingImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + leadingImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) ]) - + NSLayoutConstraint.activate([ titleLabel.leadingAnchor.constraint(equalTo: leadingImageView.trailingAnchor, constant: 10), titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) ]) - + NSLayoutConstraint.activate([ accessoryImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), accessoryImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) ]) } - + + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func layoutSubviews() { super.layoutSubviews() } - + override func prepareForReuse() { super.prepareForReuse() titleLabel.text = nil diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift index 942dcc1db..7080bf36a 100644 --- a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift @@ -5,7 +5,7 @@ enum TooltipTitles: String, CaseIterable { case leading = "Leading" case trailing = "Trailing" case bottom = "Bottom" - + var text: String { switch self { case .leading: @@ -16,8 +16,8 @@ enum TooltipTitles: String, CaseIterable { return "こんにちは This is a tooltip and here is testing it's multiple line feature" } } - - func configCell(cell: TooltipTableViewCell) { + + func configCell(cell: TooltipTableViewCell) { cell.titleLabel.text = rawValue switch self { case .leading: @@ -36,7 +36,7 @@ public final class TooltipsController: UIViewController { view.translatesAutoresizingMaskIntoConstraints = false return view }() - + lazy var bottomInfoImage: UIImageView = { let imageView = UIImageView(image: CharcoalAsset.Images.info16.image) imageView.translatesAutoresizingMaskIntoConstraints = false @@ -88,9 +88,9 @@ public final class TooltipsController: UIViewController { tableView.dataSource = self tableView.delegate = self - + view.addSubview(bottomInfoImage) - + NSLayoutConstraint.activate([ bottomInfoImage.centerXAnchor.constraint(equalTo: view.centerXAnchor), bottomInfoImage.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20) @@ -116,7 +116,7 @@ extension TooltipsController: UITableViewDelegate, UITableViewDataSource { public func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { return Sections.allCases[section].items.count } - + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let cell = tableView.cellForRow(at: indexPath) as! TooltipTableViewCell tableView.deselectRow(at: indexPath, animated: true) @@ -140,7 +140,6 @@ extension TooltipsController: UITableViewDelegate, UITableViewDataSource { } } - @available(iOS 17.0, *) #Preview { let viewController = TooltipsController() diff --git a/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift b/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift index 5ed444d7e..321cc0dd0 100644 --- a/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift +++ b/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift @@ -1,4 +1,5 @@ import SwiftUI + struct CharcoalModalView: View { /// The title of the modal view. var title: String? diff --git a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift index 4b3674557..e4b25976a 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift @@ -1,9 +1,9 @@ import UIKit enum CharcoalOverlayInteractionMode { -case passThrough -case block -case dimissOnTap + case passThrough + case block + case dimissOnTap } /** @@ -33,7 +33,7 @@ extension ChacoalOverlayManager { .filter { $0.activationState == .foregroundActive } .first mainView = scene?.windows.filter { $0.isKeyWindow }.first ?? - UIApplication.shared.windows.first + UIApplication.shared.windows.first } } } @@ -46,7 +46,7 @@ extension ChacoalOverlayManager { backgroundView?.removeFromSuperview() backgroundView = nil } - + private func setupBackground() { if backgroundView == nil { backgroundView = UIView(frame: .zero) @@ -54,21 +54,20 @@ extension ChacoalOverlayManager { guard let backgroundView = backgroundView else { fatalError("Background view is nil.") } - + backgroundView.translatesAutoresizingMaskIntoConstraints = false - + mainView.addSubview(backgroundView) - + let constraints: [NSLayoutConstraint] = [ backgroundView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: 0), backgroundView.topAnchor.constraint(equalTo: mainView.topAnchor, constant: 0), backgroundView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: 0), backgroundView.rightAnchor.constraint(equalTo: mainView.rightAnchor, constant: 0) ] - + NSLayoutConstraint.activate(constraints) } - } } @@ -85,11 +84,11 @@ extension ChacoalOverlayManager { containerView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor), containerView.topAnchor.constraint(equalTo: mainView.topAnchor), containerView.heightAnchor.constraint(equalTo: mainView.heightAnchor), - containerView.widthAnchor.constraint(equalTo: mainView.widthAnchor), + containerView.widthAnchor.constraint(equalTo: mainView.widthAnchor) ] - + NSLayoutConstraint.activate(constraints) - + return containerView } } @@ -109,51 +108,51 @@ extension ChacoalOverlayManager { setupBackground() let containerView = setupContainer(interactionMode) containerView.addSubview(view) - - if let anchorView = anchorView, let anchorableView = view as? CharcoalAnchorable { + + if let anchorView = anchorView, let anchorableView = view as? CharcoalAnchorable { let spacingToScreen: CGFloat = 16 let gap: CGFloat = 4 let viewSize = view.intrinsicContentSize let anchorPoint = anchorView.superview!.convert(anchorView.frame.origin, to: containerView) let targetPoint = anchorView.superview!.convert(anchorView.center, to: view) let newAnchorRect = CGRect(x: anchorPoint.x, y: anchorPoint.y, width: anchorView.frame.width, height: anchorView.frame.height) - + let viewLeadingConstant = tooltipX(anchorFrame: newAnchorRect, tooltipSize: viewSize, canvasGeometrySize: mainView.frame.size, spacingToScreen: spacingToScreen) - + let viewTopConstant = tooltipY(anchorFrame: newAnchorRect, arrowHeight: anchorableView.arrowHeight, tooltipSize: viewSize, canvasGeometrySize: mainView.frame.size, spacingToTarget: gap) - + let newTargetPoint = CGPoint(x: targetPoint.x - viewLeadingConstant, y: targetPoint.y - viewTopConstant) anchorableView.updateTargetPoint(point: newTargetPoint) - + let constraints: [NSLayoutConstraint] = [ view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: viewLeadingConstant), - view.topAnchor.constraint(equalTo: containerView.topAnchor, constant: viewTopConstant), + view.topAnchor.constraint(equalTo: containerView.topAnchor, constant: viewTopConstant) ] NSLayoutConstraint.activate(constraints) } - + display(view: containerView) - + return containerView.id } - + func display() { for containerView in overlayContainerViews { containerView.display() } } - + @objc func dismiss() { for containerView in overlayContainerViews { containerView.dismiss() } } - + /// Displays the overlay. func display(view: CharcoalIdentifiableOverlayView) { view.display() } - + /// Dismisses the overlay with the given identifier. func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { let containerView = overlayContainerViews.first { $0.id == id } @@ -162,6 +161,7 @@ extension ChacoalOverlayManager { } // MARK: Layout + extension ChacoalOverlayManager { func tooltipX(anchorFrame: CGRect, tooltipSize: CGSize, canvasGeometrySize: CGSize, spacingToScreen: CGFloat) -> CGFloat { let minX = anchorFrame.midX - (tooltipSize.width / 2.0) @@ -192,7 +192,7 @@ extension ChacoalOverlayManager { extension ChacoalOverlayManager: CharcoalIdentifiableOverlayDelegate { func overlayViewDidDismiss(_ overlayView: CharcoalIdentifiableOverlayView) { - overlayContainerViews = overlayContainerViews.filter({ $0.id != overlayView.id}) + overlayContainerViews = overlayContainerViews.filter { $0.id != overlayView.id } if overlayContainerViews.isEmpty { removeBackground() } diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index dbc8a81ac..a5421235d 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -6,18 +6,18 @@ protocol CharcoalIdentifiableOverlayDelegate: AnyObject { class CharcoalIdentifiableOverlayView: UIView, Identifiable { typealias IDValue = UUID - + let id = IDValue() - + let interactionMode: CharcoalOverlayInteractionMode - + weak var delegate: CharcoalIdentifiableOverlayDelegate? - + init(interactionMode: CharcoalOverlayInteractionMode) { self.interactionMode = interactionMode super.init(frame: .zero) - self.backgroundColor = UIColor.clear - + backgroundColor = UIColor.clear + switch interactionMode { case .block: isUserInteractionEnabled = true @@ -30,23 +30,24 @@ class CharcoalIdentifiableOverlayView: UIView, Identifiable { isUserInteractionEnabled = false } } - + + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + func display() { UIView.animate(withDuration: 0.25, animations: { [weak self] in self?.alpha = 1 }) } - + @objc func dismiss() { UIView.animate(withDuration: 0.25, animations: { [weak self] in self?.alpha = 0 }) { [weak self] _ in - guard let self = self else {return} + guard let self = self else { return } self.removeFromSuperview() self.delegate?.overlayViewDidDismiss(self) } diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalBubbleShape_UIKit.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalBubbleShape_UIKit.swift index 7262277d4..68d3ffb64 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalBubbleShape_UIKit.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalBubbleShape_UIKit.swift @@ -12,26 +12,27 @@ class CharcoalBubbleShape: CAShapeLayer { var arrowHeight: CGFloat var bubbleRadius: CGFloat var arrowWidth: CGFloat - + init(targetPoint: CGPoint, arrowHeight: CGFloat, bubbleRadius: CGFloat, arrowWidth: CGFloat) { self.targetPoint = targetPoint self.arrowHeight = arrowHeight self.bubbleRadius = bubbleRadius self.arrowWidth = arrowWidth super.init() - self.fillColor = CharcoalAsset.ColorPaletteGenerated.surface8.color.cgColor + fillColor = CharcoalAsset.ColorPaletteGenerated.surface8.color.cgColor updatePath() } - + + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func updatePath() { let rect = bounds - + var pointPosition: CharcoalTooltipLayoutPriority - + if targetPoint.x < rect.minX && targetPoint.y > rect.minY { pointPosition = .left } else if targetPoint.x > rect.maxX && targetPoint.y > rect.minY { @@ -41,67 +42,66 @@ class CharcoalBubbleShape: CAShapeLayer { } else { pointPosition = .bottom } - + let p = UIBezierPath() p.move(to: .init(x: rect.minX + bubbleRadius, y: rect.minY)) if pointPosition == .top { let arrowY = rect.minY - arrowHeight let arrowBaseY = rect.minY - + // The minimum and maximum x position of the arrow let minX = rect.minX + bubbleRadius + arrowWidth let maxX = rect.maxX - bubbleRadius - arrowWidth - + // The x position of the arrow let arrowMidX = min(max(minX, targetPoint.x), maxX) - + let arrowMaxX = arrowMidX + arrowWidth - + let arrowMinX = arrowMidX - arrowWidth - + p.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) p.addLine(to: CGPoint(x: arrowMidX, y: arrowY)) p.addLine(to: CGPoint(x: arrowMaxX, y: arrowBaseY)) } - + p.addLine(to: .init(x: rect.maxX - bubbleRadius, y: rect.minY)) p.addArc(withCenter: .init(x: rect.maxX - bubbleRadius, y: rect.minY + bubbleRadius), radius: bubbleRadius, startAngle: CGFloat.pi * 3 / 2, endAngle: 0, clockwise: true) - + if pointPosition == .right { let arrowX = rect.maxX + arrowHeight let arrowBaseX = rect.maxX - + // The minimum and maximum x position of the arrow let minY = rect.minY + bubbleRadius + arrowWidth let maxY = rect.maxY - bubbleRadius - arrowWidth - + // The x position of the arrow let arrowMidY = min(max(minY, targetPoint.y), maxY) - + let arrowMaxY = arrowMidY + arrowWidth - + let arrowMinY = arrowMidY - arrowWidth - + p.addLine(to: CGPoint(x: arrowBaseX, y: arrowMinY)) p.addLine(to: CGPoint(x: arrowX, y: arrowMidY)) p.addLine(to: CGPoint(x: arrowBaseX, y: arrowMaxY)) } - + p.addLine(to: .init(x: rect.maxX, y: rect.maxY - bubbleRadius)) p.addArc(withCenter: .init(x: rect.maxX - bubbleRadius, y: rect.maxY - bubbleRadius), radius: bubbleRadius, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true) - + if pointPosition == .bottom { let arrowY = rect.maxY + arrowHeight let arrowBaseY = rect.maxY - + // The minimum and maximum x position of the arrow let minX = rect.minX + bubbleRadius + arrowWidth let maxX = rect.maxX - bubbleRadius - arrowWidth - + // The x position of the arrow let arrowMidX = min(max(minX, targetPoint.x), maxX) - - + let arrowMaxX = arrowMidX - arrowWidth let arrowMinX = arrowMidX + arrowWidth p.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) @@ -113,18 +113,18 @@ class CharcoalBubbleShape: CAShapeLayer { if pointPosition == .left { let arrowX = rect.minX - arrowHeight let arrowBaseX = rect.minX - + // The minimum and maximum x position of the arrow let minY = rect.minY + bubbleRadius + arrowWidth let maxY = rect.maxY - bubbleRadius - arrowWidth - + // The x position of the arrow let arrowMidY = min(max(minY, targetPoint.y), maxY) - + let arrowMaxY = arrowMidY + arrowWidth - + let arrowMinY = arrowMidY - arrowWidth - + p.addLine(to: CGPoint(x: arrowBaseX, y: arrowMaxY)) p.addLine(to: CGPoint(x: arrowX, y: arrowMidY)) p.addLine(to: CGPoint(x: arrowBaseX, y: arrowMinY)) @@ -132,10 +132,10 @@ class CharcoalBubbleShape: CAShapeLayer { p.addLine(to: .init(x: rect.minX, y: rect.minY + bubbleRadius)) p.addArc(withCenter: .init(x: rect.minX + bubbleRadius, y: rect.minX + bubbleRadius), radius: bubbleRadius, startAngle: CGFloat.pi, endAngle: CGFloat.pi * 3 / 2, clockwise: true) p.close() - - self.path = p.cgPath + + path = p.cgPath } - + override func layoutSublayers() { super.layoutSublayers() updatePath() // 确保路径更新匹配图层的大小 diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index 0dac75643..c2ce8d8fb 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -5,53 +5,52 @@ public class CharcoalTooltip {} public extension CharcoalTooltip { static func show(text: String, anchorView: UIView, on: UIView? = nil) { let tooltip = CharcoalTooltipView(text: text, targetPoint: .zero) - + tooltip.translatesAutoresizingMaskIntoConstraints = false ChacoalOverlayManager.shared.show(view: tooltip, interactionMode: .dimissOnTap, anchorView: anchorView, on: on) } } - @available(iOS 17.0, *) #Preview() { let view = UIView() view.backgroundColor = UIColor.lightGray - + let button = CharcoalPrimaryMButton() button.setTitle("OK", for: .normal) button.translatesAutoresizingMaskIntoConstraints = false view.addSubview(button) - + NSLayoutConstraint.activate([ button.centerXAnchor.constraint(equalTo: view.centerXAnchor), button.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) - + let button2 = CharcoalPrimaryMButton() button2.setTitle("OK", for: .normal) button2.translatesAutoresizingMaskIntoConstraints = false view.addSubview(button2) - + NSLayoutConstraint.activate([ button2.topAnchor.constraint(equalTo: view.topAnchor), button2.leadingAnchor.constraint(equalTo: view.leadingAnchor) ]) - + let button3 = CharcoalPrimaryMButton() button3.setTitle("OK", for: .normal) button3.translatesAutoresizingMaskIntoConstraints = false view.addSubview(button3) - + NSLayoutConstraint.activate([ button3.bottomAnchor.constraint(equalTo: view.bottomAnchor), button3.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) - + DispatchQueue.main.async { CharcoalTooltip.show(text: "Hello World", anchorView: button) - + CharcoalTooltip.show(text: "Hello World This is a tooltip", anchorView: button2) - + CharcoalTooltip.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", anchorView: button3) } diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift index 33fa46441..3bcda5408 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift @@ -2,11 +2,10 @@ import UIKit protocol CharcoalAnchorable { var arrowHeight: CGFloat { get } - func updateTargetPoint(point:CGPoint) + func updateTargetPoint(point: CGPoint) } class CharcoalTooltipView: UIView, CharcoalAnchorable { - lazy var label: CharcoalTypography12 = { let label = CharcoalTypography12() label.numberOfLines = 0 @@ -14,41 +13,41 @@ class CharcoalTooltipView: UIView, CharcoalAnchorable { label.textColor = CharcoalAsset.ColorPaletteGenerated.text5.color return label }() - + let text: String - + let bubbleShape: CharcoalBubbleShape - + /// The corner radius of the tooltip let cornerRadius: CGFloat = 4 /// The height of the arrow let arrowHeight: CGFloat = 3 - + /// The width of the arrow let arrowWidth: CGFloat = 5 - + /// The max width of the tooltip let maxWidth: CGFloat - + /// Padding around the bubble let padding = UIEdgeInsets(top: 4, left: 12, bottom: 4, right: 12) - + /// Text frame size private var textFrameSize: CGSize = .zero init(text: String, targetPoint: CGPoint, maxWidth: CGFloat = 184) { - self.bubbleShape = CharcoalBubbleShape(targetPoint: targetPoint, arrowHeight: arrowHeight, bubbleRadius: cornerRadius, arrowWidth: arrowWidth) + bubbleShape = CharcoalBubbleShape(targetPoint: targetPoint, arrowHeight: arrowHeight, bubbleRadius: cornerRadius, arrowWidth: arrowWidth) self.maxWidth = maxWidth self.text = text super.init(frame: .zero) - self.textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) - self.setupLayer() + textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) + setupLayer() } - - func updateTargetPoint(point:CGPoint) { - self.bubbleShape.targetPoint = point - self.setNeedsLayout() + + func updateTargetPoint(point: CGPoint) { + bubbleShape.targetPoint = point + setNeedsLayout() } @available(*, unavailable) @@ -58,31 +57,29 @@ class CharcoalTooltipView: UIView, CharcoalAnchorable { private func setupLayer() { // Setup Bubble Shape - self.layer.addSublayer(self.bubbleShape) + layer.addSublayer(bubbleShape) // Setup Label addSubview(label) label.text = text } - private func startAnimating() { + private func startAnimating() {} - } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - - self.textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) + + textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) } override var intrinsicContentSize: CGSize { - self.textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) + textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) return CGSize(width: padding.left + textFrameSize.width + padding.right, height: padding.top + textFrameSize.height + padding.bottom) } - + override func layoutSubviews() { super.layoutSubviews() - self.bubbleShape.frame = self.bounds - self.label.frame = CGRect(x: padding.left, y: padding.top, width: textFrameSize.width, height: textFrameSize.height) + bubbleShape.frame = bounds + label.frame = CGRect(x: padding.left, y: padding.top, width: textFrameSize.width, height: textFrameSize.height) } } @@ -95,11 +92,11 @@ class CharcoalTooltipView: UIView, CharcoalAnchorable { stackView.spacing = 8.0 let tooltip = CharcoalTooltipView(text: "Hello World", targetPoint: CGPoint(x: 15, y: -5)) - + let tooltip2 = CharcoalTooltipView(text: "Hello World This is a tooltip", targetPoint: CGPoint(x: 110, y: 10)) - + let tooltip3 = CharcoalTooltipView(text: "here is testing it's multiple line feature", targetPoint: CGPoint(x: 50, y: 55)) - + let tooltip4 = CharcoalTooltipView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", targetPoint: CGPoint(x: -10, y: 25)) stackView.addArrangedSubview(tooltip) diff --git a/Sources/CharcoalUIKit/Extensions/StringExtension.swift b/Sources/CharcoalUIKit/Extensions/StringExtension.swift index 6662659e3..2dfce8b9b 100644 --- a/Sources/CharcoalUIKit/Extensions/StringExtension.swift +++ b/Sources/CharcoalUIKit/Extensions/StringExtension.swift @@ -5,7 +5,7 @@ extension String { let size = CGSize(width: maxWidth, height: .greatestFiniteMagnitude) let options: NSStringDrawingOptions = [.usesLineFragmentOrigin, .usesFontLeading] let attributes = [NSAttributedString.Key.font: font] - let rect = self.boundingRect(with: size, options: options, attributes: attributes, context: nil) + let rect = boundingRect(with: size, options: options, attributes: attributes, context: nil) return rect.size } } From dfe1553484b197638e83343d5758d2710d6d605f Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 17:46:56 +0800 Subject: [PATCH 135/199] Update StringExtension.swift --- Sources/CharcoalUIKit/Extensions/StringExtension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CharcoalUIKit/Extensions/StringExtension.swift b/Sources/CharcoalUIKit/Extensions/StringExtension.swift index 2dfce8b9b..6662659e3 100644 --- a/Sources/CharcoalUIKit/Extensions/StringExtension.swift +++ b/Sources/CharcoalUIKit/Extensions/StringExtension.swift @@ -5,7 +5,7 @@ extension String { let size = CGSize(width: maxWidth, height: .greatestFiniteMagnitude) let options: NSStringDrawingOptions = [.usesLineFragmentOrigin, .usesFontLeading] let attributes = [NSAttributedString.Key.font: font] - let rect = boundingRect(with: size, options: options, attributes: attributes, context: nil) + let rect = self.boundingRect(with: size, options: options, attributes: attributes, context: nil) return rect.size } } From a71477b7f0b8ccd2ac1f7750e6afdace5e2ce51a Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 1 May 2024 17:49:02 +0800 Subject: [PATCH 136/199] Add to UIKitSample --- .../Sources/CharcoalUIKitSample/ContentViewController.swift | 3 +++ .../CharcoalUIKitSample/Views/Tooltips/Tooltips.swift | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/ContentViewController.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/ContentViewController.swift index 4f21bd48d..891c727a0 100644 --- a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/ContentViewController.swift +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/ContentViewController.swift @@ -49,6 +49,7 @@ public final class ContentViewController: UIViewController { case colors = "Colors" case typographies = "Typographies" case icons = "Icons" + case tooltips = "Tooltips" var viewController: UIViewController { switch self { @@ -64,6 +65,8 @@ public final class ContentViewController: UIViewController { return SelectionsViewController() case .textFields: return TextFieldsViewController() + case .tooltips: + return TooltipsViewController() } } } diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift index 7080bf36a..52ab2c9ab 100644 --- a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Tooltips/Tooltips.swift @@ -30,7 +30,7 @@ enum TooltipTitles: String, CaseIterable { } } -public final class TooltipsController: UIViewController { +public final class TooltipsViewController: UIViewController { private lazy var tableView: UITableView = { let view = UITableView(frame: .zero, style: .insetGrouped) view.translatesAutoresizingMaskIntoConstraints = false @@ -98,7 +98,7 @@ public final class TooltipsController: UIViewController { } } -extension TooltipsController: UITableViewDelegate, UITableViewDataSource { +extension TooltipsViewController: UITableViewDelegate, UITableViewDataSource { public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let section = Sections.allCases[indexPath.section] @@ -142,6 +142,6 @@ extension TooltipsController: UITableViewDelegate, UITableViewDataSource { @available(iOS 17.0, *) #Preview { - let viewController = TooltipsController() + let viewController = TooltipsViewController() return viewController } From 11c805a9be55d5a78ec3fbb0870c080879c43203 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 8 May 2024 12:06:13 +0800 Subject: [PATCH 137/199] Fix public requirements --- .../CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 863e4401f..c50d5f285 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -58,7 +58,7 @@ struct CharcoalTooltip: CharcoalPopupView { return min(minX, edgeBottom) } - var body: some View { + public var body: some View { GeometryReader(content: { canvasGeometry in VStack { Text(text) @@ -94,7 +94,7 @@ struct CharcoalTooltip: CharcoalPopupView { }) } - static func == (lhs: CharcoalTooltip, rhs: CharcoalTooltip) -> Bool { + public static func == (lhs: CharcoalTooltip, rhs: CharcoalTooltip) -> Bool { return lhs.text == rhs.text && lhs.targetFrame == rhs.targetFrame && lhs.maxWidth == rhs.maxWidth && lhs.tooltipSize == rhs.tooltipSize } } From 6d1c0399243a40ff9aa9cb78d5fe3c9c41709c69 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 8 May 2024 12:15:15 +0800 Subject: [PATCH 138/199] Reformat --- .../Tooltip/CharcoalAnchorable.swift | 6 ++++++ .../Components/Tooltip/CharcoalTooltip.swift | 18 +++++++++++++++++- .../Tooltip/CharcoalTooltipView.swift | 8 -------- 3 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 Sources/CharcoalUIKit/Components/Tooltip/CharcoalAnchorable.swift diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalAnchorable.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalAnchorable.swift new file mode 100644 index 000000000..0565fd402 --- /dev/null +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalAnchorable.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol CharcoalAnchorable { + var arrowHeight: CGFloat { get } + func updateTargetPoint(point: CGPoint) +} diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index c2ce8d8fb..1d9258dbf 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -3,11 +3,27 @@ import UIKit public class CharcoalTooltip {} public extension CharcoalTooltip { + /** + Show a tooltip anchored to a view. + + - Parameters: + - text: The text to be displayed in the tooltip. + - anchorView: The view to which the tooltip will be anchored. + - on: The view on which the tooltip will be displayed. If not provided, the tooltip will be displayed on the window. + + # Example # + ```swift + CharcoalTooltip.show(text: "This is a tooltip", anchorView: someView) + ``` + */ static func show(text: String, anchorView: UIView, on: UIView? = nil) { let tooltip = CharcoalTooltipView(text: text, targetPoint: .zero) tooltip.translatesAutoresizingMaskIntoConstraints = false - ChacoalOverlayManager.shared.show(view: tooltip, interactionMode: .dimissOnTap, anchorView: anchorView, on: on) + + DispatchQueue.main.async { + ChacoalOverlayManager.shared.show(view: tooltip, interactionMode: .dimissOnTap, anchorView: anchorView, on: on) + } } } diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift index 3bcda5408..4e8513624 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltipView.swift @@ -1,10 +1,5 @@ import UIKit -protocol CharcoalAnchorable { - var arrowHeight: CGFloat { get } - func updateTargetPoint(point: CGPoint) -} - class CharcoalTooltipView: UIView, CharcoalAnchorable { lazy var label: CharcoalTypography12 = { let label = CharcoalTypography12() @@ -63,8 +58,6 @@ class CharcoalTooltipView: UIView, CharcoalAnchorable { label.text = text } - private func startAnimating() {} - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) @@ -72,7 +65,6 @@ class CharcoalTooltipView: UIView, CharcoalAnchorable { } override var intrinsicContentSize: CGSize { - textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) return CGSize(width: padding.left + textFrameSize.width + padding.right, height: padding.top + textFrameSize.height + padding.bottom) } From 9dcdb2c78e286cd7951aea65988e6c6913ac0d0a Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 8 May 2024 13:55:32 +0800 Subject: [PATCH 139/199] Use touch began to handle dismiss on touch --- .../Components/Overlay/ChacoalOverlayManager.swift | 8 +------- .../Overlay/CharcoalIdentifiableOverlayView.swift | 14 ++++++++++---- .../Overlay/CharcoalOverlayInteractionMode.swift | 7 +++++++ .../Components/Tooltip/CharcoalTooltip.swift | 2 +- 4 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 Sources/CharcoalUIKit/Components/Overlay/CharcoalOverlayInteractionMode.swift diff --git a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift index e4b25976a..29b46c73c 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift @@ -1,11 +1,5 @@ import UIKit -enum CharcoalOverlayInteractionMode { - case passThrough - case block - case dimissOnTap -} - /** Displays a overlay on the screen. */ @@ -100,7 +94,7 @@ extension ChacoalOverlayManager { func show( view: UIView, transparentBackground: Bool = false, - interactionMode: CharcoalOverlayInteractionMode = .dimissOnTap, + interactionMode: CharcoalOverlayInteractionMode = .dimissOnTouch, anchorView: UIView? = nil, on superView: UIView? = nil ) -> CharcoalIdentifiableOverlayView.IDValue { diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index a5421235d..978081f42 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -21,15 +21,21 @@ class CharcoalIdentifiableOverlayView: UIView, Identifiable { switch interactionMode { case .block: isUserInteractionEnabled = true - case .dimissOnTap: + case .dimissOnTouch: isUserInteractionEnabled = true - // Add dismiss tap gesture - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismiss)) - addGestureRecognizer(tapGesture) case .passThrough: isUserInteractionEnabled = false } } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + + // Dismiss on tap or scroll + if interactionMode == .dimissOnTouch { + dismiss() + } + } @available(*, unavailable) required init?(coder: NSCoder) { diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalOverlayInteractionMode.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalOverlayInteractionMode.swift new file mode 100644 index 000000000..13974a6d5 --- /dev/null +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalOverlayInteractionMode.swift @@ -0,0 +1,7 @@ +import Foundation + +enum CharcoalOverlayInteractionMode { + case passThrough + case block + case dimissOnTouch +} diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index 1d9258dbf..874fa8295 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -22,7 +22,7 @@ public extension CharcoalTooltip { tooltip.translatesAutoresizingMaskIntoConstraints = false DispatchQueue.main.async { - ChacoalOverlayManager.shared.show(view: tooltip, interactionMode: .dimissOnTap, anchorView: anchorView, on: on) + ChacoalOverlayManager.shared.show(view: tooltip, interactionMode: .dimissOnTouch, anchorView: anchorView, on: on) } } } From 7708f102d18ed341c938708fef8fffe253e145ce Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 8 May 2024 13:55:56 +0800 Subject: [PATCH 140/199] Update CharcoalIdentifiableOverlayView.swift --- .../Components/Overlay/CharcoalIdentifiableOverlayView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 978081f42..ee7b302d7 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -27,10 +27,10 @@ class CharcoalIdentifiableOverlayView: UIView, Identifiable { isUserInteractionEnabled = false } } - + override func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) - + // Dismiss on tap or scroll if interactionMode == .dimissOnTouch { dismiss() From 67a9672a21563f1bc07147fd12d5dca4e6fc7fde Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 14 May 2024 17:53:20 +0800 Subject: [PATCH 141/199] Add CharcoalToastView --- .../Components/CharcoalToastAppearance.swift | 13 +++ .../Components/Toast/CharcoalToast.swift | 16 +-- .../Components/Toast/CharcoalToastView.swift | 110 ++++++++++++++++++ 3 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 Sources/CharcoalShared/Components/CharcoalToastAppearance.swift create mode 100644 Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift diff --git a/Sources/CharcoalShared/Components/CharcoalToastAppearance.swift b/Sources/CharcoalShared/Components/CharcoalToastAppearance.swift new file mode 100644 index 000000000..7600d232b --- /dev/null +++ b/Sources/CharcoalShared/Components/CharcoalToastAppearance.swift @@ -0,0 +1,13 @@ +public enum CharcoalToastAppearance { + case success + case error + + public var background: ColorAsset.Color { + switch self { + case .success: + return CharcoalAsset.ColorPaletteGenerated.success.color + case .error: + return CharcoalAsset.ColorPaletteGenerated.assertive.color + } + } +} diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index ed9998106..55ed6cb47 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -79,7 +79,7 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalToastB .padding(EdgeInsets(top: 8, leading: 24, bottom: 8, trailing: 24)) } .background( - appearance.background + Color(appearance.background) ) .charcoalAnimatableToast( isPresenting: $isPresenting, @@ -110,20 +110,6 @@ struct PopupViewSizeKey: PreferenceKey { static var defaultValue: CGSize = .zero } -public enum CharcoalToastAppearance { - case success - case error - - var background: Color { - switch self { - case .success: - return Color(CharcoalAsset.ColorPaletteGenerated.success.color) - case .error: - return Color(CharcoalAsset.ColorPaletteGenerated.assertive.color) - } - } -} - public struct CharcoalToastAnimationConfiguration { public let enablePositionAnimation: Bool public let animation: Animation diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift new file mode 100644 index 000000000..930471397 --- /dev/null +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift @@ -0,0 +1,110 @@ +import UIKit + +class CharcoalToastView: UIView { + lazy var label: CharcoalTypography14 = { + let label = CharcoalTypography14() + label.numberOfLines = 0 + label.isBold = true + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = CharcoalAsset.ColorPaletteGenerated.text5.color + return label + }() + + let text: String + + /// The appearance of the Toast + let appearance: CharcoalToastAppearance + + lazy var capsuleShape: UIView = { + let view = UIView(frame: CGRect.zero) + view.layer.cornerCurve = .continuous + return view + }() + + /// The corner radius of the tooltip + let cornerRadius: CGFloat = 32 + + let borderColor: ColorAsset.Color + + let borderLineWidth: CGFloat = 2 + + /// The max width of the tooltip + let maxWidth: CGFloat + + /// Padding around the bubble + let padding = UIEdgeInsets(top: 8, left: 24, bottom: 8, right: 24) + + /// Text frame size + private var textFrameSize: CGSize = .zero + + init(text: String, targetPoint: CGPoint, maxWidth: CGFloat = 312, appearance: CharcoalToastAppearance = .success) { + self.maxWidth = maxWidth + self.text = text + self.appearance = appearance + self.borderColor = CharcoalAsset.ColorPaletteGenerated.background1.color + super.init(frame: .zero) + textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) + setupLayer() + } + + func updateTargetPoint(point: CGPoint) { + setNeedsLayout() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayer() { + // Setup Bubble Shape + addSubview(capsuleShape) + capsuleShape.backgroundColor = appearance.background + capsuleShape.layer.borderColor = borderColor.cgColor + capsuleShape.layer.borderWidth = borderLineWidth + // Setup Label + addSubview(label) + label.text = text + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) + } + + override var intrinsicContentSize: CGSize { + return CGSize(width: padding.left + textFrameSize.width + padding.right, height: padding.top + textFrameSize.height + padding.bottom) + } + + override func layoutSubviews() { + super.layoutSubviews() + capsuleShape.frame = bounds + capsuleShape.layer.cornerRadius = min(cornerRadius, bounds.height / 2.0) + label.frame = CGRect(x: padding.left, y: padding.top, width: textFrameSize.width, height: textFrameSize.height) + } +} + +@available(iOS 17.0, *) +#Preview(traits: .sizeThatFitsLayout) { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.distribution = .fill + stackView.alignment = .center + stackView.spacing = 8.0 + + let tooltip = CharcoalToastView(text: "Hello World", targetPoint: CGPoint(x: 15, y: -5)) + + let tooltip2 = CharcoalToastView(text: "Hello World This is a tooltip", targetPoint: CGPoint(x: 110, y: 10)) + + let tooltip3 = CharcoalToastView(text: "here is testing it's multiple line feature", targetPoint: CGPoint(x: 50, y: 55)) + + let tooltip4 = CharcoalToastView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", targetPoint: CGPoint(x: -10, y: 25)) + + stackView.addArrangedSubview(tooltip) + stackView.addArrangedSubview(tooltip2) + stackView.addArrangedSubview(tooltip3) + stackView.addArrangedSubview(tooltip4) + + return stackView +} From 03b5735d17ea48daeb2f585f7024aac0c36ffa47 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 14 May 2024 17:56:59 +0800 Subject: [PATCH 142/199] change cornerRadius --- .../Components/Toast/CharcoalToastView.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift index 930471397..dd856206b 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift @@ -60,8 +60,10 @@ class CharcoalToastView: UIView { // Setup Bubble Shape addSubview(capsuleShape) capsuleShape.backgroundColor = appearance.background - capsuleShape.layer.borderColor = borderColor.cgColor - capsuleShape.layer.borderWidth = borderLineWidth + layer.borderColor = borderColor.cgColor + layer.borderWidth = borderLineWidth + layer.masksToBounds = true + layer.cornerCurve = .continuous // Setup Label addSubview(label) label.text = text @@ -80,7 +82,7 @@ class CharcoalToastView: UIView { override func layoutSubviews() { super.layoutSubviews() capsuleShape.frame = bounds - capsuleShape.layer.cornerRadius = min(cornerRadius, bounds.height / 2.0) + layer.cornerRadius = min(cornerRadius, bounds.height / 2.0) label.frame = CGRect(x: padding.left, y: padding.top, width: textFrameSize.width, height: textFrameSize.height) } } @@ -95,7 +97,7 @@ class CharcoalToastView: UIView { let tooltip = CharcoalToastView(text: "Hello World", targetPoint: CGPoint(x: 15, y: -5)) - let tooltip2 = CharcoalToastView(text: "Hello World This is a tooltip", targetPoint: CGPoint(x: 110, y: 10)) + let tooltip2 = CharcoalToastView(text: "Hello World This is a tooltip", targetPoint: CGPoint(x: 110, y: 10), appearance: .error) let tooltip3 = CharcoalToastView(text: "here is testing it's multiple line feature", targetPoint: CGPoint(x: 50, y: 55)) From 4ccd0465bd3befe05dc181519e0faf42a1000d56 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 13:21:43 +0800 Subject: [PATCH 143/199] Add CharcoalToast --- .../SwiftUISample.xcodeproj/project.pbxproj | 1 - .../CharcoalButtonSizes.swift | 0 .../Enums/CharcoalPopupViewEdge.swift | 4 + .../CharcoalToastAppearance.swift | 0 .../Overlay/CharcoalPopupProtocol.swift | 5 +- .../Components/Toast/CharcoalToast.swift | 82 +++++++++++++++++++ .../Components/Toast/CharcoalToastView.swift | 10 +-- 7 files changed, 92 insertions(+), 10 deletions(-) rename Sources/CharcoalShared/{Components => Enums}/CharcoalButtonSizes.swift (100%) create mode 100644 Sources/CharcoalShared/Enums/CharcoalPopupViewEdge.swift rename Sources/CharcoalShared/{Components => Enums}/CharcoalToastAppearance.swift (100%) create mode 100644 Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift diff --git a/Examples/SwiftUISample/SwiftUISample.xcodeproj/project.pbxproj b/Examples/SwiftUISample/SwiftUISample.xcodeproj/project.pbxproj index 7eeebbcfd..824c2d302 100644 --- a/Examples/SwiftUISample/SwiftUISample.xcodeproj/project.pbxproj +++ b/Examples/SwiftUISample/SwiftUISample.xcodeproj/project.pbxproj @@ -157,7 +157,6 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "unset SDKROOT\nswift run -c release --package-path ../../BuildTools swiftlint --config ../../.swiftlint.yml\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/Sources/CharcoalShared/Components/CharcoalButtonSizes.swift b/Sources/CharcoalShared/Enums/CharcoalButtonSizes.swift similarity index 100% rename from Sources/CharcoalShared/Components/CharcoalButtonSizes.swift rename to Sources/CharcoalShared/Enums/CharcoalButtonSizes.swift diff --git a/Sources/CharcoalShared/Enums/CharcoalPopupViewEdge.swift b/Sources/CharcoalShared/Enums/CharcoalPopupViewEdge.swift new file mode 100644 index 000000000..65961f063 --- /dev/null +++ b/Sources/CharcoalShared/Enums/CharcoalPopupViewEdge.swift @@ -0,0 +1,4 @@ +public enum CharcoalPopupViewEdge { + case top + case bottom +} diff --git a/Sources/CharcoalShared/Components/CharcoalToastAppearance.swift b/Sources/CharcoalShared/Enums/CharcoalToastAppearance.swift similarity index 100% rename from Sources/CharcoalShared/Components/CharcoalToastAppearance.swift rename to Sources/CharcoalShared/Enums/CharcoalToastAppearance.swift diff --git a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift index fae2d331c..00f470e5b 100644 --- a/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift +++ b/Sources/CharcoalSwiftUI/Components/Overlay/CharcoalPopupProtocol.swift @@ -2,10 +2,7 @@ import SwiftUI typealias CharcoalPopupProtocol = Equatable & Identifiable & View -public enum CharcoalPopupViewEdge { - case top - case bottom - +extension CharcoalPopupViewEdge { var alignment: Alignment { switch self { case .top: diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift new file mode 100644 index 000000000..385215931 --- /dev/null +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift @@ -0,0 +1,82 @@ +import UIKit + +public class CharcoalToast {} + +public extension CharcoalToast { + /** + Show a toast. + + - Parameters: + - text: The text to be displayed in the toast. + - maxWidth: The maximum width of the toast. + - appearance: The appearance of the toast. + - screenEdge: The edge of the screen where the toast will be displayed. + - screenEdgeSpacing: The spacing between the toast and the screen edge. + - on: The view on which the toast will be displayed. + + # Example # + ```swift + CharcoalToast.show(text: "This is a toast") + ``` + */ + static func show(text: String, + maxWidth: CGFloat = 312, + appearance: CharcoalToastAppearance = .success, + screenEdge: CharcoalPopupViewEdge = .bottom, + screenEdgeSpacing: CGFloat = 120, + on: UIView? = nil) { + let toastView = CharcoalToastView(text: text, maxWidth: maxWidth, appearance: appearance) + + toastView.translatesAutoresizingMaskIntoConstraints = false + + DispatchQueue.main.async { + ChacoalOverlayManager.shared.show(view: toastView, interactionMode: .passThrough, on: on) + } + } +} + +@available(iOS 17.0, *) +#Preview() { + let view = UIView() + view.backgroundColor = UIColor.lightGray + + let button = CharcoalPrimaryMButton() + button.setTitle("OK", for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(button) + + NSLayoutConstraint.activate([ + button.centerXAnchor.constraint(equalTo: view.centerXAnchor), + button.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ]) + + let button2 = CharcoalPrimaryMButton() + button2.setTitle("OK", for: .normal) + button2.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(button2) + + NSLayoutConstraint.activate([ + button2.topAnchor.constraint(equalTo: view.topAnchor), + button2.leadingAnchor.constraint(equalTo: view.leadingAnchor) + ]) + + let button3 = CharcoalPrimaryMButton() + button3.setTitle("OK", for: .normal) + button3.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(button3) + + NSLayoutConstraint.activate([ + button3.bottomAnchor.constraint(equalTo: view.bottomAnchor), + button3.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + + DispatchQueue.main.async { + CharcoalTooltip.show(text: "Hello World", anchorView: button) + + CharcoalTooltip.show(text: "Hello World This is a tooltip", anchorView: button2) + + CharcoalTooltip.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", anchorView: button3) + } + + return view +} diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift index dd856206b..cf0b98262 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift @@ -37,7 +37,7 @@ class CharcoalToastView: UIView { /// Text frame size private var textFrameSize: CGSize = .zero - init(text: String, targetPoint: CGPoint, maxWidth: CGFloat = 312, appearance: CharcoalToastAppearance = .success) { + init(text: String, maxWidth: CGFloat = 312, appearance: CharcoalToastAppearance = .success) { self.maxWidth = maxWidth self.text = text self.appearance = appearance @@ -95,13 +95,13 @@ class CharcoalToastView: UIView { stackView.alignment = .center stackView.spacing = 8.0 - let tooltip = CharcoalToastView(text: "Hello World", targetPoint: CGPoint(x: 15, y: -5)) + let tooltip = CharcoalToastView(text: "Hello World") - let tooltip2 = CharcoalToastView(text: "Hello World This is a tooltip", targetPoint: CGPoint(x: 110, y: 10), appearance: .error) + let tooltip2 = CharcoalToastView(text: "Hello World This is a tooltip", appearance: .error) - let tooltip3 = CharcoalToastView(text: "here is testing it's multiple line feature", targetPoint: CGPoint(x: 50, y: 55)) + let tooltip3 = CharcoalToastView(text: "here is testing it's multiple line feature") - let tooltip4 = CharcoalToastView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", targetPoint: CGPoint(x: -10, y: 25)) + let tooltip4 = CharcoalToastView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature") stackView.addArrangedSubview(tooltip) stackView.addArrangedSubview(tooltip2) From 752ba6daa9bbfcad7fb2213c0e2c236da22b9e8e Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 14:27:44 +0800 Subject: [PATCH 144/199] Move show logic out --- .../Overlay/ChacoalOverlayManager.swift | 60 +------------------ .../CharcoalIdentifiableOverlayView.swift | 8 +-- .../Components/Tooltip/CharcoalTooltip.swift | 59 +++++++++++++++++- 3 files changed, 63 insertions(+), 64 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift index 29b46c73c..7452d73a8 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift @@ -91,43 +91,17 @@ extension ChacoalOverlayManager { extension ChacoalOverlayManager { @discardableResult - func show( + func layout( view: UIView, transparentBackground: Bool = false, interactionMode: CharcoalOverlayInteractionMode = .dimissOnTouch, - anchorView: UIView? = nil, on superView: UIView? = nil - ) -> CharcoalIdentifiableOverlayView.IDValue { + ) -> CharcoalIdentifiableOverlayView { setupSuperView(view: superView) setupBackground() let containerView = setupContainer(interactionMode) containerView.addSubview(view) - - if let anchorView = anchorView, let anchorableView = view as? CharcoalAnchorable { - let spacingToScreen: CGFloat = 16 - let gap: CGFloat = 4 - let viewSize = view.intrinsicContentSize - let anchorPoint = anchorView.superview!.convert(anchorView.frame.origin, to: containerView) - let targetPoint = anchorView.superview!.convert(anchorView.center, to: view) - let newAnchorRect = CGRect(x: anchorPoint.x, y: anchorPoint.y, width: anchorView.frame.width, height: anchorView.frame.height) - - let viewLeadingConstant = tooltipX(anchorFrame: newAnchorRect, tooltipSize: viewSize, canvasGeometrySize: mainView.frame.size, spacingToScreen: spacingToScreen) - - let viewTopConstant = tooltipY(anchorFrame: newAnchorRect, arrowHeight: anchorableView.arrowHeight, tooltipSize: viewSize, canvasGeometrySize: mainView.frame.size, spacingToTarget: gap) - - let newTargetPoint = CGPoint(x: targetPoint.x - viewLeadingConstant, y: targetPoint.y - viewTopConstant) - anchorableView.updateTargetPoint(point: newTargetPoint) - - let constraints: [NSLayoutConstraint] = [ - view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: viewLeadingConstant), - view.topAnchor.constraint(equalTo: containerView.topAnchor, constant: viewTopConstant) - ] - NSLayoutConstraint.activate(constraints) - } - - display(view: containerView) - - return containerView.id + return containerView } func display() { @@ -154,34 +128,6 @@ extension ChacoalOverlayManager { } } -// MARK: Layout - -extension ChacoalOverlayManager { - func tooltipX(anchorFrame: CGRect, tooltipSize: CGSize, canvasGeometrySize: CGSize, spacingToScreen: CGFloat) -> CGFloat { - let minX = anchorFrame.midX - (tooltipSize.width / 2.0) - - var edgeLeft = minX - - if edgeLeft + tooltipSize.width >= canvasGeometrySize.width { - edgeLeft = canvasGeometrySize.width - tooltipSize.width - spacingToScreen - } else if edgeLeft < spacingToScreen { - edgeLeft = spacingToScreen - } - - return edgeLeft - } - - func tooltipY(anchorFrame: CGRect, arrowHeight: CGFloat, tooltipSize: CGSize, canvasGeometrySize: CGSize, spacingToTarget: CGFloat) -> CGFloat { - let minX = anchorFrame.maxY + spacingToTarget + arrowHeight - var edgeBottom = anchorFrame.maxY + spacingToTarget + anchorFrame.height - if edgeBottom + tooltipSize.height >= canvasGeometrySize.height { - edgeBottom = anchorFrame.minY - tooltipSize.height - spacingToTarget - arrowHeight - } - - return min(minX, edgeBottom) - } -} - // MARK: - CharcoalIdentifiableOverlayDelegate extension ChacoalOverlayManager: CharcoalIdentifiableOverlayDelegate { diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index ee7b302d7..900c22569 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -4,10 +4,10 @@ protocol CharcoalIdentifiableOverlayDelegate: AnyObject { func overlayViewDidDismiss(_ overlayView: CharcoalIdentifiableOverlayView) } -class CharcoalIdentifiableOverlayView: UIView, Identifiable { - typealias IDValue = UUID +public class CharcoalIdentifiableOverlayView: UIView, Identifiable { + public typealias IDValue = UUID - let id = IDValue() + public let id = IDValue() let interactionMode: CharcoalOverlayInteractionMode @@ -28,7 +28,7 @@ class CharcoalIdentifiableOverlayView: UIView, Identifiable { } } - override func touchesBegan(_ touches: Set, with event: UIEvent?) { + public override func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) // Dismiss on tap or scroll diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index 874fa8295..440f6eff0 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -16,14 +16,67 @@ public extension CharcoalTooltip { CharcoalTooltip.show(text: "This is a tooltip", anchorView: someView) ``` */ - static func show(text: String, anchorView: UIView, on: UIView? = nil) { + @discardableResult + static func show(text: String, anchorView: UIView, on: UIView? = nil) -> CharcoalIdentifiableOverlayView.IDValue { let tooltip = CharcoalTooltipView(text: text, targetPoint: .zero) tooltip.translatesAutoresizingMaskIntoConstraints = false - DispatchQueue.main.async { - ChacoalOverlayManager.shared.show(view: tooltip, interactionMode: .dimissOnTouch, anchorView: anchorView, on: on) + let containerView = ChacoalOverlayManager.shared.layout(view: tooltip, interactionMode: .dimissOnTouch, on: on) + let mainView = ChacoalOverlayManager.shared.mainView! + let spacingToScreen: CGFloat = 16 + let gap: CGFloat = 4 + let viewSize = tooltip.intrinsicContentSize + let anchorPoint = anchorView.superview!.convert(anchorView.frame.origin, to: containerView) + let targetPoint = anchorView.superview!.convert(anchorView.center, to: tooltip) + let newAnchorRect = CGRect(x: anchorPoint.x, y: anchorPoint.y, width: anchorView.frame.width, height: anchorView.frame.height) + + let viewLeadingConstant = tooltipX(anchorFrame: newAnchorRect, tooltipSize: viewSize, canvasGeometrySize: mainView.frame.size, spacingToScreen: spacingToScreen) + + let viewTopConstant = tooltipY(anchorFrame: newAnchorRect, arrowHeight: tooltip.arrowHeight, tooltipSize: viewSize, canvasGeometrySize: mainView.frame.size, spacingToTarget: gap) + + let newTargetPoint = CGPoint(x: targetPoint.x - viewLeadingConstant, y: targetPoint.y - viewTopConstant) + tooltip.updateTargetPoint(point: newTargetPoint) + + let constraints: [NSLayoutConstraint] = [ + tooltip.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: viewLeadingConstant), + tooltip.topAnchor.constraint(equalTo: containerView.topAnchor, constant: viewTopConstant) + ] + NSLayoutConstraint.activate(constraints) + + ChacoalOverlayManager.shared.display(view: containerView) + + return containerView.id + } + + /// Dismisses the tooltip with the given identifier. + static func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { + let containerView = ChacoalOverlayManager.shared.overlayContainerViews.first { $0.id == id } + containerView?.dismiss() + } + + static func tooltipX(anchorFrame: CGRect, tooltipSize: CGSize, canvasGeometrySize: CGSize, spacingToScreen: CGFloat) -> CGFloat { + let minX = anchorFrame.midX - (tooltipSize.width / 2.0) + + var edgeLeft = minX + + if edgeLeft + tooltipSize.width >= canvasGeometrySize.width { + edgeLeft = canvasGeometrySize.width - tooltipSize.width - spacingToScreen + } else if edgeLeft < spacingToScreen { + edgeLeft = spacingToScreen } + + return edgeLeft + } + + static func tooltipY(anchorFrame: CGRect, arrowHeight: CGFloat, tooltipSize: CGSize, canvasGeometrySize: CGSize, spacingToTarget: CGFloat) -> CGFloat { + let minX = anchorFrame.maxY + spacingToTarget + arrowHeight + var edgeBottom = anchorFrame.maxY + spacingToTarget + anchorFrame.height + if edgeBottom + tooltipSize.height >= canvasGeometrySize.height { + edgeBottom = anchorFrame.minY - tooltipSize.height - spacingToTarget - arrowHeight + } + + return min(minX, edgeBottom) } } From 1f104197ed1f55254a383e881314ff652b848a84 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 14:28:06 +0800 Subject: [PATCH 145/199] Reformat --- .../Components/Overlay/ChacoalOverlayManager.swift | 2 +- .../Overlay/CharcoalIdentifiableOverlayView.swift | 2 +- .../CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift index 7452d73a8..bca0693ab 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift @@ -96,7 +96,7 @@ extension ChacoalOverlayManager { transparentBackground: Bool = false, interactionMode: CharcoalOverlayInteractionMode = .dimissOnTouch, on superView: UIView? = nil - ) -> CharcoalIdentifiableOverlayView { + ) -> CharcoalIdentifiableOverlayView { setupSuperView(view: superView) setupBackground() let containerView = setupContainer(interactionMode) diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 900c22569..8b4e8e0a2 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -28,7 +28,7 @@ public class CharcoalIdentifiableOverlayView: UIView, Identifiable { } } - public override func touchesBegan(_ touches: Set, with event: UIEvent?) { + override public func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) // Dismiss on tap or scroll diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index 440f6eff0..5b159035e 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -43,9 +43,9 @@ public extension CharcoalTooltip { tooltip.topAnchor.constraint(equalTo: containerView.topAnchor, constant: viewTopConstant) ] NSLayoutConstraint.activate(constraints) - + ChacoalOverlayManager.shared.display(view: containerView) - + return containerView.id } @@ -54,7 +54,7 @@ public extension CharcoalTooltip { let containerView = ChacoalOverlayManager.shared.overlayContainerViews.first { $0.id == id } containerView?.dismiss() } - + static func tooltipX(anchorFrame: CGRect, tooltipSize: CGSize, canvasGeometrySize: CGSize, spacingToScreen: CGFloat) -> CGFloat { let minX = anchorFrame.midX - (tooltipSize.width / 2.0) From e0231f09f3f1a0b571369867d0cfce23c1ba82e2 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 14:29:05 +0800 Subject: [PATCH 146/199] Refine dismiss method --- Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index 5b159035e..27dab2f0d 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -51,8 +51,7 @@ public extension CharcoalTooltip { /// Dismisses the tooltip with the given identifier. static func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { - let containerView = ChacoalOverlayManager.shared.overlayContainerViews.first { $0.id == id } - containerView?.dismiss() + ChacoalOverlayManager.shared.dismiss(id: id) } static func tooltipX(anchorFrame: CGRect, tooltipSize: CGSize, canvasGeometrySize: CGSize, spacingToScreen: CGFloat) -> CGFloat { From a8cde5d4487e8b120ec8bc4e88505dc485edff6f Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 14:48:06 +0800 Subject: [PATCH 147/199] Update CharcoalToast.swift --- Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift index 385215931..a413dca35 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift @@ -29,9 +29,7 @@ public extension CharcoalToast { toastView.translatesAutoresizingMaskIntoConstraints = false - DispatchQueue.main.async { - ChacoalOverlayManager.shared.show(view: toastView, interactionMode: .passThrough, on: on) - } + let containerView = ChacoalOverlayManager.shared.layout(view: toastView, interactionMode: .passThrough, on: on) } } From f75fb7af1919e82f29cee4be2f641fef273c9fe3 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 14:57:39 +0800 Subject: [PATCH 148/199] Add ActionContent and ActionComplete callback --- .../CharcoalIdentifiableOverlayView.swift | 19 ++++++++++++------- .../Components/Tooltip/CharcoalTooltip.swift | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 8b4e8e0a2..8e7630928 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -6,10 +6,20 @@ protocol CharcoalIdentifiableOverlayDelegate: AnyObject { public class CharcoalIdentifiableOverlayView: UIView, Identifiable { public typealias IDValue = UUID + + public typealias ActionComplete = (Bool) -> Void + + public typealias ActionContent = (ActionComplete?) -> Void public let id = IDValue() let interactionMode: CharcoalOverlayInteractionMode + + /// Action to show the overlay. + var showAction: ActionContent? + + /// Action to dismiss the overlay. + var dismissAction: ActionContent? weak var delegate: CharcoalIdentifiableOverlayDelegate? @@ -43,16 +53,11 @@ public class CharcoalIdentifiableOverlayView: UIView, Identifiable { } func display() { - UIView.animate(withDuration: 0.25, animations: { [weak self] in - self?.alpha = 1 - }) + showAction?(nil) } @objc func dismiss() { - UIView.animate(withDuration: 0.25, animations: { [weak self] in - self?.alpha = 0 - }) { - [weak self] _ in + dismissAction?() { [weak self] finished in guard let self = self else { return } self.removeFromSuperview() self.delegate?.overlayViewDidDismiss(self) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index 27dab2f0d..e2ddc6e6e 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -43,6 +43,22 @@ public extension CharcoalTooltip { tooltip.topAnchor.constraint(equalTo: containerView.topAnchor, constant: viewTopConstant) ] NSLayoutConstraint.activate(constraints) + + containerView.showAction = { actionCallback in + UIView.animate(withDuration: 0.25, animations: { + containerView.alpha = 1 + }) { completion in + actionCallback?(completion) + } + } + + containerView.dismissAction = { actionCallback in + UIView.animate(withDuration: 0.25, animations: { + containerView.alpha = 0 + }) { completion in + actionCallback?(completion) + } + } ChacoalOverlayManager.shared.display(view: containerView) From 6b6bd1a0fd1c7f342dcdce012e2c422694f912d8 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 15:06:21 +0800 Subject: [PATCH 149/199] Update CharcoalToast.swift --- .../Components/Toast/CharcoalToast.swift | 73 ++++++++++--------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift index a413dca35..a8a50dba7 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift @@ -19,17 +19,51 @@ public extension CharcoalToast { CharcoalToast.show(text: "This is a toast") ``` */ + @discardableResult static func show(text: String, maxWidth: CGFloat = 312, appearance: CharcoalToastAppearance = .success, screenEdge: CharcoalPopupViewEdge = .bottom, screenEdgeSpacing: CGFloat = 120, - on: UIView? = nil) { + on: UIView? = nil) -> CharcoalIdentifiableOverlayView.IDValue { let toastView = CharcoalToastView(text: text, maxWidth: maxWidth, appearance: appearance) toastView.translatesAutoresizingMaskIntoConstraints = false let containerView = ChacoalOverlayManager.shared.layout(view: toastView, interactionMode: .passThrough, on: on) + + var constraints = [ + toastView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) + ] + + switch screenEdge { + case .top: + + case .bottom: + + } + + NSLayoutConstraint.activate(constraints) + + containerView.showAction = { actionCallback in + UIView.animate(withDuration: 0.25, animations: { + containerView.alpha = 1 + }) { completion in + actionCallback?(completion) + } + } + + containerView.dismissAction = { actionCallback in + UIView.animate(withDuration: 0.25, animations: { + containerView.alpha = 0 + }) { completion in + actionCallback?(completion) + } + } + + ChacoalOverlayManager.shared.display(view: containerView) + + return containerView.id } } @@ -38,42 +72,9 @@ public extension CharcoalToast { let view = UIView() view.backgroundColor = UIColor.lightGray - let button = CharcoalPrimaryMButton() - button.setTitle("OK", for: .normal) - button.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(button) - - NSLayoutConstraint.activate([ - button.centerXAnchor.constraint(equalTo: view.centerXAnchor), - button.centerYAnchor.constraint(equalTo: view.centerYAnchor) - ]) - - let button2 = CharcoalPrimaryMButton() - button2.setTitle("OK", for: .normal) - button2.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(button2) - - NSLayoutConstraint.activate([ - button2.topAnchor.constraint(equalTo: view.topAnchor), - button2.leadingAnchor.constraint(equalTo: view.leadingAnchor) - ]) - - let button3 = CharcoalPrimaryMButton() - button3.setTitle("OK", for: .normal) - button3.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(button3) - - NSLayoutConstraint.activate([ - button3.bottomAnchor.constraint(equalTo: view.bottomAnchor), - button3.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) - DispatchQueue.main.async { - CharcoalTooltip.show(text: "Hello World", anchorView: button) - - CharcoalTooltip.show(text: "Hello World This is a tooltip", anchorView: button2) - - CharcoalTooltip.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", anchorView: button3) + CharcoalToast.show(text: "Hello World", appearance: .success, screenEdge: .top) + CharcoalToast.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", appearance: .error, screenEdge: .bottom) } return view From 0f18ca5bcae5607002c6f0753e8556d4ed8dcbcb Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 15:06:43 +0800 Subject: [PATCH 150/199] Update CharcoalBubbleShape_UIKit.swift --- .../Components/Tooltip/CharcoalBubbleShape_UIKit.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalBubbleShape_UIKit.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalBubbleShape_UIKit.swift index 68d3ffb64..1441ad6f0 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalBubbleShape_UIKit.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalBubbleShape_UIKit.swift @@ -138,6 +138,6 @@ class CharcoalBubbleShape: CAShapeLayer { override func layoutSublayers() { super.layoutSublayers() - updatePath() // 确保路径更新匹配图层的大小 + updatePath() // Update the path when the layer is resized } } From fd50bc339fc038618dd9535831cb024860d3f51d Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 15:15:31 +0800 Subject: [PATCH 151/199] Refine layout animation logic --- .../Enums/CharcoalPopupViewEdge.swift | 11 +++++++++++ .../Components/Toast/CharcoalToast.swift | 18 +++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Sources/CharcoalShared/Enums/CharcoalPopupViewEdge.swift b/Sources/CharcoalShared/Enums/CharcoalPopupViewEdge.swift index 65961f063..3bbaf0e4f 100644 --- a/Sources/CharcoalShared/Enums/CharcoalPopupViewEdge.swift +++ b/Sources/CharcoalShared/Enums/CharcoalPopupViewEdge.swift @@ -1,4 +1,15 @@ +import Foundation + public enum CharcoalPopupViewEdge { case top case bottom + + public var direction: CGFloat { + switch self { + case .top: + return 1 + case .bottom: + return -1 + } + } } diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift index a8a50dba7..7a7b75e40 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift @@ -36,18 +36,24 @@ public extension CharcoalToast { toastView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) ] + var screenEdgeSpacingConstraint: NSLayoutConstraint + switch screenEdge { case .top: - + screenEdgeSpacingConstraint = toastView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: -screenEdgeSpacing*screenEdge.direction) case .bottom: - + screenEdgeSpacingConstraint = toastView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -screenEdgeSpacing*screenEdge.direction) } + constraints.append(screenEdgeSpacingConstraint) + NSLayoutConstraint.activate(constraints) containerView.showAction = { actionCallback in - UIView.animate(withDuration: 0.25, animations: { - containerView.alpha = 1 + containerView.alpha = 1 + UIView.animate(withDuration: 0.6, animations: { + screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 + containerView.layoutIfNeeded() }) { completion in actionCallback?(completion) } @@ -55,8 +61,10 @@ public extension CharcoalToast { containerView.dismissAction = { actionCallback in UIView.animate(withDuration: 0.25, animations: { - containerView.alpha = 0 + screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 + containerView.layoutIfNeeded() }) { completion in + containerView.alpha = 0 actionCallback?(completion) } } From e203f766e0960afe889fb5012144041eb73b847e Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 15:47:12 +0800 Subject: [PATCH 152/199] Refine animation --- .../Components/Toast/CharcoalToast.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift index 7a7b75e40..ac712b9e4 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift @@ -31,6 +31,7 @@ public extension CharcoalToast { toastView.translatesAutoresizingMaskIntoConstraints = false let containerView = ChacoalOverlayManager.shared.layout(view: toastView, interactionMode: .passThrough, on: on) + containerView.alpha = 1 var constraints = [ toastView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) @@ -49,10 +50,12 @@ public extension CharcoalToast { NSLayoutConstraint.activate(constraints) + containerView.layoutIfNeeded() + containerView.showAction = { actionCallback in - containerView.alpha = 1 - UIView.animate(withDuration: 0.6, animations: { - screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 + screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 + UIView.animate(withDuration: 0.65, delay: 0, + usingSpringWithDamping: 0.75, initialSpringVelocity: 0.0, options: [], animations: { containerView.layoutIfNeeded() }) { completion in actionCallback?(completion) @@ -60,8 +63,8 @@ public extension CharcoalToast { } containerView.dismissAction = { actionCallback in + screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 UIView.animate(withDuration: 0.25, animations: { - screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 containerView.layoutIfNeeded() }) { completion in containerView.alpha = 0 From 0ebb97413524d86de97943965fdb902ba9478d11 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 15:49:19 +0800 Subject: [PATCH 153/199] Add dismiss --- .../Components/Toast/CharcoalToast.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift index ac712b9e4..8cc8ead49 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift @@ -64,7 +64,7 @@ public extension CharcoalToast { containerView.dismissAction = { actionCallback in screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 - UIView.animate(withDuration: 0.25, animations: { + UIView.animate(withDuration: 0.3, animations: { containerView.layoutIfNeeded() }) { completion in containerView.alpha = 0 @@ -76,6 +76,11 @@ public extension CharcoalToast { return containerView.id } + + /// Dismisses the toast with the given identifier. + static func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { + ChacoalOverlayManager.shared.dismiss(id: id) + } } @available(iOS 17.0, *) @@ -87,6 +92,10 @@ public extension CharcoalToast { CharcoalToast.show(text: "Hello World", appearance: .success, screenEdge: .top) CharcoalToast.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", appearance: .error, screenEdge: .bottom) } + + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + ChacoalOverlayManager.shared.dismiss() + } return view } From 56d9243fb83af56cdfa1b975e28550c65c559d90 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 16:53:55 +0800 Subject: [PATCH 154/199] Add example --- .../Views/Buttons/Buttons.swift | 2 + .../Views/Toasts/Toasts.swift | 127 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Buttons/Buttons.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Buttons/Buttons.swift index eb0df9fcd..3ddf69c7b 100644 --- a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Buttons/Buttons.swift +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Buttons/Buttons.swift @@ -18,6 +18,8 @@ final class ButtonsViewController: UIViewController { view.translatesAutoresizingMaskIntoConstraints = false return view }() + + let cellReuseIdentifier = "cell" private var buttons: [ButtonExample] = [ // Primary diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift new file mode 100644 index 000000000..2fb5717f3 --- /dev/null +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift @@ -0,0 +1,127 @@ +import Charcoal +import UIKit + +enum ToastsTitles: String, CaseIterable { + case top = "Top" + case bottom = "Bottom" + case multiline = "Multiline" + + var text: String { + switch self { + case .top: + return "Hello World" + case .bottom: + return "Hello World This is a tooltip with mutiple line" + case .multiline: + return "こんにちは This is a tooltip and here is testing it's multiple line feature" + } + } + + func configCell(cell: UITableViewCell) { + cell.textLabel!.text = rawValue + } +} + +public final class ToastsViewController: UIViewController { + private lazy var tableView: UITableView = { + let view = UITableView(frame: .zero, style: .insetGrouped) + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + let cellReuseIdentifier = "cell" + + private enum Sections: Int, CaseIterable { + case components + + var title: String { + switch self { + case .components: + return "Toasts" + } + } + + var items: [any CaseIterable] { + switch self { + case .components: + return TooltipTitles.allCases + } + } + } + + override public func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setupUI() + } + + private func setupNavigationBar() { + navigationItem.title = "Charcoal" + } + + private func setupUI() { + view.addSubview(tableView) + + NSLayoutConstraint.activate([ + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + tableView.dataSource = self + tableView.delegate = self + } +} + +extension ToastsViewController: UITableViewDelegate, UITableViewDataSource { + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let section = Sections.allCases[indexPath.section] + + let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as UITableViewCell? ?? UITableViewCell(style: .default, reuseIdentifier: cellReuseIdentifier) + + switch section { + case .components: + let titleCase = ToastsTitles.allCases[indexPath.row] + titleCase.configCell(cell: cell) + return cell + } + } + + public func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { + return Sections.allCases[section].items.count + } + + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let titleCase = ToastsTitles.allCases[indexPath.row] + + var toastID: CharcoalIdentifiableOverlayView.IDValue + switch titleCase { + case .top: + toastID = CharcoalToast.show(text: titleCase.text, screenEdge: .top) + case .bottom: + toastID = CharcoalToast.show(text: titleCase.text, appearance: .error, screenEdge: .bottom) + case .multiline: + toastID = CharcoalToast.show(text: titleCase.text, screenEdge: .bottom) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + CharcoalToast.dismiss(id: toastID) + } + } + + public func numberOfSections(in tableView: UITableView) -> Int { + return Sections.allCases.count + } + + public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return Sections.allCases[section].title + } +} + +@available(iOS 17.0, *) +#Preview { + let viewController = ToastsViewController() + return viewController +} From 72f12b0dc0a110de8cce13e7735864625e689c97 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 16:54:24 +0800 Subject: [PATCH 155/199] Reformat --- .../Views/Buttons/Buttons.swift | 2 +- .../Views/Toasts/Toasts.swift | 10 ++-- .../Enums/CharcoalPopupViewEdge.swift | 2 +- .../Enums/CharcoalToastAppearance.swift | 2 +- .../CharcoalIdentifiableOverlayView.swift | 10 ++-- .../Components/Toast/CharcoalToast.swift | 54 +++++++++++-------- .../Components/Toast/CharcoalToastView.swift | 6 +-- .../Components/Tooltip/CharcoalTooltip.swift | 4 +- 8 files changed, 49 insertions(+), 41 deletions(-) diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Buttons/Buttons.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Buttons/Buttons.swift index 3ddf69c7b..1e5dbd845 100644 --- a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Buttons/Buttons.swift +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Buttons/Buttons.swift @@ -18,7 +18,7 @@ final class ButtonsViewController: UIViewController { view.translatesAutoresizingMaskIntoConstraints = false return view }() - + let cellReuseIdentifier = "cell" private var buttons: [ButtonExample] = [ diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift index 2fb5717f3..3867b9e1a 100644 --- a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift @@ -28,9 +28,9 @@ public final class ToastsViewController: UIViewController { view.translatesAutoresizingMaskIntoConstraints = false return view }() - + let cellReuseIdentifier = "cell" - + private enum Sections: Int, CaseIterable { case components @@ -95,17 +95,17 @@ extension ToastsViewController: UITableViewDelegate, UITableViewDataSource { public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) let titleCase = ToastsTitles.allCases[indexPath.row] - + var toastID: CharcoalIdentifiableOverlayView.IDValue switch titleCase { case .top: toastID = CharcoalToast.show(text: titleCase.text, screenEdge: .top) case .bottom: - toastID = CharcoalToast.show(text: titleCase.text, appearance: .error, screenEdge: .bottom) + toastID = CharcoalToast.show(text: titleCase.text, appearance: .error, screenEdge: .bottom) case .multiline: toastID = CharcoalToast.show(text: titleCase.text, screenEdge: .bottom) } - + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { CharcoalToast.dismiss(id: toastID) } diff --git a/Sources/CharcoalShared/Enums/CharcoalPopupViewEdge.swift b/Sources/CharcoalShared/Enums/CharcoalPopupViewEdge.swift index 3bbaf0e4f..48454ad7f 100644 --- a/Sources/CharcoalShared/Enums/CharcoalPopupViewEdge.swift +++ b/Sources/CharcoalShared/Enums/CharcoalPopupViewEdge.swift @@ -3,7 +3,7 @@ import Foundation public enum CharcoalPopupViewEdge { case top case bottom - + public var direction: CGFloat { switch self { case .top: diff --git a/Sources/CharcoalShared/Enums/CharcoalToastAppearance.swift b/Sources/CharcoalShared/Enums/CharcoalToastAppearance.swift index 7600d232b..ef09de9d0 100644 --- a/Sources/CharcoalShared/Enums/CharcoalToastAppearance.swift +++ b/Sources/CharcoalShared/Enums/CharcoalToastAppearance.swift @@ -1,7 +1,7 @@ public enum CharcoalToastAppearance { case success case error - + public var background: ColorAsset.Color { switch self { case .success: diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 8e7630928..373f9d8a9 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -6,18 +6,18 @@ protocol CharcoalIdentifiableOverlayDelegate: AnyObject { public class CharcoalIdentifiableOverlayView: UIView, Identifiable { public typealias IDValue = UUID - + public typealias ActionComplete = (Bool) -> Void - + public typealias ActionContent = (ActionComplete?) -> Void public let id = IDValue() let interactionMode: CharcoalOverlayInteractionMode - + /// Action to show the overlay. var showAction: ActionContent? - + /// Action to dismiss the overlay. var dismissAction: ActionContent? @@ -57,7 +57,7 @@ public class CharcoalIdentifiableOverlayView: UIView, Identifiable { } @objc func dismiss() { - dismissAction?() { [weak self] finished in + dismissAction?() { [weak self] _ in guard let self = self else { return } self.removeFromSuperview() self.delegate?.overlayViewDidDismiss(self) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift index 8cc8ead49..b1cc31140 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift @@ -20,48 +20,56 @@ public extension CharcoalToast { ``` */ @discardableResult - static func show(text: String, - maxWidth: CGFloat = 312, - appearance: CharcoalToastAppearance = .success, - screenEdge: CharcoalPopupViewEdge = .bottom, - screenEdgeSpacing: CGFloat = 120, - on: UIView? = nil) -> CharcoalIdentifiableOverlayView.IDValue { + static func show( + text: String, + maxWidth: CGFloat = 312, + appearance: CharcoalToastAppearance = .success, + screenEdge: CharcoalPopupViewEdge = .bottom, + screenEdgeSpacing: CGFloat = 120, + on: UIView? = nil + ) -> CharcoalIdentifiableOverlayView.IDValue { let toastView = CharcoalToastView(text: text, maxWidth: maxWidth, appearance: appearance) toastView.translatesAutoresizingMaskIntoConstraints = false let containerView = ChacoalOverlayManager.shared.layout(view: toastView, interactionMode: .passThrough, on: on) containerView.alpha = 1 - + var constraints = [ toastView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) ] - + var screenEdgeSpacingConstraint: NSLayoutConstraint - + switch screenEdge { case .top: - screenEdgeSpacingConstraint = toastView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: -screenEdgeSpacing*screenEdge.direction) + screenEdgeSpacingConstraint = toastView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: -screenEdgeSpacing * screenEdge.direction) case .bottom: - screenEdgeSpacingConstraint = toastView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -screenEdgeSpacing*screenEdge.direction) + screenEdgeSpacingConstraint = toastView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -screenEdgeSpacing * screenEdge.direction) } - + constraints.append(screenEdgeSpacingConstraint) - + NSLayoutConstraint.activate(constraints) - + containerView.layoutIfNeeded() - + containerView.showAction = { actionCallback in screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 - UIView.animate(withDuration: 0.65, delay: 0, - usingSpringWithDamping: 0.75, initialSpringVelocity: 0.0, options: [], animations: { - containerView.layoutIfNeeded() - }) { completion in + UIView.animate( + withDuration: 0.65, + delay: 0, + usingSpringWithDamping: 0.75, + initialSpringVelocity: 0.0, + options: [], + animations: { + containerView.layoutIfNeeded() + } + ) { completion in actionCallback?(completion) } } - + containerView.dismissAction = { actionCallback in screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 UIView.animate(withDuration: 0.3, animations: { @@ -71,12 +79,12 @@ public extension CharcoalToast { actionCallback?(completion) } } - + ChacoalOverlayManager.shared.display(view: containerView) return containerView.id } - + /// Dismisses the toast with the given identifier. static func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { ChacoalOverlayManager.shared.dismiss(id: id) @@ -92,7 +100,7 @@ public extension CharcoalToast { CharcoalToast.show(text: "Hello World", appearance: .success, screenEdge: .top) CharcoalToast.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", appearance: .error, screenEdge: .bottom) } - + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { ChacoalOverlayManager.shared.dismiss() } diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift index cf0b98262..5ea24ed51 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift @@ -11,7 +11,7 @@ class CharcoalToastView: UIView { }() let text: String - + /// The appearance of the Toast let appearance: CharcoalToastAppearance @@ -23,7 +23,7 @@ class CharcoalToastView: UIView { /// The corner radius of the tooltip let cornerRadius: CGFloat = 32 - + let borderColor: ColorAsset.Color let borderLineWidth: CGFloat = 2 @@ -41,7 +41,7 @@ class CharcoalToastView: UIView { self.maxWidth = maxWidth self.text = text self.appearance = appearance - self.borderColor = CharcoalAsset.ColorPaletteGenerated.background1.color + borderColor = CharcoalAsset.ColorPaletteGenerated.background1.color super.init(frame: .zero) textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) setupLayer() diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index e2ddc6e6e..ac2069035 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -43,7 +43,7 @@ public extension CharcoalTooltip { tooltip.topAnchor.constraint(equalTo: containerView.topAnchor, constant: viewTopConstant) ] NSLayoutConstraint.activate(constraints) - + containerView.showAction = { actionCallback in UIView.animate(withDuration: 0.25, animations: { containerView.alpha = 1 @@ -51,7 +51,7 @@ public extension CharcoalTooltip { actionCallback?(completion) } } - + containerView.dismissAction = { actionCallback in UIView.animate(withDuration: 0.25, animations: { containerView.alpha = 0 From eb25b09728946573586c789efdce23cfc5999ef2 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 15 May 2024 16:56:38 +0800 Subject: [PATCH 156/199] Add toasts example --- .../Sources/CharcoalUIKitSample/ContentViewController.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/ContentViewController.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/ContentViewController.swift index 891c727a0..d4e7234c3 100644 --- a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/ContentViewController.swift +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/ContentViewController.swift @@ -50,6 +50,7 @@ public final class ContentViewController: UIViewController { case typographies = "Typographies" case icons = "Icons" case tooltips = "Tooltips" + case toasts = "Toasts" var viewController: UIViewController { switch self { @@ -67,6 +68,8 @@ public final class ContentViewController: UIViewController { return TextFieldsViewController() case .tooltips: return TooltipsViewController() + case .toasts: + return ToastsViewController() } } } From d6a457732d6b3830a9d308b5961ffcfff092d25f Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 28 May 2024 14:55:15 +0900 Subject: [PATCH 157/199] Add CharcoalSnackBarView --- .../UIKitSample.xcodeproj/project.pbxproj | 3 +- .../Extensions/UIColor+Extension.swift | 11 ++ .../Components/Toast/CharcoalSnackBar.swift | 10 - .../Toast/CharcoalSnackBarView.swift | 179 ++++++++++++++++++ 4 files changed, 192 insertions(+), 11 deletions(-) create mode 100644 Sources/CharcoalShared/Extensions/UIColor+Extension.swift create mode 100644 Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift diff --git a/Examples/UIKitSample/UIKitSample.xcodeproj/project.pbxproj b/Examples/UIKitSample/UIKitSample.xcodeproj/project.pbxproj index d2600f040..516d12c6e 100644 --- a/Examples/UIKitSample/UIKitSample.xcodeproj/project.pbxproj +++ b/Examples/UIKitSample/UIKitSample.xcodeproj/project.pbxproj @@ -158,7 +158,8 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "unset SDKROOT\nswift run -c release --package-path ../../BuildTools swiftlint --config ../../.swiftlint.yml\n"; + shellScript = " +"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/Sources/CharcoalShared/Extensions/UIColor+Extension.swift b/Sources/CharcoalShared/Extensions/UIColor+Extension.swift new file mode 100644 index 000000000..45f8f9bc9 --- /dev/null +++ b/Sources/CharcoalShared/Extensions/UIColor+Extension.swift @@ -0,0 +1,11 @@ +import UIKit + +public extension UIColor { + func imageWithColor(width: Int, height: Int) -> UIImage { + let size = CGSize(width: width, height: height) + return UIGraphicsImageRenderer(size: size).image { rendererContext in + self.setFill() + rendererContext.fill(CGRect(origin: .zero, size: size)) + } + } +} diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 8fe6bc812..95a6df988 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -203,16 +203,6 @@ public extension View { } } -private extension UIColor { - func imageWithColor(width: Int, height: Int) -> UIImage { - let size = CGSize(width: width, height: height) - return UIGraphicsImageRenderer(size: size).image { rendererContext in - self.setFill() - rendererContext.fill(CGRect(origin: .zero, size: size)) - } - } -} - private struct SnackBarsPreviewView: View { @State var isPresenting = true diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift new file mode 100644 index 000000000..4843e2f10 --- /dev/null +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift @@ -0,0 +1,179 @@ +import UIKit + +class CharcoalSnackBarView: UIView { + lazy var hStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.alignment = .center + stackView.distribution = .fill + stackView.spacing = 0 + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + lazy var label: CharcoalTypography14 = { + let label = CharcoalTypography14() + label.numberOfLines = 1 + label.isBold = true + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = CharcoalAsset.ColorPaletteGenerated.text1.color + return label + }() + + lazy var thumbnailImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + return imageView + }() + + var thumbnailImage: UIImage? + + let text: String + + lazy var capsuleShape: UIView = { + let view = UIView(frame: CGRect.zero) + view.layer.cornerCurve = .continuous + return view + }() + + /// The corner radius of the tooltip + let cornerRadius: CGFloat = 32 + + let borderColor: ColorAsset.Color + + let borderLineWidth: CGFloat = 1 + + /// The max width of the tooltip + let maxWidth: CGFloat + + /// Padding around the bubble + let padding = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16) + + /// Text frame size + private var textFrameSize: CGSize = .zero + + init(text: String, thumbnailImage: UIImage? = nil, maxWidth: CGFloat = 312) { + self.thumbnailImage = thumbnailImage + self.maxWidth = maxWidth + self.text = text + borderColor = CharcoalAsset.ColorPaletteGenerated.border.color + super.init(frame: .zero) + textFrameSize = text.calculateFrame(font: label.font, maxWidth: preferredTextMaxWidth) + setupLayer() + } + + func updateTargetPoint(point: CGPoint) { + setNeedsLayout() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayer() { + // Setup Bubble Shape + addSubview(capsuleShape) + layer.backgroundColor = CharcoalAsset.ColorPaletteGenerated.background1.color.cgColor + layer.borderColor = borderColor.cgColor + layer.borderWidth = borderLineWidth + layer.masksToBounds = true + layer.cornerCurve = .continuous + + // Add HStack + addSubview(hStackView) + NSLayoutConstraint.activate([ + hStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + hStackView.topAnchor.constraint(equalTo: topAnchor), + hStackView.bottomAnchor.constraint(equalTo: bottomAnchor), + hStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + hStackView.widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth) + ]) + + if let thumbnailImage = thumbnailImage { + thumbnailImageView.image = thumbnailImage + hStackView.addArrangedSubview(thumbnailImageView) + NSLayoutConstraint.activate([ + thumbnailImageView.widthAnchor.constraint(equalToConstant: 64), + thumbnailImageView.heightAnchor.constraint(equalToConstant: 64) + ]) + } + + let leftPaddingView = UIView() + let rightPaddingView = UIView() + leftPaddingView.widthAnchor.constraint(equalToConstant: padding.left).isActive = true + rightPaddingView.widthAnchor.constraint(equalToConstant: padding.right).isActive = true + // Setup Label + hStackView.addArrangedSubview(leftPaddingView) + hStackView.addArrangedSubview(label) + hStackView.addArrangedSubview(rightPaddingView) + label.text = text + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + textFrameSize = text.calculateFrame(font: label.font, maxWidth: preferredTextMaxWidth) + } + + var preferredTextMaxWidth: CGFloat { + if let _ = thumbnailImage { + return maxWidth - padding.left - padding.right - 64 + } else { + return maxWidth - padding.left - padding.right + } + } + + var preferredLayoutWidth: CGFloat { + if let _ = thumbnailImage { + return 64 + padding.left + textFrameSize.width + padding.right + } else { + return padding.left + textFrameSize.width + padding.right + } + } + + var preferredLayoutHeight: CGFloat { + if let _ = thumbnailImage { + return 64 + } else { + return padding.top + textFrameSize.height + padding.bottom + } + } + + override var intrinsicContentSize: CGSize { + return CGSize(width: preferredLayoutWidth, height: preferredLayoutHeight) + } + + override func layoutSubviews() { + super.layoutSubviews() + capsuleShape.frame = bounds + layer.cornerRadius = min(cornerRadius, bounds.height / 2.0) + label.frame = CGRect(x: padding.left, y: padding.top, width: textFrameSize.width, height: textFrameSize.height) + } +} + +@available(iOS 17.0, *) +#Preview(traits: .sizeThatFitsLayout) { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.distribution = .fill + stackView.alignment = .center + stackView.spacing = 8.0 + + let tooltip = CharcoalSnackBarView(text: "Hello World") + + let tooltip2 = CharcoalSnackBarView(text: "ブックマークしました", thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64)) + + let tooltip3 = CharcoalSnackBarView(text: "here is testing it's multiple line feature") + + let tooltip4 = CharcoalSnackBarView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature") + + stackView.addArrangedSubview(tooltip) + stackView.addArrangedSubview(tooltip2) + stackView.addArrangedSubview(tooltip3) + stackView.addArrangedSubview(tooltip4) + + return stackView +} From beed8a7268453db657941f26de0d19e12d978ec9 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 28 May 2024 14:56:14 +0900 Subject: [PATCH 158/199] Update project.pbxproj --- Examples/UIKitSample/UIKitSample.xcodeproj/project.pbxproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Examples/UIKitSample/UIKitSample.xcodeproj/project.pbxproj b/Examples/UIKitSample/UIKitSample.xcodeproj/project.pbxproj index 516d12c6e..d2600f040 100644 --- a/Examples/UIKitSample/UIKitSample.xcodeproj/project.pbxproj +++ b/Examples/UIKitSample/UIKitSample.xcodeproj/project.pbxproj @@ -158,8 +158,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = " -"; + shellScript = "unset SDKROOT\nswift run -c release --package-path ../../BuildTools swiftlint --config ../../.swiftlint.yml\n"; }; /* End PBXShellScriptBuildPhase section */ From ef5f9d7b0e2b9114b84db7b7bbaf5623ca7cd0af Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 28 May 2024 15:05:41 +0900 Subject: [PATCH 159/199] Clean code --- .../CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift | 4 ---- .../CharcoalUIKit/Components/Toast/CharcoalToastView.swift | 4 ---- 2 files changed, 8 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift index 4843e2f10..af00ad9ed 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift @@ -64,10 +64,6 @@ class CharcoalSnackBarView: UIView { setupLayer() } - func updateTargetPoint(point: CGPoint) { - setNeedsLayout() - } - @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift index 5ea24ed51..88c6396d8 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift @@ -47,10 +47,6 @@ class CharcoalToastView: UIView { setupLayer() } - func updateTargetPoint(point: CGPoint) { - setNeedsLayout() - } - @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") From 23b6c019f8b7bac60bb92104f4f2117ea78227d9 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 28 May 2024 15:39:23 +0900 Subject: [PATCH 160/199] Refine layout logic --- .../Components/Toast/CharcoalSnackBar.swift | 1 - .../Toast/CharcoalSnackBarView.swift | 69 ++++++++++++++++--- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 95a6df988..16b423918 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -241,7 +241,6 @@ private struct SnackBarsPreviewView: View { isPresenting: $isPresenting2, screenEdge: .bottom, text: "ブックマークしました", - dismissAfter: 2, action: { Button { print("Tapped") diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift index af00ad9ed..9128ba95a 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift @@ -1,11 +1,12 @@ import UIKit class CharcoalSnackBarView: UIView { + typealias ActionCallback = () -> Void + lazy var hStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal stackView.alignment = .center - stackView.distribution = .fill stackView.spacing = 0 stackView.translatesAutoresizingMaskIntoConstraints = false return stackView @@ -28,7 +29,16 @@ class CharcoalSnackBarView: UIView { return imageView }() - var thumbnailImage: UIImage? + lazy var actionButton: CharcoalDefaultSButton = { + let button = CharcoalDefaultSButton() + return button + }() + + let thumbnailImage: UIImage? + + var action: ActionCallback? + + let actionTitle: String? let text: String @@ -52,15 +62,24 @@ class CharcoalSnackBarView: UIView { let padding = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16) /// Text frame size - private var textFrameSize: CGSize = .zero + private var textFrameSize: CGSize = .zero { + didSet { + textWidthConstraint.constant = textFrameSize.width + } + } + + private var textWidthConstraint: NSLayoutConstraint! - init(text: String, thumbnailImage: UIImage? = nil, maxWidth: CGFloat = 312) { + init(text: String, thumbnailImage: UIImage? = nil, maxWidth: CGFloat = 312, actionTitle: String? = nil, action: ActionCallback? = nil) { + self.action = action + self.actionTitle = actionTitle self.thumbnailImage = thumbnailImage self.maxWidth = maxWidth self.text = text borderColor = CharcoalAsset.ColorPaletteGenerated.border.color super.init(frame: .zero) textFrameSize = text.calculateFrame(font: label.font, maxWidth: preferredTextMaxWidth) + textWidthConstraint = label.widthAnchor.constraint(equalToConstant: textFrameSize.width) setupLayer() } @@ -106,6 +125,20 @@ class CharcoalSnackBarView: UIView { hStackView.addArrangedSubview(label) hStackView.addArrangedSubview(rightPaddingView) label.text = text + textWidthConstraint.isActive = true + // Add action button + if let _ = action { + actionButton.setTitle(actionTitle, for: .normal) + actionButton.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside) + hStackView.addArrangedSubview(actionButton) + let rightPaddingView = UIView() + rightPaddingView.widthAnchor.constraint(equalToConstant: padding.right).isActive = true + hStackView.addArrangedSubview(rightPaddingView) + } + } + + @objc func actionButtonTapped() { + action?() } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -115,11 +148,16 @@ class CharcoalSnackBarView: UIView { } var preferredTextMaxWidth: CGFloat { + var width = maxWidth - padding.left - padding.right if let _ = thumbnailImage { - return maxWidth - padding.left - padding.right - 64 - } else { - return maxWidth - padding.left - padding.right + width = width - 64 + } + + if let _ = action { + width = width - actionButton.intrinsicContentSize.width - padding.right } + + return width } var preferredLayoutWidth: CGFloat { @@ -133,8 +171,10 @@ class CharcoalSnackBarView: UIView { var preferredLayoutHeight: CGFloat { if let _ = thumbnailImage { return 64 + } else if let _ = action { + return max(padding.top + label.font.lineHeight + padding.bottom, padding.top + actionButton.intrinsicContentSize.height + padding.bottom) } else { - return padding.top + textFrameSize.height + padding.bottom + return padding.top + label.font.lineHeight + padding.bottom } } @@ -146,7 +186,6 @@ class CharcoalSnackBarView: UIView { super.layoutSubviews() capsuleShape.frame = bounds layer.cornerRadius = min(cornerRadius, bounds.height / 2.0) - label.frame = CGRect(x: padding.left, y: padding.top, width: textFrameSize.width, height: textFrameSize.height) } } @@ -162,14 +201,22 @@ class CharcoalSnackBarView: UIView { let tooltip2 = CharcoalSnackBarView(text: "ブックマークしました", thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64)) - let tooltip3 = CharcoalSnackBarView(text: "here is testing it's multiple line feature") + let tooltip3 = CharcoalSnackBarView(text: "ブックマークしました", actionTitle: "編集") { + print("編集 taped") + } - let tooltip4 = CharcoalSnackBarView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature") + let tooltip4 = CharcoalSnackBarView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", + thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), actionTitle: "編集") { + print("編集 taped") + } + + let tooltip5 = CharcoalSnackBarView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature") stackView.addArrangedSubview(tooltip) stackView.addArrangedSubview(tooltip2) stackView.addArrangedSubview(tooltip3) stackView.addArrangedSubview(tooltip4) + stackView.addArrangedSubview(tooltip5) return stackView } From 95ccac0f8bf80739b5711ff5e64f93b4676005ac Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 28 May 2024 15:55:56 +0900 Subject: [PATCH 161/199] Add CharcoalSnackBar --- .../Components/Toast/CharcoalSnackBar.swift | 113 ++++++++++++++++++ .../Toast/CharcoalSnackBarView.swift | 30 +++-- 2 files changed, 130 insertions(+), 13 deletions(-) create mode 100644 Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift new file mode 100644 index 000000000..ae50e96b0 --- /dev/null +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift @@ -0,0 +1,113 @@ +import UIKit + +public class CharcoalSnackBar {} + +public extension CharcoalSnackBar { + /** + Show a snackbar. + + - Parameters: + - text: The text to be displayed in the toast. + - maxWidth: The maximum width of the toast. + - thumbnailImage: The thumbnail image to be displayed in the toast. + - screenEdge: The edge of the screen where the toast will be displayed. + - screenEdgeSpacing: The spacing between the toast and the screen edge. + - on: The view on which the toast will be displayed. + + # Example # + ```swift + CharcoalToast.show(text: "This is a toast") + ``` + */ + @discardableResult + static func show( + text: String, + maxWidth: CGFloat = 312, + thumbnailImage: UIImage? = nil, + screenEdge: CharcoalPopupViewEdge = .bottom, + screenEdgeSpacing: CGFloat = 120, + action: CharcoalAction? = nil, + on: UIView? = nil + ) -> CharcoalIdentifiableOverlayView.IDValue { + let toastView = CharcoalSnackBarView(text: text, thumbnailImage: thumbnailImage, maxWidth: maxWidth, action: action) + + toastView.translatesAutoresizingMaskIntoConstraints = false + + let containerView = ChacoalOverlayManager.shared.layout(view: toastView, interactionMode: .passThrough, on: on) + containerView.alpha = 1 + + var constraints = [ + toastView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) + ] + + var screenEdgeSpacingConstraint: NSLayoutConstraint + + switch screenEdge { + case .top: + screenEdgeSpacingConstraint = toastView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: -screenEdgeSpacing * screenEdge.direction) + case .bottom: + screenEdgeSpacingConstraint = toastView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -screenEdgeSpacing * screenEdge.direction) + } + + constraints.append(screenEdgeSpacingConstraint) + + NSLayoutConstraint.activate(constraints) + + containerView.layoutIfNeeded() + + containerView.showAction = { actionCallback in + screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 + UIView.animate( + withDuration: 0.65, + delay: 0, + usingSpringWithDamping: 0.75, + initialSpringVelocity: 0.0, + options: [], + animations: { + containerView.layoutIfNeeded() + } + ) { completion in + actionCallback?(completion) + } + } + + containerView.dismissAction = { actionCallback in + screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 + UIView.animate(withDuration: 0.3, animations: { + containerView.layoutIfNeeded() + }) { completion in + containerView.alpha = 0 + actionCallback?(completion) + } + } + + ChacoalOverlayManager.shared.display(view: containerView) + + return containerView.id + } + + /// Dismisses the toast with the given identifier. + static func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { + ChacoalOverlayManager.shared.dismiss(id: id) + } +} + +@available(iOS 17.0, *) +#Preview() { + let view = UIView() + view.backgroundColor = UIColor.lightGray + + DispatchQueue.main.async { + CharcoalSnackBar.show(text: "Hello World", screenEdge: .top) + CharcoalSnackBar.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", screenEdge: .top, screenEdgeSpacing: 220) + CharcoalSnackBar.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), screenEdge: .bottom, action: CharcoalAction(title: "編集", actionCallback: { + print("Tapped 編集") + })) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + ChacoalOverlayManager.shared.dismiss() + } + + return view +} diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift index 9128ba95a..51a38196d 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift @@ -1,8 +1,13 @@ import UIKit +public typealias ActionCallback = () -> Void + +public struct CharcoalAction { + let title: String + let actionCallback: ActionCallback +} + class CharcoalSnackBarView: UIView { - typealias ActionCallback = () -> Void - lazy var hStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal @@ -36,9 +41,7 @@ class CharcoalSnackBarView: UIView { let thumbnailImage: UIImage? - var action: ActionCallback? - - let actionTitle: String? + var action: CharcoalAction? let text: String @@ -70,14 +73,16 @@ class CharcoalSnackBarView: UIView { private var textWidthConstraint: NSLayoutConstraint! - init(text: String, thumbnailImage: UIImage? = nil, maxWidth: CGFloat = 312, actionTitle: String? = nil, action: ActionCallback? = nil) { + init(text: String, thumbnailImage: UIImage? = nil, maxWidth: CGFloat = 312, action: CharcoalAction? = nil) { self.action = action - self.actionTitle = actionTitle self.thumbnailImage = thumbnailImage self.maxWidth = maxWidth self.text = text borderColor = CharcoalAsset.ColorPaletteGenerated.border.color super.init(frame: .zero) + if let action = action { + actionButton.setTitle(action.title, for: .normal) + } textFrameSize = text.calculateFrame(font: label.font, maxWidth: preferredTextMaxWidth) textWidthConstraint = label.widthAnchor.constraint(equalToConstant: textFrameSize.width) setupLayer() @@ -128,7 +133,6 @@ class CharcoalSnackBarView: UIView { textWidthConstraint.isActive = true // Add action button if let _ = action { - actionButton.setTitle(actionTitle, for: .normal) actionButton.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside) hStackView.addArrangedSubview(actionButton) let rightPaddingView = UIView() @@ -138,7 +142,7 @@ class CharcoalSnackBarView: UIView { } @objc func actionButtonTapped() { - action?() + action?.actionCallback() } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -201,14 +205,14 @@ class CharcoalSnackBarView: UIView { let tooltip2 = CharcoalSnackBarView(text: "ブックマークしました", thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64)) - let tooltip3 = CharcoalSnackBarView(text: "ブックマークしました", actionTitle: "編集") { + let tooltip3 = CharcoalSnackBarView(text: "ブックマークしました", action: CharcoalAction(title: "編集", actionCallback: { print("編集 taped") - } + })) let tooltip4 = CharcoalSnackBarView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", - thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), actionTitle: "編集") { + thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), action: CharcoalAction(title: "編集", actionCallback: { print("編集 taped") - } + })) let tooltip5 = CharcoalSnackBarView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature") From 90fbeb5fc5a515f87ec19f6ba5a0460225d75f7d Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 28 May 2024 15:56:07 +0900 Subject: [PATCH 162/199] Update CharcoalSnackBar.swift --- Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift index ae50e96b0..13e50d0c9 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift @@ -16,7 +16,7 @@ public extension CharcoalSnackBar { # Example # ```swift - CharcoalToast.show(text: "This is a toast") + CharcoalSnackBar.show(text: "Hello") ``` */ @discardableResult From 639a46ebb3cb5bc423a98847b671f94a49115a0d Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 28 May 2024 16:17:16 +0900 Subject: [PATCH 163/199] Update CharcoalSnackBar.swift --- .../Components/Toast/CharcoalSnackBar.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift index 13e50d0c9..4a7f18fa7 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift @@ -7,12 +7,12 @@ public extension CharcoalSnackBar { Show a snackbar. - Parameters: - - text: The text to be displayed in the toast. - - maxWidth: The maximum width of the toast. - - thumbnailImage: The thumbnail image to be displayed in the toast. - - screenEdge: The edge of the screen where the toast will be displayed. - - screenEdgeSpacing: The spacing between the toast and the screen edge. - - on: The view on which the toast will be displayed. + - text: The text to be displayed in the snackbar. + - maxWidth: The maximum width of the snackbar. + - thumbnailImage: The thumbnail image to be displayed in the snackbar. + - screenEdge: The edge of the screen where the snackbar will be displayed. + - screenEdgeSpacing: The spacing between the snackbar and the screen edge. + - on: The view on which the snackbar will be displayed. # Example # ```swift @@ -104,10 +104,10 @@ public extension CharcoalSnackBar { print("Tapped 編集") })) } - - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - ChacoalOverlayManager.shared.dismiss() - } +// +// DispatchQueue.main.asyncAfter(deadline: .now() + 3) { +// ChacoalOverlayManager.shared.dismiss() +// } return view } From 7a06da41316c8032a780e0c38129e5ee1868142d Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 28 May 2024 16:53:08 +0900 Subject: [PATCH 164/199] Refine layout logic --- .../Toast/CharcoalSnackBarView.swift | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift index 51a38196d..b5b2dbe9b 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift @@ -21,7 +21,6 @@ class CharcoalSnackBarView: UIView { let label = CharcoalTypography14() label.numberOfLines = 1 label.isBold = true - label.textAlignment = .center label.translatesAutoresizingMaskIntoConstraints = false label.textColor = CharcoalAsset.ColorPaletteGenerated.text1.color return label @@ -65,13 +64,7 @@ class CharcoalSnackBarView: UIView { let padding = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16) /// Text frame size - private var textFrameSize: CGSize = .zero { - didSet { - textWidthConstraint.constant = textFrameSize.width - } - } - - private var textWidthConstraint: NSLayoutConstraint! + private var textFrameSize: CGSize = .zero init(text: String, thumbnailImage: UIImage? = nil, maxWidth: CGFloat = 312, action: CharcoalAction? = nil) { self.action = action @@ -84,7 +77,6 @@ class CharcoalSnackBarView: UIView { actionButton.setTitle(action.title, for: .normal) } textFrameSize = text.calculateFrame(font: label.font, maxWidth: preferredTextMaxWidth) - textWidthConstraint = label.widthAnchor.constraint(equalToConstant: textFrameSize.width) setupLayer() } @@ -92,26 +84,17 @@ class CharcoalSnackBarView: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - private func setupLayer() { - // Setup Bubble Shape + + private func setupCapsuleShape() { addSubview(capsuleShape) layer.backgroundColor = CharcoalAsset.ColorPaletteGenerated.background1.color.cgColor layer.borderColor = borderColor.cgColor layer.borderWidth = borderLineWidth layer.masksToBounds = true layer.cornerCurve = .continuous - - // Add HStack - addSubview(hStackView) - NSLayoutConstraint.activate([ - hStackView.leadingAnchor.constraint(equalTo: leadingAnchor), - hStackView.topAnchor.constraint(equalTo: topAnchor), - hStackView.bottomAnchor.constraint(equalTo: bottomAnchor), - hStackView.trailingAnchor.constraint(equalTo: trailingAnchor), - hStackView.widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth) - ]) - + } + + private func addThumbnailView() { if let thumbnailImage = thumbnailImage { thumbnailImageView.image = thumbnailImage hStackView.addArrangedSubview(thumbnailImageView) @@ -120,7 +103,9 @@ class CharcoalSnackBarView: UIView { thumbnailImageView.heightAnchor.constraint(equalToConstant: 64) ]) } - + } + + private func addTextLabel() { let leftPaddingView = UIView() let rightPaddingView = UIView() leftPaddingView.widthAnchor.constraint(equalToConstant: padding.left).isActive = true @@ -130,16 +115,42 @@ class CharcoalSnackBarView: UIView { hStackView.addArrangedSubview(label) hStackView.addArrangedSubview(rightPaddingView) label.text = text - textWidthConstraint.isActive = true - // Add action button + } + + private func addActionButton() { if let _ = action { actionButton.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside) hStackView.addArrangedSubview(actionButton) + actionButton.widthAnchor.constraint(equalToConstant: actionButton.intrinsicContentSize.width).isActive = true let rightPaddingView = UIView() rightPaddingView.widthAnchor.constraint(equalToConstant: padding.right).isActive = true hStackView.addArrangedSubview(rightPaddingView) } } + + private func setupLayer() { + // Setup Bubble Shape + setupCapsuleShape() + + // Add HStack + addSubview(hStackView) + NSLayoutConstraint.activate([ + hStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + hStackView.topAnchor.constraint(equalTo: topAnchor), + hStackView.bottomAnchor.constraint(equalTo: bottomAnchor), + hStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + hStackView.widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth) + ]) + + // Add thumbnail view + addThumbnailView() + + // Add text label with padding view + addTextLabel() + + // Add action button + addActionButton() + } @objc func actionButtonTapped() { action?.actionCallback() From 9ff4f22cc53aa582cf79e7ae897a8d2055241b50 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 28 May 2024 16:55:54 +0900 Subject: [PATCH 165/199] Refine toasts text --- .../Toast/CharcoalSnackBarView.swift | 28 +++++++++++-------- .../Components/Toast/CharcoalToastView.swift | 20 ++++++------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift index b5b2dbe9b..9ec1bc603 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift @@ -50,14 +50,14 @@ class CharcoalSnackBarView: UIView { return view }() - /// The corner radius of the tooltip + /// The corner radius of the snackbar let cornerRadius: CGFloat = 32 let borderColor: ColorAsset.Color let borderLineWidth: CGFloat = 1 - /// The max width of the tooltip + /// The max width of the snackbar let maxWidth: CGFloat /// Padding around the bubble @@ -162,12 +162,16 @@ class CharcoalSnackBarView: UIView { textFrameSize = text.calculateFrame(font: label.font, maxWidth: preferredTextMaxWidth) } + /// The max width of the text label var preferredTextMaxWidth: CGFloat { var width = maxWidth - padding.left - padding.right + + // Check if has thumbnail image if let _ = thumbnailImage { width = width - 64 } + // Check if has action button if let _ = action { width = width - actionButton.intrinsicContentSize.width - padding.right } @@ -212,26 +216,26 @@ class CharcoalSnackBarView: UIView { stackView.alignment = .center stackView.spacing = 8.0 - let tooltip = CharcoalSnackBarView(text: "Hello World") + let snackbar = CharcoalSnackBarView(text: "Hello World") - let tooltip2 = CharcoalSnackBarView(text: "ブックマークしました", thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64)) + let snackbar2 = CharcoalSnackBarView(text: "ブックマークしました", thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64)) - let tooltip3 = CharcoalSnackBarView(text: "ブックマークしました", action: CharcoalAction(title: "編集", actionCallback: { + let snackbar3 = CharcoalSnackBarView(text: "ブックマークしました", action: CharcoalAction(title: "編集", actionCallback: { print("編集 taped") })) - let tooltip4 = CharcoalSnackBarView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", + let snackbar4 = CharcoalSnackBarView(text: "こんにちは This is a snackbar and here is testing it's multiple line feature", thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), action: CharcoalAction(title: "編集", actionCallback: { print("編集 taped") })) - let tooltip5 = CharcoalSnackBarView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature") + let snackbar5 = CharcoalSnackBarView(text: "こんにちは This is a snackbar and here is testing it's multiple line feature") - stackView.addArrangedSubview(tooltip) - stackView.addArrangedSubview(tooltip2) - stackView.addArrangedSubview(tooltip3) - stackView.addArrangedSubview(tooltip4) - stackView.addArrangedSubview(tooltip5) + stackView.addArrangedSubview(snackbar) + stackView.addArrangedSubview(snackbar2) + stackView.addArrangedSubview(snackbar3) + stackView.addArrangedSubview(snackbar4) + stackView.addArrangedSubview(snackbar5) return stackView } diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift index 88c6396d8..20fd9fd4a 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift @@ -21,14 +21,14 @@ class CharcoalToastView: UIView { return view }() - /// The corner radius of the tooltip + /// The corner radius of the toast let cornerRadius: CGFloat = 32 let borderColor: ColorAsset.Color let borderLineWidth: CGFloat = 2 - /// The max width of the tooltip + /// The max width of the toast let maxWidth: CGFloat /// Padding around the bubble @@ -91,18 +91,18 @@ class CharcoalToastView: UIView { stackView.alignment = .center stackView.spacing = 8.0 - let tooltip = CharcoalToastView(text: "Hello World") + let toast = CharcoalToastView(text: "Hello World") - let tooltip2 = CharcoalToastView(text: "Hello World This is a tooltip", appearance: .error) + let toast2 = CharcoalToastView(text: "Hello World This is a toast", appearance: .error) - let tooltip3 = CharcoalToastView(text: "here is testing it's multiple line feature") + let toast3 = CharcoalToastView(text: "here is testing it's multiple line feature") - let tooltip4 = CharcoalToastView(text: "こんにちは This is a tooltip and here is testing it's multiple line feature") + let toast4 = CharcoalToastView(text: "こんにちは This is a toast and here is testing it's multiple line feature") - stackView.addArrangedSubview(tooltip) - stackView.addArrangedSubview(tooltip2) - stackView.addArrangedSubview(tooltip3) - stackView.addArrangedSubview(tooltip4) + stackView.addArrangedSubview(toast) + stackView.addArrangedSubview(toast2) + stackView.addArrangedSubview(toast3) + stackView.addArrangedSubview(toast4) return stackView } From b5256e1fccade31a2388fb8bab1bb300a41635e5 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 28 May 2024 17:08:22 +0900 Subject: [PATCH 166/199] Update CharcoalToastDraggableModifier.swift --- .../Components/Toast/CharcoalToastDraggableModifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastDraggableModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastDraggableModifier.swift index f883d53fe..e62c2682d 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastDraggableModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastDraggableModifier.swift @@ -27,7 +27,7 @@ struct CharcoalToastDraggableModifier: ViewModifier, CharcoalToastDraggable { height: gesture.translation.height ) } else { - // the less the faster resistance + // Rubber band effect let limit: CGFloat = 60 let yOff = gesture.translation.height let dist = sqrt(yOff * yOff) From eb59f4f7f7674b0610aed6433351225f2cc474b2 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 29 May 2024 14:42:07 +0900 Subject: [PATCH 167/199] Add CharcoalRubberGesture --- .../CharcoalIdentifiableOverlayView.swift | 8 +++ .../Toast/CharcoalRubberGesture.swift | 63 +++++++++++++++++++ .../Components/Toast/CharcoalSnackBar.swift | 9 +++ 3 files changed, 80 insertions(+) create mode 100644 Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 373f9d8a9..de9e50cd1 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -22,6 +22,8 @@ public class CharcoalIdentifiableOverlayView: UIView, Identifiable { var dismissAction: ActionContent? weak var delegate: CharcoalIdentifiableOverlayDelegate? + + var gesture: CharcoalGesture? init(interactionMode: CharcoalOverlayInteractionMode) { self.interactionMode = interactionMode @@ -63,4 +65,10 @@ public class CharcoalIdentifiableOverlayView: UIView, Identifiable { self.delegate?.overlayViewDidDismiss(self) } } + + /// Add gesture to this view + func addGesture(_ gesture: CharcoalGesture) { + self.gesture = gesture + addGestureRecognizer(gesture.gesture) + } } diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift new file mode 100644 index 000000000..1d3197cf4 --- /dev/null +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift @@ -0,0 +1,63 @@ +import UIKit + +protocol CharcoalGesture { + var gesture: UIGestureRecognizer { get } +} + +class CharcoalRubberGesture: NSObject, CharcoalGesture { + var gesture: UIGestureRecognizer + + override init() { + self.gesture = UIPanGestureRecognizer() + super.init() + gesture.addTarget(self, action: #selector(handlePan(_:))) + } + + @objc func handlePan(_ gesture: UIPanGestureRecognizer) { + print("Pan") + guard let view = gesture.view else { return } + + let translation = gesture.translation(in: view) + let velocity = gesture.velocity(in: view) + + switch gesture.state { + case .began, .changed: + view.transform = CGAffineTransform(translationX: 0, y: translation.y) + case .ended: + let damping: CGFloat = 0.75 + let initialSpringVelocity: CGFloat = 0.0 + let duration: TimeInterval = 0.65 + let completion: ((Bool) -> Void)? = nil + + let isDismissing = velocity.y > 0 + + if isDismissing { + UIView.animate( + withDuration: duration, + delay: 0, + usingSpringWithDamping: damping, + initialSpringVelocity: initialSpringVelocity, + options: [], + animations: { + view.transform = CGAffineTransform(translationX: 0, y: view.frame.height) + }, + completion: completion + ) + } else { + UIView.animate( + withDuration: duration, + delay: 0, + usingSpringWithDamping: damping, + initialSpringVelocity: initialSpringVelocity, + options: [], + animations: { + view.transform = .identity + }, + completion: completion + ) + } + default: + break + } + } +} diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift index 4a7f18fa7..6a9b76a03 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift @@ -35,6 +35,9 @@ public extension CharcoalSnackBar { let containerView = ChacoalOverlayManager.shared.layout(view: toastView, interactionMode: .passThrough, on: on) containerView.alpha = 1 + containerView.isUserInteractionEnabled = true + + addRubberGesture(view: containerView) var constraints = [ toastView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) @@ -85,6 +88,12 @@ public extension CharcoalSnackBar { return containerView.id } + + /// Adds a rubber gesture to the given view. + static func addRubberGesture(view: CharcoalIdentifiableOverlayView) { + let rubberGesture = CharcoalRubberGesture() + view.addGesture(rubberGesture) + } /// Dismisses the toast with the given identifier. static func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { From 5773c8dfd185a1fb00ef6f735d16bd30cda6a2ac Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 29 May 2024 15:16:11 +0900 Subject: [PATCH 168/199] use id to notify did dismiss --- .../Components/Overlay/ChacoalOverlayManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift index bca0693ab..4c70c6736 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/ChacoalOverlayManager.swift @@ -131,8 +131,8 @@ extension ChacoalOverlayManager { // MARK: - CharcoalIdentifiableOverlayDelegate extension ChacoalOverlayManager: CharcoalIdentifiableOverlayDelegate { - func overlayViewDidDismiss(_ overlayView: CharcoalIdentifiableOverlayView) { - overlayContainerViews = overlayContainerViews.filter { $0.id != overlayView.id } + func overlayViewDidDismiss(_ overlayID: CharcoalIdentifiableOverlayView.ID) { + overlayContainerViews = overlayContainerViews.filter { $0.id != overlayID } if overlayContainerViews.isEmpty { removeBackground() } From e14e16137f23a0ba267917b1388621c08d24d1f2 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 29 May 2024 15:32:24 +0900 Subject: [PATCH 169/199] Add rubber gesture --- .../CharcoalIdentifiableOverlayView.swift | 12 ++-- .../Toast/CharcoalRubberGesture.swift | 56 ++++++++++++------- .../Components/Toast/CharcoalSnackBar.swift | 18 +++--- .../Toast/CharcoalSnackBarView.swift | 8 +++ 4 files changed, 56 insertions(+), 38 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index de9e50cd1..883d308bc 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -1,7 +1,7 @@ import UIKit protocol CharcoalIdentifiableOverlayDelegate: AnyObject { - func overlayViewDidDismiss(_ overlayView: CharcoalIdentifiableOverlayView) + func overlayViewDidDismiss(_ id: CharcoalIdentifiableOverlayView.IDValue) } public class CharcoalIdentifiableOverlayView: UIView, Identifiable { @@ -22,8 +22,6 @@ public class CharcoalIdentifiableOverlayView: UIView, Identifiable { var dismissAction: ActionContent? weak var delegate: CharcoalIdentifiableOverlayDelegate? - - var gesture: CharcoalGesture? init(interactionMode: CharcoalOverlayInteractionMode) { self.interactionMode = interactionMode @@ -62,13 +60,11 @@ public class CharcoalIdentifiableOverlayView: UIView, Identifiable { dismissAction?() { [weak self] _ in guard let self = self else { return } self.removeFromSuperview() - self.delegate?.overlayViewDidDismiss(self) + self.delegate?.overlayViewDidDismiss(self.id) } } - /// Add gesture to this view - func addGesture(_ gesture: CharcoalGesture) { - self.gesture = gesture - addGestureRecognizer(gesture.gesture) + deinit { + print("deinit CharcoalIdentifiableOverlayView") } } diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift index 1d3197cf4..fd60d4c10 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift @@ -5,9 +5,20 @@ protocol CharcoalGesture { } class CharcoalRubberGesture: NSObject, CharcoalGesture { + let screenEdge: CharcoalPopupViewEdge + var gesture: UIGestureRecognizer - override init() { + var dragVelocity: CGPoint = .zero + + var isDragging: Bool = false + + var offset: CGSize = .zero + + var dismiss: (() -> Void)? + + init(screenEdge: CharcoalPopupViewEdge) { + self.screenEdge = screenEdge self.gesture = UIPanGestureRecognizer() super.init() gesture.addTarget(self, action: #selector(handlePan(_:))) @@ -17,32 +28,37 @@ class CharcoalRubberGesture: NSObject, CharcoalGesture { print("Pan") guard let view = gesture.view else { return } - let translation = gesture.translation(in: view) - let velocity = gesture.velocity(in: view) + let translation = gesture.translation(in: gesture.view) + let velocity = gesture.velocity(in: gesture.view) + let translationInDirection = translation.y * screenEdge.direction + let movingVelocityInDirection = velocity.y * screenEdge.direction + let offsetInDirection = offset.height * screenEdge.direction switch gesture.state { - case .began, .changed: - view.transform = CGAffineTransform(translationX: 0, y: translation.y) - case .ended: + case .began: + isDragging = true + case .changed: + dragVelocity = velocity + if translationInDirection < 0 { + offset = CGSize(width: 0, height: translation.y) + view.transform = CGAffineTransform(translationX: 0, y: translation.y) + } else { + let limit: CGFloat = 60 + let dist = sqrt(translation.y * translation.y) + let factor = 1 / (dist / limit + 1) + offset = CGSize(width: 0, height: translation.y * factor) + view.transform = CGAffineTransform(translationX: 0, y: translation.y * factor) + } + case .ended, .cancelled: let damping: CGFloat = 0.75 let initialSpringVelocity: CGFloat = 0.0 let duration: TimeInterval = 0.65 let completion: ((Bool) -> Void)? = nil - let isDismissing = velocity.y > 0 - - if isDismissing { - UIView.animate( - withDuration: duration, - delay: 0, - usingSpringWithDamping: damping, - initialSpringVelocity: initialSpringVelocity, - options: [], - animations: { - view.transform = CGAffineTransform(translationX: 0, y: view.frame.height) - }, - completion: completion - ) + isDragging = false + if offsetInDirection < -50 || movingVelocityInDirection < -100 { + // Dismiss + dismiss?() } else { UIView.animate( withDuration: duration, diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift index 6a9b76a03..e5d47c30a 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift @@ -30,15 +30,19 @@ public extension CharcoalSnackBar { on: UIView? = nil ) -> CharcoalIdentifiableOverlayView.IDValue { let toastView = CharcoalSnackBarView(text: text, thumbnailImage: thumbnailImage, maxWidth: maxWidth, action: action) - + toastView.isUserInteractionEnabled = true toastView.translatesAutoresizingMaskIntoConstraints = false let containerView = ChacoalOverlayManager.shared.layout(view: toastView, interactionMode: .passThrough, on: on) containerView.alpha = 1 containerView.isUserInteractionEnabled = true - addRubberGesture(view: containerView) - + let rubberGesture = CharcoalRubberGesture(screenEdge: screenEdge) + toastView.addGesture(rubberGesture) + rubberGesture.dismiss = { + ChacoalOverlayManager.shared.dismiss(id: containerView.id) + } + var constraints = [ toastView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) ] @@ -88,12 +92,6 @@ public extension CharcoalSnackBar { return containerView.id } - - /// Adds a rubber gesture to the given view. - static func addRubberGesture(view: CharcoalIdentifiableOverlayView) { - let rubberGesture = CharcoalRubberGesture() - view.addGesture(rubberGesture) - } /// Dismisses the toast with the given identifier. static func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { @@ -113,7 +111,7 @@ public extension CharcoalSnackBar { print("Tapped 編集") })) } -// + // DispatchQueue.main.asyncAfter(deadline: .now() + 3) { // ChacoalOverlayManager.shared.dismiss() // } diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift index 9ec1bc603..254d02036 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift @@ -65,6 +65,8 @@ class CharcoalSnackBarView: UIView { /// Text frame size private var textFrameSize: CGSize = .zero + + var gesture: CharcoalGesture? init(text: String, thumbnailImage: UIImage? = nil, maxWidth: CGFloat = 312, action: CharcoalAction? = nil) { self.action = action @@ -206,6 +208,12 @@ class CharcoalSnackBarView: UIView { capsuleShape.frame = bounds layer.cornerRadius = min(cornerRadius, bounds.height / 2.0) } + + /// Add gesture to this view + func addGesture(_ gesture: CharcoalGesture) { + self.gesture = gesture + addGestureRecognizer(gesture.gesture) + } } @available(iOS 17.0, *) From ac6f35c00dad8cb360849acbb8a7a7341388a013 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 29 May 2024 15:33:23 +0900 Subject: [PATCH 170/199] Refactor --- .../Components/Toast/CharcoalRubberGesture.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift index fd60d4c10..d72c9034c 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift @@ -25,7 +25,6 @@ class CharcoalRubberGesture: NSObject, CharcoalGesture { } @objc func handlePan(_ gesture: UIPanGestureRecognizer) { - print("Pan") guard let view = gesture.view else { return } let translation = gesture.translation(in: gesture.view) @@ -34,6 +33,11 @@ class CharcoalRubberGesture: NSObject, CharcoalGesture { let movingVelocityInDirection = velocity.y * screenEdge.direction let offsetInDirection = offset.height * screenEdge.direction + // Rubber band effect + let damping: CGFloat = 0.75 + let initialSpringVelocity: CGFloat = 0.0 + let duration: TimeInterval = 0.65 + switch gesture.state { case .began: isDragging = true @@ -50,11 +54,6 @@ class CharcoalRubberGesture: NSObject, CharcoalGesture { view.transform = CGAffineTransform(translationX: 0, y: translation.y * factor) } case .ended, .cancelled: - let damping: CGFloat = 0.75 - let initialSpringVelocity: CGFloat = 0.0 - let duration: TimeInterval = 0.65 - let completion: ((Bool) -> Void)? = nil - isDragging = false if offsetInDirection < -50 || movingVelocityInDirection < -100 { // Dismiss @@ -69,7 +68,7 @@ class CharcoalRubberGesture: NSObject, CharcoalGesture { animations: { view.transform = .identity }, - completion: completion + completion: nil ) } default: From 9b0346030ad515b31b9f53c475a01c02889edd27 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 29 May 2024 15:40:20 +0900 Subject: [PATCH 171/199] Refactor --- .../CharcoalIdentifiableOverlayView.swift | 16 ++++- .../Toast/CharcoalRubberGesture.swift | 24 ++++---- .../Components/Toast/CharcoalSnackBar.swift | 8 +-- .../Toast/CharcoalSnackBarView.swift | 59 ++++++++++--------- 4 files changed, 62 insertions(+), 45 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 883d308bc..a1d6e7d50 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -63,8 +63,22 @@ public class CharcoalIdentifiableOverlayView: UIView, Identifiable { self.delegate?.overlayViewDidDismiss(self.id) } } - + deinit { print("deinit CharcoalIdentifiableOverlayView") } + + /// Make sure that the view is not blocking the touch events of the subview. + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !isUserInteractionEnabled { + for subview in subviews { + let convertedPoint = subview.convert(point, from: self) + if let hitView = subview.hitTest(convertedPoint, with: event) { + return hitView + } + } + return nil + } + return super.hitTest(point, with: event) + } } diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift index d72c9034c..4891e4a98 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift @@ -6,38 +6,38 @@ protocol CharcoalGesture { class CharcoalRubberGesture: NSObject, CharcoalGesture { let screenEdge: CharcoalPopupViewEdge - + var gesture: UIGestureRecognizer - + var dragVelocity: CGPoint = .zero - + var isDragging: Bool = false - + var offset: CGSize = .zero - + var dismiss: (() -> Void)? - + init(screenEdge: CharcoalPopupViewEdge) { self.screenEdge = screenEdge - self.gesture = UIPanGestureRecognizer() + gesture = UIPanGestureRecognizer() super.init() gesture.addTarget(self, action: #selector(handlePan(_:))) } - + @objc func handlePan(_ gesture: UIPanGestureRecognizer) { guard let view = gesture.view else { return } - + let translation = gesture.translation(in: gesture.view) let velocity = gesture.velocity(in: gesture.view) let translationInDirection = translation.y * screenEdge.direction let movingVelocityInDirection = velocity.y * screenEdge.direction let offsetInDirection = offset.height * screenEdge.direction - + // Rubber band effect let damping: CGFloat = 0.75 let initialSpringVelocity: CGFloat = 0.0 let duration: TimeInterval = 0.65 - + switch gesture.state { case .began: isDragging = true @@ -51,7 +51,7 @@ class CharcoalRubberGesture: NSObject, CharcoalGesture { let dist = sqrt(translation.y * translation.y) let factor = 1 / (dist / limit + 1) offset = CGSize(width: 0, height: translation.y * factor) - view.transform = CGAffineTransform(translationX: 0, y: translation.y * factor) + view.transform = CGAffineTransform(translationX: 0, y: translation.y * factor) } case .ended, .cancelled: isDragging = false diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift index e5d47c30a..fe9fa4e14 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift @@ -13,7 +13,7 @@ public extension CharcoalSnackBar { - screenEdge: The edge of the screen where the snackbar will be displayed. - screenEdgeSpacing: The spacing between the snackbar and the screen edge. - on: The view on which the snackbar will be displayed. - + # Example # ```swift CharcoalSnackBar.show(text: "Hello") @@ -35,14 +35,14 @@ public extension CharcoalSnackBar { let containerView = ChacoalOverlayManager.shared.layout(view: toastView, interactionMode: .passThrough, on: on) containerView.alpha = 1 - containerView.isUserInteractionEnabled = true - + containerView.isUserInteractionEnabled = false + let rubberGesture = CharcoalRubberGesture(screenEdge: screenEdge) toastView.addGesture(rubberGesture) rubberGesture.dismiss = { ChacoalOverlayManager.shared.dismiss(id: containerView.id) } - + var constraints = [ toastView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) ] diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift index 254d02036..4bbaadd8b 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift @@ -16,7 +16,7 @@ class CharcoalSnackBarView: UIView { stackView.translatesAutoresizingMaskIntoConstraints = false return stackView }() - + lazy var label: CharcoalTypography14 = { let label = CharcoalTypography14() label.numberOfLines = 1 @@ -25,21 +25,21 @@ class CharcoalSnackBarView: UIView { label.textColor = CharcoalAsset.ColorPaletteGenerated.text1.color return label }() - + lazy var thumbnailImageView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleAspectFill return imageView }() - + lazy var actionButton: CharcoalDefaultSButton = { let button = CharcoalDefaultSButton() return button }() - + let thumbnailImage: UIImage? - + var action: CharcoalAction? let text: String @@ -65,7 +65,7 @@ class CharcoalSnackBarView: UIView { /// Text frame size private var textFrameSize: CGSize = .zero - + var gesture: CharcoalGesture? init(text: String, thumbnailImage: UIImage? = nil, maxWidth: CGFloat = 312, action: CharcoalAction? = nil) { @@ -86,7 +86,7 @@ class CharcoalSnackBarView: UIView { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func setupCapsuleShape() { addSubview(capsuleShape) layer.backgroundColor = CharcoalAsset.ColorPaletteGenerated.background1.color.cgColor @@ -95,7 +95,7 @@ class CharcoalSnackBarView: UIView { layer.masksToBounds = true layer.cornerCurve = .continuous } - + private func addThumbnailView() { if let thumbnailImage = thumbnailImage { thumbnailImageView.image = thumbnailImage @@ -106,7 +106,7 @@ class CharcoalSnackBarView: UIView { ]) } } - + private func addTextLabel() { let leftPaddingView = UIView() let rightPaddingView = UIView() @@ -118,7 +118,7 @@ class CharcoalSnackBarView: UIView { hStackView.addArrangedSubview(rightPaddingView) label.text = text } - + private func addActionButton() { if let _ = action { actionButton.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside) @@ -133,7 +133,7 @@ class CharcoalSnackBarView: UIView { private func setupLayer() { // Setup Bubble Shape setupCapsuleShape() - + // Add HStack addSubview(hStackView) NSLayoutConstraint.activate([ @@ -143,17 +143,17 @@ class CharcoalSnackBarView: UIView { hStackView.trailingAnchor.constraint(equalTo: trailingAnchor), hStackView.widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth) ]) - + // Add thumbnail view addThumbnailView() - + // Add text label with padding view addTextLabel() - + // Add action button addActionButton() } - + @objc func actionButtonTapped() { action?.actionCallback() } @@ -163,24 +163,24 @@ class CharcoalSnackBarView: UIView { textFrameSize = text.calculateFrame(font: label.font, maxWidth: preferredTextMaxWidth) } - + /// The max width of the text label var preferredTextMaxWidth: CGFloat { var width = maxWidth - padding.left - padding.right - + // Check if has thumbnail image if let _ = thumbnailImage { - width = width - 64 + width = width - 64 } - + // Check if has action button if let _ = action { width = width - actionButton.intrinsicContentSize.width - padding.right } - + return width } - + var preferredLayoutWidth: CGFloat { if let _ = thumbnailImage { return 64 + padding.left + textFrameSize.width + padding.right @@ -188,7 +188,7 @@ class CharcoalSnackBarView: UIView { return padding.left + textFrameSize.width + padding.right } } - + var preferredLayoutHeight: CGFloat { if let _ = thumbnailImage { return 64 @@ -208,7 +208,7 @@ class CharcoalSnackBarView: UIView { capsuleShape.frame = bounds layer.cornerRadius = min(cornerRadius, bounds.height / 2.0) } - + /// Add gesture to this view func addGesture(_ gesture: CharcoalGesture) { self.gesture = gesture @@ -232,11 +232,14 @@ class CharcoalSnackBarView: UIView { print("編集 taped") })) - let snackbar4 = CharcoalSnackBarView(text: "こんにちは This is a snackbar and here is testing it's multiple line feature", - thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), action: CharcoalAction(title: "編集", actionCallback: { - print("編集 taped") - })) - + let snackbar4 = CharcoalSnackBarView( + text: "こんにちは This is a snackbar and here is testing it's multiple line feature", + thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), + action: CharcoalAction(title: "編集", actionCallback: { + print("編集 taped") + }) + ) + let snackbar5 = CharcoalSnackBarView(text: "こんにちは This is a snackbar and here is testing it's multiple line feature") stackView.addArrangedSubview(snackbar) From 53558347784c56c35e55f3a21f45fea8a0f83822 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 29 May 2024 16:02:22 +0900 Subject: [PATCH 172/199] Fix memory leak --- .../CharcoalIdentifiableOverlayView.swift | 6 +- .../Components/Toast/CharcoalSnackBar.swift | 83 ++++++++++--------- 2 files changed, 43 insertions(+), 46 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index a1d6e7d50..52891ef1b 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -59,15 +59,11 @@ public class CharcoalIdentifiableOverlayView: UIView, Identifiable { @objc func dismiss() { dismissAction?() { [weak self] _ in guard let self = self else { return } - self.removeFromSuperview() self.delegate?.overlayViewDidDismiss(self.id) + self.removeFromSuperview() } } - deinit { - print("deinit CharcoalIdentifiableOverlayView") - } - /// Make sure that the view is not blocking the touch events of the subview. override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !isUserInteractionEnabled { diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift index fe9fa4e14..5b5958b5c 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift @@ -4,19 +4,19 @@ public class CharcoalSnackBar {} public extension CharcoalSnackBar { /** - Show a snackbar. - + Show a snackbar. + - Parameters: - - text: The text to be displayed in the snackbar. - - maxWidth: The maximum width of the snackbar. - - thumbnailImage: The thumbnail image to be displayed in the snackbar. - - screenEdge: The edge of the screen where the snackbar will be displayed. - - screenEdgeSpacing: The spacing between the snackbar and the screen edge. - - on: The view on which the snackbar will be displayed. - + - text: The text to be displayed in the snackbar. + - maxWidth: The maximum width of the snackbar. + - thumbnailImage: The thumbnail image to be displayed in the snackbar. + - screenEdge: The edge of the screen where the snackbar will be displayed. + - screenEdgeSpacing: The spacing between the snackbar and the screen edge. + - on: The view on which the snackbar will be displayed. + # Example # ```swift - CharcoalSnackBar.show(text: "Hello") + CharcoalSnackBar.show(text: "Hello") ``` */ @discardableResult @@ -32,37 +32,40 @@ public extension CharcoalSnackBar { let toastView = CharcoalSnackBarView(text: text, thumbnailImage: thumbnailImage, maxWidth: maxWidth, action: action) toastView.isUserInteractionEnabled = true toastView.translatesAutoresizingMaskIntoConstraints = false - + let containerView = ChacoalOverlayManager.shared.layout(view: toastView, interactionMode: .passThrough, on: on) containerView.alpha = 1 containerView.isUserInteractionEnabled = false - + containerView.delegate = ChacoalOverlayManager.shared + let rubberGesture = CharcoalRubberGesture(screenEdge: screenEdge) toastView.addGesture(rubberGesture) + + let containerID = containerView.id rubberGesture.dismiss = { - ChacoalOverlayManager.shared.dismiss(id: containerView.id) + ChacoalOverlayManager.shared.dismiss(id: containerID) } - + var constraints = [ toastView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) ] - + var screenEdgeSpacingConstraint: NSLayoutConstraint - + switch screenEdge { case .top: screenEdgeSpacingConstraint = toastView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: -screenEdgeSpacing * screenEdge.direction) case .bottom: screenEdgeSpacingConstraint = toastView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -screenEdgeSpacing * screenEdge.direction) } - + constraints.append(screenEdgeSpacingConstraint) - + NSLayoutConstraint.activate(constraints) - + containerView.layoutIfNeeded() - - containerView.showAction = { actionCallback in + + containerView.showAction = {[weak containerView] actionCallback in screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 UIView.animate( withDuration: 0.65, @@ -71,28 +74,28 @@ public extension CharcoalSnackBar { initialSpringVelocity: 0.0, options: [], animations: { - containerView.layoutIfNeeded() + containerView?.layoutIfNeeded() } ) { completion in actionCallback?(completion) } } - - containerView.dismissAction = { actionCallback in + + containerView.dismissAction = {[weak containerView] actionCallback in screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 UIView.animate(withDuration: 0.3, animations: { - containerView.layoutIfNeeded() + containerView?.layoutIfNeeded() }) { completion in - containerView.alpha = 0 + containerView?.alpha = 0 actionCallback?(completion) } } - + ChacoalOverlayManager.shared.display(view: containerView) - + return containerView.id } - + /// Dismisses the toast with the given identifier. static func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { ChacoalOverlayManager.shared.dismiss(id: id) @@ -103,18 +106,16 @@ public extension CharcoalSnackBar { #Preview() { let view = UIView() view.backgroundColor = UIColor.lightGray - - DispatchQueue.main.async { - CharcoalSnackBar.show(text: "Hello World", screenEdge: .top) - CharcoalSnackBar.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", screenEdge: .top, screenEdgeSpacing: 220) - CharcoalSnackBar.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), screenEdge: .bottom, action: CharcoalAction(title: "編集", actionCallback: { - print("Tapped 編集") - })) + + CharcoalSnackBar.show(text: "Hello World", screenEdge: .top) + CharcoalSnackBar.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", screenEdge: .top, screenEdgeSpacing: 220) + CharcoalSnackBar.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), screenEdge: .bottom, action: CharcoalAction(title: "編集", actionCallback: { + print("Tapped 編集") + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + ChacoalOverlayManager.shared.dismiss() } - -// DispatchQueue.main.asyncAfter(deadline: .now() + 3) { -// ChacoalOverlayManager.shared.dismiss() -// } - + return view } From 6167fb7f0e3c2f3deec9276cf4a4d0a39d8921aa Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 29 May 2024 16:02:42 +0900 Subject: [PATCH 173/199] Format --- .../Components/Toast/CharcoalSnackBar.swift | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift index 5b5958b5c..d78c23595 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBar.swift @@ -5,7 +5,7 @@ public class CharcoalSnackBar {} public extension CharcoalSnackBar { /** Show a snackbar. - + - Parameters: - text: The text to be displayed in the snackbar. - maxWidth: The maximum width of the snackbar. @@ -13,7 +13,7 @@ public extension CharcoalSnackBar { - screenEdge: The edge of the screen where the snackbar will be displayed. - screenEdgeSpacing: The spacing between the snackbar and the screen edge. - on: The view on which the snackbar will be displayed. - + # Example # ```swift CharcoalSnackBar.show(text: "Hello") @@ -32,40 +32,40 @@ public extension CharcoalSnackBar { let toastView = CharcoalSnackBarView(text: text, thumbnailImage: thumbnailImage, maxWidth: maxWidth, action: action) toastView.isUserInteractionEnabled = true toastView.translatesAutoresizingMaskIntoConstraints = false - + let containerView = ChacoalOverlayManager.shared.layout(view: toastView, interactionMode: .passThrough, on: on) containerView.alpha = 1 containerView.isUserInteractionEnabled = false containerView.delegate = ChacoalOverlayManager.shared - + let rubberGesture = CharcoalRubberGesture(screenEdge: screenEdge) toastView.addGesture(rubberGesture) - + let containerID = containerView.id rubberGesture.dismiss = { ChacoalOverlayManager.shared.dismiss(id: containerID) } - + var constraints = [ toastView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) ] - + var screenEdgeSpacingConstraint: NSLayoutConstraint - + switch screenEdge { case .top: screenEdgeSpacingConstraint = toastView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: -screenEdgeSpacing * screenEdge.direction) case .bottom: screenEdgeSpacingConstraint = toastView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -screenEdgeSpacing * screenEdge.direction) } - + constraints.append(screenEdgeSpacingConstraint) - + NSLayoutConstraint.activate(constraints) - + containerView.layoutIfNeeded() - - containerView.showAction = {[weak containerView] actionCallback in + + containerView.showAction = { [weak containerView] actionCallback in screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 UIView.animate( withDuration: 0.65, @@ -80,8 +80,8 @@ public extension CharcoalSnackBar { actionCallback?(completion) } } - - containerView.dismissAction = {[weak containerView] actionCallback in + + containerView.dismissAction = { [weak containerView] actionCallback in screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 UIView.animate(withDuration: 0.3, animations: { containerView?.layoutIfNeeded() @@ -90,12 +90,12 @@ public extension CharcoalSnackBar { actionCallback?(completion) } } - + ChacoalOverlayManager.shared.display(view: containerView) - + return containerView.id } - + /// Dismisses the toast with the given identifier. static func dismiss(id: CharcoalIdentifiableOverlayView.IDValue) { ChacoalOverlayManager.shared.dismiss(id: id) @@ -106,16 +106,16 @@ public extension CharcoalSnackBar { #Preview() { let view = UIView() view.backgroundColor = UIColor.lightGray - + CharcoalSnackBar.show(text: "Hello World", screenEdge: .top) CharcoalSnackBar.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", screenEdge: .top, screenEdgeSpacing: 220) CharcoalSnackBar.show(text: "こんにちは This is a tooltip and here is testing it's multiple line feature", thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), screenEdge: .bottom, action: CharcoalAction(title: "編集", actionCallback: { print("Tapped 編集") })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { ChacoalOverlayManager.shared.dismiss() } - + return view } From 85dba89e6b4953b4f5f853d57268c6cad709247c Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 29 May 2024 16:04:32 +0900 Subject: [PATCH 174/199] Fix memory leak --- .../Overlay/CharcoalIdentifiableOverlayView.swift | 2 +- .../Components/Toast/CharcoalToast.swift | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 52891ef1b..853f0e652 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -63,7 +63,7 @@ public class CharcoalIdentifiableOverlayView: UIView, Identifiable { self.removeFromSuperview() } } - + /// Make sure that the view is not blocking the touch events of the subview. override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !isUserInteractionEnabled { diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift index b1cc31140..f5a9e951a 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift @@ -34,6 +34,7 @@ public extension CharcoalToast { let containerView = ChacoalOverlayManager.shared.layout(view: toastView, interactionMode: .passThrough, on: on) containerView.alpha = 1 + containerView.delegate = ChacoalOverlayManager.shared var constraints = [ toastView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor) @@ -54,7 +55,7 @@ public extension CharcoalToast { containerView.layoutIfNeeded() - containerView.showAction = { actionCallback in + containerView.showAction = {[weak containerView] actionCallback in screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 UIView.animate( withDuration: 0.65, @@ -63,19 +64,19 @@ public extension CharcoalToast { initialSpringVelocity: 0.0, options: [], animations: { - containerView.layoutIfNeeded() + containerView?.layoutIfNeeded() } ) { completion in actionCallback?(completion) } } - containerView.dismissAction = { actionCallback in + containerView.dismissAction = {[weak containerView] actionCallback in screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 UIView.animate(withDuration: 0.3, animations: { - containerView.layoutIfNeeded() + containerView?.layoutIfNeeded() }) { completion in - containerView.alpha = 0 + containerView?.alpha = 0 actionCallback?(completion) } } From d153e9507324812d2c309d4ef121f43b7b7c568e Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 29 May 2024 17:44:58 +0900 Subject: [PATCH 175/199] Add example --- .../Views/Toasts/Toasts.swift | 86 +++++++++++++++---- .../CharcoalIdentifiableOverlayView.swift | 2 +- .../Toast/CharcoalSnackBarView.swift | 5 ++ .../Components/Toast/CharcoalToast.swift | 4 +- 4 files changed, 76 insertions(+), 21 deletions(-) diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift index 3867b9e1a..018bf49c2 100644 --- a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift @@ -1,6 +1,21 @@ import Charcoal import UIKit +enum SnackbarTitles: String, CaseIterable { + case normal = "Normal" + case withAction = "with Action" + case withActionAndThumbnail = "with Action and Thumbnail" + + var text: String { + return "ブックマークしました" + } + + func configCell(cell: UITableViewCell) { + cell.textLabel!.text = "SnackBar" + cell.detailTextLabel?.text = rawValue + } +} + enum ToastsTitles: String, CaseIterable { case top = "Top" case bottom = "Bottom" @@ -32,19 +47,24 @@ public final class ToastsViewController: UIViewController { let cellReuseIdentifier = "cell" private enum Sections: Int, CaseIterable { - case components + case toasts + case snackbars var title: String { switch self { - case .components: + case .toasts: return "Toasts" + case .snackbars: + return "Snackbars" } } var items: [any CaseIterable] { switch self { - case .components: - return TooltipTitles.allCases + case .toasts: + return ToastsTitles.allCases + case .snackbars: + return SnackbarTitles.allCases } } } @@ -78,13 +98,17 @@ extension ToastsViewController: UITableViewDelegate, UITableViewDataSource { public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let section = Sections.allCases[indexPath.section] - let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as UITableViewCell? ?? UITableViewCell(style: .default, reuseIdentifier: cellReuseIdentifier) + let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as UITableViewCell? ?? UITableViewCell(style: .subtitle, reuseIdentifier: cellReuseIdentifier) switch section { - case .components: + case .toasts: let titleCase = ToastsTitles.allCases[indexPath.row] titleCase.configCell(cell: cell) return cell + case .snackbars: + let titleCase = SnackbarTitles.allCases[indexPath.row] + titleCase.configCell(cell: cell) + return cell } } @@ -94,20 +118,46 @@ extension ToastsViewController: UITableViewDelegate, UITableViewDataSource { public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - let titleCase = ToastsTitles.allCases[indexPath.row] - var toastID: CharcoalIdentifiableOverlayView.IDValue - switch titleCase { - case .top: - toastID = CharcoalToast.show(text: titleCase.text, screenEdge: .top) - case .bottom: - toastID = CharcoalToast.show(text: titleCase.text, appearance: .error, screenEdge: .bottom) - case .multiline: - toastID = CharcoalToast.show(text: titleCase.text, screenEdge: .bottom) - } + let section = Sections.allCases[indexPath.section] + + switch section { + case .toasts: + let titleCase = ToastsTitles.allCases[indexPath.row] - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - CharcoalToast.dismiss(id: toastID) + var toastID: CharcoalIdentifiableOverlayView.IDValue + switch titleCase { + case .top: + toastID = CharcoalToast.show(text: titleCase.text, screenEdge: .top) + case .bottom: + toastID = CharcoalToast.show(text: titleCase.text, appearance: .error, screenEdge: .bottom) + case .multiline: + toastID = CharcoalToast.show(text: titleCase.text, screenEdge: .bottom) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + CharcoalToast.dismiss(id: toastID) + } + case .snackbars: + let titleCase = SnackbarTitles.allCases[indexPath.row] + + var toastID: CharcoalIdentifiableOverlayView.IDValue + switch titleCase { + case .normal: + toastID = CharcoalSnackBar.show(text: titleCase.text, screenEdge: .top) + case .withAction: + toastID = CharcoalSnackBar.show(text: titleCase.text, screenEdge: .bottom, action: CharcoalAction(title: "編集", actionCallback: { + print("Tapped 編集") + })) + case .withActionAndThumbnail: + toastID = CharcoalSnackBar.show(text: titleCase.text, thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), screenEdge: .bottom, action: CharcoalAction(title: "編集", actionCallback: { + print("Tapped 編集") + })) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + CharcoalSnackBar.dismiss(id: toastID) + } } } diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 853f0e652..52891ef1b 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -63,7 +63,7 @@ public class CharcoalIdentifiableOverlayView: UIView, Identifiable { self.removeFromSuperview() } } - + /// Make sure that the view is not blocking the touch events of the subview. override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !isUserInteractionEnabled { diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift index 4bbaadd8b..d341871e2 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift @@ -5,6 +5,11 @@ public typealias ActionCallback = () -> Void public struct CharcoalAction { let title: String let actionCallback: ActionCallback + + public init(title: String, actionCallback: @escaping ActionCallback) { + self.title = title + self.actionCallback = actionCallback + } } class CharcoalSnackBarView: UIView { diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift index f5a9e951a..27df4c6bd 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToast.swift @@ -55,7 +55,7 @@ public extension CharcoalToast { containerView.layoutIfNeeded() - containerView.showAction = {[weak containerView] actionCallback in + containerView.showAction = { [weak containerView] actionCallback in screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 UIView.animate( withDuration: 0.65, @@ -71,7 +71,7 @@ public extension CharcoalToast { } } - containerView.dismissAction = {[weak containerView] actionCallback in + containerView.dismissAction = { [weak containerView] actionCallback in screenEdgeSpacingConstraint.constant = screenEdgeSpacingConstraint.constant * -1 UIView.animate(withDuration: 0.3, animations: { containerView?.layoutIfNeeded() From 2c346f3d41958b03e625ebb85e9af0d105bd968d Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 29 May 2024 17:51:15 +0900 Subject: [PATCH 176/199] Refine snackbar --- .../Media.xcassets/Contents.json | 6 ++++++ .../SnackbarDemo.imageset/Contents.json | 12 ++++++++++++ ...203\203\343\203\210 2024-02-21 17.28.10.png" | Bin 0 -> 27570 bytes .../Views/Toasts/Toasts.swift | 2 +- .../Components/Toast/CharcoalSnackBarView.swift | 1 + 5 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/Contents.json create mode 100644 CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/Contents.json create mode 100644 "CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210 2024-02-21 17.28.10.png" diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/Contents.json b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/Contents.json b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/Contents.json new file mode 100644 index 000000000..46d35b5c8 --- /dev/null +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "スクリーンショット 2024-02-21 17.28.10.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210 2024-02-21 17.28.10.png" "b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210 2024-02-21 17.28.10.png" new file mode 100644 index 0000000000000000000000000000000000000000..c79e6b15327c6e0ce48ad4f44169af654b24263e GIT binary patch literal 27570 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 A`Tzg` literal 0 HcmV?d00001 diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift index 018bf49c2..fb252a6a8 100644 --- a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Toasts/Toasts.swift @@ -150,7 +150,7 @@ extension ToastsViewController: UITableViewDelegate, UITableViewDataSource { print("Tapped 編集") })) case .withActionAndThumbnail: - toastID = CharcoalSnackBar.show(text: titleCase.text, thumbnailImage: CharcoalAsset.ColorPaletteGenerated.border.color.imageWithColor(width: 64, height: 64), screenEdge: .bottom, action: CharcoalAction(title: "編集", actionCallback: { + toastID = CharcoalSnackBar.show(text: titleCase.text, thumbnailImage: UIImage(named: "SnackbarDemo", in: Bundle.module, with: nil), screenEdge: .bottom, action: CharcoalAction(title: "編集", actionCallback: { print("Tapped 編集") })) } diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift index d341871e2..0664cb3c6 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift @@ -27,6 +27,7 @@ class CharcoalSnackBarView: UIView { label.numberOfLines = 1 label.isBold = true label.translatesAutoresizingMaskIntoConstraints = false + label.textAlignment = .center label.textColor = CharcoalAsset.ColorPaletteGenerated.text1.color return label }() From fb2cb70e91e9fedf0f20d75edca039b7051f6f46 Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 31 May 2024 22:10:24 +0900 Subject: [PATCH 177/199] Refine swift lint --- .../SwiftUISample/SwiftUISample.xcodeproj/project.pbxproj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Examples/SwiftUISample/SwiftUISample.xcodeproj/project.pbxproj b/Examples/SwiftUISample/SwiftUISample.xcodeproj/project.pbxproj index 824c2d302..5c268ef62 100644 --- a/Examples/SwiftUISample/SwiftUISample.xcodeproj/project.pbxproj +++ b/Examples/SwiftUISample/SwiftUISample.xcodeproj/project.pbxproj @@ -81,7 +81,7 @@ 24651B2727043658005AD842 /* Sources */, 24651B2827043658005AD842 /* Frameworks */, 24651B2927043658005AD842 /* Resources */, - 242CCCD62771748B00897251 /* SwiftLint */, + 9F0036972C0A0399003BE2C2 /* SwiftLint */, ); buildRules = ( ); @@ -141,7 +141,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 242CCCD62771748B00897251 /* SwiftLint */ = { + 9F0036972C0A0399003BE2C2 /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -157,6 +157,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; + shellScript = "unset SDKROOT\nswift run -c release --package-path ../../BuildTools swiftlint --config ../../.swiftlint.yml\n"; }; /* End PBXShellScriptBuildPhase section */ From a573a8be840ed87859cf5bea80d0b28931c88963 Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 31 May 2024 22:19:57 +0900 Subject: [PATCH 178/199] Refactor CharcoalToastView --- .../Components/Toast/CharcoalToastView.swift | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift index 20fd9fd4a..2a57f91af 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift @@ -34,16 +34,12 @@ class CharcoalToastView: UIView { /// Padding around the bubble let padding = UIEdgeInsets(top: 8, left: 24, bottom: 8, right: 24) - /// Text frame size - private var textFrameSize: CGSize = .zero - init(text: String, maxWidth: CGFloat = 312, appearance: CharcoalToastAppearance = .success) { self.maxWidth = maxWidth self.text = text self.appearance = appearance borderColor = CharcoalAsset.ColorPaletteGenerated.background1.color super.init(frame: .zero) - textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) setupLayer() } @@ -63,23 +59,20 @@ class CharcoalToastView: UIView { // Setup Label addSubview(label) label.text = text - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - textFrameSize = text.calculateFrame(font: label.font, maxWidth: maxWidth) - } - - override var intrinsicContentSize: CGSize { - return CGSize(width: padding.left + textFrameSize.width + padding.right, height: padding.top + textFrameSize.height + padding.bottom) + label.preferredMaxLayoutWidth = maxWidth - padding.left - padding.right + + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding.left), + label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding.right), + label.topAnchor.constraint(equalTo: topAnchor, constant: padding.top), + label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -padding.bottom) + ]) } override func layoutSubviews() { super.layoutSubviews() capsuleShape.frame = bounds layer.cornerRadius = min(cornerRadius, bounds.height / 2.0) - label.frame = CGRect(x: padding.left, y: padding.top, width: textFrameSize.width, height: textFrameSize.height) } } From 4bf320823ff0c87565584484dfed0f85f6c111fa Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 31 May 2024 22:49:29 +0900 Subject: [PATCH 179/199] Refactor CharcoalSnackBarView --- .../Toast/CharcoalSnackBarView.swift | 82 +++++++++---------- 1 file changed, 37 insertions(+), 45 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift index 0664cb3c6..78eae49c9 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift @@ -17,10 +17,23 @@ class CharcoalSnackBarView: UIView { let stackView = UIStackView() stackView.axis = .horizontal stackView.alignment = .center + stackView.distribution = .fill stackView.spacing = 0 stackView.translatesAutoresizingMaskIntoConstraints = false return stackView }() + + lazy var buttonContainer: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + lazy var labelContainer: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() lazy var label: CharcoalTypography14 = { let label = CharcoalTypography14() @@ -41,6 +54,7 @@ class CharcoalSnackBarView: UIView { lazy var actionButton: CharcoalDefaultSButton = { let button = CharcoalDefaultSButton() + button.translatesAutoresizingMaskIntoConstraints = false return button }() @@ -69,9 +83,6 @@ class CharcoalSnackBarView: UIView { /// Padding around the bubble let padding = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16) - /// Text frame size - private var textFrameSize: CGSize = .zero - var gesture: CharcoalGesture? init(text: String, thumbnailImage: UIImage? = nil, maxWidth: CGFloat = 312, action: CharcoalAction? = nil) { @@ -84,7 +95,6 @@ class CharcoalSnackBarView: UIView { if let action = action { actionButton.setTitle(action.title, for: .normal) } - textFrameSize = text.calculateFrame(font: label.font, maxWidth: preferredTextMaxWidth) setupLayer() } @@ -114,25 +124,35 @@ class CharcoalSnackBarView: UIView { } private func addTextLabel() { - let leftPaddingView = UIView() - let rightPaddingView = UIView() - leftPaddingView.widthAnchor.constraint(equalToConstant: padding.left).isActive = true - rightPaddingView.widthAnchor.constraint(equalToConstant: padding.right).isActive = true - // Setup Label - hStackView.addArrangedSubview(leftPaddingView) - hStackView.addArrangedSubview(label) - hStackView.addArrangedSubview(rightPaddingView) + hStackView.addArrangedSubview(labelContainer) + + labelContainer.addSubview(label) + + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(equalTo: labelContainer.leadingAnchor, constant: padding.left), + label.trailingAnchor.constraint(equalTo: labelContainer.trailingAnchor, constant: -padding.right), + label.topAnchor.constraint(equalTo: labelContainer.topAnchor, constant: padding.top), + label.bottomAnchor.constraint(equalTo: labelContainer.bottomAnchor, constant: -padding.bottom) + ]) + label.text = text + label.preferredMaxLayoutWidth = preferredTextMaxWidth + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) } private func addActionButton() { if let _ = action { actionButton.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside) - hStackView.addArrangedSubview(actionButton) - actionButton.widthAnchor.constraint(equalToConstant: actionButton.intrinsicContentSize.width).isActive = true - let rightPaddingView = UIView() - rightPaddingView.widthAnchor.constraint(equalToConstant: padding.right).isActive = true - hStackView.addArrangedSubview(rightPaddingView) + actionButton.alpha = 1 + hStackView.addArrangedSubview(buttonContainer) + buttonContainer.addSubview(actionButton) + + NSLayoutConstraint.activate([ + actionButton.leadingAnchor.constraint(equalTo: buttonContainer.leadingAnchor), + actionButton.trailingAnchor.constraint(equalTo: buttonContainer.trailingAnchor, constant: -padding.right), + actionButton.topAnchor.constraint(equalTo: buttonContainer.topAnchor, constant: padding.top), + actionButton.bottomAnchor.constraint(equalTo: buttonContainer.bottomAnchor, constant: -padding.bottom), + ]) } } @@ -164,12 +184,6 @@ class CharcoalSnackBarView: UIView { action?.actionCallback() } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - textFrameSize = text.calculateFrame(font: label.font, maxWidth: preferredTextMaxWidth) - } - /// The max width of the text label var preferredTextMaxWidth: CGFloat { var width = maxWidth - padding.left - padding.right @@ -187,28 +201,6 @@ class CharcoalSnackBarView: UIView { return width } - var preferredLayoutWidth: CGFloat { - if let _ = thumbnailImage { - return 64 + padding.left + textFrameSize.width + padding.right - } else { - return padding.left + textFrameSize.width + padding.right - } - } - - var preferredLayoutHeight: CGFloat { - if let _ = thumbnailImage { - return 64 - } else if let _ = action { - return max(padding.top + label.font.lineHeight + padding.bottom, padding.top + actionButton.intrinsicContentSize.height + padding.bottom) - } else { - return padding.top + label.font.lineHeight + padding.bottom - } - } - - override var intrinsicContentSize: CGSize { - return CGSize(width: preferredLayoutWidth, height: preferredLayoutHeight) - } - override func layoutSubviews() { super.layoutSubviews() capsuleShape.frame = bounds From 397ebec7fd3eff2eeb53b3ac75850ee6d2dde322 Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 31 May 2024 22:53:22 +0900 Subject: [PATCH 180/199] Reformat --- .../Components/Toast/CharcoalAction.swift | 11 ++++++++ .../Toast/CharcoalSnackBarView.swift | 28 ++++++------------- .../Components/Toast/CharcoalToastView.swift | 2 +- 3 files changed, 20 insertions(+), 21 deletions(-) create mode 100644 Sources/CharcoalUIKit/Components/Toast/CharcoalAction.swift diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalAction.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalAction.swift new file mode 100644 index 000000000..89383f5bc --- /dev/null +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalAction.swift @@ -0,0 +1,11 @@ +public typealias ActionCallback = () -> Void + +public struct CharcoalAction { + let title: String + let actionCallback: ActionCallback + + public init(title: String, actionCallback: @escaping ActionCallback) { + self.title = title + self.actionCallback = actionCallback + } +} diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift index 78eae49c9..0f9c0ca21 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalSnackBarView.swift @@ -1,17 +1,5 @@ import UIKit -public typealias ActionCallback = () -> Void - -public struct CharcoalAction { - let title: String - let actionCallback: ActionCallback - - public init(title: String, actionCallback: @escaping ActionCallback) { - self.title = title - self.actionCallback = actionCallback - } -} - class CharcoalSnackBarView: UIView { lazy var hStackView: UIStackView = { let stackView = UIStackView() @@ -22,13 +10,13 @@ class CharcoalSnackBarView: UIView { stackView.translatesAutoresizingMaskIntoConstraints = false return stackView }() - + lazy var buttonContainer: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false return view }() - + lazy var labelContainer: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -125,16 +113,16 @@ class CharcoalSnackBarView: UIView { private func addTextLabel() { hStackView.addArrangedSubview(labelContainer) - + labelContainer.addSubview(label) - + NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: labelContainer.leadingAnchor, constant: padding.left), - label.trailingAnchor.constraint(equalTo: labelContainer.trailingAnchor, constant: -padding.right), + label.trailingAnchor.constraint(equalTo: labelContainer.trailingAnchor, constant: -padding.right), label.topAnchor.constraint(equalTo: labelContainer.topAnchor, constant: padding.top), label.bottomAnchor.constraint(equalTo: labelContainer.bottomAnchor, constant: -padding.bottom) ]) - + label.text = text label.preferredMaxLayoutWidth = preferredTextMaxWidth label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) @@ -146,12 +134,12 @@ class CharcoalSnackBarView: UIView { actionButton.alpha = 1 hStackView.addArrangedSubview(buttonContainer) buttonContainer.addSubview(actionButton) - + NSLayoutConstraint.activate([ actionButton.leadingAnchor.constraint(equalTo: buttonContainer.leadingAnchor), actionButton.trailingAnchor.constraint(equalTo: buttonContainer.trailingAnchor, constant: -padding.right), actionButton.topAnchor.constraint(equalTo: buttonContainer.topAnchor, constant: padding.top), - actionButton.bottomAnchor.constraint(equalTo: buttonContainer.bottomAnchor, constant: -padding.bottom), + actionButton.bottomAnchor.constraint(equalTo: buttonContainer.bottomAnchor, constant: -padding.bottom) ]) } } diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift index 2a57f91af..64bd48918 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift +++ b/Sources/CharcoalUIKit/Components/Toast/CharcoalToastView.swift @@ -60,7 +60,7 @@ class CharcoalToastView: UIView { addSubview(label) label.text = text label.preferredMaxLayoutWidth = maxWidth - padding.left - padding.right - + NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding.left), label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding.right), From 2c5dd372a93aa7f3df98467043657b66960a77bb Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 31 May 2024 23:13:00 +0900 Subject: [PATCH 181/199] Refine self logic --- .swiftformat | 2 +- Sources/CharcoalUIKit/Extensions/StringExtension.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.swiftformat b/.swiftformat index 37fcf7d79..b8a564540 100644 --- a/.swiftformat +++ b/.swiftformat @@ -30,4 +30,4 @@ --wraparguments before-first --wrapcollections before-first ---exclude Sources/Charcoal/Generated +--exclude Sources/CharcoalShared/Generated diff --git a/Sources/CharcoalUIKit/Extensions/StringExtension.swift b/Sources/CharcoalUIKit/Extensions/StringExtension.swift index 6662659e3..e0e643dc9 100644 --- a/Sources/CharcoalUIKit/Extensions/StringExtension.swift +++ b/Sources/CharcoalUIKit/Extensions/StringExtension.swift @@ -1,6 +1,7 @@ import UIKit extension String { + // swiftformat:disable redundantSelf func calculateFrame(font: UIFont, maxWidth: CGFloat) -> CGSize { let size = CGSize(width: maxWidth, height: .greatestFiniteMagnitude) let options: NSStringDrawingOptions = [.usesLineFragmentOrigin, .usesFontLeading] From 4229a9c09d9ea2a5a04025968524eb4f2c8f6188 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 10 Jul 2024 16:51:35 +0900 Subject: [PATCH 182/199] Reformat --- .../Overlay/CharcoalIdentifiableOverlayView.swift | 10 +++++----- .../Components/Tooltip/CharcoalTooltip.swift | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift index 8e7630928..373f9d8a9 100644 --- a/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift +++ b/Sources/CharcoalUIKit/Components/Overlay/CharcoalIdentifiableOverlayView.swift @@ -6,18 +6,18 @@ protocol CharcoalIdentifiableOverlayDelegate: AnyObject { public class CharcoalIdentifiableOverlayView: UIView, Identifiable { public typealias IDValue = UUID - + public typealias ActionComplete = (Bool) -> Void - + public typealias ActionContent = (ActionComplete?) -> Void public let id = IDValue() let interactionMode: CharcoalOverlayInteractionMode - + /// Action to show the overlay. var showAction: ActionContent? - + /// Action to dismiss the overlay. var dismissAction: ActionContent? @@ -57,7 +57,7 @@ public class CharcoalIdentifiableOverlayView: UIView, Identifiable { } @objc func dismiss() { - dismissAction?() { [weak self] finished in + dismissAction?() { [weak self] _ in guard let self = self else { return } self.removeFromSuperview() self.delegate?.overlayViewDidDismiss(self) diff --git a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift index e2ddc6e6e..ac2069035 100644 --- a/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalUIKit/Components/Tooltip/CharcoalTooltip.swift @@ -43,7 +43,7 @@ public extension CharcoalTooltip { tooltip.topAnchor.constraint(equalTo: containerView.topAnchor, constant: viewTopConstant) ] NSLayoutConstraint.activate(constraints) - + containerView.showAction = { actionCallback in UIView.animate(withDuration: 0.25, animations: { containerView.alpha = 1 @@ -51,7 +51,7 @@ public extension CharcoalTooltip { actionCallback?(completion) } } - + containerView.dismissAction = { actionCallback in UIView.animate(withDuration: 0.25, animations: { containerView.alpha = 0 From 4247dd223f4936bdb4233c2b01d388a933052a78 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 10 Jul 2024 16:52:28 +0900 Subject: [PATCH 183/199] Fix name --- .../CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift | 4 ++-- Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 8fe6bc812..0de514240 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -164,10 +164,10 @@ struct CharcoalSnackBarModifier: ViewModifier { public extension View { /** - Add a tooltip to the view + Add a Snackbar to the view - Parameters: - - isPresenting: A binding to whether the Tooltip is presented. + - isPresenting: A binding to whether the Snackbar is presented. - text: The text to be displayed in the snackbar. - thumbnailImage: The thumbnail image to be displayed in the snackbar. - dismissAfter: The overlay will be dismissed after a certain time interval. diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index ed9998106..8caad46b0 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -186,10 +186,10 @@ struct CharcoalToastModifier: ViewModifier { public extension View { /** - Add a tooltip to the view + Add a Toast to the view - Parameters: - - isPresenting: A binding to whether the Tooltip is presented. + - isPresenting: A binding to whether the Toast is presented. - dismissAfter: The overlay will be dismissed after a certain time interval. - screenEdge: The edge of the screen where the snackbar will be presented - screenEdgeSpacing: The spacing between the snackbar and the screen edge From a89bf66feb82de8b2da1ba8c242ef4ac09d8f99c Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 10 Jul 2024 17:06:37 +0900 Subject: [PATCH 184/199] Fix geometry --- .../CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift | 4 ++-- Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 8fe02d741..747e3f7e8 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -176,14 +176,14 @@ struct CharcoalTooltipModifier: ViewModifier { func body(content: Content) -> some View { content - .overlay(GeometryReader(content: { proxy in + .overlay(GeometryReader(content: { geometry in Color.clear .modifier(CharcoalOverlayUpdaterContainer( isPresenting: $isPresenting, view: CharcoalTooltip( id: viewID, text: text, - targetFrame: proxy.frame(in: .global), + targetFrame: geometry.frame(in: .global), isPresenting: $isPresenting, dismissAfter: dismissAfter ), diff --git a/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift b/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift index 491455d7a..321cc0dd0 100644 --- a/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift +++ b/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift @@ -73,7 +73,7 @@ struct CharcoalModalView: View { } var body: some View { - return GeometryReader(content: { proxy in + return GeometryReader(content: { geometry in ZStack(alignment: style.alignment, content: { Rectangle() .foregroundColor(Color.black.opacity(0.6)) @@ -101,7 +101,7 @@ struct CharcoalModalView: View { } .padding(EdgeInsets(top: 20, leading: 20, bottom: style == .center ? 20 : indicatorInset, trailing: 20)) .onAppear { - indicatorInset = max(proxy.safeAreaInsets.bottom, 30) + indicatorInset = max(geometry.safeAreaInsets.bottom, 30) } } } From ae59b414e1838c852047f62aabd81040661eab4c Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 10 Jul 2024 17:07:18 +0900 Subject: [PATCH 185/199] Revert "Fix geometry" This reverts commit a89bf66feb82de8b2da1ba8c242ef4ac09d8f99c. --- .../CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift | 4 ++-- Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 747e3f7e8..8fe02d741 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -176,14 +176,14 @@ struct CharcoalTooltipModifier: ViewModifier { func body(content: Content) -> some View { content - .overlay(GeometryReader(content: { geometry in + .overlay(GeometryReader(content: { proxy in Color.clear .modifier(CharcoalOverlayUpdaterContainer( isPresenting: $isPresenting, view: CharcoalTooltip( id: viewID, text: text, - targetFrame: geometry.frame(in: .global), + targetFrame: proxy.frame(in: .global), isPresenting: $isPresenting, dismissAfter: dismissAfter ), diff --git a/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift b/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift index 321cc0dd0..491455d7a 100644 --- a/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift +++ b/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift @@ -73,7 +73,7 @@ struct CharcoalModalView: View { } var body: some View { - return GeometryReader(content: { geometry in + return GeometryReader(content: { proxy in ZStack(alignment: style.alignment, content: { Rectangle() .foregroundColor(Color.black.opacity(0.6)) @@ -101,7 +101,7 @@ struct CharcoalModalView: View { } .padding(EdgeInsets(top: 20, leading: 20, bottom: style == .center ? 20 : indicatorInset, trailing: 20)) .onAppear { - indicatorInset = max(geometry.safeAreaInsets.bottom, 30) + indicatorInset = max(proxy.safeAreaInsets.bottom, 30) } } } From d55ed42718239db369f4ec6f4c8f838d131d137d Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 10 Jul 2024 17:08:25 +0900 Subject: [PATCH 186/199] Fix proxy name --- .../Toast/CharcoalToastAnimatableModifier.swift | 4 ++-- .../Components/Tooltip/CharcoalTooltip.swift | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift index 3f5c4c053..a2631e00a 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift @@ -35,8 +35,8 @@ struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBase { .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(borderColor, lineWidth: borderLineWidth)) .overlay( - GeometryReader(content: { geometry in - Color.clear.preference(key: PopupViewSizeKey.self, value: geometry.size) + GeometryReader(content: { proxy in + Color.clear.preference(key: PopupViewSizeKey.self, value: proxy.size) }) ) .offset(CGSize(width: 0, height: animationConfiguration.enablePositionAnimation ? (isActuallyPresenting ? screenEdge.direction * screenEdgeSpacing : -screenEdge.direction * (tooltipSize.height)) : screenEdge.direction * screenEdgeSpacing)) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 8fe02d741..7bcc572fc 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -226,7 +226,7 @@ private struct TooltipsPreviewView: View { @State var textOfLabel = "Hello" var body: some View { - GeometryReader(content: { geometry in + GeometryReader(content: { proxy in ScrollView { ZStack(alignment: .topLeading) { Color.clear @@ -265,7 +265,7 @@ private struct TooltipsPreviewView: View { } .charcoalPrimaryButton(size: .medium) .charcoalTooltip(isPresenting: $isPresenting3, text: "here is testing it's multiple line feature") - .offset(CGSize(width: geometry.size.width - 100, height: 100.0)) + .offset(CGSize(width: proxy.size.width - 100, height: 100.0)) Button { isPresenting4.toggle() @@ -273,7 +273,7 @@ private struct TooltipsPreviewView: View { Image(charocalIcon: .question24) } .charcoalTooltip(isPresenting: $isPresenting4, text: "Hello World This is a tooltip and here is testing it's multiple line feature") - .offset(CGSize(width: geometry.size.width - 30, height: geometry.size.height - 40)) + .offset(CGSize(width: proxy.size.width - 30, height: proxy.size.height - 40)) Button { isPresenting5.toggle() @@ -286,7 +286,7 @@ private struct TooltipsPreviewView: View { text: "Hello World This is a tooltip and here is testing it's multiple line feature", dismissAfter: 2 ) - .offset(CGSize(width: geometry.size.width - 240, height: geometry.size.height - 40)) + .offset(CGSize(width: proxy.size.width - 240, height: proxy.size.height - 40)) Button { isPresenting6.toggle() @@ -294,7 +294,7 @@ private struct TooltipsPreviewView: View { Image(charocalIcon: .question24) } .charcoalTooltip(isPresenting: $isPresenting6, text: "Hello World This is a tooltip and here is testing it's multiple line feature") - .offset(CGSize(width: geometry.size.width - 380, height: geometry.size.height - 240)) + .offset(CGSize(width: proxy.size.width - 380, height: proxy.size.height - 240)) } } }) From 2a603dcd4a27ab25fb11c4ac604acb49261310c8 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 10 Jul 2024 17:10:34 +0900 Subject: [PATCH 187/199] Adjust unused text --- .../Components/Toast/CharcoalToastAnimatableModifier.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift index a2631e00a..0ea4d76bd 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift @@ -2,7 +2,7 @@ import Combine import SwiftUI struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBase { - var text: String + var text: String = "" var maxWidth: CGFloat @@ -82,6 +82,6 @@ extension View { dismissAfter: TimeInterval? = nil, animationConfiguration: CharcoalToastAnimationConfiguration ) -> some View { - modifier(CharcoalToastAnimatableModifier(text: "", maxWidth: 0, isPresenting: isPresenting, cornerRadius: cornerRadius, borderColor: borderColor, borderLineWidth: borderLineWidth, screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, tooltipSize: tooltipSize, isActuallyPresenting: isActuallyPresenting, animationConfiguration: animationConfiguration, dismissAfter: dismissAfter, isDragging: isDragging)) + modifier(CharcoalToastAnimatableModifier(maxWidth: 0, isPresenting: isPresenting, cornerRadius: cornerRadius, borderColor: borderColor, borderLineWidth: borderLineWidth, screenEdge: screenEdge, screenEdgeSpacing: screenEdgeSpacing, tooltipSize: tooltipSize, isActuallyPresenting: isActuallyPresenting, animationConfiguration: animationConfiguration, dismissAfter: dismissAfter, isDragging: isDragging)) } } From e823b5bb77399416f20523c4789c5ded028fb761 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 23 Jul 2024 14:00:02 +0800 Subject: [PATCH 188/199] Replace charcoal logo --- .../SnackbarDemo.imageset/Contents.json | 2 +- .../SnackbarDemo.imageset/charcoal-logo.png | Bin 0 -> 2144 bytes ...203\203\343\203\210 2024-02-21 17.28.10.png" | Bin 27570 -> 0 bytes 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/charcoal-logo.png delete mode 100644 "CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210 2024-02-21 17.28.10.png" diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/Contents.json b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/Contents.json index 46d35b5c8..f10cd6bfb 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/Contents.json +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "スクリーンショット 2024-02-21 17.28.10.png", + "filename" : "charcoal-logo.png", "idiom" : "universal" } ], diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/charcoal-logo.png b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/charcoal-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..85a24cbc9a88afcb1621cdd66b36e3614af4e617 GIT binary patch literal 2144 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 literal 0 HcmV?d00001 diff --git "a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210 2024-02-21 17.28.10.png" "b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/Media.xcassets/SnackbarDemo.imageset/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210 2024-02-21 17.28.10.png" deleted file mode 100644 index c79e6b15327c6e0ce48ad4f44169af654b24263e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27570 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 A`Tzg` From fa577b19bee0c0aef3a4446d9e73e47320d7627e Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 23 Jul 2024 14:40:42 +0800 Subject: [PATCH 189/199] Remove conditional modifier --- .../Components/Tooltip/CharcoalTooltip.swift | 34 ++++++++++--------- .../Extensions/ConditionalViewModifier.swift | 16 --------- 2 files changed, 18 insertions(+), 32 deletions(-) delete mode 100644 Sources/CharcoalSwiftUI/Extensions/ConditionalViewModifier.swift diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 7bcc572fc..2a72ce8cc 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -85,22 +85,24 @@ struct CharcoalTooltip: CharcoalPopupProtocol { var body: some View { ZStack { - Color.clear - .if(dismissOnTouchOutside && isPresenting) { view in - view.contentShape(Rectangle()) - .simultaneousGesture( - TapGesture() - .onEnded { _ in - isPresenting = false - } - ) - .simultaneousGesture( - DragGesture() - .onChanged { _ in - isPresenting = false - } - ) - } + if dismissOnTouchOutside && isPresenting { + Color.clear + .contentShape(Rectangle()) + .simultaneousGesture( + TapGesture() + .onEnded { _ in + isPresenting = false + } + ) + .simultaneousGesture( + DragGesture() + .onChanged { _ in + isPresenting = false + } + ) + } else { + Color.clear + } if isPresenting { GeometryReader(content: { canvasGeometry in VStack { diff --git a/Sources/CharcoalSwiftUI/Extensions/ConditionalViewModifier.swift b/Sources/CharcoalSwiftUI/Extensions/ConditionalViewModifier.swift deleted file mode 100644 index f6b2eb5c4..000000000 --- a/Sources/CharcoalSwiftUI/Extensions/ConditionalViewModifier.swift +++ /dev/null @@ -1,16 +0,0 @@ -import SwiftUI - -extension View { - /// Applies the given transform if the given condition evaluates to `true`. - /// - Parameters: - /// - condition: The condition to evaluate. - /// - transform: The transform to apply to the source `View`. - /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. - @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { - if condition { - transform(self) - } else { - self - } - } -} From 84cdc67226937060accb2e1d237073de5a8b3632 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 23 Jul 2024 14:45:05 +0800 Subject: [PATCH 190/199] Add default dismiss time to toasts --- .../CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift | 4 ++-- Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift index 0de514240..2a1f73364 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalSnackBar.swift @@ -51,7 +51,7 @@ struct CharcoalSnackBar: CharcoalPopupProtocol, CharcoalToa thumbnailImage: Image?, @ViewBuilder action: () -> ActionContent?, isPresenting: Binding, - dismissAfter: TimeInterval? = nil, + dismissAfter: TimeInterval?, animationConfiguration: CharcoalToastAnimationConfiguration = .default ) { self.id = id @@ -186,7 +186,7 @@ public extension View { screenEdgeSpacing: CGFloat = 120, text: String, thumbnailImage: Image? = nil, - dismissAfter: TimeInterval? = nil, + dismissAfter: TimeInterval? = 2, @ViewBuilder action: @escaping () -> Content = { EmptyView() } ) -> some View where Content: View { return modifier( diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift index 8caad46b0..8c46d4352 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToast.swift @@ -45,7 +45,7 @@ struct CharcoalToast: CharcoalPopupProtocol, CharcoalToastB screenEdgeSpacing: CGFloat, @ViewBuilder action: () -> ActionContent?, isPresenting: Binding, - dismissAfter: TimeInterval? = nil, + dismissAfter: TimeInterval?, appearance: CharcoalToastAppearance = .success, animationConfiguration: CharcoalToastAnimationConfiguration ) { @@ -207,7 +207,7 @@ public extension View { screenEdge: CharcoalPopupViewEdge = .bottom, screenEdgeSpacing: CGFloat = 96, text: String, - dismissAfter: TimeInterval? = nil, + dismissAfter: TimeInterval? = 2, appearance: CharcoalToastAppearance = .success, animationConfiguration: CharcoalToastAnimationConfiguration = .default, @ViewBuilder action: @escaping () -> Content = { EmptyView() } From 716fa666b5d47edfe3d1fb55670b0b0fb1bba7de Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 23 Jul 2024 14:45:19 +0800 Subject: [PATCH 191/199] Reformat --- .../Components/Tooltip/CharcoalTooltip.swift | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 2a72ce8cc..36e05b0bd 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -87,19 +87,19 @@ struct CharcoalTooltip: CharcoalPopupProtocol { ZStack { if dismissOnTouchOutside && isPresenting { Color.clear - .contentShape(Rectangle()) - .simultaneousGesture( - TapGesture() - .onEnded { _ in - isPresenting = false - } - ) - .simultaneousGesture( - DragGesture() - .onChanged { _ in - isPresenting = false - } - ) + .contentShape(Rectangle()) + .simultaneousGesture( + TapGesture() + .onEnded { _ in + isPresenting = false + } + ) + .simultaneousGesture( + DragGesture() + .onChanged { _ in + isPresenting = false + } + ) } else { Color.clear } From 75c267771ebbbdcd9ef3a29d88f76fa8e7f11e9c Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 23 Jul 2024 15:53:46 +0800 Subject: [PATCH 192/199] Reformat --- .../CharcoalSwiftUISample/BalloonsView.swift | 23 ++--- .../Components/Balloon/CharcoalBalloon.swift | 83 +++++++++---------- .../Tooltip/TooltipBubbleShape.swift | 29 +++---- 3 files changed, 66 insertions(+), 69 deletions(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift index f63182659..d239962fa 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift @@ -20,8 +20,10 @@ public struct BalloonsView: View { } label: { Image(charocalIcon: .question24) } - .charcoalBalloon(isPresenting: $isPresented, - text: "作品中の特定単語について") + .charcoalBalloon( + isPresenting: $isPresented, + text: "作品中の特定単語について" + ) } HStack { @@ -32,9 +34,7 @@ public struct BalloonsView: View { Image(charocalIcon: .question24) } .charcoalBalloon(isPresenting: $isPresented2, text: "作品中の特定単語について、単語変換をして読めるようになりました") { - Button(action: { - - }, label: { + Button(action: {}, label: { Text("詳しく") }) } @@ -48,8 +48,10 @@ public struct BalloonsView: View { } label: { Image(charocalIcon: .question24) } - .charcoalBalloon(isPresenting: $isPresented3, - text: "作品中の特定単語について") + .charcoalBalloon( + isPresenting: $isPresented3, + text: "作品中の特定単語について" + ) } } Spacer() @@ -60,8 +62,10 @@ public struct BalloonsView: View { } label: { Image(charocalIcon: .question24) } - .charcoalBalloon(isPresenting: $isPresented4, - text: "作品中の特定単語について、単語変換をして読めるようになりました") + .charcoalBalloon( + isPresenting: $isPresented4, + text: "作品中の特定単語について、単語変換をして読めるようになりました" + ) } } .navigationBarTitle("Tooltips") @@ -71,4 +75,3 @@ public struct BalloonsView: View { #Preview { BalloonsView().charcoalOverlayContainer() } - diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index 8c181d99c..257330de8 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -10,7 +10,7 @@ enum CharcoalTooltipLayoutPriority: Codable { struct LayoutPriority { var priority: CharcoalTooltipLayoutPriority var spaceArea: CGSize - + var rect: CGRect { return CGRect(x: 0, y: 0, width: spaceArea.width, height: spaceArea.height) } @@ -22,8 +22,7 @@ extension CGSize { } } -struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToastActionable { - +struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToastActionable { typealias IDValue = UUID /// The unique ID of the overlay. @@ -59,9 +58,9 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? - + let action: ActionContent? - + @State var timer: Timer? init( @@ -83,36 +82,36 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast self.dismissAfter = dismissAfter self.action = action() } - + /// Calculate the position of the tooltip func positionOfOverlay(canvasGeometrySize: CGSize) -> CGSize { // Check avaliable area for each direction, compare area size with tooltip size. // The priorty is Bottom > Top > Right > Left var priorities: [LayoutPriority] = [] - + // Calculate layout it by sides let rightWidth = canvasGeometrySize.width - targetFrame.maxX - spacingToScreen - arrowHeight let rightHeight = canvasGeometrySize.height - targetFrame.height priorities.append(LayoutPriority(priority: .right, spaceArea: CGSize(width: rightWidth, height: rightHeight))) - + let leftWidth = targetFrame.minX - spacingToScreen - arrowHeight let leftHeight = canvasGeometrySize.height - targetFrame.height priorities.append(LayoutPriority(priority: .left, spaceArea: CGSize(width: leftWidth, height: leftHeight))) - + // Calculate layout it by top and bottom let bottomHeight = canvasGeometrySize.height - targetFrame.maxY - spacingToScreen - spacingToTarget - arrowHeight let buttonWidth = canvasGeometrySize.width - spacingToScreen * 2 priorities.append(LayoutPriority(priority: .bottom, spaceArea: CGSize(width: buttonWidth, height: bottomHeight))) - + let topHeight = targetFrame.minY - spacingToScreen - arrowHeight - spacingToTarget let topWidth = canvasGeometrySize.width - spacingToScreen * 2 priorities.append(LayoutPriority(priority: .top, spaceArea: CGSize(width: topWidth, height: topHeight))) - + let tooltipRect = CGRect(x: 0, y: 0, width: tooltipSize.width, height: tooltipSize.height) - + // Get the ideal layout plan - let layoutPlan = priorities.first(where: { $0.spaceArea.width >= tooltipSize.width && $0.spaceArea.height >= tooltipSize.height }) ?? priorities.sorted(by: { $0.rect.intersectionArea(tooltipRect) > $1.rect.intersectionArea(tooltipRect)}).first! - + let layoutPlan = priorities.first(where: { $0.spaceArea.width >= tooltipSize.width && $0.spaceArea.height >= tooltipSize.height }) ?? priorities.sorted(by: { $0.rect.intersectionArea(tooltipRect) > $1.rect.intersectionArea(tooltipRect) }).first! + switch layoutPlan.priority { case .bottom: return CGSize(width: tooltipX(canvasGeometrySize: canvasGeometrySize), height: targetFrame.maxY + spacingToTarget + arrowHeight) @@ -141,22 +140,23 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast var body: some View { ZStack { - Color.clear - .if(dismissOnTouchOutside && isPresenting) { view in - view.contentShape(Rectangle()) - .simultaneousGesture( - TapGesture() - .onEnded { _ in - isPresenting = false - } - ) - .simultaneousGesture( - DragGesture() - .onChanged { _ in - isPresenting = false - } - ) - } + if dismissOnTouchOutside && isPresenting { + Color.clear.contentShape(Rectangle()) + .simultaneousGesture( + TapGesture() + .onEnded { _ in + isPresenting = false + } + ) + .simultaneousGesture( + DragGesture() + .onChanged { _ in + isPresenting = false + } + ) + } else { + Color.clear + } if isPresenting { GeometryReader(content: { canvasGeometry in ZStack { @@ -167,21 +167,21 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) .foregroundColor(Color(CharcoalAsset.ColorPaletteGenerated.text5.color)) - + Image(charocalIcon: .remove16) .renderingMode(.template) .foregroundColor(Color.white) - .frame(width: 16+6) + .frame(width: 16 + 6) .background(Circle() .fill(Color.black.opacity(0.35)) - .frame(width: 16+6, height: 16+6)) + .frame(width: 16 + 6, height: 16 + 6)) .overlay( EmptyView().frame(width: 45, height: 45) .contentShape(Rectangle()).onTapGesture { isPresenting = false }) } - + if let action = action { action .charcoalTypography14Bold() @@ -239,7 +239,6 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToast } } - struct CharcoalBalloonModifier: ViewModifier { /// Presentation `Binding` @Binding var isPresenting: Bool @@ -252,7 +251,7 @@ struct CharcoalBalloonModifier: ViewModifier { /// The overlay will be dismissed after a certain time interval. let dismissAfter: TimeInterval? - + @ViewBuilder let action: () -> ActionContent? func body(content: Content) -> some View { @@ -329,8 +328,10 @@ private struct BalloonsPreviewView: View { } label: { Image(charocalIcon: .question24) } - .charcoalBalloon(isPresenting: $isPresenting, - text: "作品中の特定単語について") + .charcoalBalloon( + isPresenting: $isPresenting, + text: "作品中の特定単語について" + ) .offset(CGSize(width: 20.0, height: 80.0)) Button { @@ -340,13 +341,11 @@ private struct BalloonsPreviewView: View { } .charcoalDefaultButton() .charcoalBalloon(isPresenting: $isPresenting2, text: "作品中の特定単語について、単語変換をして読めるようになりました") { - Button(action: { - - }, label: { + Button(action: {}, label: { Text("詳しく") }) } - + .offset(CGSize(width: 100.0, height: 150.0)) Button { diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift index dad4bbece..89eb8cb36 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/TooltipBubbleShape.swift @@ -11,9 +11,8 @@ struct TooltipBubbleShape: Shape { let arrowWidth: CGFloat func path(in rect: CGRect) -> Path { - var pointPosition: CharcoalTooltipLayoutPriority - + if targetPoint.x < rect.minX && targetPoint.y > rect.minY { pointPosition = .left } else if targetPoint.x > rect.maxX && targetPoint.y > rect.minY { @@ -23,13 +22,13 @@ struct TooltipBubbleShape: Shape { } else { pointPosition = .bottom } - + let path = Path { p in p.move(to: .init(x: rect.minX + cornerRadius, y: rect.minY)) if pointPosition == .top { let arrowY = rect.minY - arrowHeight let arrowBaseY = rect.minY - + // The minimum and maximum x position of the arrow let minX = rect.minX + cornerRadius + arrowWidth let maxX = rect.maxX - cornerRadius - arrowWidth @@ -54,7 +53,7 @@ struct TooltipBubbleShape: Shape { if pointPosition == .right { let arrowX = rect.maxX + arrowHeight let arrowBaseX = rect.maxX - + // The minimum and maximum x position of the arrow let minY = rect.minY + cornerRadius + arrowWidth let maxY = rect.maxY - cornerRadius - arrowWidth @@ -79,15 +78,14 @@ struct TooltipBubbleShape: Shape { if pointPosition == .bottom { let arrowY = rect.maxY + arrowHeight let arrowBaseY = rect.maxY - + // The minimum and maximum x position of the arrow let minX = rect.minX + cornerRadius + arrowWidth let maxX = rect.maxX - cornerRadius - arrowWidth // The x position of the arrow let arrowMidX = min(max(minX, targetPoint.x), maxX) - - + let arrowMaxX = arrowMidX - arrowWidth let arrowMinX = arrowMidX + arrowWidth p.addLine(to: CGPoint(x: arrowMinX, y: arrowBaseY)) @@ -103,7 +101,7 @@ struct TooltipBubbleShape: Shape { if pointPosition == .left { let arrowX = rect.minX - arrowHeight let arrowBaseX = rect.minX - + // The minimum and maximum x position of the arrow let minY = rect.minY + cornerRadius + arrowWidth let maxY = rect.maxY - cornerRadius - arrowWidth @@ -132,9 +130,9 @@ struct TooltipBubbleShape: Shape { } } -extension Shape { +public extension Shape { /// fills and strokes a shape - public func fill( + func fill( _ fillContent: S, strokeColor: Color, lineWidth: CGFloat @@ -163,7 +161,7 @@ private struct BubbleShapePreview: View { ) .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) .frame(width: 240, height: 100) - + TooltipBubbleShape( targetPoint: CGPoint( @@ -176,8 +174,7 @@ private struct BubbleShapePreview: View { ) .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) .frame(width: 240, height: 100) - - + TooltipBubbleShape( targetPoint: CGPoint( @@ -190,8 +187,7 @@ private struct BubbleShapePreview: View { ) .fill(Color(CharcoalAsset.ColorPaletteGenerated.brand.color), strokeColor: Color.white, lineWidth: 2) .frame(width: 240, height: 100) - - + TooltipBubbleShape( targetPoint: CGPoint( @@ -209,7 +205,6 @@ private struct BubbleShapePreview: View { } } - #Preview { BubbleShapePreview() } From 588a957483e128ebdb5fc97448dd1a432628f49d Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 24 Jul 2024 14:17:45 +0800 Subject: [PATCH 193/199] Fix logo image --- .../SnackbarDemo.imageset/Contents.json | 2 +- .../SnackbarDemo.imageset/charcoal-logo.png | Bin 0 -> 2144 bytes ...203\203\343\203\210 2024-02-21 17.28.10.png" | Bin 27570 -> 0 bytes 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/charcoal-logo.png delete mode 100644 "CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210 2024-02-21 17.28.10.png" diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/Contents.json b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/Contents.json index 46d35b5c8..f10cd6bfb 100644 --- a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/Contents.json +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "スクリーンショット 2024-02-21 17.28.10.png", + "filename" : "charcoal-logo.png", "idiom" : "universal" } ], diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/charcoal-logo.png b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/charcoal-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..85a24cbc9a88afcb1621cdd66b36e3614af4e617 GIT binary patch literal 2144 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 literal 0 HcmV?d00001 diff --git "a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210 2024-02-21 17.28.10.png" "b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Media.xcassets/SnackbarDemo.imageset/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210 2024-02-21 17.28.10.png" deleted file mode 100644 index c79e6b15327c6e0ce48ad4f44169af654b24263e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27570 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 A`Tzg` From 6a3378857e6be202f7c9fb90d5c4240f665dd5bd Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 24 Jul 2024 14:58:21 +0800 Subject: [PATCH 194/199] Fix balloon name --- .../Sources/CharcoalSwiftUISample/BalloonsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift index d239962fa..8a67350c6 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/BalloonsView.swift @@ -68,7 +68,7 @@ public struct BalloonsView: View { ) } } - .navigationBarTitle("Tooltips") + .navigationBarTitle("Balloons") } } From cb76630964d86b3287afbb928c264b98b84eea28 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 7 Aug 2024 15:33:16 +0800 Subject: [PATCH 195/199] Remove CharcoalRubberGesture --- .../Toast/CharcoalRubberGesture.swift | 78 ------------------- 1 file changed, 78 deletions(-) delete mode 100644 Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift b/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift deleted file mode 100644 index 4891e4a98..000000000 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalRubberGesture.swift +++ /dev/null @@ -1,78 +0,0 @@ -import UIKit - -protocol CharcoalGesture { - var gesture: UIGestureRecognizer { get } -} - -class CharcoalRubberGesture: NSObject, CharcoalGesture { - let screenEdge: CharcoalPopupViewEdge - - var gesture: UIGestureRecognizer - - var dragVelocity: CGPoint = .zero - - var isDragging: Bool = false - - var offset: CGSize = .zero - - var dismiss: (() -> Void)? - - init(screenEdge: CharcoalPopupViewEdge) { - self.screenEdge = screenEdge - gesture = UIPanGestureRecognizer() - super.init() - gesture.addTarget(self, action: #selector(handlePan(_:))) - } - - @objc func handlePan(_ gesture: UIPanGestureRecognizer) { - guard let view = gesture.view else { return } - - let translation = gesture.translation(in: gesture.view) - let velocity = gesture.velocity(in: gesture.view) - let translationInDirection = translation.y * screenEdge.direction - let movingVelocityInDirection = velocity.y * screenEdge.direction - let offsetInDirection = offset.height * screenEdge.direction - - // Rubber band effect - let damping: CGFloat = 0.75 - let initialSpringVelocity: CGFloat = 0.0 - let duration: TimeInterval = 0.65 - - switch gesture.state { - case .began: - isDragging = true - case .changed: - dragVelocity = velocity - if translationInDirection < 0 { - offset = CGSize(width: 0, height: translation.y) - view.transform = CGAffineTransform(translationX: 0, y: translation.y) - } else { - let limit: CGFloat = 60 - let dist = sqrt(translation.y * translation.y) - let factor = 1 / (dist / limit + 1) - offset = CGSize(width: 0, height: translation.y * factor) - view.transform = CGAffineTransform(translationX: 0, y: translation.y * factor) - } - case .ended, .cancelled: - isDragging = false - if offsetInDirection < -50 || movingVelocityInDirection < -100 { - // Dismiss - dismiss?() - } else { - UIView.animate( - withDuration: duration, - delay: 0, - usingSpringWithDamping: damping, - initialSpringVelocity: initialSpringVelocity, - options: [], - animations: { - view.transform = .identity - }, - completion: nil - ) - } - default: - break - } - } -} From b3ff4b4c86d0ee8e1494856f2b3da4872a2c839a Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 4 Sep 2024 15:13:23 +0900 Subject: [PATCH 196/199] Remove unused UIColor Extension --- .../CharcoalShared/Extensions/UIColor+Extension.swift | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 Sources/CharcoalShared/Extensions/UIColor+Extension.swift diff --git a/Sources/CharcoalShared/Extensions/UIColor+Extension.swift b/Sources/CharcoalShared/Extensions/UIColor+Extension.swift deleted file mode 100644 index 45f8f9bc9..000000000 --- a/Sources/CharcoalShared/Extensions/UIColor+Extension.swift +++ /dev/null @@ -1,11 +0,0 @@ -import UIKit - -public extension UIColor { - func imageWithColor(width: Int, height: Int) -> UIImage { - let size = CGSize(width: width, height: height) - return UIGraphicsImageRenderer(size: size).image { rendererContext in - self.setFill() - rendererContext.fill(CGRect(origin: .zero, size: size)) - } - } -} From 1ff35d10e12ee2c1cdb243f4b771c9ed28b01efd Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 4 Sep 2024 15:14:03 +0900 Subject: [PATCH 197/199] Clean Code --- .../Components/Balloon/CharcoalBalloon.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index 257330de8..a7edc292a 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -313,16 +313,6 @@ private struct BalloonsPreviewView: View { ScrollView { ZStack(alignment: .topLeading) { Color.clear -// VStack { -// Text(textOfLabel) -// -// Button { -// textOfLabel = ["Changed", "Hello"].randomElement()! -// } label: { -// Text("Change Label") -// } -// } - Button { isPresenting.toggle() } label: { From 1f338f33c2e0c97daeaba1610db494461d6c7388 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 4 Sep 2024 15:17:19 +0900 Subject: [PATCH 198/199] Clean proxy usage --- .../Components/Balloon/CharcoalBalloon.swift | 10 +++++----- .../Toast/CharcoalToastAnimatableModifier.swift | 4 ++-- .../Components/Tooltip/CharcoalTooltip.swift | 16 ++++++++-------- .../Modal/CharcoalModalView.swift | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift index a7edc292a..4c57481b9 100644 --- a/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalSwiftUI/Components/Balloon/CharcoalBalloon.swift @@ -158,7 +158,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToas Color.clear } if isPresenting { - GeometryReader(content: { canvasGeometry in + GeometryReader { proxy in ZStack { VStack { HStack(alignment: .firstTextBaseline, spacing: 5) { @@ -210,7 +210,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToas // GeometryReader size is zero in background, so we use overlay instead Color.clear.preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) })) - .offset(positionOfOverlay(canvasGeometrySize: canvasGeometry.size)) + .offset(positionOfOverlay(canvasGeometrySize: proxy.size)) .onPreferenceChange(TooltipSizeKey.self, perform: { value in tooltipSize = value }) @@ -218,7 +218,7 @@ struct CharcoalBalloon: CharcoalPopupProtocol, CharcoalToas .animation(.none, value: targetFrame) } .frame(minWidth: 0, maxWidth: maxWidth, alignment: .leading) - }) + } .onAppear { if let dismissAfter = dismissAfter { timer = Timer.scheduledTimer(withTimeInterval: dismissAfter, repeats: false, block: { _ in @@ -256,7 +256,7 @@ struct CharcoalBalloonModifier: ViewModifier { func body(content: Content) -> some View { content - .overlay(GeometryReader(content: { proxy in + .overlay(GeometryReader { proxy in Color.clear .modifier(CharcoalOverlayUpdaterContainer( isPresenting: $isPresenting, @@ -270,7 +270,7 @@ struct CharcoalBalloonModifier: ViewModifier { ), viewID: viewID )) - })) + }) } } diff --git a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift index 0ea4d76bd..b5837555f 100644 --- a/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift +++ b/Sources/CharcoalSwiftUI/Components/Toast/CharcoalToastAnimatableModifier.swift @@ -35,9 +35,9 @@ struct CharcoalToastAnimatableModifier: ViewModifier, CharcoalToastBase { .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) .overlay(RoundedRectangle(cornerRadius: cornerRadius).stroke(borderColor, lineWidth: borderLineWidth)) .overlay( - GeometryReader(content: { proxy in + GeometryReader { proxy in Color.clear.preference(key: PopupViewSizeKey.self, value: proxy.size) - }) + } ) .offset(CGSize(width: 0, height: animationConfiguration.enablePositionAnimation ? (isActuallyPresenting ? screenEdge.direction * screenEdgeSpacing : -screenEdge.direction * (tooltipSize.height)) : screenEdge.direction * screenEdgeSpacing)) .opacity(isActuallyPresenting ? 1 : 0) diff --git a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift index 69fe94694..2c3a3be0e 100644 --- a/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift +++ b/Sources/CharcoalSwiftUI/Components/Tooltip/CharcoalTooltip.swift @@ -100,7 +100,7 @@ struct CharcoalTooltip: CharcoalPopupProtocol { Color.clear } if isPresenting { - GeometryReader(content: { canvasGeometry in + GeometryReader { proxy in VStack { Text(text) .charcoalTypography12Regular() @@ -124,8 +124,8 @@ struct CharcoalTooltip: CharcoalPopupProtocol { .preference(key: TooltipSizeKey.self, value: tooltipGeometry.size) })) .offset(CGSize( - width: tooltipX(canvasGeometrySize: canvasGeometry.size), - height: tooltipY(canvasGeometrySize: canvasGeometry.size) + width: tooltipX(canvasGeometrySize: proxy.size), + height: tooltipY(canvasGeometrySize: proxy.size) )) .onPreferenceChange(TooltipSizeKey.self, perform: { value in tooltipSize = value @@ -134,7 +134,7 @@ struct CharcoalTooltip: CharcoalPopupProtocol { .animation(.none, value: targetFrame) } .frame(minWidth: 0, maxWidth: maxWidth, alignment: .leading) - }) + } .onAppear { if let dismissAfter = dismissAfter { DispatchQueue.main.asyncAfter(deadline: .now() + dismissAfter) { @@ -175,7 +175,7 @@ struct CharcoalTooltipModifier: ViewModifier { func body(content: Content) -> some View { content - .overlay(GeometryReader(content: { proxy in + .overlay(GeometryReader { proxy in Color.clear .modifier(CharcoalOverlayUpdaterContainer( isPresenting: $isPresenting, @@ -188,7 +188,7 @@ struct CharcoalTooltipModifier: ViewModifier { ), viewID: viewID )) - })) + }) } } @@ -225,7 +225,7 @@ private struct TooltipsPreviewView: View { @State var textOfLabel = "Hello" var body: some View { - GeometryReader(content: { proxy in + GeometryReader { proxy in ScrollView { ZStack(alignment: .topLeading) { Color.clear @@ -296,7 +296,7 @@ private struct TooltipsPreviewView: View { .offset(CGSize(width: proxy.size.width - 380, height: proxy.size.height - 240)) } } - }) + } .charcoalOverlayContainer() } } diff --git a/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift b/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift index 491455d7a..766d05cd8 100644 --- a/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift +++ b/Sources/CharcoalSwiftUI/Modal/CharcoalModalView.swift @@ -73,7 +73,7 @@ struct CharcoalModalView: View { } var body: some View { - return GeometryReader(content: { proxy in + return GeometryReader { proxy in ZStack(alignment: style.alignment, content: { Rectangle() .foregroundColor(Color.black.opacity(0.6)) @@ -133,7 +133,7 @@ struct CharcoalModalView: View { } }) .ignoresSafeArea(.container, edges: .bottom) - }) + } .onChange(of: isPresented, perform: { newValue in if !newValue { prepareAnimation() From 89074cc0b2b0adae02580d064a46f84290897189 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 4 Sep 2024 15:17:29 +0900 Subject: [PATCH 199/199] Update Package.resolved --- BuildTools/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BuildTools/Package.resolved b/BuildTools/Package.resolved index 773336e74..f1cd6af21 100644 --- a/BuildTools/Package.resolved +++ b/BuildTools/Package.resolved @@ -114,8 +114,8 @@ "repositoryURL": "https://github.com/apple/swift-syntax.git", "state": { "branch": null, - "revision": "303e5c5c36d6a558407d364878df131c3546fad8", - "version": "510.0.2" + "revision": "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", + "version": "510.0.3" } }, {