damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit c100c6db47d62e7155a622528ee6acb30d5dde5c
parent 8d3fb397f7943087ada0326ab4edf2ee37909ed6
Author: OlegAba <mail@olegaba.com>
Date:   Wed, 15 Feb 2023 12:31:50 -0800

Merge remote-tracking branch 'oleg/custom-profile-navbar'

Changelog-Added: Improved profile navbar

Diffstat:
Mdamus/Components/ImageCarousel.swift | 14+++-----------
Mdamus/Util/Theme.swift | 8++++++++
Mdamus/Views/Profile/ProfileNameView.swift | 3++-
Mdamus/Views/ProfileView.swift | 360+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mdamus/Views/ProfileZoomView.swift | 143++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
5 files changed, 289 insertions(+), 239 deletions(-)

diff --git a/damus/Components/ImageCarousel.swift b/damus/Components/ImageCarousel.swift @@ -127,14 +127,6 @@ struct ImageView: View { @State private var selectedIndex = 0 @State var showMenu = true - var safeAreaInsets: UIEdgeInsets? { - return UIApplication - .shared - .connectedScenes - .flatMap { ($0 as? UIWindowScene)?.windows ?? [] } - .first { $0.isKeyWindow }?.safeAreaInsets - } - var navBarView: some View { VStack { HStack { @@ -180,8 +172,8 @@ struct ImageView: View { ZoomableScrollView { ImageContainerView(url: urls[index]) .aspectRatio(contentMode: .fit) - .padding(.top, safeAreaInsets?.top) - .padding(.bottom, safeAreaInsets?.bottom) + .padding(.top, Theme.safeAreaInsets?.top) + .padding(.bottom, Theme.safeAreaInsets?.bottom) } .modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: { presentationMode.wrappedValue.dismiss() @@ -210,7 +202,7 @@ struct ImageView: View { } } .animation(.easeInOut, value: showMenu) - .padding(.bottom, safeAreaInsets?.bottom) + .padding(.bottom, Theme.safeAreaInsets?.bottom) ) } } diff --git a/damus/Util/Theme.swift b/damus/Util/Theme.swift @@ -25,4 +25,12 @@ class Theme { UINavigationBar.appearance().tintColor = tintColor ?? titleColor ?? .black } + + static var safeAreaInsets: UIEdgeInsets? { + return UIApplication + .shared + .connectedScenes + .flatMap { ($0 as? UIWindowScene)?.windows ?? [] } + .first { $0.isKeyWindow }?.safeAreaInsets + } } diff --git a/damus/Views/Profile/ProfileNameView.swift b/damus/Views/Profile/ProfileNameView.swift @@ -18,7 +18,7 @@ struct ProfileNameView: View { var body: some View { Group { if let real_name = profile?.display_name { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 0) { Text(real_name) .font(.title3.weight(.bold)) HStack(alignment: .center, spacing: spacing) { @@ -30,6 +30,7 @@ struct ProfileNameView: View { FollowsYou() } } + Spacer() KeyView(pubkey: pubkey) .pubkey_context_menu(bech32_pubkey: pubkey) } diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift @@ -80,9 +80,24 @@ struct EditButton: View { } } +struct VisualEffectView: UIViewRepresentable { + var effect: UIVisualEffect? + + func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView { + UIVisualEffectView() + } + + func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) { + uiView.effect = effect + } +} + struct ProfileView: View { let damus_state: DamusState - let zoom_size: CGFloat = 350 + let pfp_size: CGFloat = 90.0 + let bannerHeight: CGFloat = 150.0 + + static let markdown = Markdown() @State private var selected_tab: ProfileTab = .posts @StateObject var profile: ProfileModel @@ -92,20 +107,12 @@ struct ProfileView: View { @State var is_zoomed: Bool = false @State var show_share_sheet: Bool = false @State var action_sheet_presented: Bool = false + @State var yOffset: CGFloat = 0 @Environment(\.dismiss) var dismiss @Environment(\.colorScheme) var colorScheme @Environment(\.openURL) var openURL - - // We just want to have a white "< Home" text here, however, - // setting the initialiser is causing issues, and it's late. - // Ref: https://blog.techchee.com/navigation-bar-title-style-color-and-custom-back-button-in-swiftui/ - /* - init(damus_state: DamusState, zoom_size: CGFloat = 350) { - self.damus_state = damus_state - self.zoom_size = zoom_size - Theme.navigationBarColors(background: nil, titleColor: .white, tintColor: nil) - }*/ + @Environment(\.presentationMode) var presentationMode func fillColor() -> Color { colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey") @@ -115,39 +122,66 @@ struct ProfileView: View { colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack") } - func LNButton(lnurl: String, profile: Profile) -> some View { - Button(action: { - if damus_state.settings.show_wallet_selector { - showing_select_wallet = true - } else { - open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: lnurl) + func bannerBlurViewOpacity() -> Double { + let progress = -(yOffset + navbarHeight) / 100 + return Double(-yOffset > navbarHeight ? progress : 0) + } + + var bannerSection: some View { + GeometryReader { proxy -> AnyView in + + let minY = proxy.frame(in: .global).minY + + DispatchQueue.main.async { + self.yOffset = minY } - }) { - Image(systemName: "bolt.circle") - .profile_button_style(scheme: colorScheme) - .contextMenu { - Button { - UIPasteboard.general.string = profile.lnurl ?? "" - } label: { - Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), systemImage: "doc.on.doc") + + return AnyView( + VStack(spacing: 0) { + ZStack { + BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles) + .aspectRatio(contentMode: .fill) + .frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight) + .clipped() + + VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)).opacity(bannerBlurViewOpacity()) } + + Divider().opacity(bannerBlurViewOpacity()) } - + .frame(height: minY > 0 ? bannerHeight + minY : nil) + .offset(y: minY > 0 ? -minY : -minY < navbarHeight ? 0 : -minY - navbarHeight) + ) + } - .cornerRadius(24) - .sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) { - SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl) + .frame(height: bannerHeight) + } + + var navbarHeight: CGFloat { + return 100.0 - (Theme.safeAreaInsets?.top ?? 0) + } + + @ViewBuilder + func navImage(systemImage: String) -> some View { + Image(systemName: systemImage) + .frame(width: 33, height: 33) + .background(Color.black.opacity(0.6)) + .clipShape(Circle()) + } + + var navBackButton: some View { + Button { + presentationMode.wrappedValue.dismiss() + } label: { + navImage(systemImage: "chevron.left") } } - - static let markdown = Markdown() - var ActionSheetButton: some View { + var navActionSheetButton: some View { Button(action: { action_sheet_presented = true }) { - Image(systemName: "ellipsis.circle") - .profile_button_style(scheme: colorScheme) + navImage(systemImage: "ellipsis") } .confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or block a profile."), isPresented: $action_sheet_presented) { Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) { @@ -166,19 +200,45 @@ struct ProfileView: View { } } } - } - var ShareButton: some View { + var customNavbar: some View { + HStack { + navBackButton + Spacer() + navActionSheetButton + } + .padding(.top, 5) + .padding(.horizontal) + .accentColor(Color("DamusWhite")) + } + + func lnButton(lnurl: String, profile: Profile) -> some View { Button(action: { - show_share_sheet = true + if damus_state.settings.show_wallet_selector { + showing_select_wallet = true + } else { + open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: lnurl) + } }) { - Image(systemName: "square.and.arrow.up.circle") + Image(systemName: "bolt.circle") .profile_button_style(scheme: colorScheme) + .contextMenu { + Button { + UIPasteboard.general.string = profile.lnurl ?? "" + } label: { + Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), systemImage: "doc.on.doc") + } + } + + } + .cornerRadius(24) + .sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) { + SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl) } } - var DMButton: some View { + var dmButton: some View { let dm_model = damus_state.dms.lookup_or_create(profile.pubkey) let dmview = DMChatView(damus_state: damus_state, pubkey: profile.pubkey) .environmentObject(dm_model) @@ -187,44 +247,17 @@ struct ProfileView: View { .profile_button_style(scheme: colorScheme) } } - - private func getScrollOffset(_ geometry: GeometryProxy) -> CGFloat { - geometry.frame(in: .global).minY - } - - private func getHeightForHeaderImage(_ geometry: GeometryProxy) -> CGFloat { - let offset = getScrollOffset(geometry) - let imageHeight = 150.0 - - if offset > 0 { - return imageHeight + offset - } - - return imageHeight - } - - private func getOffsetForHeaderImage(_ geometry: GeometryProxy) -> CGFloat { - let offset = getScrollOffset(geometry) - - // Image was pulled down - if offset > 0 { - return -offset - } - - return 0 - } - func ActionSection(profile_data: Profile?) -> some View { + func actionSection(profile_data: Profile?) -> some View { return Group { - ActionSheetButton if let profile = profile_data { if let lnurl = profile.lnurl, lnurl != "" { - LNButton(lnurl: lnurl, profile: profile) + lnButton(lnurl: lnurl, profile: profile) } } - DMButton + dmButton if profile.pubkey != damus_state.pubkey { FollowButtonView( @@ -241,136 +274,139 @@ struct ProfileView: View { } } - func NameSection(profile_data: Profile?) -> some View { + func pfpOffset() -> CGFloat { + let progress = -yOffset / navbarHeight + let offset = (pfp_size / 4.0) * (progress < 1.0 ? progress : 1) + return offset > 0 ? offset : 0 + } + + func pfpScale() -> CGFloat { + let progress = -yOffset / navbarHeight + let scale = 1.0 - (0.5 * (progress < 1.0 ? progress : 1)) + return scale < 1 ? scale : 1 + } + + func nameSection(profile_data: Profile?) -> some View { return Group { HStack(alignment: .center) { ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles) + .padding(.top, -(pfp_size / 2.0)) + .offset(y: pfpOffset()) + .scaleEffect(pfpScale()) .onTapGesture { is_zoomed.toggle() } .fullScreenCover(isPresented: $is_zoomed) { ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles) } - .offset(y: -(pfp_size/2.0)) // Increase if set a frame Spacer() - ActionSection(profile_data: profile_data) - .offset(y: -15.0) // Increase if set a frame + actionSection(profile_data: profile_data) } let follows_you = profile.follows(pubkey: damus_state.pubkey) ProfileNameView(pubkey: profile.pubkey, profile: profile_data, follows_you: follows_you, damus: damus_state) - //.padding(.bottom) - .padding(.top,-(pfp_size/2.0)) } } - var pfp_size: CGFloat { - return 90.0 + var followersCount: some View { + HStack { + if followers.count == nil { + Image(systemName: "square.and.arrow.down") + Text("Followers", comment: "Label describing followers of a user.") + .font(.subheadline) + .foregroundColor(.gray) + } else { + let followerCount = followers.count! + Text("\(Text(String("\(followerCount)")).font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("followers_count", comment: "Part of a larger sentence to describe how many people are following a user."), followerCount)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.") + } + } } - var TopSection: some View { - ZStack(alignment: .top) { - GeometryReader { geometry in - BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles) - .aspectRatio(contentMode: .fill) - .frame(width: geometry.size.width, height: self.getHeightForHeaderImage(geometry)) - .clipped() - .offset(x: 0, y: self.getOffsetForHeaderImage(geometry)) - - }.frame(height: BANNER_HEIGHT) + var aboutSection: some View { + VStack(alignment: .leading, spacing: 8.0) { + let profile_data = damus_state.profiles.lookup(id: profile.pubkey) - VStack(alignment: .leading, spacing: 8.0) { - let profile_data = damus_state.profiles.lookup(id: profile.pubkey) - - NameSection(profile_data: profile_data) - - Text(ProfileView.markdown.process(profile_data?.about ?? "")) - .font(.subheadline).textSelection(.enabled) - - if let url = profile_data?.website_url { - WebsiteLink(url: url) - } - - Divider() - - HStack { - if let contact = profile.contacts { - let contacts = contact.referenced_pubkeys.map { $0.ref_id } - let following_model = FollowingModel(damus_state: damus_state, contacts: contacts) - NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) { - HStack { - Text("\(Text(String("\(profile.following)")).font(.subheadline.weight(.medium))) \(Text("Following", comment: "Part of a larger sentence to describe how many profiles a user is following.").font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.") - } + nameSection(profile_data: profile_data) + + Text(ProfileView.markdown.process(profile_data?.about ?? "")) + .font(.subheadline).textSelection(.enabled) + + if let url = profile_data?.website_url { + WebsiteLink(url: url) + } + + Divider() + + HStack { + if let contact = profile.contacts { + let contacts = contact.referenced_pubkeys.map { $0.ref_id } + let following_model = FollowingModel(damus_state: damus_state, contacts: contacts) + NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) { + HStack { + Text("\(Text("\(profile.following)", comment: "Number of profiles a user is following.").font(.subheadline.weight(.medium))) \(Text("Following", comment: "Part of a larger sentence to describe how many profiles a user is following.").font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.") } - .buttonStyle(PlainButtonStyle()) } - let fview = FollowersView(damus_state: damus_state, whos: profile.pubkey) - .environmentObject(followers) - if followers.contacts != nil { - NavigationLink(destination: fview) { - FollowersCount + .buttonStyle(PlainButtonStyle()) + } + let fview = FollowersView(damus_state: damus_state, whos: profile.pubkey) + .environmentObject(followers) + if followers.contacts != nil { + NavigationLink(destination: fview) { + followersCount + } + .buttonStyle(PlainButtonStyle()) + } else { + followersCount + .onTapGesture { + UIImpactFeedbackGenerator(style: .light).impactOccurred() + followers.contacts = [] + followers.subscribe() + } + } + + if let relays = profile.relays { + // Only open relay config view if the user is logged in with private key and they are looking at their own profile. + let relay_text = Text("\(Text("\(relays.keys.count)", comment: "Number of relay servers a user is connected.").font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("relays_count", comment: "Part of a larger sentence to describe how many relay servers a user is connected."), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.") + if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user { + NavigationLink(destination: RelayConfigView(state: damus_state)) { + relay_text } .buttonStyle(PlainButtonStyle()) } else { - FollowersCount - .onTapGesture { - UIImpactFeedbackGenerator(style: .light).impactOccurred() - followers.contacts = [] - followers.subscribe() - } - } - - if let relays = profile.relays { - // Only open relay config view if the user is logged in with private key and they are looking at their own profile. - let relay_text = Text("\(Text(String("\(relays.keys.count)")).font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("relays_count", comment: "Part of a larger sentence to describe how many relay servers a user is connected."), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.") - if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user { - NavigationLink(destination: RelayConfigView(state: damus_state)) { - relay_text - } - .buttonStyle(PlainButtonStyle()) - } else { - NavigationLink(destination: UserRelaysView(state: damus_state, pubkey: profile.pubkey, relays: Array(relays.keys).sorted())) { - relay_text - } - .buttonStyle(PlainButtonStyle()) + NavigationLink(destination: UserRelaysView(state: damus_state, pubkey: profile.pubkey, relays: Array(relays.keys).sorted())) { + relay_text } + .buttonStyle(PlainButtonStyle()) } } } - .padding(.horizontal,18) - //.offset(y:120) - .padding(.top,150) - } - } - - var FollowersCount: some View { - HStack { - if followers.count == nil { - Image(systemName: "square.and.arrow.down") - Text("Followers", comment: "Label describing followers of a user.") - .font(.subheadline) - .foregroundColor(.gray) - } else { - let followerCount = followers.count! - Text("\(Text(String("\(followerCount)")).font(.subheadline.weight(.medium))) \(Text(String(format: NSLocalizedString("followers_count", comment: "Part of a larger sentence to describe how many people are following a user."), followerCount)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.") - } } + .padding(.horizontal) } var body: some View { - VStack(alignment: .leading) { - ScrollView { - TopSection - - Divider() + ScrollView(.vertical) { + VStack(spacing: 0) { + bannerSection + .zIndex(1) - InnerTimelineView(events: $profile.events, damus: damus_state, show_friend_icon: false, filter: { _ in true }) + VStack() { + aboutSection + + Divider() + + InnerTimelineView(events: $profile.events, damus: damus_state, show_friend_icon: false, filter: { _ in true }) + } + .padding(.horizontal, Theme.safeAreaInsets?.left) + .zIndex(-yOffset > navbarHeight ? 0 : 1) } - .frame(maxHeight: .infinity, alignment: .topLeading) } - .frame(maxWidth: .infinity, alignment: .topLeading) + .ignoresSafeArea() + .navigationTitle("") + .navigationBarHidden(true) + .overlay(customNavbar, alignment: .top) .onReceive(handle_notify(.switched_timeline)) { _ in dismiss() } @@ -390,7 +426,6 @@ struct ProfileView: View { } } } - .ignoresSafeArea() } } @@ -403,7 +438,6 @@ struct ProfileView_Previews: PreviewProvider { } } - func test_damus_state() -> DamusState { let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" let damus = DamusState.empty diff --git a/damus/Views/ProfileZoomView.swift b/damus/Views/ProfileZoomView.swift @@ -5,84 +5,99 @@ // Created by scoder1747 on 12/27/22. // import SwiftUI +import Kingfisher -struct ProfileZoomView: View { - - @Environment(\.presentationMode) var presentationMode - let pubkey: String - let profiles: Profiles - - @GestureState private var scaleState: CGFloat = 1 - @GestureState private var offsetState = CGSize.zero - - @State private var offset = CGSize.zero - @State private var scale: CGFloat = 1 - - func resetStatus(){ - self.offset = CGSize.zero - self.scale = 1 +private struct ImageContainerView: View { + + @ObservedObject var imageModel: KFImageModel + + @State private var image: UIImage? + @State private var showShareSheet = false + + init(url: URL?) { + self.imageModel = KFImageModel( + url: url, + fallbackUrl: nil, + maxByteSize: 2000000, // 2 MB + downsampleSize: CGSize(width: 400, height: 400) + ) } - - var zoomGesture: some Gesture { - MagnificationGesture() - .updating($scaleState) { currentState, gestureState, _ in - gestureState = currentState + + private struct ImageHandler: ImageModifier { + @Binding var handler: UIImage? + + func modify(_ image: UIImage) -> UIImage { + handler = image + return image + } + } + + var body: some View { + + KFAnimatedImage(imageModel.url) + .callbackQueue(.dispatch(.global(qos: .background))) + .processingQueue(.dispatch(.global(qos: .background))) + .cacheOriginalImage() + .configure { view in + view.framePreloadCount = 1 } - .onEnded { value in - scale *= value + .scaleFactor(UIScreen.main.scale) + .loadDiskFileSynchronously() + .fade(duration: 0.1) + .imageModifier(ImageHandler(handler: $image)) + .onFailure { _ in + imageModel.downloadFailed() } - } - - var dragGesture: some Gesture { - DragGesture() - .updating($offsetState) { currentState, gestureState, _ in - gestureState = currentState.translation - }.onEnded { value in - offset.height += value.translation.height - offset.width += value.translation.width + .id(imageModel.refreshID) + .clipShape(Circle()) + .modifier(ImageContextMenuModifier(url: imageModel.url, image: image, showShareSheet: $showShareSheet)) + .sheet(isPresented: $showShareSheet) { + ShareSheet(activityItems: [imageModel.url]) } } +} - var doubleTapGesture : some Gesture { - TapGesture(count: 2).onEnded { value in - resetStatus() +struct ProfileZoomView: View { + + let pubkey: String + let profiles: Profiles + + @Environment(\.presentationMode) var presentationMode + + var navBarView: some View { + HStack { + Button(action: { + presentationMode.wrappedValue.dismiss() + }, label: { + Image(systemName: "xmark") + .frame(width: 33, height: 33) + .background(.regularMaterial) + .clipShape(Circle()) + }) + + Spacer() } + .padding() } var body: some View { - ZStack(alignment: .topLeading) { - Color("DamusDarkGrey") // Or Color("DamusBlack") - .edgesIgnoringSafeArea(.all) + ZStack { + Color(.systemBackground) + .ignoresSafeArea() - Button { - presentationMode.wrappedValue.dismiss() - } label: { - Image(systemName: "xmark") - .foregroundColor(.white) - .font(.subheadline) - .padding(.leading, 20) - } - .zIndex(1) - - VStack(alignment: .center) { - - Spacer() - - ProfilePicView(pubkey: pubkey, size: 200.0, highlight: .none, profiles: profiles) - .padding(100) - .scaledToFit() - .scaleEffect(self.scale * scaleState) - .offset(x: offset.width + offsetState.width, y: offset.height + offsetState.height) - .gesture(SimultaneousGesture(zoomGesture, dragGesture)) - .gesture(doubleTapGesture) - .modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: { - presentationMode.wrappedValue.dismiss() - })) - - Spacer() - + ZoomableScrollView { + ImageContainerView(url: get_profile_url(picture: nil, pubkey: pubkey, profiles: profiles)) + .aspectRatio(contentMode: .fit) + .padding(.top, Theme.safeAreaInsets?.top) + .padding(.bottom, Theme.safeAreaInsets?.bottom) + .padding(.horizontal) } + .ignoresSafeArea() + .modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: { + presentationMode.wrappedValue.dismiss() + })) } + .overlay(navBarView, alignment: .top) } }