Изменение состояния в onAppear breaks PageViewController - PullRequest
1 голос
/ 15 февраля 2020

Я изо всех сил пытаюсь понять, почему PageViewController не работает должным образом при изменении переменной @Published EnvironmentObject во время .onAppear в его родительском представлении.

Если я закомментирую .onAppear модификатор в DetailView, тогда все работает нормально - слайды смахивают, как и ожидалось от 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: $0) }
  }

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

Ответы [ 2 ]

1 голос
/ 16 февраля 2020

Это основано на полезном ответе @ Mac3n, который правильно указал, что создание ViewControllers, переданного в часть состояния 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: $0) })
  }

  var body: some View {
    VStack(alignment: .center) {
      PageViewController(
        viewControllers: viewControllers,
        currentPageIndex: $currentPage
      )
    }
  }
}
0 голосов
/ 16 февраля 2020

Когда вы изменяете переменную EnvironmentObject во время .onAppear в родительском представлении, оно снова визуализирует представление, и я предполагаю, что значения обновляются новыми объектами. Если вы установили точку останова в своем методе 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 и передайте его из родительского представления

...