From 5dc38b6c9194a9a3b2de8720049efbb436b251b0 Mon Sep 17 00:00:00 2001 From: "benoit.martins" Date: Fri, 27 Oct 2023 17:04:11 +0200 Subject: [PATCH] add bottom sheet to add a friend to the favorites list --- Linphone.xcodeproj/project.pbxproj | 12 ++ .../heart.imageset/Contents.json | 21 ++ .../Assets.xcassets/heart.imageset/heart.svg | 1 + .../share-network.imageset/Contents.json | 21 ++ .../share-network.imageset/share-network.svg | 1 + .../trash-simple.imageset/Contents.json | 21 ++ .../trash-simple.imageset/trash-simple.svg | 1 + Linphone/Localizable.xcstrings | 12 ++ .../Contacts/Fragments/ContactsFragment.swift | 57 +----- .../Fragments/ContactsInnerFragment.swift | 78 ++++++++ .../Fragments/ContactsListBottomSheet.swift | 155 +++++++++++++++ .../Fragments/ContactsListFragment.swift | 188 ++++++++++-------- .../FavoriteContactsListFragment.swift | 32 ++- .../Contacts/ViewModel/ContactViewModel.swift | 2 + .../UI/Main/Fragments/CustomBottomSheet.swift | 107 ++++++++++ 15 files changed, 568 insertions(+), 141 deletions(-) create mode 100644 Linphone/Assets.xcassets/heart.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/heart.imageset/heart.svg create mode 100644 Linphone/Assets.xcassets/share-network.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/share-network.imageset/share-network.svg create mode 100644 Linphone/Assets.xcassets/trash-simple.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/trash-simple.imageset/trash-simple.svg create mode 100644 Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift create mode 100644 Linphone/UI/Main/Contacts/Fragments/ContactsListBottomSheet.swift create mode 100644 Linphone/UI/Main/Fragments/CustomBottomSheet.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 1ab50b742..83de29f47 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -56,6 +56,9 @@ D7DA67642ACCB31700E95002 /* ProfileModeFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */; }; D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */; }; D7E6D04B2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D04A2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift */; }; + D7E6D04D2AEBD77600A57AAF /* CustomBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D04C2AEBD77600A57AAF /* CustomBottomSheet.swift */; }; + D7E6D0512AEBDBD500A57AAF /* ContactsListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D0502AEBDBD500A57AAF /* ContactsListBottomSheet.swift */; }; + D7E6D0552AEBFCCE00A57AAF /* ContactsInnerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D0542AEBFCCE00A57AAF /* ContactsInnerFragment.swift */; }; D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EAACCE2AD6ED8000AA6A8A /* PermissionsFragment.swift */; }; D7FB55112AD447FD00A5AB15 /* RegisterFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FB55102AD447FD00A5AB15 /* RegisterFragment.swift */; }; /* End PBXBuildFile section */ @@ -114,6 +117,9 @@ D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModeFragment.swift; sourceTree = ""; }; D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteContactsListFragment.swift; sourceTree = ""; }; D7E6D04A2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteContactsListViewModel.swift; sourceTree = ""; }; + D7E6D04C2AEBD77600A57AAF /* CustomBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomBottomSheet.swift; sourceTree = ""; }; + D7E6D0502AEBDBD500A57AAF /* ContactsListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsListBottomSheet.swift; sourceTree = ""; }; + D7E6D0542AEBFCCE00A57AAF /* ContactsInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsInnerFragment.swift; sourceTree = ""; }; D7EAACCE2AD6ED8000AA6A8A /* PermissionsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsFragment.swift; sourceTree = ""; }; D7FB55102AD447FD00A5AB15 /* RegisterFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterFragment.swift; sourceTree = ""; }; FB718F405DAF7B9993AEB878 /* Pods-Linphone.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Linphone.debug.xcconfig"; path = "Target Support Files/Pods-Linphone/Pods-Linphone.debug.xcconfig"; sourceTree = ""; }; @@ -288,6 +294,7 @@ D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */, D706BA812ADD72D100278F45 /* DeviceRotationViewModifier.swift */, D72250682ADFBF2D008FB426 /* SideMenu.swift */, + D7E6D04C2AEBD77600A57AAF /* CustomBottomSheet.swift */, ); path = Fragments; sourceTree = ""; @@ -316,6 +323,8 @@ D71FCA802AE14CFC00D2E43E /* ContactsListFragment.swift */, D71FCA822AE14D6E00D2E43E /* ContactFragment.swift */, D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */, + D7E6D0502AEBDBD500A57AAF /* ContactsListBottomSheet.swift */, + D7E6D0542AEBFCCE00A57AAF /* ContactsInnerFragment.swift */, ); path = Fragments; sourceTree = ""; @@ -539,18 +548,21 @@ D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */, D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */, D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */, + D7E6D04D2AEBD77600A57AAF /* CustomBottomSheet.swift in Sources */, D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */, D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */, D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */, D71FCA812AE14CFC00D2E43E /* ContactsListFragment.swift in Sources */, D719ABB72ABC67BF00B41C10 /* LinphoneApp.swift in Sources */, D72250632ADE9615008FB426 /* HistoryViewModel.swift in Sources */, + D7E6D0512AEBDBD500A57AAF /* ContactsListBottomSheet.swift in Sources */, D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */, D7A03FC62ACC458A0081A588 /* SplashScreen.swift in Sources */, D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */, D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */, D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */, D74C9CF82ACACECE0021626A /* WelcomePage1Fragment.swift in Sources */, + D7E6D0552AEBFCCE00A57AAF /* ContactsInnerFragment.swift in Sources */, D72343362AD037AF009AA24E /* ToastView.swift in Sources */, D7FB55112AD447FD00A5AB15 /* RegisterFragment.swift in Sources */, D72343322ACEFF58009AA24E /* QRScannerController.swift in Sources */, diff --git a/Linphone/Assets.xcassets/heart.imageset/Contents.json b/Linphone/Assets.xcassets/heart.imageset/Contents.json new file mode 100644 index 000000000..0b277d7d6 --- /dev/null +++ b/Linphone/Assets.xcassets/heart.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "heart.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/heart.imageset/heart.svg b/Linphone/Assets.xcassets/heart.imageset/heart.svg new file mode 100644 index 000000000..98674b649 --- /dev/null +++ b/Linphone/Assets.xcassets/heart.imageset/heart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/share-network.imageset/Contents.json b/Linphone/Assets.xcassets/share-network.imageset/Contents.json new file mode 100644 index 000000000..46fe445fb --- /dev/null +++ b/Linphone/Assets.xcassets/share-network.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "share-network.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/share-network.imageset/share-network.svg b/Linphone/Assets.xcassets/share-network.imageset/share-network.svg new file mode 100644 index 000000000..835f930ac --- /dev/null +++ b/Linphone/Assets.xcassets/share-network.imageset/share-network.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/trash-simple.imageset/Contents.json b/Linphone/Assets.xcassets/trash-simple.imageset/Contents.json new file mode 100644 index 000000000..3c4c29331 --- /dev/null +++ b/Linphone/Assets.xcassets/trash-simple.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "trash-simple.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/trash-simple.imageset/trash-simple.svg b/Linphone/Assets.xcassets/trash-simple.imageset/trash-simple.svg new file mode 100644 index 000000000..6e48d22fc --- /dev/null +++ b/Linphone/Assets.xcassets/trash-simple.imageset/trash-simple.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 28e74c979..ab4385540 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -92,6 +92,9 @@ }, "Accept all" : { + }, + "Add to favourites" : { + }, "All contacts" : { @@ -142,6 +145,9 @@ }, "Default mode" : { + }, + "Delete" : { + }, "Demande d’autorisations" : { @@ -249,6 +255,9 @@ }, "Register" : { + }, + "Remove to favourites" : { + }, "Scan QR code" : { @@ -261,6 +270,9 @@ }, "See Linphone contact" : { + }, + "Share" : { + }, "sip.linphone.org" : { diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift index 155004e04..5c0b65bb0 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift @@ -21,57 +21,22 @@ import SwiftUI struct ContactsFragment: View { - @ObservedObject var magicSearch = MagicSearchSingleton.shared @ObservedObject var contactViewModel: ContactViewModel - @State private var orientation = UIDevice.current.orientation - - @State var isFavoriteOpen: Bool = true + @State private var showingSheet = false var body: some View { - VStack(alignment: .leading) { - if !magicSearch.lastSearch.filter({ $0.friend?.starred == true }).isEmpty { - HStack(alignment: .center) { - Text("Favourites") - .default_text_style_800(styleSize: 16) - - Spacer() - - Image(isFavoriteOpen ? "caret-up" : "caret-down") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c600) - .frame(width: 25, height: 25, alignment: .leading) + if #available(iOS 16.0, *) { + ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet) + .sheet(isPresented: $showingSheet) { + ContactsListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet) + .presentationDetents([.fraction(0.2)]) } - .padding(.top, 30) - .padding(.horizontal, 16) - .background(.white) - .onTapGesture { - withAnimation { - isFavoriteOpen.toggle() - } - } - - if isFavoriteOpen { - FavoriteContactsListFragment(contactViewModel: contactViewModel, favoriteContactsListViewModel: FavoriteContactsListViewModel()) - .zIndex(-1) - .transition(.move(edge: .top)) - } - - HStack(alignment: .center) { - Text("All contacts") - .default_text_style_800(styleSize: 16) - - Spacer() - } - .padding(.top, 10) - .padding(.horizontal, 16) - } - ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel()) - } - .navigationBarHidden(true) - .onRotate { newOrientation in - orientation = newOrientation + } else { + ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet) + .halfSheet(showSheet: $showingSheet) { + ContactsListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet) + } onDismiss: {} } } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift new file mode 100644 index 000000000..a04b3bc68 --- /dev/null +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import SwiftUI + +struct ContactsInnerFragment: View { + + @ObservedObject var magicSearch = MagicSearchSingleton.shared + @ObservedObject var contactViewModel: ContactViewModel + + @State private var isFavoriteOpen = true + + @Binding var showingSheet: Bool + + var body: some View { + VStack(alignment: .leading) { + if !magicSearch.lastSearch.filter({ $0.friend?.starred == true }).isEmpty { + HStack(alignment: .center) { + Text("Favourites") + .default_text_style_800(styleSize: 16) + + Spacer() + + Image(isFavoriteOpen ? "caret-up" : "caret-down") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + } + .padding(.top, 30) + .padding(.horizontal, 16) + .background(.white) + .onTapGesture { + withAnimation { + isFavoriteOpen.toggle() + } + } + + if isFavoriteOpen { + FavoriteContactsListFragment(contactViewModel: contactViewModel, favoriteContactsListViewModel: FavoriteContactsListViewModel(), showingSheet: $showingSheet) + .zIndex(-1) + .transition(.move(edge: .top)) + } + + HStack(alignment: .center) { + Text("All contacts") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.top, 10) + .padding(.horizontal, 16) + } + ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet) + } + .navigationBarHidden(true) + } +} + +#Preview { + ContactsInnerFragment(contactViewModel: ContactViewModel(), showingSheet: .constant(false)) +} diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsListBottomSheet.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsListBottomSheet.swift new file mode 100644 index 000000000..a55a0b96b --- /dev/null +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsListBottomSheet.swift @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import SwiftUI +import linphonesw + +struct ContactsListBottomSheet: View { + + @ObservedObject var magicSearch = MagicSearchSingleton.shared + + @ObservedObject var contactViewModel: ContactViewModel + + @State private var orientation = UIDevice.current.orientation + + @Environment(\.dismiss) var dismiss + + @Binding var showingSheet: Bool + + var body: some View { + VStack(alignment: .leading) { + if orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height { + Spacer() + HStack { + Spacer() + Button("Close") { + if #available(iOS 16.0, *) { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } + } + .padding(.trailing) + } + + Spacer() + Button { + if contactViewModel.selectedFriend != nil { + contactViewModel.selectedFriend!.starred.toggle() + } + self.magicSearch.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + + if #available(iOS 16.0, *) { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } label: { + HStack { + Image("heart") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + Text(contactViewModel.selectedFriend != nil && contactViewModel.selectedFriend!.starred == true + ? "Remove to favourites" + : "Add to favourites") + .default_text_style(styleSize: 16) + Spacer() + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + + VStack { + Divider() + } + .frame(maxWidth: .infinity) + + Button { + if #available(iOS 16.0, *) { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } label: { + HStack { + Image("share-network") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + Text("Share") + .default_text_style(styleSize: 16) + Spacer() + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + + VStack { + Divider() + } + .frame(maxWidth: .infinity) + + Button { + if contactViewModel.selectedFriend != nil { + contactViewModel.selectedFriend!.remove() + } + self.magicSearch.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + + if #available(iOS 16.0, *) { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } label: { + HStack { + Image("trash-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.redDanger500) + .frame(width: 25, height: 25, alignment: .leading) + Text("Delete") + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 16) + Spacer() + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + + } + .onRotate { newOrientation in + orientation = newOrientation + } + .background(Color.gray100) + .frame(maxWidth: .infinity) + } +} diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift index 174f00fa0..527488c95 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift @@ -21,95 +21,109 @@ import SwiftUI import linphonesw struct ContactsListFragment: View { - - @ObservedObject var magicSearch = MagicSearchSingleton.shared - - @ObservedObject var contactViewModel: ContactViewModel - @ObservedObject var contactsListViewModel: ContactsListViewModel - - var body: some View { - VStack { - List { - ForEach(0... + */ + +import SwiftUI + +extension View { + //binding show bariable... + func halfSheet( + showSheet: Binding, + @ViewBuilder content: @escaping () -> Content, + onDismiss: @escaping () -> Void + ) -> some View { + return self + .background( + HalfSheetHelper(sheetView: content(), showSheet: showSheet, onDismiss: onDismiss) + ) + } +} + +// UIKit integration +struct HalfSheetHelper: UIViewControllerRepresentable { + + var sheetView: Content + let controller: UIViewController = UIViewController() + @Binding var showSheet: Bool + var onDismiss: () -> Void + + func makeCoordinator() -> Coordinator { + Coordinator(parent: self) + } + + func makeUIViewController(context: Context) -> UIViewController { + controller.view.backgroundColor = .clear + return controller + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + if showSheet { + let sheetController = CustomHostingController(rootView: sheetView) + sheetController.presentationController?.delegate = context.coordinator + uiViewController.present(sheetController, animated: true) + } + } + + //on dismiss... + final class Coordinator: NSObject, UISheetPresentationControllerDelegate { + + var parent: HalfSheetHelper + + init(parent: HalfSheetHelper) { + self.parent = parent + } + + func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { + parent.showSheet = false + } + } +} + +// Custom UIHostingController for halfSheet... +final class CustomHostingController: UIHostingController { + override func viewDidLoad() { + view.backgroundColor = .clear + if let presentationController = presentationController as? UISheetPresentationController { + presentationController.detents = [ + .medium() + ] + + //MARK: - sheet grabber visbility + presentationController.prefersGrabberVisible = false // i wanted to design my own grabber hehehe + + // this allows you to scroll even during medium detent + presentationController.prefersScrollingExpandsWhenScrolledToEdge = false + + //MARK: - sheet corner radius + presentationController.preferredCornerRadius = 30 + + // for more sheet customisation check out this great article https://sarunw.com/posts/bottom-sheet-in-ios-15-with-uisheetpresentationcontroller/#scrolling + } + } +} + +public struct LazyView: View { + private let build: () -> Content + public init(_ build: @autoclosure @escaping () -> Content) { + self.build = build + } + public var body: Content { + build() + } +}