From e9784ddc6173dd08f85bcdfbe45d794664ef9408 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 11 Jan 2024 14:17:05 +0100 Subject: [PATCH] Add custom bottom sheet in call view --- Linphone/UI/Call/CallView.swift | 1167 +++++++++-------- .../Contacts/Fragments/ContactFragment.swift | 66 +- .../Contacts/Fragments/ContactsFragment.swift | 48 +- .../Fragments/EditContactFragment.swift | 38 +- Linphone/UI/Main/ContentView.swift | 55 +- .../History/Fragments/HistoryFragment.swift | 43 +- 6 files changed, 715 insertions(+), 702 deletions(-) diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index 8d06e91b6..755575926 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -49,390 +49,131 @@ struct CallView: View { var body: some View { GeometryReader { geo in - if #available(iOS 16.4, *) { + if #available(iOS 16.0, *), idiom != .pad { innerView(geometry: geo) - .sheet(isPresented: - .constant( - telecomManager.callStarted - && !fullscreenVideo - && !hideButtonsSheet - && idiom != .pad - && !(orientation == .landscapeLeft || orientation == .landscapeRight || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) - ) - ) { - GeometryReader { _ in - VStack(spacing: 0) { - HStack(spacing: 12) { - Button { - callViewModel.terminateCall() - } label: { - Image("phone-disconnect") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - - } - .frame(width: 90, height: 60) - .background(Color.redDanger500) - .cornerRadius(40) - - Spacer() - - Button { - callViewModel.toggleVideo() - } label: { - Image(callViewModel.cameraDisplayed ? "video-camera" : "video-camera-slash") - .renderingMode(.template) - .resizable() - .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) - .frame(width: 32, height: 32) - - } - .frame(width: 60, height: 60) - .background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : Color.gray500) - .cornerRadius(40) - .disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) - - Button { - callViewModel.toggleMuteMicrophone() - } label: { - Image(callViewModel.micMutted ? "microphone-slash" : "microphone") - .renderingMode(.template) - .resizable() - .foregroundStyle(callViewModel.micMutted ? .black : .white) - .frame(width: 32, height: 32) - - } - .frame(width: 60, height: 60) - .background(callViewModel.micMutted ? .white : Color.gray500) - .cornerRadius(40) - - Button { - if AVAudioSession.sharedInstance().availableInputs != nil - && !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty { - - hideButtonsSheet = true - - DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { - audioRouteSheet = true - } - } else { - do { - try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty ? .speaker : .none) - } catch _ { - - } - } - - } label: { - Image(imageAudioRoute) - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - .onAppear(perform: getAudioRouteImage) - .onReceive(pub) { _ in - self.getAudioRouteImage() - } - - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - } - .frame(height: geo.size.height * 0.15) - .padding(.horizontal, 20) - .padding(.top, -6) - - HStack(spacing: 0) { - VStack { - Button { - } label: { - Image("phone-transfer") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text("Transfer") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - - VStack { - Button { - } label: { - Image("phone-plus") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text("New call") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - - VStack { - Button { - } label: { - Image("phone-list") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text("Call list") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - - VStack { - Button { - } label: { - Image("dialer") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text("Dialer") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - } - .frame(height: geo.size.height * 0.15) - - HStack(spacing: 0) { - VStack { - Button { - } label: { - Image("chat-teardrop-text") - .renderingMode(.template) - .resizable() - //.foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) - .foregroundStyle(Color.gray500) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - //.background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : Color.gray500) - .background(Color.gray600) - .cornerRadius(40) - //.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) - .disabled(true) - - Text("Messages") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - - VStack { - Button { - callViewModel.togglePause() - } label: { - Image(callViewModel.isPaused ? "play" : "pause") - .renderingMode(.template) - .resizable() - .foregroundStyle(telecomManager.isPausedByRemote ? Color.gray500 : .white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(telecomManager.isPausedByRemote ? Color.gray600 : (callViewModel.isPaused ? Color.greenSuccess500 : Color.gray500)) - .cornerRadius(40) - .disabled(telecomManager.isPausedByRemote) - - Text("Pause") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - - VStack { - Button { - callViewModel.toggleRecording() - } label: { - Image("record-fill") - .renderingMode(.template) - .resizable() - .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : (callViewModel.isRecording ? Color.redDanger500 : Color.gray500)) - .cornerRadius(40) - .disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) - - Text("Record") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - - VStack { - Button { - } label: { - Image("video-camera") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text("Disposition") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - .hidden() - } - .frame(height: geo.size.height * 0.15) - - Spacer() - } - .frame(maxHeight: .infinity, alignment: .top) - .presentationBackground(.black) - .presentationDetents([.fraction(0.1), .fraction(0.45)]) - .interactiveDismissDisabled() - .presentationBackgroundInteraction(.enabled) - } - } .sheet(isPresented: $audioRouteSheet, onDismiss: { audioRouteSheet = false hideButtonsSheet = false }) { - VStack(spacing: 0) { - Button(action: { - options = 1 - - do { - try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) - if callViewModel.isHeadPhoneAvailable() { - try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Receiver") }).first) - } else { - try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.first) - } - } catch _ { - - } - }, label: { - HStack { - Image(options == 1 ? "radio-button-fill" : "radio-button") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 25, height: 25, alignment: .leading) - - Text(!callViewModel.isHeadPhoneAvailable() ? "Earpiece" : "Headphones") - .default_text_style_white(styleSize: 15) - - Spacer() - - Image(!callViewModel.isHeadPhoneAvailable() ? "ear" : "headset") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 25, height: 25, alignment: .leading) - } - }) - .frame(maxHeight: .infinity) - - Button(action: { - options = 2 - - do { - try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) - } catch _ { - - } - }, label: { - HStack { - Image(options == 2 ? "radio-button-fill" : "radio-button") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 25, height: 25, alignment: .leading) - - Text("Speaker") - .default_text_style_white(styleSize: 15) - - Spacer() - - Image("speaker-high") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 25, height: 25, alignment: .leading) - } - }) - .frame(maxHeight: .infinity) - - Button(action: { - options = 3 - - do { - try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) - try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Bluetooth") }).first) - } catch _ { - - } - }, label: { - HStack { - Image(options == 3 ? "radio-button-fill" : "radio-button") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 25, height: 25, alignment: .leading) - - Text("Bluetooth") - .default_text_style_white(styleSize: 15) - - Spacer() - - Image("bluetooth") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 25, height: 25, alignment: .leading) - } - }) - .frame(maxHeight: .infinity) - } - .padding(.horizontal, 20) - .presentationBackground(Color.gray600) - .presentationDetents([.fraction(0.3)]) - .frame(maxHeight: .infinity) + innerBottomSheet() + .presentationDetents([.fraction(0.3)]) } } else { innerView(geometry: geo) + .halfSheet(showSheet: $audioRouteSheet) { + innerBottomSheet() + } onDismiss: { + audioRouteSheet = false + hideButtonsSheet = false + } } } } + @ViewBuilder + func innerBottomSheet() -> some View { + VStack(spacing: 0) { + Button(action: { + options = 1 + + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) + if callViewModel.isHeadPhoneAvailable() { + try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Receiver") }).first) + } else { + try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.first) + } + } catch _ { + + } + }, label: { + HStack { + Image(options == 1 ? "radio-button-fill" : "radio-button") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + + Text(!callViewModel.isHeadPhoneAvailable() ? "Earpiece" : "Headphones") + .default_text_style_white(styleSize: 15) + + Spacer() + + Image(!callViewModel.isHeadPhoneAvailable() ? "ear" : "headset") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + } + }) + .frame(maxHeight: .infinity) + + Button(action: { + options = 2 + + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) + } catch _ { + + } + }, label: { + HStack { + Image(options == 2 ? "radio-button-fill" : "radio-button") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + + Text("Speaker") + .default_text_style_white(styleSize: 15) + + Spacer() + + Image("speaker-high") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + } + }) + .frame(maxHeight: .infinity) + + Button(action: { + options = 3 + + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) + try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Bluetooth") }).first) + } catch _ { + + } + }, label: { + HStack { + Image(options == 3 ? "radio-button-fill" : "radio-button") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + + Text("Bluetooth") + .default_text_style_white(styleSize: 15) + + Spacer() + + Image("bluetooth") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 25, height: 25, alignment: .leading) + } + }) + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 20) + .background(Color.gray600) + .frame(maxHeight: .infinity) + } + @ViewBuilder // swiftlint:disable:next cyclomatic_complexity func innerView(geometry: GeometryProxy) -> some View { @@ -663,19 +404,20 @@ struct CallView: View { } .frame( maxWidth: fullscreenVideo ? geometry.size.width : geometry.size.width - 8, - maxHeight: fullscreenVideo ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - 140 + maxHeight: fullscreenVideo ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (0.1 * geometry.size.height) - 60 ) .background(.clear) } } .frame( maxWidth: fullscreenVideo ? geometry.size.width : geometry.size.width - 8, - maxHeight: fullscreenVideo ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - 140 + maxHeight: fullscreenVideo ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (0.1 * geometry.size.height) - 60 ) .background(Color.gray600) .cornerRadius(20) .padding(.horizontal, fullscreenVideo ? 0 : 4) .onRotate { newOrientation in + let oldOrientation = orientation orientation = newOrientation if orientation == .portrait || orientation == .portraitUpsideDown { angleDegree = 0 @@ -687,6 +429,14 @@ struct CallView: View { } } + if (oldOrientation != orientation && oldOrientation != .faceUp) || (oldOrientation == .faceUp && (orientation == .landscapeLeft || orientation == .landscapeRight)) { + telecomManager.callStarted = false + + DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { + telecomManager.callStarted = true + } + } + callViewModel.orientationUpdate(orientation: orientation) } .onAppear { @@ -700,191 +450,28 @@ struct CallView: View { } } + telecomManager.callStarted = false + + DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { + telecomManager.callStarted = true + } + callViewModel.orientationUpdate(orientation: orientation) } if !fullscreenVideo { if telecomManager.callStarted { - if #available(iOS 16.0, *) { - if telecomManager.callStarted && idiom != .pad && !(orientation == .landscapeLeft || orientation == .landscapeRight - || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { - HStack(spacing: 12) { - HStack { - - } - .frame(height: 60) - } - .padding(.horizontal, 25) - .padding(.top, 20) - } else { - HStack(spacing: 12) { - Button { - callViewModel.terminateCall() - } label: { - Image("phone-disconnect") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - - } - .frame(width: 90, height: 60) - .background(Color.redDanger500) - .cornerRadius(40) - - Spacer() - - Button { - callViewModel.toggleVideo() - } label: { - Image(callViewModel.cameraDisplayed ? "video-camera" : "video-camera-slash") - .renderingMode(.template) - .resizable() - .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) - .frame(width: 32, height: 32) - - } - .frame(width: 60, height: 60) - .background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : Color.gray500) - .cornerRadius(40) - .disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) - - Button { - callViewModel.toggleMuteMicrophone() - } label: { - Image(callViewModel.micMutted ? "microphone-slash" : "microphone") - .renderingMode(.template) - .resizable() - .foregroundStyle(callViewModel.micMutted ? .black : .white) - .frame(width: 32, height: 32) - - } - .frame(width: 60, height: 60) - .background(callViewModel.micMutted ? .white : Color.gray500) - .cornerRadius(40) - - Button { - if AVAudioSession.sharedInstance().availableInputs != nil - && !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty { - - hideButtonsSheet = true - - DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { - audioRouteSheet = true - } - } else { - do { - try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty ? .speaker : .none) - } catch _ { - - } - } - - } label: { - Image(imageAudioRoute) - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - .onAppear(perform: getAudioRouteImage) - .onReceive(pub) { _ in - self.getAudioRouteImage() - } - - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - } - .frame(height: geometry.size.height * 0.15) - .padding(.horizontal, 20) - .padding(.top, -6) - } - } else { - HStack(spacing: 12) { - Button { - callViewModel.terminateCall() - } label: { - Image("phone-disconnect") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - - } - .frame(width: 90, height: 60) - .background(Color.redDanger500) - .cornerRadius(40) - - Spacer() - - Button { - callViewModel.toggleVideo() - } label: { - Image(callViewModel.cameraDisplayed ? "video-camera" : "video-camera-slash") - .renderingMode(.template) - .resizable() - .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) - .frame(width: 32, height: 32) - - } - .frame(width: 60, height: 60) - .background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : Color.gray500) - .cornerRadius(40) - .disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) - - Button { - callViewModel.toggleMuteMicrophone() - } label: { - Image(callViewModel.micMutted ? "microphone-slash" : "microphone") - .renderingMode(.template) - .resizable() - .foregroundStyle(callViewModel.micMutted ? .black : .white) - .frame(width: 32, height: 32) - - } - .frame(width: 60, height: 60) - .background(callViewModel.micMutted ? .white : Color.gray500) - .cornerRadius(40) - - Button { - if AVAudioSession.sharedInstance().availableInputs != nil - && !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty { - - hideButtonsSheet = true - - DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { - audioRouteSheet = true - } - } else { - do { - try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty ? .speaker : .none) - } catch _ { - - } - } - - } label: { - Image(imageAudioRoute) - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - .onAppear(perform: getAudioRouteImage) - .onReceive(pub) { _ in - self.getAudioRouteImage() - } - - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - } - .frame(height: geometry.size.height * 0.15) - .padding(.horizontal, 20) - .padding(.top, -6) - } + let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene + let bottomInset = scene?.windows.first?.safeAreaInsets + + BottomSheetView( + content: bottomSheetContent(geo: geometry), + minHeight: (0.1 * geometry.size.height) + (bottomInset != nil ? bottomInset!.bottom : 0), + maxHeight: (0.45 * geometry.size.height) + (bottomInset != nil ? bottomInset!.bottom : 0), + currentHeight: (0.1 * geometry.size.height) + (bottomInset != nil ? bottomInset!.bottom : 0) + ) } else { +#if targetEnvironment(simulator) HStack(spacing: 12) { HStack { Spacer() @@ -923,6 +510,15 @@ struct CallView: View { } .padding(.horizontal, 25) .padding(.top, 20) +#else + HStack(spacing: 12) { + HStack { + } + .frame(height: 60) + } + .padding(.horizontal, 25) + .padding(.top, 20) +#endif } } } @@ -933,6 +529,418 @@ struct CallView: View { } } + func bottomSheetContent(geo: GeometryProxy) -> some View { + GeometryReader { _ in + VStack(spacing: 0) { + Rectangle() + .fill(Color.gray500) + .frame(width: 100, height: 5) + .cornerRadius(10) + .padding(.top, 5) + HStack(spacing: 12) { + Button { + callViewModel.terminateCall() + } label: { + Image("phone-disconnect") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + + } + .frame(width: 90, height: 60) + .background(Color.redDanger500) + .cornerRadius(40) + + Spacer() + + Button { + callViewModel.toggleVideo() + } label: { + Image(callViewModel.cameraDisplayed ? "video-camera" : "video-camera-slash") + .renderingMode(.template) + .resizable() + .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) + .frame(width: 32, height: 32) + + } + .frame(width: 60, height: 60) + .background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : Color.gray500) + .cornerRadius(40) + .disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) + + Button { + callViewModel.toggleMuteMicrophone() + } label: { + Image(callViewModel.micMutted ? "microphone-slash" : "microphone") + .renderingMode(.template) + .resizable() + .foregroundStyle(callViewModel.micMutted ? .black : .white) + .frame(width: 32, height: 32) + + } + .frame(width: 60, height: 60) + .background(callViewModel.micMutted ? .white : Color.gray500) + .cornerRadius(40) + + Button { + if AVAudioSession.sharedInstance().availableInputs != nil + && !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty { + + hideButtonsSheet = true + + DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { + audioRouteSheet = true + } + } else { + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty ? .speaker : .none) + } catch _ { + + } + } + + } label: { + Image(imageAudioRoute) + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + .onAppear(perform: getAudioRouteImage) + .onReceive(pub) { _ in + self.getAudioRouteImage() + } + + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + } + .frame(height: geo.size.height * 0.15) + .padding(.horizontal, 20) + .padding(.top, (orientation != .landscapeLeft && orientation != .landscapeRight) ? (geo.safeAreaInsets.bottom != 0 ? -15 : -30) : -10) + + if orientation != .landscapeLeft && orientation != .landscapeRight { + HStack(spacing: 0) { + VStack { + Button { + } label: { + Image("phone-transfer") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("Transfer") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + + VStack { + Button { + } label: { + Image("phone-plus") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("New call") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + + VStack { + Button { + } label: { + Image("phone-list") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("Call list") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + + VStack { + Button { + } label: { + Image("dialer") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("Dialer") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + } + .frame(height: geo.size.height * 0.15) + + HStack(spacing: 0) { + VStack { + Button { + } label: { + Image("chat-teardrop-text") + .renderingMode(.template) + .resizable() + //.foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) + .foregroundStyle(Color.gray500) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + //.background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : Color.gray500) + .background(Color.gray600) + .cornerRadius(40) + //.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) + .disabled(true) + + Text("Messages") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + + VStack { + Button { + callViewModel.togglePause() + } label: { + Image(callViewModel.isPaused ? "play" : "pause") + .renderingMode(.template) + .resizable() + .foregroundStyle(telecomManager.isPausedByRemote ? Color.gray500 : .white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(telecomManager.isPausedByRemote ? Color.gray600 : (callViewModel.isPaused ? Color.greenSuccess500 : Color.gray500)) + .cornerRadius(40) + .disabled(telecomManager.isPausedByRemote) + + Text("Pause") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + + VStack { + Button { + callViewModel.toggleRecording() + } label: { + Image("record-fill") + .renderingMode(.template) + .resizable() + .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : (callViewModel.isRecording ? Color.redDanger500 : Color.gray500)) + .cornerRadius(40) + .disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) + + Text("Record") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + + VStack { + Button { + } label: { + Image("video-camera") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("Disposition") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + .hidden() + } + .frame(height: geo.size.height * 0.15) + } else { + HStack { + VStack { + Button { + } label: { + Image("phone-transfer") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("Transfer") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125) + + VStack { + Button { + } label: { + Image("phone-plus") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("New call") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125) + + VStack { + Button { + } label: { + Image("phone-list") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("Call list") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125) + + VStack { + Button { + } label: { + Image("dialer") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("Dialer") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125) + + VStack { + Button { + } label: { + Image("chat-teardrop-text") + .renderingMode(.template) + .resizable() + //.foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) + .foregroundStyle(Color.gray500) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + //.background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : Color.gray500) + .background(Color.gray600) + .cornerRadius(40) + //.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) + .disabled(true) + + Text("Messages") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125) + + VStack { + Button { + callViewModel.togglePause() + } label: { + Image(callViewModel.isPaused ? "play" : "pause") + .renderingMode(.template) + .resizable() + .foregroundStyle(telecomManager.isPausedByRemote ? Color.gray500 : .white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(telecomManager.isPausedByRemote ? Color.gray600 : (callViewModel.isPaused ? Color.greenSuccess500 : Color.gray500)) + .cornerRadius(40) + .disabled(telecomManager.isPausedByRemote) + + Text("Pause") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125) + + VStack { + Button { + callViewModel.toggleRecording() + } label: { + Image("record-fill") + .renderingMode(.template) + .resizable() + .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : (callViewModel.isRecording ? Color.redDanger500 : Color.gray500)) + .cornerRadius(40) + .disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) + + Text("Record") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125) + } + .frame(height: geo.size.height * 0.15) + .padding(.horizontal, 20) + .padding(.top, 30) + } + Spacer() + } + .background(Color.gray900) + .frame(maxHeight: .infinity, alignment: .top) + } + } + func getAudioRouteImage() { imageAudioRoute = AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty ? ( @@ -948,6 +956,59 @@ struct CallView: View { } } +struct BottomSheetView: View { + let content: Content + + @State var minHeight: CGFloat + @State var maxHeight: CGFloat + + @State var currentHeight: CGFloat + + var body: some View { + GeometryReader { geometry in + VStack(spacing: 0.0) { + content + } + .onAppear { + self.currentHeight = minHeight + } + .frame( + width: geometry.size.width, + height: maxHeight, + alignment: .top + ) + .clipShape( + Path( + UIBezierPath( + roundedRect: CGRect(x: 0.0, y: 0.0, width: geometry.size.width, height: maxHeight), + byRoundingCorners: [.topLeft, .topRight], + cornerRadii: CGSize(width: 16.0, height: 16.0) + ) + .cgPath + ) + ) + .frame( + height: geometry.size.height, + alignment: .bottom + ) + .highPriorityGesture( + DragGesture() + .onChanged { value in + currentHeight -= value.translation.height + currentHeight = min(max(currentHeight, minHeight), maxHeight) + } + .onEnded { _ in + withAnimation { + currentHeight = (currentHeight - minHeight <= maxHeight - currentHeight) ? minHeight : maxHeight + } + } + ) + .offset(y: maxHeight - currentHeight) + } + .edgesIgnoringSafeArea(.bottom) + } +} + #Preview { CallView(callViewModel: CallViewModel()) } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift index e29473d97..aa805937e 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift @@ -35,45 +35,25 @@ struct ContactFragment: View { var body: some View { let indexDisplayed = contactViewModel.indexDisplayedFriend != nil ? contactViewModel.indexDisplayedFriend! : 0 - if #available(iOS 16.0, *) { - if idiom != .pad { - ContactInnerFragment( - contactAvatarModel: ContactsManager.shared.avatarListModel[indexDisplayed], - contactViewModel: contactViewModel, - editContactViewModel: editContactViewModel, - cnContact: CNContact(), - isShowDeletePopup: $isShowDeletePopup, - showingSheet: $showingSheet, - showShareSheet: $showShareSheet, - isShowDismissPopup: $isShowDismissPopup - ) - .sheet(isPresented: $showingSheet) { - ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet) - .presentationDetents([.fraction(0.2)]) - } - .sheet(isPresented: $showShareSheet) { - ShareSheet(friendToShare: ContactsManager.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) - .presentationDetents([.medium]) - .edgesIgnoringSafeArea(.bottom) - } - } else { - ContactInnerFragment( - contactAvatarModel: ContactsManager.shared.avatarListModel[indexDisplayed], - contactViewModel: contactViewModel, - editContactViewModel: editContactViewModel, - cnContact: CNContact(), - isShowDeletePopup: $isShowDeletePopup, - showingSheet: $showingSheet, - showShareSheet: $showShareSheet, - isShowDismissPopup: $isShowDismissPopup - ) - .halfSheet(showSheet: $showingSheet) { - ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet) - } onDismiss: {} - .sheet(isPresented: $showShareSheet) { - ShareSheet(friendToShare: ContactsManager.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) - .edgesIgnoringSafeArea(.bottom) - } + if #available(iOS 16.0, *), idiom != .pad { + ContactInnerFragment( + contactAvatarModel: ContactsManager.shared.avatarListModel[indexDisplayed], + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, + cnContact: CNContact(), + isShowDeletePopup: $isShowDeletePopup, + showingSheet: $showingSheet, + showShareSheet: $showShareSheet, + isShowDismissPopup: $isShowDismissPopup + ) + .sheet(isPresented: $showingSheet) { + ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet) + .presentationDetents([.fraction(0.2)]) + } + .sheet(isPresented: $showShareSheet) { + ShareSheet(friendToShare: ContactsManager.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) + .presentationDetents([.medium]) + .edgesIgnoringSafeArea(.bottom) } } else { ContactInnerFragment( @@ -89,10 +69,10 @@ struct ContactFragment: View { .halfSheet(showSheet: $showingSheet) { ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet) } onDismiss: {} - .sheet(isPresented: $showShareSheet) { - ShareSheet(friendToShare: ContactsManager.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) - .edgesIgnoringSafeArea(.bottom) - } + .sheet(isPresented: $showShareSheet) { + ShareSheet(friendToShare: ContactsManager.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) + .edgesIgnoringSafeArea(.bottom) + } } } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift index 8382da399..3a453d5d6 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift @@ -32,38 +32,22 @@ struct ContactsFragment: View { var body: some View { ZStack { - if #available(iOS 16.0, *) { - if idiom != .pad { - ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet) - .sheet(isPresented: $showingSheet) { - ContactsListBottomSheet( - contactViewModel: contactViewModel, - isShowDeletePopup: $isShowDeletePopup, - showingSheet: $showingSheet, - showShareSheet: $showShareSheet - ) - .presentationDetents([.fraction(0.2)]) - } - .sheet(isPresented: $showShareSheet) { - ShareSheet(friendToShare: contactViewModel.selectedFriendToShare!) - .presentationDetents([.medium]) - .edgesIgnoringSafeArea(.bottom) - } - } else { - ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet) - .halfSheet(showSheet: $showingSheet) { - ContactsListBottomSheet( - contactViewModel: contactViewModel, - isShowDeletePopup: $isShowDeletePopup, - showingSheet: $showingSheet, - showShareSheet: $showShareSheet - ) - } onDismiss: {} - .sheet(isPresented: $showShareSheet) { - ShareSheet(friendToShare: contactViewModel.selectedFriendToShare!) - .edgesIgnoringSafeArea(.bottom) - } - } + if #available(iOS 16.0, *), idiom != .pad { + ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet) + .sheet(isPresented: $showingSheet) { + ContactsListBottomSheet( + contactViewModel: contactViewModel, + isShowDeletePopup: $isShowDeletePopup, + showingSheet: $showingSheet, + showShareSheet: $showShareSheet + ) + .presentationDetents([.fraction(0.2)]) + } + .sheet(isPresented: $showShareSheet) { + ShareSheet(friendToShare: contactViewModel.selectedFriendToShare!) + .presentationDetents([.medium]) + .edgesIgnoringSafeArea(.bottom) + } } else { ContactsInnerFragment(contactViewModel: contactViewModel, showingSheet: $showingSheet) .halfSheet(showSheet: $showingSheet) { diff --git a/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift index 1f1c86112..9c85f1873 100644 --- a/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift @@ -27,6 +27,9 @@ struct EditContactFragment: View { @Environment(\.dismiss) var dismiss + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @State private var orientation = UIDevice.current.orientation + var contactViewModel: ContactViewModel @Binding var isShowEditContactFragment: Bool @@ -50,16 +53,33 @@ struct EditContactFragment: View { ZStack { VStack(spacing: 1) { if editContactViewModel.selectedEditFriend == nil { - Rectangle() - .foregroundColor(delayedColor) - .edgesIgnoringSafeArea(.top) - .frame(height: 0) - .task(delayColor) + if #available(iOS 16.0, *) { + Rectangle() + .foregroundColor(delayedColor) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + .task(delayColor) + } else if idiom != .pad && !(orientation == .landscapeLeft || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Rectangle() + .foregroundColor(delayedColor) + .edgesIgnoringSafeArea(.top) + .frame(height: 1) + .task(delayColor) + } } else { - Rectangle() - .foregroundColor(Color.orangeMain500) - .edgesIgnoringSafeArea(.top) - .frame(height: 0) + if #available(iOS 16.0, *) { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + } else if idiom != .pad && !(orientation == .landscapeLeft || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 1) + } } HStack { diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 267968710..a0a2a0899 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -515,40 +515,23 @@ struct ContentView: View { if isShowStartCallFragment { - if #available(iOS 16.4, *) { - if idiom != .pad { - StartCallFragment( + if #available(iOS 16.4, *), idiom != .pad { + StartCallFragment( + startCallViewModel: startCallViewModel, + isShowStartCallFragment: $isShowStartCallFragment, + showingDialer: $showingDialer + ) + .zIndex(3) + .transition(.move(edge: .bottom)) + .sheet(isPresented: $showingDialer) { + DialerBottomSheet( startCallViewModel: startCallViewModel, - isShowStartCallFragment: $isShowStartCallFragment, showingDialer: $showingDialer ) - .zIndex(3) - .transition(.move(edge: .bottom)) - .sheet(isPresented: $showingDialer) { - DialerBottomSheet( - startCallViewModel: startCallViewModel, - showingDialer: $showingDialer - ) - .presentationDetents([.medium]) - // .interactiveDismissDisabled() - .presentationBackgroundInteraction(.enabled(upThrough: .medium)) - } - } else { - StartCallFragment( - startCallViewModel: startCallViewModel, - isShowStartCallFragment: $isShowStartCallFragment, - showingDialer: $showingDialer - ) - .zIndex(3) - .transition(.move(edge: .bottom)) - .halfSheet(showSheet: $showingDialer) { - DialerBottomSheet( - startCallViewModel: startCallViewModel, - showingDialer: $showingDialer - ) - } onDismiss: {} + .presentationDetents([.medium]) + // .interactiveDismissDisabled() + .presentationBackgroundInteraction(.enabled(upThrough: .medium)) } - } else { StartCallFragment( startCallViewModel: startCallViewModel, @@ -698,12 +681,12 @@ struct ContentView: View { if newPhase == .active { coreContext.onForeground() /* - if !isShowStartCallFragment { - contactsManager.fetchContacts() - DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { - historyListViewModel.computeCallLogsList() - } - } + if !isShowStartCallFragment { + contactsManager.fetchContacts() + DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { + historyListViewModel.computeCallLogsList() + } + } */ print("Active") } else if newPhase == .inactive { diff --git a/Linphone/UI/Main/History/Fragments/HistoryFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryFragment.swift index 36a39e7b6..bce55db80 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryFragment.swift @@ -33,35 +33,20 @@ struct HistoryFragment: View { var body: some View { ZStack { - if #available(iOS 16.0, *) { - if idiom != .pad { - HistoryListFragment(historyListViewModel: historyListViewModel, historyViewModel: historyViewModel, showingSheet: $showingSheet) - .sheet(isPresented: $showingSheet) { - HistoryListBottomSheet( - historyViewModel: historyViewModel, - contactViewModel: contactViewModel, - editContactViewModel: editContactViewModel, - historyListViewModel: historyListViewModel, - showingSheet: $showingSheet, - index: $index, - isShowEditContactFragment: $isShowEditContactFragment - ) - .presentationDetents([.fraction(0.2)]) - } - } else { - HistoryListFragment(historyListViewModel: historyListViewModel, historyViewModel: historyViewModel, showingSheet: $showingSheet) - .halfSheet(showSheet: $showingSheet) { - HistoryListBottomSheet( - historyViewModel: historyViewModel, - contactViewModel: contactViewModel, - editContactViewModel: editContactViewModel, - historyListViewModel: historyListViewModel, - showingSheet: $showingSheet, - index: $index, - isShowEditContactFragment: $isShowEditContactFragment - ) - } onDismiss: {} - } + if #available(iOS 16.0, *), idiom != .pad { + HistoryListFragment(historyListViewModel: historyListViewModel, historyViewModel: historyViewModel, showingSheet: $showingSheet) + .sheet(isPresented: $showingSheet) { + HistoryListBottomSheet( + historyViewModel: historyViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, + historyListViewModel: historyListViewModel, + showingSheet: $showingSheet, + index: $index, + isShowEditContactFragment: $isShowEditContactFragment + ) + .presentationDetents([.fraction(0.2)]) + } } else { HistoryListFragment(historyListViewModel: historyListViewModel, historyViewModel: historyViewModel, showingSheet: $showingSheet) .halfSheet(showSheet: $showingSheet) {