Present sheets as a full bottom drawer
This commit is contained in:
@@ -37,9 +37,14 @@ struct AccountsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task(id: model.moneroReady) { await model.loadAccounts() }
|
.task(id: model.moneroReady) { await model.loadAccounts() }
|
||||||
.sheet(item: $qrAddress) { item in
|
.bottomDrawer(isPresented: Binding(
|
||||||
|
get: { qrAddress != nil },
|
||||||
|
set: { if !$0 { qrAddress = nil } }
|
||||||
|
)) {
|
||||||
|
if let item = qrAddress {
|
||||||
AddressQRSheet(title: item.title, address: item.address) { qrAddress = nil }
|
AddressQRSheet(title: item.title, address: item.address) { qrAddress = nil }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.alert("Account name", isPresented: $showAccountPrompt) {
|
.alert("Account name", isPresented: $showAccountPrompt) {
|
||||||
TextField("e.g. Savings", text: $pendingLabel)
|
TextField("e.g. Savings", text: $pendingLabel)
|
||||||
Button("Create") { createAccount() }
|
Button("Create") { createAccount() }
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ struct AddressQR: Identifiable {
|
|||||||
let address: String
|
let address: String
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drawer that shows one address as a scannable QR, presented from the bottom
|
/// One address as a scannable QR, shown inside the shared bottom drawer.
|
||||||
/// like the transaction detail sheet.
|
|
||||||
struct AddressQRSheet: View {
|
struct AddressQRSheet: View {
|
||||||
var title: String
|
var title: String
|
||||||
var address: String
|
var address: String
|
||||||
@@ -16,8 +15,6 @@ struct AddressQRSheet: View {
|
|||||||
@State private var copied = false
|
@State private var copied = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
|
||||||
Theme.background.ignoresSafeArea()
|
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.font(.system(size: 16, weight: .semibold))
|
||||||
@@ -43,11 +40,7 @@ struct AddressQRSheet: View {
|
|||||||
|
|
||||||
PrimaryButton(title: "Done", action: onDone)
|
PrimaryButton(title: "Done", action: onDone)
|
||||||
}
|
}
|
||||||
.padding(22)
|
.padding(.horizontal, 22)
|
||||||
.frame(maxHeight: .infinity, alignment: .top)
|
|
||||||
}
|
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
.presentationDetents([.medium, .large])
|
|
||||||
.presentationDragIndicator(.visible)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
/// Presents a bottom-anchored drawer: it fills to the screen's bottom edge
|
||||||
|
/// (no gap), shows a dimmed backdrop, and can be dragged down to dismiss.
|
||||||
|
func bottomDrawer<Content: View>(isPresented: Binding<Bool>,
|
||||||
|
@ViewBuilder content: @escaping () -> Content) -> some View {
|
||||||
|
BottomDrawerHost(host: self, isPresented: isPresented, content: content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct BottomDrawerHost<Host: View, Content: View>: View {
|
||||||
|
let host: Host
|
||||||
|
@Binding var isPresented: Bool
|
||||||
|
@ViewBuilder var content: () -> Content
|
||||||
|
|
||||||
|
@State private var dragOffset: CGFloat = 0
|
||||||
|
|
||||||
|
private var bottomInset: CGFloat {
|
||||||
|
for scene in UIApplication.shared.connectedScenes {
|
||||||
|
guard let windowScene = scene as? UIWindowScene else { continue }
|
||||||
|
if let window = windowScene.keyWindow {
|
||||||
|
return window.safeAreaInsets.bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private var maxHeight: CGFloat {
|
||||||
|
for scene in UIApplication.shared.connectedScenes {
|
||||||
|
guard let windowScene = scene as? UIWindowScene else { continue }
|
||||||
|
if let window = windowScene.keyWindow {
|
||||||
|
return window.bounds.height * 0.92
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 800
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .bottom) {
|
||||||
|
host
|
||||||
|
|
||||||
|
if isPresented {
|
||||||
|
Color.black.opacity(0.5)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onTapGesture { dismiss() }
|
||||||
|
.transition(.opacity)
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Capsule()
|
||||||
|
.fill(Theme.muted.opacity(0.55))
|
||||||
|
.frame(width: 36, height: 5)
|
||||||
|
.padding(.top, 8)
|
||||||
|
|
||||||
|
content()
|
||||||
|
.padding(.top, 14)
|
||||||
|
.padding(.bottom, bottomInset + 16)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .top)
|
||||||
|
.frame(maxHeight: maxHeight)
|
||||||
|
.background(Theme.surface)
|
||||||
|
.clipShape(UnevenRoundedRectangle(topLeadingRadius: 20, topTrailingRadius: 20, style: .continuous))
|
||||||
|
.offset(y: dragOffset)
|
||||||
|
.gesture(dismissDrag)
|
||||||
|
.transition(.move(edge: .bottom))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.animation(.spring(response: 0.32, dampingFraction: 0.86), value: isPresented)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dismiss() {
|
||||||
|
isPresented = false
|
||||||
|
dragOffset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dismissDrag: some Gesture {
|
||||||
|
DragGesture()
|
||||||
|
.onChanged { value in
|
||||||
|
dragOffset = max(0, value.translation.height)
|
||||||
|
}
|
||||||
|
.onEnded { value in
|
||||||
|
if value.translation.height > 120 || value.predictedEndTranslation.height > 200 {
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
dragOffset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,11 +108,16 @@ struct DashboardView: View {
|
|||||||
MessageBanner(message: seedError, isError: true)
|
MessageBanner(message: seedError, isError: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(item: $selectedTransaction) { record in
|
.bottomDrawer(isPresented: Binding(
|
||||||
|
get: { selectedTransaction != nil },
|
||||||
|
set: { if !$0 { selectedTransaction = nil } }
|
||||||
|
)) {
|
||||||
|
if let record = selectedTransaction {
|
||||||
TransactionDetailView(record: record,
|
TransactionDetailView(record: record,
|
||||||
symbol: model.symbol,
|
symbol: model.symbol,
|
||||||
chain: model.chain) { selectedTransaction = nil }
|
chain: model.chain) { selectedTransaction = nil }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.sheet(isPresented: $showSeed) {
|
.sheet(isPresented: $showSeed) {
|
||||||
if let revealedSeed {
|
if let revealedSeed {
|
||||||
SeedRevealView(seed: revealedSeed, symbol: model.symbol) { showSeed = false }
|
SeedRevealView(seed: revealedSeed, symbol: model.symbol) { showSeed = false }
|
||||||
|
|||||||
@@ -112,15 +112,25 @@ struct HomeView: View {
|
|||||||
MessageBanner(message: message, isError: false)
|
MessageBanner(message: message, isError: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(item: $selectedTransaction) { record in
|
.bottomDrawer(isPresented: Binding(
|
||||||
|
get: { selectedTransaction != nil },
|
||||||
|
set: { if !$0 { selectedTransaction = nil } }
|
||||||
|
)) {
|
||||||
|
if let record = selectedTransaction {
|
||||||
TransactionDetailView(record: record,
|
TransactionDetailView(record: record,
|
||||||
symbol: model.symbol,
|
symbol: model.symbol,
|
||||||
chain: model.chain) { selectedTransaction = nil }
|
chain: model.chain) { selectedTransaction = nil }
|
||||||
}
|
}
|
||||||
.sheet(item: $qrAddress) { item in
|
}
|
||||||
|
.bottomDrawer(isPresented: Binding(
|
||||||
|
get: { qrAddress != nil },
|
||||||
|
set: { if !$0 { qrAddress = nil } }
|
||||||
|
)) {
|
||||||
|
if let item = qrAddress {
|
||||||
AddressQRSheet(title: item.title, address: item.address) { qrAddress = nil }
|
AddressQRSheet(title: item.title, address: item.address) { qrAddress = nil }
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showSettings) {
|
}
|
||||||
|
.bottomDrawer(isPresented: $showSettings) {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
.environmentObject(model)
|
.environmentObject(model)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ struct SettingsView: View {
|
|||||||
private let currencies = ["USD", "MYR", "JPY"]
|
private let currencies = ["USD", "MYR", "JPY"]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ScrollView {
|
||||||
Theme.background.ignoresSafeArea()
|
|
||||||
VStack(alignment: .leading, spacing: 18) {
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
ScreenHeader(title: "Settings", subtitle: "Security and device options", onBack: { dismiss() })
|
ScreenHeader(title: "Settings", subtitle: "Security and device options", onBack: { dismiss() })
|
||||||
|
|
||||||
@@ -70,7 +69,5 @@ struct SettingsView: View {
|
|||||||
.padding(.top, 12)
|
.padding(.top, 12)
|
||||||
}
|
}
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
.presentationDetents([.medium, .large])
|
|
||||||
.presentationDragIndicator(.visible)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,8 +41,6 @@ struct TransactionDetailView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
|
||||||
Theme.background.ignoresSafeArea()
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 18) {
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
@@ -115,7 +113,6 @@ struct TransactionDetailView: View {
|
|||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
.task {
|
.task {
|
||||||
guard record.timestamp == nil,
|
guard record.timestamp == nil,
|
||||||
|
|||||||
Reference in New Issue
Block a user