Files
LCEssentials/Sources/LCEssentials/SwiftUI/LCENavigationView.swift
Daniel Arantes Loverde 0910973e9a documentation
2025-06-23 09:48:57 -03:00

331 lines
12 KiB
Swift

//
// Copyright (c) 2024 Loverde Co.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#if canImport(SwiftUI)
import SwiftUI
/// `LCENavigationState` is an `ObservableObject` that manages the state for `LCENavigationView`.
/// It includes published properties for managing button images, text, actions,
/// navigation bar visibility, and title/subtitle views.
@available(iOS 15, *)
class LCENavigationState: ObservableObject {
/// The image view for the right button.
@Published var rightButtonImage: AnyView? = nil
/// The text for the right button.
@Published var rightButtonText: Text = Text("")
/// The action to perform when the right button is tapped.
@Published var rightButtonAction: () -> Void = {}
/// The image view for the left button.
@Published var leftButtonImage: AnyView? = nil
/// The text for the left button.
@Published var leftButtonText: Text = Text("")
/// The action to perform when the left button is tapped.
@Published var leftButtonAction: () -> Void = {}
/// A boolean value that controls the visibility of the navigation bar.
@Published var hideNavigationBar: Bool = false
/// The title view of the navigation bar.
@Published var title: (any View) = Text("")
/// The subtitle view of the navigation bar.
@Published var subTitle: (any View) = Text("")
}
/// `LCENavigationView` is a SwiftUI `View` that provides a customizable navigation bar.
/// It allows setting left and right buttons, a title, and a subtitle.
@available(iOS 15, *)
public struct LCENavigationView<Content: View>: View {
/// The observed state object for the navigation view.
@ObservedObject private var state: LCENavigationState
/// The content view displayed below the navigation bar.
let content: Content
/// Initializes a new `LCENavigationView` instance.
/// - Parameters:
/// - title: The title view for the navigation bar. Defaults to an empty `Text`.
/// - subTitle: The subtitle view for the navigation bar. Defaults to an empty `Text`.
/// - content: A `ViewBuilder` that provides the content to be displayed below the navigation bar.
public init(
title: (any View) = Text(""),
subTitle: (any View) = Text(""),
@ViewBuilder content: () -> Content
) {
self.content = content()
self._state = ObservedObject(
wrappedValue: LCENavigationState()
)
self.state.title = title
self.state.subTitle = subTitle
}
/// The body of the `LCENavigationView`.
public var body: some View {
VStack {
if !state.hideNavigationBar {
NavigationBarView
}
content
}
.navigationBarHidden(true)
}
/// The private `NavigationBarView` that lays out the navigation bar components.
private var NavigationBarView: some View {
HStack {
NavLeftButton
Spacer()
TitleView
Spacer()
NavRightButton
}
.font(.headline)
.padding()
.background {
Color.clear.ignoresSafeArea(edges: .top)
}
}
/// The private `TitleView` that displays the title and subtitle.
private var TitleView: some View {
VStack {
AnyView(state.title)
if (try? state.subTitle.getTag() ?? "hidden" ) != "hidden" {
AnyView(state.subTitle)
}
}
}
/// The private `NavLeftButton` view.
private var NavLeftButton: some View {
Button(action: state.leftButtonAction) {
HStack {
if let image = state.leftButtonImage {
image
}
state.leftButtonText
}
}
}
/// The private `NavRightButton` view.
private var NavRightButton: some View {
Button(action: state.rightButtonAction) {
HStack {
state.rightButtonText
if let image = state.rightButtonImage {
image
}
}
}
}
/// Sets the configuration for the right button of the navigation bar.
/// - Parameters:
/// - text: The `Text` to display on the button. Defaults to an empty `Text`.
/// - image: An optional `View` to use as the button's icon. Defaults to `nil`.
/// - action: The closure to execute when the button is tapped.
/// - Returns: The `LCENavigationView` instance for chaining.
public func setRightButton(
text: Text = Text(""),
image: (any View)? = nil,
action: @escaping () -> Void
) -> LCENavigationView {
if let image {
state.rightButtonImage = AnyView(image)
} else {
state.rightButtonImage = nil
}
state.rightButtonText = text
state.rightButtonAction = action
if let string = state.leftButtonText.string, string.isEmpty {
state.leftButtonText = text.foregroundColor(.clear)
}
if state.leftButtonImage == nil {
state.leftButtonImage = image?.foregroundColor(.clear) as? AnyView
}
return self
}
/// Sets the configuration for the left button of the navigation bar.
/// - Parameters:
/// - text: The `Text` to display on the button. Defaults to an empty `Text`.
/// - image: An optional `View` to use as the button's icon. Defaults to `nil`.
/// - action: The closure to execute when the button is tapped.
/// - Returns: The `LCENavigationView` instance for chaining.
public func setLeftButton(
text: Text = Text(""),
image: (any View)? = nil,
action: @escaping () -> Void
) -> LCENavigationView {
if let image {
state.leftButtonImage = AnyView(image)
} else {
state.leftButtonImage = nil
}
state.leftButtonText = text
state.leftButtonAction = action
if let string = state.rightButtonText.string, string.isEmpty {
state.rightButtonText = text.foregroundColor(.clear)
}
if state.rightButtonImage == nil {
state.rightButtonImage = image?.foregroundColor(.clear) as? AnyView
}
return self
}
/// Sets the title and optional subtitle for the navigation bar.
/// - Parameters:
/// - text: The `View` to use as the main title. Defaults to an empty `Text`.
/// - subTitle: An optional `View` to use as the subtitle. Defaults to `nil`.
/// - Returns: The `LCENavigationView` instance for chaining.
public func setTitle(
text: (any View) = Text(""),
subTitle: (any View)? = nil
) -> LCENavigationView {
state.title = text
state.subTitle = subTitle ?? Text("").tag("hidden")
return self
}
/// Controls the visibility of the navigation bar.
/// - Parameter hide: A boolean value indicating whether to hide (`true`) or show (`false`) the navigation bar.
/// - Returns: The `LCENavigationView` instance for chaining.
public func hideNavigationView(_ hide: Bool) -> LCENavigationView {
state.hideNavigationBar = hide
return self
}
}
/// Extension to `FormatStyle` to format any value as a string.
@available(iOS 15.0, *)
extension FormatStyle {
/// Formats an input value if it matches the `FormatInput` type.
/// - Parameter value: The value to format as `Any`.
/// - Returns: The formatted output, or `nil` if the value type does not match.
func format(any value: Any) -> FormatOutput? {
if let v = value as? FormatInput {
return format(v)
}
return nil
}
}
/// Extension to `LocalizedStringKey` to resolve localized strings.
@available(iOS 15.0, *)
extension LocalizedStringKey {
/// Resolves the localized string key into a `String`.
/// - Returns: The resolved string, or `nil` if resolution fails.
var resolved: String? {
let mirror = Mirror(reflecting: self)
guard let key = mirror.descendant("key") as? String else {
return nil
}
guard let args = mirror.descendant("arguments") as? [Any] else {
return nil
}
let values = args.map { arg -> Any? in
let mirror = Mirror(reflecting: arg)
if let value = mirror.descendant("storage", "value", ".0") {
return value
}
guard let format = mirror.descendant("storage", "formatStyleValue", "format") as? any FormatStyle,
let input = mirror.descendant("storage", "formatStyleValue", "input") else {
return nil
}
return format.format(any: input)
}
let va = values.compactMap { arg -> CVarArg? in
switch arg {
case let i as Int: return i
case let i as Int64: return i
case let i as Int8: return i
case let i as Int16: return i
case let i as Int32: return i
case let u as UInt: return u
case let u as UInt64: return u
case let u as UInt8: return u
case let u as UInt16: return u
case let u as UInt32: return u
case let f as Float: return f
case let f as CGFloat: return f
case let d as Double: return d
case let o as NSObject: return o
default: return nil
}
}
if va.count != values.count {
return nil
}
return String.localizedStringWithFormat(key, va)
}
}
/// Extension to `Text` to retrieve its string content.
@available(iOS 15.0, *)
extension Text {
/// Returns the string representation of the `Text` view.
/// - Returns: The string content, or `nil` if it cannot be extracted.
var string: String? {
let mirror = Mirror(reflecting: self)
if let s = mirror.descendant("storage", "verbatim") as? String {
return s
} else if let attrStr = mirror.descendant("storage", "anyTextStorage", "str") as? AttributedString {
return String(attrStr.characters)
} else if let key = mirror.descendant("storage", "anyTextStorage", "key") as? LocalizedStringKey {
return key.resolved
} else if let format = mirror.descendant("storage", "anyTextStorage", "storage", "format") as? any FormatStyle,
let input = mirror.descendant("storage", "anyTextStorage", "storage", "input") {
return format.format(any: input) as? String
} else if let formatter = mirror.descendant("storage", "anyTextStorage", "formatter") as? Formatter,
let object = mirror.descendant("storage", "anyTextStorage", "object") {
return formatter.string(for: object)
}
return nil
}
}
//@available(iOS 15.0, *)
//struct LCENavigationView_Previews: PreviewProvider {
// static var previews: some View {
// LCENavigationView {
// Text("Hello, World!")
// }
// .setTitle(text: Text("Hellow Nav Title"))
// .setLeftButton(text: "Back") {
// print("Button tapped!")
// }
// }
//}
#endif