Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 90 additions & 15 deletions Sources/DashUIKit/Components/AddressFieldView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@
#if canImport(UIKit)
import SwiftUI

/// Outside the view, not nested in it: `AddressFieldView` is generic over its
/// accessory, and a generic type cannot hold static stored properties.
private enum Layout {
static let hSpacing: CGFloat = 20
static let lPadding: CGFloat = 20
static let tPadding: CGFloat = 10
static let iconSize: CGFloat = 17
static let cornerRadius: CGFloat = 16
static let actionTapArea: CGFloat = 40
}

@available(iOS 15, macOS 12, *)
public struct AddressFieldView: View {

private enum Layout {
static let hSpacing: CGFloat = 20
static let lPadding: CGFloat = 20
static let tPadding: CGFloat = 10
static let iconSize: CGFloat = 17
static let cornerRadius: CGFloat = 16
static let actionTapArea: CGFloat = 40
}
public struct AddressFieldView<Accessory: View>: View {
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Binding private var text: String
private let label: String
Expand All @@ -37,6 +39,11 @@ public struct AddressFieldView: View {
private var isDisabled: Bool
private var onScanQR: (() -> Void)?
private var onPaste: (() -> Void)?
/// Trailing content on the label row — a badge naming what the entered
/// address turned out to be, say. Sits opposite `label`, so it is for
/// something that describes the field rather than acts on it; the
/// controls that act live inside the field itself.
private let accessory: Accessory

@FocusState private var isTextFieldFocused: Bool

Expand All @@ -48,7 +55,8 @@ public struct AddressFieldView: View {
errorText: String? = nil,
isDisabled: Bool = false,
onScanQR: (() -> Void)? = nil,
onPaste: (() -> Void)? = nil
onPaste: (() -> Void)? = nil,
@ViewBuilder accessory: () -> Accessory
) {
self._text = text
self.label = label
Expand All @@ -58,14 +66,21 @@ public struct AddressFieldView: View {
self.isDisabled = isDisabled
self.onScanQR = onScanQR
self.onPaste = onPaste
self.accessory = accessory()
}

public var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(label)
.dashFont(.footnote)
.foregroundStyle(Color.dash.gray500)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 8) {
Text(label)
.dashFont(.footnote)
.foregroundStyle(Color.dash.gray500)

Spacer(minLength: 0)

accessory
}
.frame(maxWidth: .infinity, alignment: .leading)

HStack(alignment: .center, spacing: Layout.hSpacing) {
textField
Expand Down Expand Up @@ -101,6 +116,37 @@ public struct AddressFieldView: View {
}
}

}

@available(iOS 15, macOS 12, *)
public extension AddressFieldView where Accessory == EmptyView {
/// No label accessory — the original shape, unchanged for callers that
/// have nothing to put there.
init(
text: Binding<String>,
label: String,
placeholder: String,
hasError: Bool,
errorText: String? = nil,
isDisabled: Bool = false,
onScanQR: (() -> Void)? = nil,
onPaste: (() -> Void)? = nil
) {
self.init(
text: text,
label: label,
placeholder: placeholder,
hasError: hasError,
errorText: errorText,
isDisabled: isDisabled,
onScanQR: onScanQR,
onPaste: onPaste,
accessory: { EmptyView() })
}
}

@available(iOS 15, macOS 12, *)
extension AddressFieldView {
// MARK: - Subviews

private var showsPasteButton: Bool {
Expand Down Expand Up @@ -292,5 +338,34 @@ public struct AddressFieldView: View {
.padding()
}

@available(iOS 17, macOS 14, *)
#Preview("Label accessory") {
AddressFieldView(
text: .constant("yV1D1ivvSUyKPJnbFmzSTVh1MyZ3JbeVkY"),
label: "Address",
placeholder: "Dash address",
hasError: false
) {
// What the host puts here is its own: a badge naming the kind of
// address that was entered, decided by the host's own decoder.
HStack(spacing: 4) {
DashIcon.Common.iconDashCurrency.image
.renderingMode(.template)
.resizable()
.scaledToFit()
.frame(width: 10, height: 10)
Text("Transparent address")
.dashFont(.caption2)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.foregroundStyle(Color.dash.blueText)
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(Color.dash.blueAlpha10)
.clipShape(Capsule())
}
.padding()
.background(Color.dash.primaryBackground)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

#endif
#endif // canImport(UIKit)
81 changes: 69 additions & 12 deletions Sources/DashUIKit/Components/BottomSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,26 @@ public struct BottomSheet<Content: View>: View {
/// when natural sizing is needed — it guarantees `fillsHeight: false` and the modifier are
/// always applied together.
public var fillsHeight: Bool = true
/// Fill behind the whole sheet — grabber, header and content alike. Also
/// used as the presentation background so the home-indicator inset the
/// detent adds matches; a host that only restyles its own content would
/// otherwise get a strip of this colour along the bottom edge.
public var background: Color = .dash.primaryBackground
@ViewBuilder public var content: () -> Content

public init(
title: String = "",
showBackButton: Binding<Bool>,
onBackButtonPressed: (() -> Void)? = nil,
fillsHeight: Bool = true,
background: Color = .dash.primaryBackground,
@ViewBuilder content: @escaping () -> Content
) {
self.title = title
self._showBackButton = showBackButton
self.onBackButtonPressed = onBackButtonPressed
self.fillsHeight = fillsHeight
self.background = background
self.content = content
}

Expand All @@ -42,7 +49,7 @@ public struct BottomSheet<Content: View>: View {

contentSection
}
.background(Color.dash.primaryBackground)
.background(background)

if fillsHeight {
sheet.edgesIgnoringSafeArea(.bottom)
Expand Down Expand Up @@ -103,13 +110,13 @@ public struct BottomSheet<Content: View>: View {
.navigationBarHidden(true)
#endif
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.dash.primaryBackground)
.background(background)
}
} else {
// Natural height — no greedy NavigationView / maxHeight so the sheet can self-size.
content()
.frame(maxWidth: .infinity)
.background(Color.dash.primaryBackground)
.background(background)
}
}
}
Expand All @@ -134,6 +141,7 @@ public extension BottomSheet {
onBackButtonPressed: (() -> Void)? = nil,
fallback: CGFloat = 0,
maxHeightFraction: CGFloat = 0.95,
background: Color = .dash.primaryBackground,
cornerRadius: CGFloat? = nil,
@ViewBuilder content: @escaping () -> Content
) -> some View {
Expand All @@ -142,9 +150,14 @@ public extension BottomSheet {
showBackButton: showBackButton,
onBackButtonPressed: onBackButtonPressed,
fillsHeight: false,
background: background,
content: content
)
.selfSizingSheet(fallback: fallback, maxHeightFraction: maxHeightFraction, cornerRadius: cornerRadius)
.selfSizingSheet(
fallback: fallback,
maxHeightFraction: maxHeightFraction,
background: background,
cornerRadius: cornerRadius)
}
}

Expand All @@ -163,32 +176,50 @@ public extension View {
/// - fallback: Height used before the first measurement (avoids a `.medium` flash).
/// - maxHeightFraction: Caps the sheet at this fraction of the window height; taller content
/// is clipped, so wrap it in a `ScrollView`.
/// - background: Fill for the sheet and its presentation, so the bottom
/// safe-area strip matches the content. This modifier cannot see the
/// colour the wrapped `BottomSheet` was built with, so a custom one has
/// to be passed here too — or use `BottomSheet.selfSizing(...)`, which
/// forwards a single `background` to both.
/// - cornerRadius: Optional corner radius applied via `presentationCornerRadius` on
/// iOS 16.4..<26 (iOS 26+ keeps the system corner styling). When provided, the sheet
/// background is also filled so the bottom safe-area strip matches the content.
/// iOS 16.4..<26 (iOS 26+ keeps the system corner styling).
@ViewBuilder
func selfSizingSheet(
fallback: CGFloat = 0,
maxHeightFraction: CGFloat = 0.95,
background: Color = .dash.primaryBackground,
cornerRadius: CGFloat? = nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
) -> some View {
if #available(iOS 16.0, macOS 13.0, *) {
let modified = modifier(SelfSizingSheetModifier(fallback: fallback, maxHeightFraction: maxHeightFraction))
#if os(iOS)
if #available(iOS 16.4, *), let cornerRadius {
if #unavailable(iOS 26.0) {
// iOS 16.4..<26: apply the custom corner radius + fill the sheet background.
if #available(iOS 16.4, *) {
// The background is filled whatever the corner radius: the
// measured height excludes the home-indicator inset that
// `.presentationDetents([.height])` adds back, so that strip
// sits outside the sheet's own `VStack` and shows the system
// background unless this fills it.
if #unavailable(iOS 26.0), let cornerRadius {
modified
.presentationCornerRadius(cornerRadius)
.presentationBackground(Color.dash.primaryBackground)
.presentationBackground(background)
} else {
// iOS 26+: keep the system corner styling, just fill the background.
// iOS 26+ keeps the system corner styling.
modified
.presentationBackground(Color.dash.primaryBackground)
.presentationBackground(background)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
} else {
modified
}
#elseif os(macOS)
// `presentationCornerRadius` is iOS-only, but the presentation
// background lands on macOS 13.3 — apply it there too so the
// parameter is not silently ignored.
if #available(macOS 13.3, *) {
modified.presentationBackground(background)
} else {
modified
}
#else
modified
#endif
Expand Down Expand Up @@ -245,6 +276,8 @@ private struct SelfSizingSheetModifier: ViewModifier {
}
}

#if DEBUG

@available(iOS 17, macOS 14, *)
#Preview("BottomSheet Filled Height") {
BottomSheet(
Expand Down Expand Up @@ -283,3 +316,27 @@ private struct SelfSizingSheetModifier: ViewModifier {
.padding()
}
}

@available(iOS 17, macOS 14, *)
#Preview("BottomSheet Custom Background") {
BottomSheet(
title: "Bottom Sheet",
showBackButton: .constant(false),
fillsHeight: false,
background: .dash.secondaryBackground
) {
VStack(alignment: .leading, spacing: 12) {
Text("Cards on a tinted sheet")
.dashFont(.calloutMedium)
.foregroundColor(.dash.primaryText)

Text("The host picks the fill; cards drawn on top keep their own.")
.dashFont(.body)
.foregroundColor(.dash.secondaryText)
.modifier(MenuViewModifier())
}
.padding()
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

#endif
9 changes: 8 additions & 1 deletion docs/navigation-and-containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ Sheet chrome to put **inside** a SwiftUI `.sheet { }`: a grabber, a `NavigationB
title: "Details",
showBackButton: $showBack, // Binding<Bool>
onBackButtonPressed: { /* pop */ },
fillsHeight: true // greedy: fills the sheet
fillsHeight: true, // greedy: fills the sheet
background: .dash.primaryBackground // fill behind grabber, header and content
) {
MyContent()
}
Expand All @@ -94,6 +95,7 @@ the modifier are applied together:
showBackButton: .constant(false),
fallback: 240, // height before first measurement (avoids .medium flash)
maxHeightFraction: 0.95, // cap at 95% of window height (clip taller → use ScrollView)
background: .dash.secondaryBackground, // also fills the home-indicator strip
cornerRadius: 24 // iOS 16.4..<26; iOS 26+ keeps system corners
) {
MyContent()
Expand All @@ -107,6 +109,11 @@ a **no-op below iOS 16**. The measured content must have a finite intrinsic heig
greedy `Spacer`/`maxHeight: .infinity`), or the measurement is wrong.
`BottomSheetHeightPreferenceKey` is exposed for advanced cases.

`background` fills the sheet **and** its presentation. The measured height excludes the
home-indicator inset that `presentationDetents([.height])` adds back, so that strip lies
outside the sheet's own stack — without the presentation fill it shows the system
background as a pale band along the bottom edge, whatever the content is styled with.

---

## MenuViewModifier
Expand Down
Loading