Collapse a doubleColumn NavigationView detail in SwiftUI like with collapsed on UISplitViewController?

后端 未结 5 1459
渐次进展
渐次进展 2020-12-24 07:38

So when I make a list in SwiftUI, I get the master-detail split view for \"free\".

So for instance with this:

import SwiftUI

struct ContentView : Vi         


        
相关标签:
5条回答
  • 2020-12-24 07:49

    For now, in Xcode 11.2.1 it is still nothing changed. I had the same issue with SplitView on iPad and resolved it by adding padding like in Ketan Odedra response, but modified it a little:

    var body: some View {
        GeometryReader { geometry in
            NavigationView {
                MasterView()
                DetailsView()
            }
            .navigationViewStyle(DoubleColumnNavigationViewStyle())
            .padding(.leading, leadingPadding(geometry))
        }
    }
    
    private func leadingPadding(_ geometry: GeometryProxy) -> CGFloat {
        if UIDevice.current.userInterfaceIdiom == .pad {
            return 0.5
        }
        return 0
    }
    

    This works perfectly in the simulator. But when I submit my app for review, it was rejected. This little hack doesn't work on the reviewer device. I don't have a real iPad, so I don't know what caused this. Try it, maybe it will work for you.

    While it doesn't work for me, I requested help from Apple DTS. They respond to me that for now, SwiftUI API can't fully simulate UIKit`s SplitViewController behavior. But there is a workaround. You can create custom SplitView in SwiftUI:

    struct SplitView<Master: View, Detail: View>: View {
        var master: Master
        var detail: Detail
    
        init(@ViewBuilder master: () -> Master, @ViewBuilder detail: () -> Detail) {
            self.master = master()
            self.detail = detail()
        }
    
        var body: some View {
            let viewControllers = [UIHostingController(rootView: master), UIHostingController(rootView: detail)]
            return SplitViewController(viewControllers: viewControllers)
        }
    }
    
    struct SplitViewController: UIViewControllerRepresentable {
        var viewControllers: [UIViewController]
        @Environment(\.splitViewPreferredDisplayMode) var preferredDisplayMode: UISplitViewController.DisplayMode
    
        func makeUIViewController(context: Context) -> UISplitViewController {
            return UISplitViewController()
        }
    
        func updateUIViewController(_ splitController: UISplitViewController, context: Context) {
            splitController.preferredDisplayMode = preferredDisplayMode
            splitController.viewControllers = viewControllers
        }
    }
    
    struct PreferredDisplayModeKey : EnvironmentKey {
        static var defaultValue: UISplitViewController.DisplayMode = .automatic
    }
    
    extension EnvironmentValues {
        var splitViewPreferredDisplayMode: UISplitViewController.DisplayMode {
            get { self[PreferredDisplayModeKey.self] }
            set { self[PreferredDisplayModeKey.self] = newValue }
        }
    }
    
    extension View {
        /// Sets the preferred display mode for SplitView within the environment of self.
        func splitViewPreferredDisplayMode(_ mode: UISplitViewController.DisplayMode) -> some View {
            self.environment(\.splitViewPreferredDisplayMode, mode)
        }
    }
    

    And then use it:

    SplitView(master: {
                MasterView()
            }, detail: {
                DetailView()
            }).splitViewPreferredDisplayMode(.allVisible)
    

    On an iPad, it works. But there is one issue (maybe more..). This approach ruins navigation on iPhone because both MasterView and DetailView have their NavigationView.

    UPDATE: Finally, in Xcode 11.4 beta 2 they added a button in Navigation Bar that indicates hidden master view.

    0 讨论(0)
  • 2020-12-24 07:49

    Minimal testing in the Simulator, but this should be close to a real solution. The idea is to use an EnvironmentObject to hold a published var on whether to use a double column NavigationStyle, or a single one, then have the NavigationView get recreated if that var changes.

    The EnvironmentObject:

      final class AppEnvironment: ObservableObject {
        @Published var useSideBySide: Bool = false
      }
    

    In the Scene Delegate, set the variable at launch, then observe device rotations and possibly change it (the "1000" is not the correct value, starting point):

    class SceneDelegate: UIResponder, UIWindowSceneDelegate {
        var appEnvironment = AppEnvironment()
    
        @objc
        func orientationChanged() {
            let bounds = UIScreen.main.nativeBounds
            let orientation = UIDevice.current.orientation
    
            // 1000 is a starting point, should be smallest height of a + size iPhone
            if orientation.isLandscape && bounds.size.height > 1000 {
                if appEnvironment.useSideBySide == false {
                    appEnvironment.useSideBySide = true
                    print("SIDE changed to TRUE")
                }
            } else if orientation.isPortrait && appEnvironment.useSideBySide == true {
                print("SIDE changed to false")
                appEnvironment.useSideBySide = false
            }
        }
    
        func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
            let contentView = ContentView()
    
            if let windowScene = scene as? UIWindowScene {
                let window = UIWindow(windowScene: windowScene)
                window.rootViewController = UIHostingController(rootView: contentView.environmentObject(appEnvironment))
                self.window = window
                window.makeKeyAndVisible()
    
                orientationChanged()
                NotificationCenter.default.addObserver(self, selector: #selector(orientationChanged), name: UIDevice.orientationDidChangeNotification, object: nil)
                UIDevice.current.beginGeneratingDeviceOrientationNotifications()
            }
    

    In the top level content view, where the NavigationView is created, use a custom modifier instead of using a navigationViewStyle directly:

    struct ContentView: View {
        @State private var dates = [Date]()
    
        var body: some View {
            NavigationView {
                MV(dates: $dates)
                DetailView()
            }
            .modifier( WTF() )
        }
    
        struct WTF: ViewModifier {
            @EnvironmentObject var appEnvironment: AppEnvironment
    
            func body(content: Content) -> some View  {
                Group {
                    if appEnvironment.useSideBySide == true {
                        content
                            .navigationViewStyle(DoubleColumnNavigationViewStyle())
                    } else {
                        content
                            .navigationViewStyle(StackNavigationViewStyle())
                    }
                }
            }
        }
    }
    

    As mentioned earlier, just Simulator testing, but I tried launching in both orientations, rotating with Master showing, rotating with Detail showing, it all looks good to me.

    0 讨论(0)
  • 2020-12-24 07:54

    In Xcode 11 beta 3, Apple has added .navigationViewStyle(style:) to NavigationView.

    Updated for Xcode 11 Beta 5.
    create MasterView() & DetailsView().

    struct MyMasterView: View {
    
        var people = ["Angela", "Juan", "Yeji"]
    
        var body: some View {
    
            List {
                ForEach(people, id: \.self) { person in
                    NavigationLink(destination: DetailsView()) {
                        Text(person)
                    }
                }
            }
    
        }
    }
    
    struct DetailsView: View {
    
        var body: some View {
            Text("Hello world")
                .font(.largeTitle)
        }
    }
    

    inside my ContentView :

    var body: some View {
    
            NavigationView {
    
                MyMasterView()
    
                DetailsView()
    
            }.navigationViewStyle(DoubleColumnNavigationViewStyle())
             .padding()
        }
    

    Output:

    0 讨论(0)
  • 2020-12-24 08:05
    import SwiftUI
    
    var hostingController: UIViewController?
    
    func showList() {
        let split = hostingController?.children[0] as? UISplitViewController
        UIView.animate(withDuration: 0.3, animations: {
            split?.preferredDisplayMode = .primaryOverlay
        }) { _ in
            split?.preferredDisplayMode = .automatic
        }
    }
    
    func hideList() {
        let split = hostingController?.children[0] as? UISplitViewController
        split?.preferredDisplayMode = .primaryHidden
    }
    
    // =====
    
    struct Dest: View {
        var person: String
    
        var body: some View {
            VStack {
                Text("Hello! \(person)")
                Button(action: showList) {
                    Image(systemName: "sidebar.left")
                }
            }
            .onAppear(perform: hideList)
        }
    }
    
    struct ContentView : View {
        var people = ["Angela", "Juan", "Yeji"]
    
        var body: some View {
            NavigationView {
                List {
                    ForEach(people, id: \.self) { person in
                        NavigationLink(destination: Dest(person: person)) {
                            Text(person)
                        }
                    }
                }
                VStack {
                    Text("                                                                    
    0 讨论(0)
  • 2020-12-24 08:06

    For current version (iOS 13.0-13.3.x), you can use my code. I use a UIViewUpdater to access the underlaying UIView and its UIViewController to adjust the bar item.

    I think the UIViewUpdater way to solve this problem is the most Swifty and robust way, and you can use it to access and modify other UIView, UIViewController related UIKit mechanism.

    ContentView.swift

    import SwiftUI
    
    struct ContentView : View {
        var people = ["Angela", "Juan", "Yeji"]
    
        var body: some View {
            NavigationView {
                List {
                    ForEach(people, id: \.self) { person in
                        NavigationLink(destination: DetailView()) { Text(person) }
                    }
                }
    
                InitialDetailView()
    
            }
        }
    }
    
    struct DetailView : View {
        var body: some View {
            Text("Hello!")
        }
    }
    
    struct InitialDetailView : View {
        var body: some View {
            NavigationView {
                Text("                                                                    
    0 讨论(0)
提交回复
热议问题