ios: hide user picker sheet instantly, when opening another sheet

This commit is contained in:
Levitating Pineapple
2024-09-23 18:43:05 +03:00
parent d5507f2fa3
commit a35b20f54c
4 changed files with 215 additions and 69 deletions

View File

@@ -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 }
}
}

View File

@@ -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 {

View 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)
/// ]
/// )
/// }
/// }
/// }

View File

@@ -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 */,