在 onAppear 中更改状态会破坏 PageViewController
Changing state in onAppear breaks PageViewController
我很难理解为什么在其父视图中 .onAppear
期间 @Published EnvironmentObject 变量发生更改时 PageViewController 无法按预期工作。
如果我在 DetailView
中注释掉 .onAppear
修饰符,那么一切正常 - 幻灯片按 PageViewController 的预期滑动。但是一旦该修饰符重新出现,页面就不会正确滑动。
应该是因为状态改变而重新渲染,但我想不通。我知道 updateUIViewController
在状态变化时被调用,这似乎改变了 viewControllers 的数组。
如果有人指导如何解决这个问题,我将不胜感激!
如果您是运行下面的代码,请确保更改您的 SceneDelegate:
let contentView = ContentView()
到
let contentView = ContentView().environmentObject(Global())
下面是一个简单的问题示例:
import SwiftUI
import Foundation
import UIKit
struct Item: Hashable {
var text: String
}
let testItems: [Item] = [
Item(text: "I am item 1"),
Item(text: "I am item 2"),
Item(text: "I am item 3")
]
final class Global : ObservableObject {
@Published var hideText: Bool = false
}
struct ContentView: View {
@EnvironmentObject var global: Global
var body: some View {
NavigationView {
VStack {
List(testItems, id: \.self) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item.text)
}
}
Text("I should vanish when detail view loads")
.opacity(self.global.hideText ? 0 : 1)
}
.navigationBarTitle("Test")
}
}
}
struct DetailView: View {
@EnvironmentObject var global: Global
var item: Item
var testViews: [Text] = [
Text("Slide 1").font(.title),
Text("Slide 2").font(.title),
Text("Slide 3").font(.title),
]
var body: some View {
// The .onAppear modifier breaks the PageViewController
// when it is commented out, the slides work as expected
PageView(testViews)
.onAppear {
self.global.hideText = true
}
}
}
struct PageView<Page: View>: View {
@State var currentPage = 0
var viewControllers: [UIHostingController<Page>]
init(_ views: [Page]) {
self.viewControllers = views.map{ UIHostingController(rootView: [=12=]) }
}
var body: some View {
VStack(alignment: .center) {
PageViewController(
viewControllers: viewControllers,
currentPageIndex: $currentPage
)
}
}
}
struct PageViewController: UIViewControllerRepresentable {
var viewControllers: [UIViewController]
@Binding var currentPageIndex: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal
)
pageViewController.view.backgroundColor = UIColor.clear
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers([viewControllers[currentPageIndex]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
// retrieves the index of the currently displayed view controller
guard let index = parent.viewControllers.firstIndex(of: viewController) else {
return nil
}
// shows the last view controller when the user swipes back from the first view controller
if index == 0 {
return parent.viewControllers.last
}
//show the view controller before the currently displayed view controller
return parent.viewControllers[index - 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
// retrieves the index of the currently displayed view controller
guard let index = parent.viewControllers.firstIndex(of: viewController) else {
print("View controller not found!!")
return nil
}
// shows the first view controller when the user swipes further from the last view controller
if index + 1 == parent.viewControllers.count {
return parent.viewControllers.first
}
// show the view controller after the currently displayed view controller
return parent.viewControllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool
) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.viewControllers.firstIndex(of: visibleViewController) {
parent.currentPageIndex = index
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Global())
}
}
当您在 .onAppear
期间在其父视图中更改 EnvironmentObject
变量时,它会再次呈现该视图,我猜值会随着新对象的更新而更新。
如果您在 func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?
方法中设置断点并在控制台中获取 po,您可以看到视图的内存地址不同,因此无法找到下一个视图控制器
parent:
(lldb) po parent
▿ PageViewController
▿ viewControllers : 3 elements
▿ 0 : <_TtGC7SwiftUI19UIHostingControllerVS_4Text_: 0x7fc881e04b10>
▿ 1 : <_TtGC7SwiftUI19UIHostingControllerVS_4Text_: 0x7fc881f13f70>
▿ 2 : <_TtGC7SwiftUI19UIHostingControllerVS_4Text_: 0x7fc881f14dd0>
(lldb) po viewController
<_TtGC7SwiftUI19UIHostingControllerVS_4Text_: 0x7fc881f186c0>
如您所见,在父视图控制器中找不到视图控制器
对于解决方案,如果您将 PageView
更改为此,它可以解决问题,但也可能是其他解决方案。
struct PageView: View {
@State var currentPage = 0
@State var viewControllers: [UIViewController]
var body: some View {
VStack(alignment: .center) {
PageViewController(
viewControllers: viewControllers,
currentPageIndex: $currentPage
)
}
}
}
将您的 viewControllers
更改为 @State 并将其从父视图传递给它
这建立在@Mac3n 的一个有用的答案之上,他正确地指出将 ViewController 传递给状态的 UIViewControllerRepresentable 部分可以解决这个问题。所缺少的只是如何通过 init
方法实际做到这一点。
这里是 PageView
的完整更新
struct PageView<Page: View>: View {
@State var currentPage = 0
@State var viewControllers: [UIHostingController<Page>]
init(_ views: [Page]) {
_viewControllers = State(initialValue: views.map{ UIHostingController(rootView: [=10=]) })
}
var body: some View {
VStack(alignment: .center) {
PageViewController(
viewControllers: viewControllers,
currentPageIndex: $currentPage
)
}
}
}
我很难理解为什么在其父视图中 .onAppear
期间 @Published EnvironmentObject 变量发生更改时 PageViewController 无法按预期工作。
如果我在 DetailView
中注释掉 .onAppear
修饰符,那么一切正常 - 幻灯片按 PageViewController 的预期滑动。但是一旦该修饰符重新出现,页面就不会正确滑动。
应该是因为状态改变而重新渲染,但我想不通。我知道 updateUIViewController
在状态变化时被调用,这似乎改变了 viewControllers 的数组。
如果有人指导如何解决这个问题,我将不胜感激!
如果您是运行下面的代码,请确保更改您的 SceneDelegate:
let contentView = ContentView()
到
let contentView = ContentView().environmentObject(Global())
下面是一个简单的问题示例:
import SwiftUI
import Foundation
import UIKit
struct Item: Hashable {
var text: String
}
let testItems: [Item] = [
Item(text: "I am item 1"),
Item(text: "I am item 2"),
Item(text: "I am item 3")
]
final class Global : ObservableObject {
@Published var hideText: Bool = false
}
struct ContentView: View {
@EnvironmentObject var global: Global
var body: some View {
NavigationView {
VStack {
List(testItems, id: \.self) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item.text)
}
}
Text("I should vanish when detail view loads")
.opacity(self.global.hideText ? 0 : 1)
}
.navigationBarTitle("Test")
}
}
}
struct DetailView: View {
@EnvironmentObject var global: Global
var item: Item
var testViews: [Text] = [
Text("Slide 1").font(.title),
Text("Slide 2").font(.title),
Text("Slide 3").font(.title),
]
var body: some View {
// The .onAppear modifier breaks the PageViewController
// when it is commented out, the slides work as expected
PageView(testViews)
.onAppear {
self.global.hideText = true
}
}
}
struct PageView<Page: View>: View {
@State var currentPage = 0
var viewControllers: [UIHostingController<Page>]
init(_ views: [Page]) {
self.viewControllers = views.map{ UIHostingController(rootView: [=12=]) }
}
var body: some View {
VStack(alignment: .center) {
PageViewController(
viewControllers: viewControllers,
currentPageIndex: $currentPage
)
}
}
}
struct PageViewController: UIViewControllerRepresentable {
var viewControllers: [UIViewController]
@Binding var currentPageIndex: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal
)
pageViewController.view.backgroundColor = UIColor.clear
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers([viewControllers[currentPageIndex]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
// retrieves the index of the currently displayed view controller
guard let index = parent.viewControllers.firstIndex(of: viewController) else {
return nil
}
// shows the last view controller when the user swipes back from the first view controller
if index == 0 {
return parent.viewControllers.last
}
//show the view controller before the currently displayed view controller
return parent.viewControllers[index - 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
// retrieves the index of the currently displayed view controller
guard let index = parent.viewControllers.firstIndex(of: viewController) else {
print("View controller not found!!")
return nil
}
// shows the first view controller when the user swipes further from the last view controller
if index + 1 == parent.viewControllers.count {
return parent.viewControllers.first
}
// show the view controller after the currently displayed view controller
return parent.viewControllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool
) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.viewControllers.firstIndex(of: visibleViewController) {
parent.currentPageIndex = index
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Global())
}
}
当您在 .onAppear
期间在其父视图中更改 EnvironmentObject
变量时,它会再次呈现该视图,我猜值会随着新对象的更新而更新。
如果您在 func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?
方法中设置断点并在控制台中获取 po,您可以看到视图的内存地址不同,因此无法找到下一个视图控制器
parent:
(lldb) po parent
▿ PageViewController
▿ viewControllers : 3 elements
▿ 0 : <_TtGC7SwiftUI19UIHostingControllerVS_4Text_: 0x7fc881e04b10>
▿ 1 : <_TtGC7SwiftUI19UIHostingControllerVS_4Text_: 0x7fc881f13f70>
▿ 2 : <_TtGC7SwiftUI19UIHostingControllerVS_4Text_: 0x7fc881f14dd0>
(lldb) po viewController
<_TtGC7SwiftUI19UIHostingControllerVS_4Text_: 0x7fc881f186c0>
如您所见,在父视图控制器中找不到视图控制器
对于解决方案,如果您将 PageView
更改为此,它可以解决问题,但也可能是其他解决方案。
struct PageView: View {
@State var currentPage = 0
@State var viewControllers: [UIViewController]
var body: some View {
VStack(alignment: .center) {
PageViewController(
viewControllers: viewControllers,
currentPageIndex: $currentPage
)
}
}
}
将您的 viewControllers
更改为 @State 并将其从父视图传递给它
这建立在@Mac3n 的一个有用的答案之上,他正确地指出将 ViewController 传递给状态的 UIViewControllerRepresentable 部分可以解决这个问题。所缺少的只是如何通过 init
方法实际做到这一点。
这里是 PageView
struct PageView<Page: View>: View {
@State var currentPage = 0
@State var viewControllers: [UIHostingController<Page>]
init(_ views: [Page]) {
_viewControllers = State(initialValue: views.map{ UIHostingController(rootView: [=10=]) })
}
var body: some View {
VStack(alignment: .center) {
PageViewController(
viewControllers: viewControllers,
currentPageIndex: $currentPage
)
}
}
}