From 53e42d7fa0fb4dd9697c031337b3e053f88b3414 Mon Sep 17 00:00:00 2001 From: Kevin <141606011+kevinneko@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:32:15 +0900 Subject: [PATCH] Feat SwiftUI Hint (#269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Tooltip view * Refactor Tooltip style * Can adjust tooltip position * Add tooltip playground for testing * Refine layout tips logic * Refine layout logic * Update CharcoalTooltip.swift * Refine tooltip spacing * Add CharcoalIdentifiableOverlayView * Use Actor to prevent Data Race * Remove CharcoalIdentifiableOverlayView out * Clean code * Only update view when it is isPresenting * Clean access control * Add TooltipsView * Fix tooltipY layout logic * Use main actor and remove CharcoalContainerManagerKey * Fix access control on CharcoalContainerManager * Make viewID as @State * Use EnviromentObject to create CharcoalContainerManager for each container * Use ObservedObject on CharcoalContainerManager * Add use charcoal button as demo trigger * Add arrow logic on tooltip * Refine arrow logic * Refine arrow layout logic * Use StateObject to prevent unexpected reinit * Refactor TooltipBubbleShape * Fix edge layout logic * Add comment * Format code * Use new approach to remove adaptiveMaxWidth * Fix the tip bubble's position latency * Add dismiss when interaction * Reformat * Add initial Snackbar * Add thumbnail image * Add support for thumbnailImage and action * Clean code * Reformat code * Rename ActionContent * Replace thumbnailImage type * Add dismissOnTouchOutside control * Add comment on CharcoalIdentifiableOverlayView * Update CharcoalTooltip.swift * Add SnackBar demo * Replace thumbnail with charcoal logo * Use @ViewBuilder * Clean Code * Made code more readable * Update ToastsView.swift * Add auto dismiss logic * Fix dismiss comment * Add Identifiable to CharcoalIdentifiableOverlayView * Make all CharcoalPopupView identifiable * Move all control logic into CharcoalPopupView * Reformat * Refine CharcoalOverlayContainerChild logic of updating view * Rename to CharcoalOverlayUpdaterContainer * Add CharcoalToast * Refine toast control * Refine screen edge of toast * Refine comments * Rename CharcoalPopupProtocol * Refine isActuallyPresenting logic * Clean animation * Add animation configuration * Add custom animation * Add CharcoalToastProtocol * Makes CharcoalSnackBar adapt CharcoalToastProtocol * Remove time delay * Refine SnackBar Animation logic * Add CharcoalToastAnimationModifier * Reformat code * Update CharcoalPopupViewEdge of direction * Refine demo * Fix missing animation * Rename charcoalAnimatedToast to charcoalAnimatableToast * Rename CharcoalAnimatableToastProtocol * Rename for clean * Simplify protocols * Add drag control * Add Dismiss timer control logic * Refine drag damping logic * Add CharcoalToastDraggable * Use CharcoalToastDraggableModifier on CharcoalSnackBar * Format code * Add init structure * Add action button * Replace placeholder with ja text * Use CGPath union for iOS 16 * Update TooltipBubbleShape.swift * Update CharcoalTooltip.swift * Use GeometryReader on overlay * Use new path draw logic * Refine arrow width * Refine arrow width * Clean Code * Refine preview * Use timer instead of DispatchQueue * Refine layout logic * Refine arrow logic * Refine layout logic * Add missing charcoalOverlayContainer * Add Balloon to examples * Add default tutorials * Remove overlay when disappear * Refine charcoalOverlayContainer place * Refine * Update CharcoalSnackBar.swift * Add Charcoal Hint * Use surface 3 * Add isPresenting * Add hints to sample * Format code * Init CharcoalTooltipView * Init Bubble shape * Refine tooltip preview * Rename as Charcoal Bubble Shape * Add Label to tooltip * Update text frame when traitCollection did change * Update CharcoalTooltipView.swift * Add CharcoalTooltip * Can debug show on method * Can layout point * Can redraw target point * Update CharcoalTooltip.swift * Refine tooltip display * Share the logic * Use interaction mode * Use CharcoalOverlayContainerView * Update CharcoalOverlay.swift * Refactor to ChacoalOverlayManager * Makes CharcoalIdentifiableOverlayView Identifiable * Refine layout logic * Add display(view: CharcoalIdentifiableOverlayView) * Add tooltip to uikit example * Update Tooltips.swift * Add CharcoalIdentifiableOverlayDelegate * Reformat * Update StringExtension.swift * Add to UIKitSample * Fix public requirements * Reformat * Use touch began to handle dismiss on touch * Update CharcoalIdentifiableOverlayView.swift * Add CharcoalToastView * change cornerRadius * Add CharcoalToast * Move show logic out * Reformat * Refine dismiss method * Update CharcoalToast.swift * Add ActionContent and ActionComplete callback * Update CharcoalToast.swift * Update CharcoalBubbleShape_UIKit.swift * Refine layout animation logic * Refine animation * Add dismiss * Add example * Reformat * Add toasts example * Add CharcoalSnackBarView * Update project.pbxproj * Clean code * Refine layout logic * Add CharcoalSnackBar * Update CharcoalSnackBar.swift * Update CharcoalSnackBar.swift * Refine layout logic * Refine toasts text * Update CharcoalToastDraggableModifier.swift * Add CharcoalRubberGesture * use id to notify did dismiss * Add rubber gesture * Refactor * Refactor * Fix memory leak * Format * Fix memory leak * Add example * Refine snackbar * Refine swift lint * Refactor CharcoalToastView * Refactor CharcoalSnackBarView * Reformat * Refine self logic * Delete スクリーンショット 2024-02-21 17.28.10.png * Remove duplicated balloons * Delete スクリーンショット 2024-02-21 17.28.10.png * Clean extensions * Clean CharcoalRubberGesture * Delete ConditionalViewModifier.swift * Refactor Preview * Fix h spacing * Fix info16 icon * Use CharcoalAction * Reformat * Omit trailing closure --- .../CharcoalSwiftUISample/ContentView.swift | 3 + .../CharcoalSwiftUISample/HintsView.swift | 31 +++++++ .../Views/Balloons/Balloons.swift | 6 +- .../CharcoalAction.swift | 4 +- .../Extensions/CGSize+Extension.swift | 7 -- .../Components/Hint/CharcoalHint.swift | 86 +++++++++++++++++++ .../Extensions/Rect+Extension.swift | 5 +- .../Balloon/CharcoalAnchorTrackingView.swift | 2 +- .../Components/Balloon/CharcoalBalloon.swift | 34 ++++---- 9 files changed, 145 insertions(+), 33 deletions(-) create mode 100644 CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/HintsView.swift rename Sources/{CharcoalUIKit/Components/Toast => CharcoalShared}/CharcoalAction.swift (75%) delete mode 100644 Sources/CharcoalShared/Extensions/CGSize+Extension.swift create mode 100644 Sources/CharcoalSwiftUI/Components/Hint/CharcoalHint.swift diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift index 6b656fe99..2c7c45bf4 100644 --- a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/ContentView.swift @@ -49,6 +49,9 @@ public struct ContentView: View { NavigationLink(destination: ToastsView().charcoalOverlayContainer()) { Text("Toasts") } + NavigationLink(destination: HintsView()) { + Text("Hints") + } NavigationLink(destination: SpinnersView()) { Text("Spinners") } diff --git a/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/HintsView.swift b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/HintsView.swift new file mode 100644 index 000000000..fcbd4545a --- /dev/null +++ b/CharcoalSwiftUISample/Sources/CharcoalSwiftUISample/HintsView.swift @@ -0,0 +1,31 @@ +import Charcoal +import SwiftUI + +public struct HintsView: View { + @State var isPresented = true + + @State var isPresented2 = true + + @State var isPresented3 = true + + public var body: some View { + ScrollView { + VStack { + CharcoalHint(text: "ヒントテキストヒントテキスト", isPresenting: $isPresented, action: CharcoalAction(title: "Button") { + isPresented = false + }) + + CharcoalHint(text: "ヒントテキストヒントテキスト", isPresenting: $isPresented2) + + CharcoalHint(text: "ヒントテキストヒントテキスト", maxWidth: .infinity, isPresenting: $isPresented3) + } + .padding() + } + .navigationBarTitle("Hints") + } +} + +@available(iOS 17, *) +#Preview { + HintsView() +} diff --git a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Balloons/Balloons.swift b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Balloons/Balloons.swift index 66a1ee16c..2d4a190b6 100644 --- a/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Balloons/Balloons.swift +++ b/CharcoalUIKitSample/Sources/CharcoalUIKitSample/Views/Balloons/Balloons.swift @@ -125,11 +125,11 @@ extension BalloonsViewController: UITableViewDelegate, UITableViewDataSource { case .leading: // use on: self.view to stick the balloon to the view // so balloons will dismiss with the view - CharcoalBalloon.show(text: titleCase.text, anchorView: cell.leadingImageView, on: self.view) + CharcoalBalloon.show(text: titleCase.text, anchorView: cell.leadingImageView, on: view) case .trailing: - CharcoalBalloon.show(text: titleCase.text, anchorView: cell.accessoryImageView, on: self.view) + CharcoalBalloon.show(text: titleCase.text, anchorView: cell.accessoryImageView, on: view) case .bottom: - CharcoalBalloon.show(text: titleCase.text, anchorView: bottomInfoImage, on: self.view) + CharcoalBalloon.show(text: titleCase.text, anchorView: bottomInfoImage, on: view) } } diff --git a/Sources/CharcoalUIKit/Components/Toast/CharcoalAction.swift b/Sources/CharcoalShared/CharcoalAction.swift similarity index 75% rename from Sources/CharcoalUIKit/Components/Toast/CharcoalAction.swift rename to Sources/CharcoalShared/CharcoalAction.swift index 89383f5bc..4e8e31b93 100644 --- a/Sources/CharcoalUIKit/Components/Toast/CharcoalAction.swift +++ b/Sources/CharcoalShared/CharcoalAction.swift @@ -1,8 +1,8 @@ public typealias ActionCallback = () -> Void public struct CharcoalAction { - let title: String - let actionCallback: ActionCallback + public let title: String + public let actionCallback: ActionCallback public init(title: String, actionCallback: @escaping ActionCallback) { self.title = title diff --git a/Sources/CharcoalShared/Extensions/CGSize+Extension.swift b/Sources/CharcoalShared/Extensions/CGSize+Extension.swift deleted file mode 100644 index 911fe3390..000000000 --- a/Sources/CharcoalShared/Extensions/CGSize+Extension.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -public extension CGSize { - var area: CGFloat { - return width * height - } -} diff --git a/Sources/CharcoalSwiftUI/Components/Hint/CharcoalHint.swift b/Sources/CharcoalSwiftUI/Components/Hint/CharcoalHint.swift new file mode 100644 index 000000000..bb376c9e5 --- /dev/null +++ b/Sources/CharcoalSwiftUI/Components/Hint/CharcoalHint.swift @@ -0,0 +1,86 @@ +import SwiftUI + +public struct CharcoalHint: View { + /// The text of the tooltip + let text: String + + /// The text of the tooltip + let subtitle: String? + + let icon: CharcoalAsset.Images = .info16 + + /// The corner radius of the tooltip + let cornerRadius: CGFloat = 8 + + let maxWidth: CGFloat? + + /// A binding to whether the overlay is presented. + @Binding var isPresenting: Bool + + let action: CharcoalAction? + + @State var timer: Timer? + + public init( + text: String, + subtitle: String? = nil, + maxWidth: CGFloat? = nil, + isPresenting: Binding, + action: CharcoalAction? = nil + ) { + self.text = text + self.subtitle = subtitle + self.maxWidth = maxWidth + _isPresenting = isPresenting + self.action = action + } + + public var body: some View { + if isPresenting { + HStack(spacing: 4) { + Image(charocalIcon: icon) + + VStack { + Text(text).charcoalTypography14Regular() + if let subtitle = subtitle { + Text(subtitle).charcoalTypography14Regular() + } + } + + if let action = action { + Spacer() + Button(action: { + action.actionCallback() + }) { + Text(action.title) + } + .charcoalPrimaryButton(size: .small) + } + } + .frame(maxWidth: maxWidth) + .padding(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)) + .background(charcoalColor: .surface3) + .cornerRadius(cornerRadius, corners: .allCorners) + } + } +} + +@available(iOS 17, *) +#Preview { + @Previewable @State var isPresenting = true + @Previewable @State var isPresenting2 = true + @Previewable @State var isPresenting3 = true + + @Previewable @State var textOfLabel = "Hello" + + VStack { + CharcoalHint(text: "ヒントテキストヒントテキスト", isPresenting: $isPresenting, action: CharcoalAction(title: "Button", actionCallback: { + isPresenting = false + })) + + CharcoalHint(text: "ヒントテキストヒントテキスト", isPresenting: $isPresenting2) + + CharcoalHint(text: "ヒントテキストヒントテキスト", maxWidth: .infinity, isPresenting: $isPresenting3) + + }.padding() +} diff --git a/Sources/CharcoalSwiftUI/Extensions/Rect+Extension.swift b/Sources/CharcoalSwiftUI/Extensions/Rect+Extension.swift index 58e414d40..c1314f362 100644 --- a/Sources/CharcoalSwiftUI/Extensions/Rect+Extension.swift +++ b/Sources/CharcoalSwiftUI/Extensions/Rect+Extension.swift @@ -1,11 +1,10 @@ import Foundation - extension CGRect { // Calculate the intersection area of two rectangles func intersectionArea(_ rect: CGRect) -> CGFloat { - let rect = self.intersection(rect) - + let rect = intersection(rect) + return rect.width * rect.height } } diff --git a/Sources/CharcoalUIKit/Components/Balloon/CharcoalAnchorTrackingView.swift b/Sources/CharcoalUIKit/Components/Balloon/CharcoalAnchorTrackingView.swift index 6646e8578..bc49cafe4 100644 --- a/Sources/CharcoalUIKit/Components/Balloon/CharcoalAnchorTrackingView.swift +++ b/Sources/CharcoalUIKit/Components/Balloon/CharcoalAnchorTrackingView.swift @@ -28,7 +28,7 @@ class CharcoalAnchorTrackingView: UIView { displayLink?.invalidate() displayLink = nil } - + func tearDown() { invalidate() removeFromSuperview() diff --git a/Sources/CharcoalUIKit/Components/Balloon/CharcoalBalloon.swift b/Sources/CharcoalUIKit/Components/Balloon/CharcoalBalloon.swift index 39fdd80a2..69b51d2ca 100644 --- a/Sources/CharcoalUIKit/Components/Balloon/CharcoalBalloon.swift +++ b/Sources/CharcoalUIKit/Components/Balloon/CharcoalBalloon.swift @@ -4,23 +4,23 @@ public class CharcoalBalloon {} public extension CharcoalBalloon { /** - Show a balloon anchored to a view. - - - Parameters: - - text: The text to be displayed in the tooltip. - - anchorView: The view to which the tooltip will be anchored. - - interactionMode: The interaction mode of the tooltip. The default value is `.passThrough`. - - spacingToScreen: The spacing between the tooltip and the screen. The default value is `16`. - - gap: The spacing between the tooltip and the anchor view. The default value is `4`. - - on: The view on which the tooltip will be displayed. If not provided, the tooltip will be displayed on the window. - - - Returns: The identifier of the tooltip. - - # Example - ```swift - CharcoalTooltip.show(text: "This is a tooltip", anchorView: someView) - ``` - */ + Show a balloon anchored to a view. + + - Parameters: + - text: The text to be displayed in the tooltip. + - anchorView: The view to which the tooltip will be anchored. + - interactionMode: The interaction mode of the tooltip. The default value is `.passThrough`. + - spacingToScreen: The spacing between the tooltip and the screen. The default value is `16`. + - gap: The spacing between the tooltip and the anchor view. The default value is `4`. + - on: The view on which the tooltip will be displayed. If not provided, the tooltip will be displayed on the window. + + - Returns: The identifier of the tooltip. + + # Example + ```swift + CharcoalTooltip.show(text: "This is a tooltip", anchorView: someView) + ``` + */ @discardableResult static func show( text: String,