import SwiftUI import CoreData // ######################################################################################## // XCode // Version 11.5.1 (11E503a) // Simulator // Version 11.4.1 (921.9) // SimulatorKit 581.9.1 // CoreSimulator 704.12.1 // Device // iPhone SE (2nd generation) (Works as expected) // iPad (7th generation) (Has the issue) // ######################################################################################## // ######################################################################################## // You will need to create CoreDataentities as follows: // // Entity: Student // @NSManaged public var name: String? (Attribute) // @NSManaged public var teacher: Teacher? (To one relationship, inverse students) // Entity: Teacher // @NSManaged public var name: String? (Attribute) // @NSManaged public var students: NSSet? (To Many relationship, inverse teacher) // Entity: Location // @NSManaged public var name: String? (Attribute) // ######################################################################################## // ######################################################################################## // you will need to modify the scenedelegate as follows //class SceneDelegate: UIResponder, UIWindowSceneDelegate { // ... //var orientationObserver = OrientationObserver() // func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // ... // let contentView = ContentView().environment(\.managedObjectContext, context).environmentObject(orientationObserver) // ... // } // ... //} // ######################################################################################## //################################################################################ // the issue I am having is with the ipad only and when it is using .navigationViewStyle(DoubleColumnNavigationViewStyle()) // this is the default style when in landscape mode but I am also forcing it in portrait // // the iphone works as expected because it is not using DoubleColumnNavigationViewStyle() at all // if i force StackNavigationViewStyle on the iPad, the problem is not present but the user experience suffers greatly // // the issue is as follows: // a lormal navigation path would be // List1 -> Form1 -> Form2 -> Picker1 -> (change the Teacher) // Afger changing a teacher, One would expect to be returned to Form2 but this is noy the case // I am returned to a brand new Form1 that I never created // Somehow the navigation has become // List1 -> Form1 -> Form1 // Eeven if there are no navigation links from Form1 to Form1, // i can press the back button to go from a Form1 back to a previous Form1 wich brings the navigation back to what is expected // or insted, // if I decide to select another teacher again instead of going back to Form1, another level of Form1 is added after the second selection // List1 -> Form1 -> Form1 -> Form1 // as long as you keep changing the teacher and don't go back to the original Form1, another level of Form1 navigatin is added // // // I have commented the code where I found that it had effect on the issue // As far as I know there is nothing in there that is rocket science // Unless I missed something, this might be a bug with the DoubleColumnNavigationViewStyle // Everything works fine when it is no used but I would prefer to be able to use it //################################################################################ struct ContentView: View { @State private var selection = 0 @Environment(\.managedObjectContext) var moc var body: some View { TabView(selection: $selection){ Button(action: { self.CreateDataIfNeeded() }) { Text("Add CoreData Records") } .font(.title) .tabItem { VStack { Image(systemName: "gear") Text("Setup") } } .tag(0) List1() .font(.title) .tabItem { VStack { Image(systemName: "questionmark") Text("Test") } } .tag(1) } } func CreateDataIfNeeded() { let request:NSFetchRequest = Student.fetchRequest() request.fetchLimit = 1 request.sortDescriptors = [] request.returnsObjectsAsFaults = false do { let students = try self.moc.fetch(request) if students.count == 0 { let t1 = Teacher(context: moc) t1.name = "Bob" let t2 = Teacher(context: moc) t2.name = "Mary" let student = Student(context: moc) student.name = "me" student.teacher = t2 do { try moc.save() } catch { fatalError("Unable to save initial data") } } } catch { print("Fetch Failed") } } } struct List1: View { @EnvironmentObject var orientationObserver: OrientationObserver var body: some View { NavigationView{ List() { ForEach(1...5, id: \.self) { _ in List1Row() } } .navigationBarTitle(Text("List1"), displayMode: .inline) } .gridNavigationviewStyle(orientationObserver) } } struct List1Row: View { var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave) @State private var refreshing = false var body: some View { HStack { //################################################################################ // Using a simple text here does not reproduce the issue // Text("To Form1") //################################################################################ // Using a text that needs to be refreshed whenever some coredata has been saved causes the issue // it is not obvious in this sample app but my real application needs to know when records // are modified and saved in one of the froms used down the navigation path // I am using this technique for the case where an object of a 3rd level of relationship is modified and is not part of the predicate that feeds this list of rows // In this sample Picker1 does the saving forcing this View to refresh Text("To Form1" + (self.refreshing ? "" : "")) .onReceive(self.didSave) { _ in self.refreshing.toggle() } //################################################################################ NavigationLink("", destination: Form1()) } } } struct Form1: View { //################################################################################ // removing this also removes the issue even if it is not currently used in the code // in my real application i need to show a list of locations so i will need a fetch @FetchRequest(entity: Location.entity(), sortDescriptors:[], predicate: nil) var locations: FetchedResults<Location> //################################################################################ var body: some View { Form{ Section(header: Text("Locations")) { ForEach(1...3, id: \.self) { _ in Form1Row() } } } .navigationBarTitle("Form1") } } struct Form1Row: View { var body: some View { NavigationLink("To Form 2", destination: Form2()) } } struct Form2: View { @ObservedObject var student: Student var body: some View { Form{ Section(header: Text("Teacher")) { Picker1(student: self.student) } }.navigationBarTitle("Form2") } static func GetSudent() -> Student { let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext let request:NSFetchRequest = Student.fetchRequest() request.fetchLimit = 1 request.sortDescriptors = [] request.returnsObjectsAsFaults = false do { let students = try moc.fetch(request) if students.count > 0 { return students[0] } } catch { print("Fetch Failed") } fatalError("no students found") } init () { // usually I would recieve the student from the parent form but in this demo app, // i just get the first one available. self._student = ObservedObject(initialValue: Form2.GetSudent()) } } // this picker is designed to set a relationship of a core data record // Record: Student // Attribute: teacher (Type Teacher) struct Picker1: View { @ObservedObject var student: Student @FetchRequest(entity: Teacher.entity(), sortDescriptors:[], predicate: nil) var teachers: FetchedResults<Teacher> var body: some View { let pickerIndexTranslator = Binding<Int?>(get: { if self.student.teacher == nil { return -1 } else { return self.teachers.firstIndex(of: self.student.teacher!) } }, set: { if $0 == -1 { self.student.teacher = nil } else { self.student.teacher = self.teachers[$0!] } //################################################################################ // not saving the modification does not cause the issue but defeats the purpose of the class // the modifications are going to be saved as soon as possible troughout the application // the app will not contain a save button anywhere let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext do { try moc.save() } catch {} //################################################################################ }) return Picker("Teacher", selection: pickerIndexTranslator) { Text("Unavailable").tag(Int?(-1)).foregroundColor(.red) ForEach(teachers, id: \.self) { teacher in Text(teacher.name ?? "Unknown").tag(self.teachers.firstIndex(of: teacher)) } .navigationBarTitle("Teacher") } .labelsHidden() .navigationBarTitle("Form2") } } // this is used to supply the navigationviewstyle to a view based on the device type abnd the orientation extension View { func gridNavigationviewStyle(_ orientationObserver: OrientationObserver) -> some View { if UIDevice.current.userInterfaceIdiom == .phone { return AnyView(self.navigationViewStyle(StackNavigationViewStyle())) } else { if orientationObserver.isLandscape { return AnyView(self.navigationViewStyle(DoubleColumnNavigationViewStyle())) } else { return AnyView(self.navigationViewStyle(DoubleColumnNavigationViewStyle()).padding(.leading ,0.5)) } } } } // this is used to detect orientation changes class OrientationObserver: ObservableObject { enum Orientation { case portrait case landscape } @Published var orientation: Orientation private var _observer: NSObjectProtocol? init() { if UIDevice.current.orientation.isLandscape { self.orientation = .landscape } else { self.orientation = .portrait } _observer = NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { [unowned self] note in guard let device = note.object as? UIDevice else { return } if device.orientation.isPortrait { self.orientation = .portrait } else if device.orientation.isLandscape { self.orientation = .landscape } } } deinit { if let observer = _observer { NotificationCenter.default.removeObserver(observer) } } public var isPortrait: Bool { get{return self.orientation == .portrait} } public var isLandscape: Bool { get{return self.orientation == .landscape} } }