SwiftUI:如何确定一个视图是在 NavigationView 中呈现,Sheet,还是根视图?
SwiftUI: How can I determine if a view is presented in a NavigationView, Sheet, or is the root?
我正在处理一个需要自定义导航栏的项目,该导航栏将具有自定义按钮和标题样式,同时还允许在主导航部分下方有一个附件视图。
基本上,我想抽象出根据演示样式选择自定义后退按钮的需要。如果它显示在 sheet 中,我打算显示一个 X 图标。如果它被推到导航视图上,我想显示一个返回错误。如果它是根视图,我想完全隐藏按钮。
我已经映射了 presentationMode
环境变量,但是当我访问 isPresented
值时,我总是得到 true,即使在我的应用程序的根视图中也是如此。
以下是我正在从事的工作的总体思路:
import SwiftUI
struct CustomNavigationBar<Content>: View where Content: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
private let title: LocalizedStringKey
private let content: (() -> Content)?
private var backButton: AnyView? {
let button = Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
// custom image extension, just resolves to a back icon
Image.Icons.arrowBack
}
if (presentationMode.wrappedValue.isPresented) {
return AnyView(button)
} else {
return nil
}
}
public init(_ title: LocalizedStringKey, content: (() -> Content)? = nil) {
self.title = title
self.content = content
}
var body: some View {
VStack {
content?()
Divider().foregroundColor(.gray)
}.navigationBarTitle(title, displayMode: .large)
.frame(minHeight: 96)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backButton)
}
}
有没有人有使用 SwiftUI 访问视图在表示层次结构中的位置的经验或技巧?谢谢!
您可以使用 SwiftUI-Introspect
,用于“从 SwiftUI 内省底层 UIKit 组件”。
这是您正在寻找的工作示例。这是一个交互式示例,因此您可以单击不同的模式。
import Introspect
import SwiftUI
/* ... */
struct ContentView: View {
@State private var testing = 1
private let thingsToTest = 3
var body: some View {
VStack {
Picker("Testing", selection: $testing) {
ForEach(1 ... thingsToTest, id: \.self) { index in
Text("\(index)")
.tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
Divider()
Spacer()
switch testing {
case 1:
PresentationReader { kind in
Text("Hello! Kind: \(kind.rawValue)")
}
case 2:
NavigationView {
PresentationReader { kind in
Text("Hello! Kind: \(kind.rawValue)")
}
}
case 3:
Text("Parent")
.sheet(isPresented: .constant(true)) {
PresentationReader { kind in
Text("Hello! Kind: \(kind.rawValue)")
}
}
default:
fatalError("Unavailable")
}
Spacer()
}
}
}
enum Kind: String {
case navigationView
case root
case sheet
}
struct PresentationReader<Content: View>: View {
typealias PresentedContent = (Kind) -> Content
@State private var kind: Kind = .root
private let content: PresentedContent
init(@ViewBuilder content: @escaping PresentedContent) {
self.content = content
}
var body: some View {
content(kind)
.presentationReader(kind: $kind)
}
}
extension View {
func presentationReader(kind: Binding<Kind>) -> some View {
self
.introspectViewController { vc in
let rootVC = UIApplication.shared.windows.first?.rootViewController
let isRoot = vc === rootVC
var isHosted: Bool { Introspect.findHostingView(from: vc.view) != nil }
if isRoot {
kind.wrappedValue = .root
} else if isHosted {
kind.wrappedValue = .navigationView
} else {
kind.wrappedValue = .sheet
}
}
}
}
它通过获取视图所在的当前视图控制器来工作。
- 如果根视图控制器的 class 引用与当前根视图控制器相同,则这是根视图(意味着它没有嵌入
NavigationView
或 .sheet(...)
).
- 如果这不是根视图,我们将检查此视图是否嵌入到托管视图中。如果是,则在
NavigationView
. 中
- 如果视图既不是根视图也不在
NavigationView
中,因此它在 .sheet(...)
中。
这就是您的 CustomNavigationBar
经过这 3 项更改后的样子:
struct CustomNavigationBar<Content>: View where Content: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State private var kind: Kind = .root // <--- CHANGE #1
private let title: LocalizedStringKey
private let content: (() -> Content)?
private var backButton: AnyView? {
let button = Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
// custom image extension, just resolves to a back icon
Image.Icons.arrowBack
}
if kind == .navigationView { // <--- CHANGE #2
return AnyView(button)
} else {
return nil
}
}
public init(_ title: LocalizedStringKey, content: (() -> Content)? = nil) {
self.title = title
self.content = content
}
var body: some View {
VStack {
content?()
.presentationReader(kind: $kind) // <--- CHANGE #3
Divider().foregroundColor(.gray)
}.navigationBarTitle(title, displayMode: .large)
.frame(minHeight: 96)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backButton)
}
}
我正在处理一个需要自定义导航栏的项目,该导航栏将具有自定义按钮和标题样式,同时还允许在主导航部分下方有一个附件视图。
基本上,我想抽象出根据演示样式选择自定义后退按钮的需要。如果它显示在 sheet 中,我打算显示一个 X 图标。如果它被推到导航视图上,我想显示一个返回错误。如果它是根视图,我想完全隐藏按钮。
我已经映射了 presentationMode
环境变量,但是当我访问 isPresented
值时,我总是得到 true,即使在我的应用程序的根视图中也是如此。
以下是我正在从事的工作的总体思路:
import SwiftUI
struct CustomNavigationBar<Content>: View where Content: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
private let title: LocalizedStringKey
private let content: (() -> Content)?
private var backButton: AnyView? {
let button = Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
// custom image extension, just resolves to a back icon
Image.Icons.arrowBack
}
if (presentationMode.wrappedValue.isPresented) {
return AnyView(button)
} else {
return nil
}
}
public init(_ title: LocalizedStringKey, content: (() -> Content)? = nil) {
self.title = title
self.content = content
}
var body: some View {
VStack {
content?()
Divider().foregroundColor(.gray)
}.navigationBarTitle(title, displayMode: .large)
.frame(minHeight: 96)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backButton)
}
}
有没有人有使用 SwiftUI 访问视图在表示层次结构中的位置的经验或技巧?谢谢!
您可以使用 SwiftUI-Introspect
,用于“从 SwiftUI 内省底层 UIKit 组件”。
这是您正在寻找的工作示例。这是一个交互式示例,因此您可以单击不同的模式。
import Introspect
import SwiftUI
/* ... */
struct ContentView: View {
@State private var testing = 1
private let thingsToTest = 3
var body: some View {
VStack {
Picker("Testing", selection: $testing) {
ForEach(1 ... thingsToTest, id: \.self) { index in
Text("\(index)")
.tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
Divider()
Spacer()
switch testing {
case 1:
PresentationReader { kind in
Text("Hello! Kind: \(kind.rawValue)")
}
case 2:
NavigationView {
PresentationReader { kind in
Text("Hello! Kind: \(kind.rawValue)")
}
}
case 3:
Text("Parent")
.sheet(isPresented: .constant(true)) {
PresentationReader { kind in
Text("Hello! Kind: \(kind.rawValue)")
}
}
default:
fatalError("Unavailable")
}
Spacer()
}
}
}
enum Kind: String {
case navigationView
case root
case sheet
}
struct PresentationReader<Content: View>: View {
typealias PresentedContent = (Kind) -> Content
@State private var kind: Kind = .root
private let content: PresentedContent
init(@ViewBuilder content: @escaping PresentedContent) {
self.content = content
}
var body: some View {
content(kind)
.presentationReader(kind: $kind)
}
}
extension View {
func presentationReader(kind: Binding<Kind>) -> some View {
self
.introspectViewController { vc in
let rootVC = UIApplication.shared.windows.first?.rootViewController
let isRoot = vc === rootVC
var isHosted: Bool { Introspect.findHostingView(from: vc.view) != nil }
if isRoot {
kind.wrappedValue = .root
} else if isHosted {
kind.wrappedValue = .navigationView
} else {
kind.wrappedValue = .sheet
}
}
}
}
它通过获取视图所在的当前视图控制器来工作。
- 如果根视图控制器的 class 引用与当前根视图控制器相同,则这是根视图(意味着它没有嵌入
NavigationView
或.sheet(...)
). - 如果这不是根视图,我们将检查此视图是否嵌入到托管视图中。如果是,则在
NavigationView
. 中
- 如果视图既不是根视图也不在
NavigationView
中,因此它在.sheet(...)
中。
这就是您的 CustomNavigationBar
经过这 3 项更改后的样子:
struct CustomNavigationBar<Content>: View where Content: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State private var kind: Kind = .root // <--- CHANGE #1
private let title: LocalizedStringKey
private let content: (() -> Content)?
private var backButton: AnyView? {
let button = Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
// custom image extension, just resolves to a back icon
Image.Icons.arrowBack
}
if kind == .navigationView { // <--- CHANGE #2
return AnyView(button)
} else {
return nil
}
}
public init(_ title: LocalizedStringKey, content: (() -> Content)? = nil) {
self.title = title
self.content = content
}
var body: some View {
VStack {
content?()
.presentationReader(kind: $kind) // <--- CHANGE #3
Divider().foregroundColor(.gray)
}.navigationBarTitle(title, displayMode: .large)
.frame(minHeight: 96)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backButton)
}
}