mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-16 00:35:52 +00:00
ios: hide user picker sheet instantly, when opening another sheet
This commit is contained in:
@@ -58,43 +58,48 @@ struct ChatListView: View {
|
||||
destination: chatView
|
||||
) { chatListView }
|
||||
}
|
||||
.sheet(isPresented: $userPickerShown) {
|
||||
UserPicker(activeSheet: $activeUserPickerSheet)
|
||||
.sheet(item: $activeUserPickerSheet) { sheet in
|
||||
if let currentUser = chatModel.currentUser {
|
||||
switch sheet {
|
||||
case .address:
|
||||
NavigationView {
|
||||
UserAddressView(shareViaProfile: currentUser.addressShared)
|
||||
.navigationTitle("SimpleX address")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
case .chatProfiles:
|
||||
NavigationView {
|
||||
UserProfilesView()
|
||||
}
|
||||
case .currentProfile:
|
||||
NavigationView {
|
||||
UserProfile()
|
||||
.navigationTitle("Your current profile")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
case .chatPreferences:
|
||||
NavigationView {
|
||||
PreferencesView(profile: currentUser.profile, preferences: currentUser.fullPreferences, currentPreferences: currentUser.fullPreferences)
|
||||
.navigationTitle("Your preferences")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
case .useFromDesktop:
|
||||
ConnectDesktopView(viaSettings: false)
|
||||
case .settings:
|
||||
SettingsView(showSettings: $showSettings)
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
.modifier(
|
||||
SwiftUISheet(height: 400, isPresented: $userPickerShown) {
|
||||
UserPicker(activeSheet: $activeUserPickerSheet)
|
||||
}
|
||||
)
|
||||
.sheet(item: $activeUserPickerSheet) { sheet in
|
||||
if let currentUser = chatModel.currentUser {
|
||||
switch sheet {
|
||||
case .address:
|
||||
NavigationView {
|
||||
UserAddressView(shareViaProfile: currentUser.addressShared)
|
||||
.navigationTitle("SimpleX address")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
case .chatProfiles:
|
||||
NavigationView {
|
||||
UserProfilesView()
|
||||
}
|
||||
case .currentProfile:
|
||||
NavigationView {
|
||||
UserProfile()
|
||||
.navigationTitle("Your current profile")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
case .chatPreferences:
|
||||
NavigationView {
|
||||
PreferencesView(profile: currentUser.profile, preferences: currentUser.fullPreferences, currentPreferences: currentUser.fullPreferences)
|
||||
.navigationTitle("Your preferences")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
case .useFromDesktop:
|
||||
ConnectDesktopView(viaSettings: false)
|
||||
case .settings:
|
||||
SettingsView(showSettings: $showSettings)
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: activeUserPickerSheet) {
|
||||
if $0 != nil { userPickerShown = false }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,31 +21,18 @@ struct UserPicker: View {
|
||||
// Inset grouped list dimensions
|
||||
private let imageSize: CGFloat = 44
|
||||
private let rowPadding: CGFloat = 16
|
||||
private let rowVerticalPadding: CGFloat = 10
|
||||
private let sectionSpacing: CGFloat = 35
|
||||
private var sectionHorizontalPadding: CGFloat { frameWidth > 375 ? 20 : 16 }
|
||||
private let sectionShape = RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
let v = viewBody.presentationDetents([.height(400)])
|
||||
if #available(iOS 16.4, *) {
|
||||
v.scrollBounceBehavior(.basedOnSize)
|
||||
} else {
|
||||
v
|
||||
}
|
||||
} else {
|
||||
viewBody
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var viewBody: some View {
|
||||
let otherUsers: [UserInfo] = m.users
|
||||
.filter { u in !u.user.hidden && u.user.userId != m.currentUser?.userId }
|
||||
.sorted(using: KeyPathComparator<UserInfo>(\.user.activeOrder, order: .reverse))
|
||||
let sectionWidth = max(frameWidth - sectionHorizontalPadding * 2, 0)
|
||||
let currentUserWidth = max(frameWidth - sectionHorizontalPadding - rowPadding * 2 - 14 - imageSize, 0)
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: sectionSpacing) {
|
||||
if let user = m.currentUser {
|
||||
StickyScrollView {
|
||||
HStack(spacing: rowPadding) {
|
||||
@@ -56,7 +43,7 @@ struct UserPicker: View {
|
||||
}
|
||||
.padding(rowPadding)
|
||||
.frame(width: otherUsers.isEmpty ? sectionWidth : currentUserWidth, alignment: .leading)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.background(elevatedSecondarySystemGroupedBackground(colorScheme))
|
||||
.clipShape(sectionShape)
|
||||
.onTapGesture { activeSheet = .currentProfile }
|
||||
ForEach(otherUsers) { u in
|
||||
@@ -72,20 +59,23 @@ struct UserPicker: View {
|
||||
.overlay(DetermineWidth())
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
|
||||
}
|
||||
List {
|
||||
Section {
|
||||
openSheetOnTap("qrcode", title: m.userAddress == nil ? "Create SimpleX address" : "Your SimpleX address", sheet: .address)
|
||||
openSheetOnTap("switch.2", title: "Chat preferences", sheet: .chatPreferences)
|
||||
openSheetOnTap("person.crop.rectangle.stack", title: "Your chat profiles", sheet: .chatProfiles)
|
||||
openSheetOnTap("desktopcomputer", title: "Use from desktop", sheet: .useFromDesktop)
|
||||
|
||||
ZStack(alignment: .trailing) {
|
||||
openSheetOnTap("gearshape", title: "Settings", sheet: .settings)
|
||||
Image(systemName: colorScheme == .light ? "sun.max" : "moon.fill")
|
||||
VStack(spacing: 0) {
|
||||
openSheetOnTap("qrcode", title: m.userAddress == nil ? "Create SimpleX address" : "Your SimpleX address", sheet: .address)
|
||||
listDivider
|
||||
openSheetOnTap("switch.2", title: "Chat preferences", sheet: .chatPreferences)
|
||||
listDivider
|
||||
openSheetOnTap("person.crop.rectangle.stack", title: "Your chat profiles", sheet: .chatProfiles)
|
||||
listDivider
|
||||
openSheetOnTap("desktopcomputer", title: "Use from desktop", sheet: .useFromDesktop)
|
||||
listDivider
|
||||
ZStack(alignment: .trailing) {
|
||||
openSheetOnTap("gearshape", title: "Settings", sheet: .settings)
|
||||
Image(systemName: colorScheme == .light ? "sun.max" : "moon.fill")
|
||||
.resizable()
|
||||
.symbolRenderingMode(.monochrome)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.frame(maxWidth: 20, maxHeight: 20)
|
||||
.padding(.horizontal, rowPadding)
|
||||
.onTapGesture {
|
||||
if (colorScheme == .light) {
|
||||
ThemeManager.applyTheme(systemDarkThemeDefault.get())
|
||||
@@ -96,9 +86,11 @@ struct UserPicker: View {
|
||||
.onLongPressGesture {
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(elevatedSecondarySystemGroupedBackground(colorScheme))
|
||||
.clipShape(sectionShape)
|
||||
.padding(.horizontal, sectionHorizontalPadding)
|
||||
}
|
||||
.onAppear {
|
||||
// This check prevents the call of listUsers after the app is suspended, and the database is closed.
|
||||
@@ -121,6 +113,10 @@ struct UserPicker: View {
|
||||
.disabled(switchingProfile)
|
||||
}
|
||||
|
||||
private var listDivider: some View {
|
||||
Divider().padding(.leading, 48)
|
||||
}
|
||||
|
||||
private func userView(_ u: UserInfo, size: CGFloat) -> some View {
|
||||
HStack {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
@@ -133,7 +129,7 @@ struct UserPicker: View {
|
||||
Text(u.user.displayName).font(.title2).lineLimit(1)
|
||||
}
|
||||
.padding(rowPadding)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.background(elevatedSecondarySystemGroupedBackground(colorScheme))
|
||||
.clipShape(sectionShape)
|
||||
.onTapGesture {
|
||||
switchingProfile = true
|
||||
@@ -156,15 +152,14 @@ struct UserPicker: View {
|
||||
}
|
||||
|
||||
private func openSheetOnTap(_ icon: String, title: LocalizedStringKey, sheet: UserPickerSheet) -> some View {
|
||||
Button {
|
||||
activeSheet = sheet
|
||||
} label: {
|
||||
settingsRow(icon, color: theme.colors.secondary) {
|
||||
Text(title).foregroundColor(.primary)
|
||||
}
|
||||
settingsRow(icon, color: theme.colors.secondary) {
|
||||
Text(title).foregroundColor(.primary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, rowPadding)
|
||||
.padding(.vertical, rowVerticalPadding)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { activeSheet = sheet }
|
||||
}
|
||||
|
||||
private func unreadBadge(_ u: UserInfo) -> some View {
|
||||
|
||||
142
apps/ios/Shared/Views/Helpers/SwiftUISheet.swift
Normal file
142
apps/ios/Shared/Views/Helpers/SwiftUISheet.swift
Normal file
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// SwiftUISheet.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by user on 23/09/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SwiftUISheet<SheetContent: View>: ViewModifier {
|
||||
let height: Double
|
||||
@Binding var isPresented: Bool
|
||||
@ViewBuilder let sheetContent: () -> SheetContent
|
||||
@Environment(\.colorScheme) private var colorScheme: ColorScheme
|
||||
|
||||
// Represents offset relative to the height of the sheet
|
||||
// - 0: Collapsed
|
||||
// - 1: Fully expanded
|
||||
@State private var relativeOffset: Double = 0
|
||||
private let radius: Double = 16
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
let safeAreaInset: Double =
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first {
|
||||
window.safeAreaInsets.bottom
|
||||
} else { 0 }
|
||||
let sheetHeight = height + safeAreaInset
|
||||
ZStack {
|
||||
content
|
||||
Color.black.opacity(0.35 * relativeOffset)
|
||||
.ignoresSafeArea()
|
||||
.onTapGesture { isPresented = false }
|
||||
sheetContent()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.background(elevatedSystemGroupedBackground(colorScheme))
|
||||
.clipShape(ClipShape(bottomSafeAreaInset: safeAreaInset))
|
||||
.frame(height: height)
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 8)
|
||||
.onChanged {
|
||||
relativeOffset = min(max(relativeOffset - $0.translation.height / sheetHeight, 0), 1)
|
||||
}
|
||||
.onEnded {
|
||||
isPresented = relativeOffset + $0.predictedEndTranslation.height / sheetHeight > 0.5
|
||||
animate(with: $0.velocity.height)
|
||||
}
|
||||
)
|
||||
.frame(maxHeight: .infinity, alignment: .bottom)
|
||||
.offset(y: sheetHeight - sheetHeight * relativeOffset)
|
||||
}
|
||||
.onChange(of: isPresented) { _ in animate() }
|
||||
}
|
||||
|
||||
private var bottomSafeAreaInset: Double {
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first {
|
||||
window.safeAreaInsets.bottom
|
||||
} else { 0 }
|
||||
}
|
||||
|
||||
private func animate(with releaseVelocity: CGFloat? = nil) {
|
||||
// TODO: Tune animation speed depending on drag gesture's final velocity
|
||||
withAnimation { relativeOffset = isPresented ? 1 : 0 }
|
||||
}
|
||||
|
||||
struct ClipShape: Shape {
|
||||
let bottomSafeAreaInset: Double
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
Path(
|
||||
UIBezierPath(
|
||||
roundedRect: CGRect(
|
||||
origin: .zero,
|
||||
size: CGSize(width: rect.width, height: rect.height + bottomSafeAreaInset)
|
||||
),
|
||||
byRoundingCorners: [.topLeft, .topRight],
|
||||
cornerRadii: CGSize(width: 10, height: 10)
|
||||
).cgPath
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func elevatedSystemGroupedBackground(_ colorScheme: ColorScheme) -> Color {
|
||||
switch colorScheme {
|
||||
case .dark: Color(0xFF1C1C1E)
|
||||
default: Color(0xFFF2F2F7)
|
||||
}
|
||||
}
|
||||
|
||||
func elevatedSecondarySystemGroupedBackground(_ colorScheme: ColorScheme) -> Color {
|
||||
switch colorScheme {
|
||||
case .dark: Color(0xFF2C2C2E)
|
||||
default: Color(0xFFFFFFFF)
|
||||
}
|
||||
}
|
||||
|
||||
/// # Extracting Sheet Colors Programatically
|
||||
///
|
||||
/// System colors are returned dynamically, depending on the context:
|
||||
///
|
||||
/// struct ColorResolverView: View {
|
||||
/// @Environment(\.self) var environment
|
||||
/// let colors: [Color]
|
||||
///
|
||||
/// var body: some View {
|
||||
/// HStack {
|
||||
/// column.environment(\.colorScheme, .dark)
|
||||
/// column.environment(\.colorScheme, .light)
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// var column: some View {
|
||||
/// VStack {
|
||||
/// ForEach(colors, id: \.self) {
|
||||
/// Text("\($0.resolve(in: environment))")
|
||||
/// .frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
/// .background($0)
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// Place `ColorResolverView` inside a sheet to acquire elevated color versions:
|
||||
///
|
||||
/// struct ContentView: View {
|
||||
/// var body: some View {
|
||||
/// EmptyView()
|
||||
/// .sheet(isPresented: .constant(true)) {
|
||||
/// ColorResolverView(
|
||||
/// colors: [
|
||||
/// Color(.systemGroupedBackground),
|
||||
/// Color(.secondarySystemGroupedBackground)
|
||||
/// ]
|
||||
/// )
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
|
||||
|
||||
@@ -211,6 +211,7 @@
|
||||
CEE723F02C3D25C70009AE93 /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723EF2C3D25C70009AE93 /* ShareView.swift */; };
|
||||
CEE723F22C3D25ED0009AE93 /* ShareModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723F12C3D25ED0009AE93 /* ShareModel.swift */; };
|
||||
CEEA861D2C2ABCB50084E1EA /* ReverseList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */; };
|
||||
CEFB2EDF2CA1BCC7004B1ECE /* SwiftUISheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFB2EDE2CA1BCC7004B1ECE /* SwiftUISheet.swift */; };
|
||||
D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; };
|
||||
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
|
||||
D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547729AF89AF0022400A /* StoreKit.framework */; };
|
||||
@@ -553,6 +554,7 @@
|
||||
CEE723EF2C3D25C70009AE93 /* ShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = "<group>"; };
|
||||
CEE723F12C3D25ED0009AE93 /* ShareModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareModel.swift; sourceTree = "<group>"; };
|
||||
CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseList.swift; sourceTree = "<group>"; };
|
||||
CEFB2EDE2CA1BCC7004B1ECE /* SwiftUISheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUISheet.swift; sourceTree = "<group>"; };
|
||||
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
|
||||
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
@@ -799,6 +801,7 @@
|
||||
CE7548092C622630009579B7 /* SwipeLabel.swift */,
|
||||
CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */,
|
||||
CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */,
|
||||
CEFB2EDE2CA1BCC7004B1ECE /* SwiftUISheet.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
@@ -1449,6 +1452,7 @@
|
||||
5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */,
|
||||
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */,
|
||||
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
|
||||
CEFB2EDF2CA1BCC7004B1ECE /* SwiftUISheet.swift in Sources */,
|
||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */,
|
||||
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */,
|
||||
5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */,
|
||||
|
||||
Reference in New Issue
Block a user