折叠 SwiftUI 中的 doubleColumn NavigationView 详细信息,就像在 UISplitViewController 上折叠一样?
Posted
技术标签:
【中文标题】折叠 SwiftUI 中的 doubleColumn NavigationView 详细信息,就像在 UISplitViewController 上折叠一样?【英文标题】:Collapse a doubleColumn NavigationView detail in SwiftUI like with collapsed on UISplitViewController? 【发布时间】:2019-07-25 23:38:14 【问题描述】:因此,当我在 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,并且第一个细节屏幕是表情符号。但是如果人们点击一个名字,详细视图是“你好!”
一切都很棒。
但是,如果我纵向运行 iPad,用户会看到表情符号,然后没有迹象表明存在列表。您必须从左向右滑动才能使列表从侧面显示。
有没有人知道如何让导航栏出现,让用户点击以查看左侧的项目列表?所以它不是只有表情符号的屏幕?
我不想留下一个说明“从左侧滑动以查看文件/人员/任何内容的列表”
我记得 UISplitViewController 有一个可以设置的折叠属性。这里有类似的吗?
【问题讨论】:
您找到真正的解决方案了吗?因为我在 macOS 上遇到了同样的问题,并且建议的解决方法不再起作用…… 还没有真正的解决方案@K.Biermann @K.Biermann 我使用带有 .navigationViewStyle(DoubleColumnNavigationViewStyle()) 修饰符的 NavigationView 做了一个快速的 macOS 项目,我得到一个拆分视图就好了。我错过了什么吗? Xcode 11.4-beta 修复了这个问题 - 现在左上方有一个按钮,指示您可以打开主视图! @K.Biermann 我解决了。请参阅我在下面发布的代码。 【参考方案1】:在 Xcode 11 beta 3 中,Apple 已将 .navigationViewStyle(style:)
添加到 NavigationView
。
为 Xcode 11 Beta 5 更新。 创建 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(DoubleColumnNavigationViewStyle())
.padding()
输出:
【讨论】:
填充,嗯?那太随意了。我很快就会试一试! 我试过了,看起来,一旦添加了填充,就无法关闭侧边栏,这是使主视图全屏显示的必要行为。所以它仍然不像我希望的倒塌的财产。您可以使用变量将填充设置为 0 以使侧边栏可关闭,但似乎没有像 NavigationLink 这样的东西来触发它的好方法。无论如何,这都有点hacky,所以我可能需要通过反馈报告并等待下一个测试版。 这不是一个解决方案,因为(正如预期的那样)它向外部添加了填充。这意味着视图不能使用整个屏幕。 我设置了.padding(.leading, 1)
并且效果很好。谢谢!
我在一个新项目中测试了这段代码,但这不起作用。 Master 不会与 Detail 并排显示。发生的情况是细节显示在右上角有一个后退按钮,就是这样。您仍然需要从右侧滑动才能看到大师。还尝试了.padding(.leading, 1)
的建议,但这也不起作用。运行 XCode 12.5.1【参考方案2】:
目前,在 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。
更新: 最后,在 Xcode 11.4 beta 2 中,他们在导航栏中添加了一个按钮,指示隐藏的主视图。
【讨论】:
旋转(当horizontalSizeClass == .compact
)会导致问题...【参考方案3】:
在模拟器中进行最少的测试,但这应该接近真正的解决方案。这个想法是使用EnvironmentObject
来保存一个已发布的变量,以确定是使用双列NavigationStyle
还是单列,然后在该变量发生变化时重新创建NavigationView
。
环境对象:
final class AppEnvironment: ObservableObject
@Published var useSideBySide: Bool = false
在场景委托中,在启动时设置变量,然后观察设备旋转并可能更改它(“1000”不是正确的值,起点):
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()
在创建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())
如前所述,只是模拟器测试,但我尝试在两个方向上启动,显示 Master 旋转,显示 Detail 旋转,对我来说一切都很好。
【讨论】:
我正在使用您的方法,但是当从横向旋转到纵向时,整个视图会刷新并显示主视图,在我的例子中是一个列表。我想从横向显示选定的详细视图,您认为这可能吗? @laucel 去年当我接受一份新工作时,我不得不放弃学习 Swiftui。很抱歉,帮不上忙。【参考方案4】:对于当前版本(ios 13.0-13.3.x),您可以使用我的代码。 我使用 UIViewUpdater 访问底层 UIView 及其 UIViewController 来调整栏项。
我认为解决这个问题的 UIViewUpdater 方式是最 Swifty 和健壮的方式,你可以使用它来访问和修改其他 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
【讨论】:
重点是能够在不将所有内容包装在 UIKit 中的情况下工作。否则,我只会将 UISplitViewController 包装在 UIViewControllerRepresentable 中。 此代码没有包装新的 UISplitVC。它只是让您有机会操纵底层的 UISplitVC。这是目前唯一的方法。那你怎么能不碰 UIKit 就改变显示模式呢? @MScottWaller 它确实解决了 iOS 13.0 - 13.3 中的问题。在当前的 SwiftUI NavigationView 机制下,它是初始显示 primaryOverlay 的唯一方式,并提供了让您选择显示模式(例如 allVisible)的机会,比包装任何新的 UISplitVC 更好,因为它会放弃使用 DoubleColumnNavigationStyle。 所以,我们正在寻找一种完全不使用 UIKit 的方法。现在有一个底层的 UISplitVC,但也许将来不会有。现在 UIKit 是一个中间抽象,但也许苹果将来会删除 UIKit 作为中间抽象,或者至少使用一些新的东西,而不是 UISplitVC 来处理主细节。在这两种情况下,您的代码都会中断。关键是,我们只想使用 SwiftUI 来做到这一点,而不是通过混合 UIKit。 @MScottWaller 如果你想找到一个 SwiftUI 唯一的方法(并请在你的问题中宣布它)那么没有办法,因为没有 SwiftUI API。【参考方案5】: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!)
【讨论】:
以上是关于折叠 SwiftUI 中的 doubleColumn NavigationView 详细信息,就像在 UISplitViewController 上折叠一样?的主要内容,如果未能解决你的问题,请参考以下文章
在 HStack 中有 2 个 List/ScrollView 时,NavigationView 不会折叠 - SwiftUI