swiftUI - can't close modal screen after force screen to Landscape - swiftui

I open a full modal view
.fullScreenCover(isPresented: self.$isPresentedPlayerView){
NavigationLazyView((MainPlayerView(playerVM: PlayerVM(asset: self.mediaVM.asset), showModal: self.$isPresentedPlayerView)))
}
and in playerView .onApper i force screen to Landscape mode
with this code:
func forceLandscapeLeftPlayerView(){
AppDelegate.orientationLock = UIInterfaceOrientationMask.landscape
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
and when it try close view or by set isPresentedPlayerView to false or by presentationMode.wrappedValue.dismiss()
screen not close!
any idea???
this is close code:
func closeView(){
DispatchQueue.main.async {
withAnimation{
self.playerVM.pause()
self.playerVM.destropyPlayer()
AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
self.isPresentedPlayerView = false
}
}
}
BTW, this code work on Xcode 12.2 and stop work on xcode 12.3/ .4

This code works. I removed DispatchQueue, withAnimation and the first three lines. Perhaps, the problem lies somewhere else.
struct MainPlayerView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Button(action: {
self.resetOrientation()
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Click")
})
}
.onAppear(perform: {
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
})
}
func resetOrientation() {
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
}

Related

IOS detail view disappearing when contains Date object and tries to update Coredata

I trying to save a Coredata object in a SwiftUI .OnDisappear method and it conflicts with a Date() object in the View. Commenting out the Date() variable in DetailView or commenting out the code in .OnDisappear stops the view from disappearing. I made a minimum reproducible example here from the default New Project with Coredata in Xcode.
Here is a gif of the DetailView disappearing
https://imgur.com/dA2QH4D
ListView
struct ListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: DetailView(item: item), label: {
Text(item.timestamp!, formatter: itemFormatter)
})
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
DetailView
struct DetailView: View {
#State var item: Item
#State private var date = Date() // Comment this
var body: some View {
VStack {
Text(item.timestamp!, formatter: itemFormatter)
NavigationLink(destination: {
EmptyView()
}, label: {
Text("History")
})
}
.onDisappear {
item.timestamp = Date() // Or Comment this
}
}
}
In ListView, add a .navigationViewStyle(.stack) to your NavigationView, such as:
NavigationView {
//...
}.navigationViewStyle(.stack) // <-- here
NavigationView can only have one level of detail NavigationLink i.e. its isDetail property defaults to true. If you want more levels you have to use .isDetail(false) on the 2nd level and beyond.

SwiftUI: fullScreenCover with no animation?

I have this view:
struct TheFullCover: View {
#State var showModal = false
var body: some View {
Button(action: {
showModal.toggle()
}) {
Text("Show Modal")
.padding()
.foregroundColor(.blue)
}
.background(Color(.white))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.red, lineWidth:1)
)
.fullScreenCover(isPresented: $showModal, onDismiss: {
}, content: {
VStack {
Text("Here I am")
TheFullCover()
}
})
}
}
Every time I press the Button, the modal screen comes up fullscreen. All works great.
Question:
How do I disable the slide up animation? I want the view to be presented immediately fullscreen without animating to it.
Is there a way to do that?
A possible solution is to disable views animation completely (and then, if needed, enable again in .onAppear of presenting content), like
Button(action: {
UIView.setAnimationsEnabled(false) // << here !!
showModal.toggle()
}) {
and then
}, content: {
VStack {
Text("Here I am")
TheFullCover()
}
.onAppear {
UIView.setAnimationsEnabled(true) // << here !!
}
})
Tested with Xcode 13 / iOS 15
AFAIK the proper to do it as of today is using transaction https://developer.apple.com/documentation/swiftui/transaction
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
showModal.toggle()
}
I also created a handy extension for this:
extension View {
func withoutAnimation(action: #escaping () -> Void) {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
action()
}
}
}
which can be used like this:
withoutAnimation {
// do your thing
}
.fullScreenCover(isPresented: isPresented) {
content()
.background(TransparentBackground())
}
.transaction({ transaction in
transaction.disablesAnimations = true
})
this should work, based on #asamoylenko's answer
At the moment, I find it easier to use UIKit for presentation in SwiftUI.
someView
.onChange(of: isPresented) { _ in
if isPresented {
let vc = UIHostingController(rootView: MyView())
vc.modalPresentationStyle = .overFullScreen
UIApplication.shared.rootVC?.present(vc, animated: false)
} else {
UIApplication.shared.rootVC?.dismiss(animated: false)
}
}
Why not use an overlay instead?
.overlay {
if isLoading {
ZStack {
ProgressView()
}
.background(BackgroundCleanerView())
}
}

How to fire an onApper method when a sheet close?

I'm currently developing an application using SwiftUI.
I want to use an onAppear method when a sheet created from the same struct close.
Is there any way to do this?
Here are the codes:
BaseView.swift
import SwiftUI
struct BaseView: View {
#State var isSheet:Bool = false
var body: some View {
VStack{
VStack{
Text("BaseView")
Button(action:{
isSheet = true
}){
Text("SHEET")
}
}
.onAppear(){
print("onAppear fiered")
}
.sheet(isPresented: $isSheet){
Sheet()
}
}
}
}
Sheet.swift
import SwiftUI
struct Sheet: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("CLOSE")
}
}
}
Xcode:Version 12.0.1
Just move everything from onAppear into separated function and call that function on sheet dismiss as well, like
var body: some View {
VStack{
VStack{
Text("BaseView")
Button(action:{
isSheet = true
}){
Text("SHEET")
}
}
.onAppear(){
foo() // << here !!
}
.sheet(isPresented: $isSheet, onDismiss: {
foo() // << and here !!
}){
Sheet()
}
}
}
func foo() {
print("do some here")
}

SwiftUI: Replacing window dismisses only topmost modal view

I need to show a login screen when the user session is expired. I tried to achieve this by changing the current window:
#main
struct ResetViewHierarchyApp: App {
#StateObject private var state = appState
var body: some Scene {
WindowGroup {
if state.isLoggedIn {
ContentView()
} else {
LogInView()
}
}
}
}
When no modal views are presented then it works fine. If only one modal view is presented, it also works, the modal view is dismissed. But if there are more than one modal views are presented, then the root view is replaced, but only the topmost modal view is dismissed. Here is ContentView:
struct ContentView: View {
#State private var isPresentingSheet1 = false
#State private var isPresentingSheet2 = false
var body: some View {
Text("Hello, world!")
.padding()
Button(action: {
isPresentingSheet1 = true
}, label: {
Text("Present Sheet")
.padding()
}).sheet(isPresented: $isPresentingSheet1) {
sheetView1
}
}
}
private extension ContentView {
var sheetView1: some View {
VStack {
Text("Sheet 1")
.padding()
Button(action: {
isPresentingSheet2 = true
}, label: {
Text("Present Sheet")
.padding()
}).sheet(isPresented: $isPresentingSheet2) {
sheetView2
}
}
}
var sheetView2: some View {
VStack {
Text("Sheet 2")
.padding()
Button(action: {
appState.isLoggedIn = false
}, label: {
Text("Log Out")
.padding()
})
}
}
}
The same happens if I use fullScreenCover instead of sheet.
Does anybody know how to solve this issue, to dismiss all the presented modals at once?
I've solved this issue with UIKit windows:
#StateObject private var state = appState
#State private var contentWindow: UIWindow?
var body: some Scene {
WindowGroup {
EmptyView()
.onAppear {
updateContentWindow(isLoggedIn: state.isLoggedIn)
}.onReceive(state.$isLoggedIn) { isLoggedIn in
updateContentWindow(isLoggedIn: isLoggedIn)
}
}
}
var window: UIWindow? {
guard let scene = UIApplication.shared.connectedScenes.first,
let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate,
let window = windowSceneDelegate.window else {
return nil
}
return window
}
func updateContentWindow(isLoggedIn: Bool) {
contentWindow?.isHidden = true
contentWindow = nil
if let windowScene = window?.windowScene {
contentWindow = UIWindow(windowScene: windowScene)
contentWindow?.windowLevel = UIWindow.Level.normal
if isLoggedIn {
contentWindow?.rootViewController = UIHostingController(rootView: ContentView())
} else {
contentWindow?.rootViewController = UIHostingController(rootView: LogInView())
}
contentWindow?.makeKeyAndVisible()
}
}
It is indeed a strange bug.. however I found a workaround for it.
You can keep your States of the modal View inside your Observable / Environment Object. When logging out, you have to make sure to close all your sheets.
Here is a example:
First adding showSheet as Published Value in the AppState
class AppState : ObservableObject {
#Published var isLoggedIn : Bool = true
#Published var showSheet1 : Bool = false
#Published var showSheet2 : Bool = false
}
When logging out, turn all your sheets to false.
Button(action: {
self.state.isLoggedIn = false
self.state.showSheet1 = false
self.state.showSheet2 = false
}, label: {
Text("Log Out")
.padding()
})
Of course you have to use these values in your Button for toggling sheet and in your sheet.
.sheet(isPresented: $state.showSheet2) {
Edit:
Even simpler, you don't have to manually set it to false in the LogOut action. Instead do it all in the appState
#Published var isLoggedIn : Bool = true {
willSet {
if newValue == false {
showSheet1 = false
showSheet2 = false
}
}
}

SwiftUI: After I dismiss a sheet all buttons inside navigationBarItems do not work anymore

after I dismiss a sheet my buttons on the screen above do not work anymore. Only after pressing on a non-interacting surface the buttons work again. I use swift version 5 and the error occurs in the simulator and on the device.
#Edit
Code Snippets
AddView this will be displayed in a sheet
struct AddView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode
// some state
var body: some View {
NavigationView {
Form {
Section(header: Text("Name")) {
TextField("Task-Name (e.g. Eat the 🍰)", text: $title)
}
Section(header: Text("Settings")) {
DatePicker("Date", selection: $timestamp, displayedComponents: .date)
Toggle(isOn: $highPrio) {
Text("High Priority")
}
}
}
.navigationBarItems(trailing: Button("Add"){
// logic
do {
try self.moc.save()
} catch {
print(error.localizedDescription)
}
self.presentationMode.wrappedValue.dismiss()
}.alert(isPresented: $showAlert) {
Alert(title: Text("Name field is empty"), message: Text("Please enter a name"), dismissButton: .default(Text("Got it!")))
})
.navigationBarTitle("New Task")
}
}
}
struct AddView_Previews: PreviewProvider {
static var previews: some View {
AddView()
}
}
ContentView includes a FetchRequest with some functions and nothing more.
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Task.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Task.timestamp, ascending: true),
NSSortDescriptor(keyPath: \Task.status, ascending: false),
NSSortDescriptor(keyPath: \Task.highPriority, ascending: false),
]) var tasks: FetchedResults<Task>
#State private var showingAddSheet = false
#State private var showAlert = false
#State private var editMode = false
var body: some View {
NavigationView {
List {
ForEach(tasks.filter{return self.filterTasks(task: $0)}, id: \.self) { task in
HStack {
TaskRowView(
title: task.wrappedTitle,
status: task.wrappedStatus,
timestamp: task.wrappedTimestamp,
highPriority: task.highPriority,
showDetail: self.editMode
).onTapGesture {
self.toggleStatus(item: task)
}
}
}
.onDelete(perform: removeTask)
}
.navigationBarTitle(self.editMode ? "All Tasks" : "Today")
.navigationBarItems(leading: Button(self.editMode ? "Done" : "Edit") {self.editMode.toggle()}, trailing: Button("Add") {self.showingAddSheet.toggle()})
.sheet(isPresented: $showingAddSheet) {
AddView().environment(\.managedObjectContext, self.moc)
}
}.onAppear(perform: {
self.cleanupTasks()
}).alert(isPresented: $showAlert) {
Alert(title: Text("Unfished Task found"),
message: Text("Do you want to take over the old tasks or delete them?"),
primaryButton: .destructive(Text("Delete all")) {
self.removeOldTasks()
},
secondaryButton: .default(Text("Take over")) {
self.takeOldTasksOver()
}
)
}
}
// functions...
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return ContentView().environment(\.managedObjectContext, context)
}
}
#endif
Solution
This is a Bug that is related to the .large navigationBarItem. You can set that to .inline to go around it for now:
NavigationView {
,,,
.navigationBarTitle(Text(""), displayMode: .inline)
}
Related Thread: SwiftUI - Navigation bar button not clickable after sheet has been presented
The problem happens when there is a navigationView inside your "AddView" struct. From what I have tested, If you remove the navigationView and just use a button (for dismissal) somewhere else inside the AddView it works perfectly. as below:
var body: some View {
VStack{
HStack {
Spacer()
Button(action: {
// logic ..
self.presentationMode.wrappedValue.dismiss()
}){
Text("Add")
}.alert(isPresented: $showAlert) {
Alert(title: Text("Name field is empty"), message: Text("Please enter a name"), dismissButton: .default(Text("Got it!")))
}
.padding(24)
}
Form {
Section(header: Text("Name")) {
TextField("Task-Name (e.g. Eat the 🍰)", text: $title)
}
Section(header: Text("Settings")) {
DatePicker("Date", selection: $timestamp, displayedComponents: .date)
Toggle(isOn: $highPrio) {
Text("High Priority")
}
}
}
}
}
I have this problem in the simulator, but on a real device it works well. Consider updating of xcode, mac OS or iOS.
It's working on device with latest Xcode.