像在UISplitViewController上折叠一样,在SwiftUI中折叠doubleColumn NavigationView详细信息?

时间:2019-07-25 23:38:14

标签: ios uisplitviewcontroller swiftui

因此,当我在SwiftUI中列出列表时,我会获得“免费”的主从细节拆分视图。

因此例如:

import SwiftUI

struct ContentView : View {
    var people = ["Angela", "Juan", "Yeji"]

    var body: some View {
        NavigationView {
            List {
                ForEach(people, id: \.self) { person in
                    NavigationLink(destination: Text("Hello!")) {
                        Text(person)
                    }
                }
            }
            Text("?")
        }
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

如果iPad模拟器在横向,并且第一个详细信息屏幕是表情符号,我会得到splitView。但是,如果人们点击一个名称,则详细信息视图将为“ Hello!”

这一切都很棒。

但是,如果我纵向运行iPad,则表情符号会向用户表示欢迎,然后就不会显示列表了。您必须从左向右滑动才能使列表从侧面显示。

有没有人知道一种使导航栏出现的方法,该导航栏可以让用户点击以查看左侧的项目列表?这样它不是仅带有表情符号的屏幕吗?

我不愿留下一个便条,上面写着“从左侧扫入以查看文件/人员/所有内容的列表”

我记得UISplitViewController有一个可以设置的折叠属性。这里有这样的东西吗?

5 个答案:

答案 0 :(得分:4)

目前,在Xcode 11.2.1中仍然没有任何更改。 我在iPad上使用SplitView遇到相同的问题,并通过像Ketan Odedra响应中那样添加填充来解决它,但是做了一些修改:

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
}

这在模拟器中完美运行。但是,当我将我的应用提交审核时,它被拒绝了。这个小技巧在审阅者设备上不起作用。我没有真正的iPad,所以我不知道是什么原因造成的。试试吧,也许它会为您工作。

虽然它对我不起作用,但我要求Apple DTS帮助。 他们回应我,SwiftUI API目前还不能完全模拟UIKit的SplitViewController行为。但是有一种解决方法。 您可以在SwiftUI中创建自定义SplitView:

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)
    }
}

然后使用它:

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

在iPad上可以使用。但是有一个问题(也许更多..)。 这种方法破坏了iPhone上的导航,因为MasterView和DetailView都有其NavigationView。

答案 1 :(得分:2)

在Simulator中进行最少的测试,但这应该接近实际的解决方案。想法是使用EnvironmentObject来保存已发布的var,以决定使用双列NavigationStyle还是使用单列NavigationView,如果该var更改,则重新创建 final class AppEnvironment: ObservableObject { @Published var useSideBySide: Bool = false } 。 / p>

环境对象:

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()
        }

在“场景委托”中,在启动时设置变量,然后观察设备旋转并可能对其进行更改(“ 1000”不是正确的值,起点):

NavigationView

在创建navigationViewStyle的顶级内容视图中,使用自定义修饰符而不是直接使用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()) } } } } }

{{1}}

如前所述,仅是模拟器测试,但我尝试了两种方向的启动,即以“ Master”(主)显示,“ Detail”(细节)显示进行旋转。

答案 2 :(得分:0)

Xcode 11 beta 3 中,Apple已将.navigationViewStyle(style:)添加到NavigationView

enter image description here

所以,我尝试了 Xcode 11 Beta 4

,然后创建 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)
    }
}

在我的 ContentView 内:

var body: some View {

        NavigationView {

            MyMasterView()

            DetailsView()

        }.navigationViewStyle(.doubleColumn)

    }

我碰到了iPad Pro(11英寸),但结果[仅在“纵向”下,在“横向”下工作]是相同的,没什么变化。

是错误吗? (嗯,我也不知道)。

因此,我更改了.navigationViewStyle(style:) property to。default`

  .navigationViewStyle(.doubleColumn)

相同的结果。

尝试使用最后一个属性.stack

结果不同。

它将强制navigationViewStyle.stack

输出

enter image description here

**之后再尝试一次:**

简单的“ .padding()”工作。

var body: some View {

        NavigationView {

            MyMasterView()

            DetailsView()

        }.navigationViewStyle(.doubleColumn)
            .padding()

    }

输出:

enter image description here

如果与.padding()一起使用,

navigationViewStyle 必须存在。

答案 3 :(得分:0)

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("?")
                Button(action: showList) {
                    Image(systemName: "sidebar.left")
                }
            }
        }
    }
}

import PlaygroundSupport
hostingController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.setLiveView(hostingController!)

答案 4 :(得分:0)

对于当前版本(iOS 13.0-13.3.x),您可以使用我的代码。 我使用UIViewUpdater来访问底层的UIView及其UIViewController来调整条形项目。

我认为解决这个问题的UIViewUpdater方法是最快捷,最可靠的方法,您可以使用它来访问和修改其他UIView,与UIViewController相关的UIKit机制。

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("?")
                .navigationBarTitle("", displayMode: .inline) // .inline is neccesary for showing the left button item
                .updateUIViewController {
                    $0.splitViewController?.preferredDisplayMode = .primaryOverlay // for showing overlay at initial
                    $0.splitViewController?.preferredDisplayMode = .automatic
                }
                .displayModeButtonItem()
        }.navigationViewStyle(StackNavigationViewStyle())
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

解决方案的实用程序代码。将其放在该项目的任何Swift文件中。 Utility.swift

// View decoration

public extension View {
    func updateUIView(_ action: @escaping (UIView) -> Void) -> some View {
        background(UIViewUpdater(action: action).opacity(0))
    }

    func updateUIViewController(_ action: @escaping (UIViewController) -> Void) -> some View {
        updateUIView {
            guard let viewController = $0.viewController else { return }
            action(viewController)
        }
    }

    func displayModeButtonItem(_ position: NavigationBarPostion = .left) -> some View {
        updateUIViewController { $0.setDisplayModeButtonItem(position) }
    }
}

// UpdateUIView


struct UIViewUpdater : UIViewRepresentable {
    let action: (UIView) -> Void
    typealias UIViewType = InnerUIView

    func makeUIView(context: UIViewRepresentableContext<Self>) -> UIViewType {
        UIViewType(action: action)
    }

    func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<Self>) {
        // DispatchQueue.main.async { [action] in action(uiView) }
    }

    class InnerUIView : UIView {
        let action: (UIView) -> Void
        init(action: @escaping (UIView) -> Void) {
            self.action = action
            super.init(frame: .zero)
        }

        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        override func didMoveToWindow() {
            super.didMoveToWindow()
            update()
        }

        func update() {
            action(self)
        }
    }
}

// UIView.viewController

public extension UIView {
    var viewController: UIViewController? {
        var i: UIResponder? = self
        while i != nil {
            if let vc = i as? UIViewController { return vc }
            i = i?.next
        }
        return nil
    }
}

// UIViewController.setDisplayModeButtonItem

public enum NavigationBarPostion {
    case left
    case right
}

public extension UIViewController {
    func setDisplayModeButtonItem(_ position: NavigationBarPostion) {
        guard let splitViewController = splitViewController else { return }
        switch position {
        case .left:
            // keep safe to avoid replacing other left bar button item, e.g. navigation back
            navigationItem.leftItemsSupplementBackButton = true
            navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
        case .right:
            navigationItem.rightBarButtonItem = splitViewController.displayModeButtonItem
        }
    }
}