331 lines
12 KiB
Swift
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
|