diff --git a/Sources/HandySwiftUI/HandySwiftUI.docc/Essentials/View Modifiers.md b/Sources/HandySwiftUI/HandySwiftUI.docc/Essentials/View Modifiers.md index ec57d95..3242d85 100644 --- a/Sources/HandySwiftUI/HandySwiftUI.docc/Essentials/View Modifiers.md +++ b/Sources/HandySwiftUI/HandySwiftUI.docc/Essentials/View Modifiers.md @@ -262,6 +262,7 @@ The example shows how `.confirmDeleteDialog` handles the entire deletion flow - ``SwiftUICore/View/macOSOnlyPadding(_:_:)`` - ``SwiftUICore/View/macOSOnlyPadding(insets:)`` - ``SwiftUICore/View/macOSOnlyFrame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)`` +- ``SwiftUICore/View/onFirstAppear(perform:)`` - ``SwiftUICore/View/progressOverlay(type:)`` - ``SwiftUICore/View/roundedRectangleBorder(_:cornerRadius:lineWidth:)`` - ``SwiftUICore/View/throwingTask(asyncAction:catchError:)`` diff --git a/Sources/HandySwiftUI/Modifiers/FirstAppearModifier.swift b/Sources/HandySwiftUI/Modifiers/FirstAppearModifier.swift new file mode 100644 index 0000000..10bbc36 --- /dev/null +++ b/Sources/HandySwiftUI/Modifiers/FirstAppearModifier.swift @@ -0,0 +1,56 @@ +import SwiftUI + +struct FirstAppearModifier: ViewModifier { + @State private var hasAppeared = false + let perform: () -> Void + + func body(content: Content) -> some View { + content.onAppear { + guard !hasAppeared else { return } + self.hasAppeared = true + self.perform() + } + } +} + +extension View { + /// Adds a modifier that runs a closure only on the first appearance of a view, + /// even across navigation stack transitions or view reconstructions. + /// + /// This is particularly useful in SwiftUI navigation scenarios where you want to perform + /// an action only once when a view is initially displayed, but not when navigating back + /// to it in a NavigationStack. + /// + /// Example usage: + /// ```swift + /// struct ProductListView: View { + /// @State private var selectedProduct: Product? + /// let products: [Product] + /// + /// var body: some View { + /// List(products) { product in + /// ProductRow(product: product) + /// } + /// .onFirstAppear { + /// // Auto-select first product only once, + /// // not when navigating back + /// if let firstProduct = products.first { + /// selectedProduct = firstProduct + /// } + /// } + /// } + /// } + /// ``` + /// + /// Common use cases include: + /// - Initial data loading or setup + /// - One-time animations or tutorials + /// - Analytics events that should only fire once + /// - Auto-selection of initial values + /// + /// - Parameter perform: The closure to execute on first appearance. + /// - Returns: A view with the first-appear modifier applied. + public func onFirstAppear(perform: @escaping () -> Void) -> some View { + self.modifier(FirstAppearModifier(perform: perform)) + } +}