[fix] LCENavigationView: remove phantom glass button, center title safely #11
@@ -49,6 +49,39 @@ class LCENavigationState: ObservableObject {
|
||||
@Published var subTitle: (any View) = Text("")
|
||||
/// The background color of the navigation bar.
|
||||
@Published var navigationBarBackgroundColor: Color = .clear
|
||||
|
||||
/// Whether a left button was actually configured via `setLeftButton`.
|
||||
@Published var hasLeftButton: Bool = false
|
||||
/// Whether a right button was actually configured via `setRightButton`.
|
||||
@Published var hasRightButton: Bool = false
|
||||
/// Whether buttons should opt into the system Liquid Glass button style (iOS 26+). Off by default.
|
||||
@Published var useGlassButtons: Bool = false
|
||||
}
|
||||
|
||||
/// `PreferenceKey` used to measure the widest side button so the title can be padded symmetrically and stay centered without overlapping either button.
|
||||
@available(iOS 15, *)
|
||||
private struct LCENavButtonWidthPreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGFloat { 0 }
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value = max(value, nextValue())
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15, *)
|
||||
private extension View {
|
||||
/// Applies `.glass` button style on iOS 26+ when enabled, otherwise falls back to `.plain` — the navigation bar is not designed around glass, but stays opt-in ready.
|
||||
@ViewBuilder
|
||||
func lce_applyGlassIfEnabled(_ enabled: Bool) -> some View {
|
||||
if enabled {
|
||||
if #available(iOS 26.0, *) {
|
||||
self.buttonStyle(.glass)
|
||||
} else {
|
||||
self.buttonStyle(.plain)
|
||||
}
|
||||
} else {
|
||||
self.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `LCENavigationView` is a SwiftUI `View` that provides a customizable navigation bar.
|
||||
@@ -61,6 +94,9 @@ public struct LCENavigationView<Content: View>: View {
|
||||
/// The content view displayed below the navigation bar.
|
||||
let content: Content
|
||||
|
||||
/// The measured width of the widest side button, used to pad the title so it never overlaps either button.
|
||||
@State private var maxButtonWidth: CGFloat = 0
|
||||
|
||||
/// Initializes a new `LCENavigationView` instance.
|
||||
/// - Parameters:
|
||||
/// - title: The title view for the navigation bar. Defaults to an empty `Text`.
|
||||
@@ -92,18 +128,29 @@ public struct LCENavigationView<Content: View>: View {
|
||||
|
||||
/// The private `NavigationBarView` that lays out the navigation bar components.
|
||||
private var NavigationBarView: some View {
|
||||
HStack {
|
||||
NavLeftButton
|
||||
Spacer()
|
||||
ZStack {
|
||||
TitleView
|
||||
.padding(.horizontal, maxButtonWidth)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.7)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
HStack {
|
||||
if state.hasLeftButton {
|
||||
NavLeftButton
|
||||
}
|
||||
Spacer()
|
||||
if state.hasRightButton {
|
||||
NavRightButton
|
||||
}
|
||||
}
|
||||
}
|
||||
.font(.headline)
|
||||
.padding()
|
||||
.background {
|
||||
state.navigationBarBackgroundColor.ignoresSafeArea(edges: .top)
|
||||
}
|
||||
.onPreferenceChange(LCENavButtonWidthPreferenceKey.self) { maxButtonWidth = $0 }
|
||||
}
|
||||
|
||||
/// The private `TitleView` that displays the title and subtitle.
|
||||
@@ -116,7 +163,7 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The private `NavLeftButton` view.
|
||||
/// The private `NavLeftButton` view. Only rendered when `setLeftButton` was actually called — never a hidden placeholder.
|
||||
private var NavLeftButton: some View {
|
||||
Button(action: state.leftButtonAction) {
|
||||
HStack {
|
||||
@@ -126,9 +173,15 @@ public struct LCENavigationView<Content: View>: View {
|
||||
state.leftButtonText
|
||||
}
|
||||
}
|
||||
.lce_applyGlassIfEnabled(state.useGlassButtons)
|
||||
.background(
|
||||
GeometryReader { proxy in
|
||||
Color.clear.preference(key: LCENavButtonWidthPreferenceKey.self, value: proxy.size.width)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// The private `NavRightButton` view.
|
||||
/// The private `NavRightButton` view. Only rendered when `setRightButton` was actually called — never a hidden placeholder.
|
||||
private var NavRightButton: some View {
|
||||
Button(action: state.rightButtonAction) {
|
||||
HStack {
|
||||
@@ -138,6 +191,12 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.lce_applyGlassIfEnabled(state.useGlassButtons)
|
||||
.background(
|
||||
GeometryReader { proxy in
|
||||
Color.clear.preference(key: LCENavButtonWidthPreferenceKey.self, value: proxy.size.width)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets the configuration for the right button of the navigation bar.
|
||||
@@ -158,13 +217,7 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
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
|
||||
}
|
||||
state.hasRightButton = true
|
||||
|
||||
return self
|
||||
}
|
||||
@@ -187,14 +240,17 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
state.leftButtonText = text
|
||||
state.leftButtonAction = action
|
||||
state.hasLeftButton = true
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// Opts the navigation bar's buttons into the system Liquid Glass `.glass` button style on iOS 26+.
|
||||
/// The navigation bar is designed as a plain, non-glass component by default; call this to enable glass explicitly.
|
||||
/// - Parameter enabled: Whether buttons should use the glass style.
|
||||
/// - Returns: The `LCENavigationView` instance for chaining.
|
||||
public func setGlassButtonsEnabled(_ enabled: Bool) -> LCENavigationView {
|
||||
state.useGlassButtons = enabled
|
||||
return self
|
||||
}
|
||||
|
||||
@@ -229,101 +285,6 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
Reference in New Issue
Block a user