iOS UIPageViewController 变得混乱并且不显示 ViewControllers
iOS UIPageViewController Gets Confused and ViewControllers Are Not Displayed
我们设置了左右按钮,以便用户快速翻阅不同的汽车。如果用户快速点击下一页 10 次或更多次,我们的页面视图控制器将丢失视图控制器。
这是正确显示汽车的车辆页面(模糊以隐藏不相关的信息)。在这里查看图片:
如果滚动动画开启 (true),它会在快速点击向右箭头 6 次或更多次后丢失车辆页面。在这里查看图片:
代码:
private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
let viewControllers = [viewController]
let isAnimated = true // false always works. However, animation is required.
setViewControllers(viewControllers, direction: direction, animated: isAnimated, completion: nil)
}
调试时,当页面视图控制器停止显示汽车时,我确保设置的视图控制器不为零,列表(汽车)也不为零。
我尝试了 UIPageViewController, how do I correctly jump to a specific page without messing up the order specified by the data source? 中使用完成块的解决方案的变体。但是,它没有用。
weak var pvcw: UIPageViewController? = self
setViewControllers(viewControllers, direction: direction, animated: true, completion: {(_ finished: Bool) -> Void in
let pvcs: UIPageViewController? = pvcw
if pvcs == nil {
return
}
DispatchQueue.main.async(execute: {() -> Void in
pvcs?.setViewControllers(viewControllers, direction: direction, animated: false) {(_ finished: Bool) -> Void in }
})
})
有什么想法吗?谢谢。
更新
我注意到有时包含的视图控制器可能会偏离中心而不是完全缺失。
我更深入地研究了视图控制器完全丢失的情况。单击 "Debug View Hierarchy" 并打开“Show Clipped Content”会在视图控制器完全丢失时显示以下内容:
因此,缺失的内容似乎被剪裁/越界了。
仅显示线框图显示以下内容:
页面视图控制器有一个
- _UIPageViewControllerContentView 包含
- _UIQueuingScrollView 包含一个
- UIView 包含一个
- VehicleDetailTableViewController(带有汽车图像和详细信息的 UITableViewController)。
我还看到 _UIQueuingScrollView 的边界在情况很奇怪时有很大不同。当一切正常时,边界的 x 为 1125,而 X 为 375。
只有在使用滚动过渡样式而不是卷页时才会发生这种情况。使用 Page Curl 时,一切正常。
我们如何预防/解决这个问题?
第二次更新
此代码使问题消失。然而,它留下了更刺耳的体验。可能是延迟了0.4秒,正常使用时有时会出现蓝色背景
private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
let viewControllers = [viewController]
setViewControllers(viewControllers, direction: direction, animated: true, completion: { (_) in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: {
self.setViewControllers(viewControllers, direction: direction, animated: false, completion: nil)
})
})
}
这不是一个好的用户体验。 有没有更好的方法?
我希望滚动过渡平滑,不要短暂显示蓝色背景,并且不要丢失其内容,即视图控制器内容。
虽然真正的答案是让视图控制器尽可能简单(但并不简单),但这里的代码解决了当用户导航到下一个视图控制器。
private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
let viewControllers = [viewController]
setViewControllers(viewControllers, direction: direction, animated: true, completion: { (_) in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: {
self.setViewControllers(viewControllers, direction: direction, animated: false, completion: nil)
})
})
}
我不确定此解决方案是否适合您的用户,但如果由于用户快速导航而出现问题,您可以实施锁定以禁止此快速导航。本质上:
private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
guard !isChangingPages else { return }
isChangingPages = true
let viewControllers = [viewController]
let isAnimated = true // false always works. However, animation is required.
setViewControllers(viewControllers, direction: direction, animated: isAnimated, completion: { [weak self] _ in
self?.isChangingPages = false
})
}
这样,您必须先完成到新页面的转换,然后才能转换到下一页。
如果您在将此布尔值设置为 true 时保持导航按钮处于启用状态(点击看不到结果),这可能会导致用户感到困惑。但是可以更改逻辑以禁用按钮并在完成块中重新启用它们(这样它们会在页面更改期间消失 in/out)。
一个简单的解决方案是通过添加一个小 "tap ahead" 缓冲区将按钮点击与视图控制器更改分离。创建一个按钮队列(使用一个简单的 NSMutableArray
作为 FIFO 队列),在其中添加每个导航按钮点击,然后调用出队函数 如果 在添加之前队列为空.
在出队函数中,您删除第一个条目并相应地更改视图,然后在 setViewControllers
完成处理程序中再次调用自身 如果 队列不为空。
确保只在主线程上进行处理以避免线程问题。如果需要,您还可以对允许的数量添加限制 "tap ahead",并可能在方向更改时刷新队列。
您好,我创建了一个 Sample Project,应该可以解决您的问题。我已经添加了 100 ViewControllers(通过循环)并且它在滚动动画中工作正常。事情就在他们的地方。
我在这个项目中所做的是:
为具有两个属性的页面创建了一个基类
a) Int 类型的 pageIndex
b) 回调协议委托
通过 ContainerView
将 UIPageViewController 添加到 ViewController
创建了一个 ViewController 扩展 PageViewBase 的命名页面
运行 循环计数为 100 并将数据添加到数组并将数据源和委托设置为自身 (PageControlelr) 并根据 pageIndex 属性[=15 进行管理=]
页ViewController
class PageViewController: UIPageViewController {
var list = [Page]()
var sb: UIStoryboard?
var viewController: ViewController! // settting from ViewController
override func viewDidLoad() {
super.viewDidLoad()
sb = UIStoryboard(name: "Main", bundle: nil)
DispatchQueue.main.asyncAfter(deadline: .now()+0.4, execute: {
self.setupList()
})
}
func setupList(){
for i in 0..<100{
let model = PageModel(title: "Title \(i + 1)", subTitle: "SubTitle \(i + 1)")
let page = sb?.instantiateViewController(withIdentifier: "PageID") as! Page
page.data = model
page.pageIndex = i
page.delegate = viewController
list.append(page)
}
self.delegate = self
self.dataSource = self
setViewControllers([list[0]], direction: .forward, animated: true, completion: nil)
self.updateCurrentPageLabel(index: 0)
}
func movePage(index: Int){
let currentIndex = self.viewControllers![0] as! Page
self.updateCurrentPageLabel(index: index)
setViewControllers([list[index]], direction: index > currentIndex.pageIndex ? .forward : .reverse, animated: true)
}
func getCurrentPageIndex() -> Int{
return (self.viewControllers![0] as! Page).pageIndex
}
func updateCurrentPageLabel(index: Int){
(self.parent as? ViewController)?.currentListingLabel.text = "\(index + 1) of \(list.count)"
}
}
extension PageViewController: UIPageViewControllerDelegate{
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
let currentIndex = (self.viewControllers![0] as! Page).pageIndex
self.updateCurrentPageLabel(index: currentIndex)
}
}
extension PageViewController: UIPageViewControllerDataSource{
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let index = (viewController as! Page).pageIndex
if index > 0 {
return list[index-1]
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let index = (viewController as! Page).pageIndex
if index < list.count-1 {
return list[index+1]
}
return nil
}
}
第
页
import UIKit
struct PageModel {
var title: String
var subTitle: String
}
class Page: PageViewBase {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var subTitleLabel: UILabel!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var btnWorking: UIButton!
var data: PageModel?
override func viewDidLoad() {
super.viewDidLoad()
setupTags()
setupActions()
setupData()
}
func setupData(){
if let data = data{
self.titleLabel.text = data.title
self.subTitleLabel.text = data.subTitle
imageView.image = #imageLiteral(resourceName: "car")
}
}
enum buttonTags: Int{
case working = 1
}
func setupTags(){
btnWorking.tag = buttonTags.working.rawValue
}
func setupActions(){
btnWorking.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
}
@objc func didSelect(_ sender: UIView){
if let tag = buttonTags.init(rawValue: sender.tag){
switch tag{
case .working:
delegate?.didReceive(withMessage: "wokring button clicked of index \(pageIndex)")
}
}
}
}
ViewController // 主控制器
import UIKit
protocol CallBack {
func didReceive(withMessage message: String)
}
class ViewController: UIViewController {
@IBOutlet weak var containerView: UIView!
@IBOutlet weak var btnCall: UIButton!
@IBOutlet weak var btnMessage: UIButton!
@IBOutlet weak var btnNext: UIButton!
@IBOutlet weak var btnBack: UIButton!
@IBOutlet weak var currentListingLabel: UILabel!
var pageController: PageViewController?
override func viewDidLoad() {
super.viewDidLoad()
setupTags()
setupActions()
setupContainerView()
}
enum buttonTags: Int{
case call = 1
case message
case next
case back
}
func setupTags(){
btnCall.tag = buttonTags.call.rawValue
btnMessage.tag = buttonTags.message.rawValue
btnNext.tag = buttonTags.next.rawValue
btnBack.tag = buttonTags.back.rawValue
}
func setupActions(){
btnCall.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
btnMessage.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
btnNext.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
btnBack.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
}
@objc func didSelect(_ sender: UIView){
if let tag = buttonTags.init(rawValue: sender.tag){
switch tag{
case .call:
print("Call button called for index \(pageController?.getCurrentPageIndex() ?? 0)")
case .message:
print("message button called for index \(pageController?.getCurrentPageIndex() ?? 0)")
case .next:
if let p = pageController{
let currentIndex = p.getCurrentPageIndex()
if currentIndex < p.list.count - 1{
p.movePage(index: currentIndex + 1)
}
}
case .back:
if let p = pageController{
let currentIndex = p.getCurrentPageIndex()
if currentIndex > 0{
p.movePage(index: currentIndex - 1)
}
}
}
}
}
func setupContainerView(){
let sb = UIStoryboard(name: "Main", bundle: nil)
pageController = sb.instantiateViewController(withIdentifier: "PageViewControllerID") as? PageViewController
pageController?.viewController = self
addViewIntoParentViewController(vc: pageController)
}
func addViewIntoParentViewController(vc: UIViewController?){
if let vc = vc{
for v in self.containerView.subviews{
v.removeFromSuperview()
}
self.containerView.addSubview(vc.view)
self.containerView.translatesAutoresizingMaskIntoConstraints = false
vc.view.translatesAutoresizingMaskIntoConstraints = false
addChildViewController(vc)
NSLayoutConstraint.activate([
vc.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
vc.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
vc.view.topAnchor.constraint(equalTo: containerView.topAnchor),
vc.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])
vc.didMove(toParentViewController: self)
}
}
}
extension ViewController: CallBack{
func didReceive(withMessage message: String) {
print("message: \(message)")
}
}
PageViewBase
import UIKit
class PageViewBase: UIViewController {
var pageIndex = -1
var delegate: CallBack?
}
我们设置了左右按钮,以便用户快速翻阅不同的汽车。如果用户快速点击下一页 10 次或更多次,我们的页面视图控制器将丢失视图控制器。
这是正确显示汽车的车辆页面(模糊以隐藏不相关的信息)。在这里查看图片:
如果滚动动画开启 (true),它会在快速点击向右箭头 6 次或更多次后丢失车辆页面。在这里查看图片:
代码:
private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
let viewControllers = [viewController]
let isAnimated = true // false always works. However, animation is required.
setViewControllers(viewControllers, direction: direction, animated: isAnimated, completion: nil)
}
调试时,当页面视图控制器停止显示汽车时,我确保设置的视图控制器不为零,列表(汽车)也不为零。
我尝试了 UIPageViewController, how do I correctly jump to a specific page without messing up the order specified by the data source? 中使用完成块的解决方案的变体。但是,它没有用。
weak var pvcw: UIPageViewController? = self
setViewControllers(viewControllers, direction: direction, animated: true, completion: {(_ finished: Bool) -> Void in
let pvcs: UIPageViewController? = pvcw
if pvcs == nil {
return
}
DispatchQueue.main.async(execute: {() -> Void in
pvcs?.setViewControllers(viewControllers, direction: direction, animated: false) {(_ finished: Bool) -> Void in }
})
})
有什么想法吗?谢谢。
更新
我注意到有时包含的视图控制器可能会偏离中心而不是完全缺失。
我更深入地研究了视图控制器完全丢失的情况。单击 "Debug View Hierarchy" 并打开“Show Clipped Content”会在视图控制器完全丢失时显示以下内容:
因此,缺失的内容似乎被剪裁/越界了。
仅显示线框图显示以下内容:
页面视图控制器有一个
- _UIPageViewControllerContentView 包含
- _UIQueuingScrollView 包含一个
- UIView 包含一个
- VehicleDetailTableViewController(带有汽车图像和详细信息的 UITableViewController)。
我还看到 _UIQueuingScrollView 的边界在情况很奇怪时有很大不同。当一切正常时,边界的 x 为 1125,而 X 为 375。
只有在使用滚动过渡样式而不是卷页时才会发生这种情况。使用 Page Curl 时,一切正常。
我们如何预防/解决这个问题?
第二次更新
此代码使问题消失。然而,它留下了更刺耳的体验。可能是延迟了0.4秒,正常使用时有时会出现蓝色背景
private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
let viewControllers = [viewController]
setViewControllers(viewControllers, direction: direction, animated: true, completion: { (_) in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: {
self.setViewControllers(viewControllers, direction: direction, animated: false, completion: nil)
})
})
}
这不是一个好的用户体验。 有没有更好的方法?
我希望滚动过渡平滑,不要短暂显示蓝色背景,并且不要丢失其内容,即视图控制器内容。
虽然真正的答案是让视图控制器尽可能简单(但并不简单),但这里的代码解决了当用户导航到下一个视图控制器。
private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
let viewControllers = [viewController]
setViewControllers(viewControllers, direction: direction, animated: true, completion: { (_) in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: {
self.setViewControllers(viewControllers, direction: direction, animated: false, completion: nil)
})
})
}
我不确定此解决方案是否适合您的用户,但如果由于用户快速导航而出现问题,您可以实施锁定以禁止此快速导航。本质上:
private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
guard !isChangingPages else { return }
isChangingPages = true
let viewControllers = [viewController]
let isAnimated = true // false always works. However, animation is required.
setViewControllers(viewControllers, direction: direction, animated: isAnimated, completion: { [weak self] _ in
self?.isChangingPages = false
})
}
这样,您必须先完成到新页面的转换,然后才能转换到下一页。
如果您在将此布尔值设置为 true 时保持导航按钮处于启用状态(点击看不到结果),这可能会导致用户感到困惑。但是可以更改逻辑以禁用按钮并在完成块中重新启用它们(这样它们会在页面更改期间消失 in/out)。
一个简单的解决方案是通过添加一个小 "tap ahead" 缓冲区将按钮点击与视图控制器更改分离。创建一个按钮队列(使用一个简单的 NSMutableArray
作为 FIFO 队列),在其中添加每个导航按钮点击,然后调用出队函数 如果 在添加之前队列为空.
在出队函数中,您删除第一个条目并相应地更改视图,然后在 setViewControllers
完成处理程序中再次调用自身 如果 队列不为空。
确保只在主线程上进行处理以避免线程问题。如果需要,您还可以对允许的数量添加限制 "tap ahead",并可能在方向更改时刷新队列。
您好,我创建了一个 Sample Project,应该可以解决您的问题。我已经添加了 100 ViewControllers(通过循环)并且它在滚动动画中工作正常。事情就在他们的地方。
我在这个项目中所做的是:
为具有两个属性的页面创建了一个基类
a) Int 类型的 pageIndex
b) 回调协议委托
通过 ContainerView
将 UIPageViewController 添加到 ViewController
创建了一个 ViewController 扩展 PageViewBase 的命名页面
运行 循环计数为 100 并将数据添加到数组并将数据源和委托设置为自身 (PageControlelr) 并根据 pageIndex 属性[=15 进行管理=]
页ViewController
class PageViewController: UIPageViewController {
var list = [Page]()
var sb: UIStoryboard?
var viewController: ViewController! // settting from ViewController
override func viewDidLoad() {
super.viewDidLoad()
sb = UIStoryboard(name: "Main", bundle: nil)
DispatchQueue.main.asyncAfter(deadline: .now()+0.4, execute: {
self.setupList()
})
}
func setupList(){
for i in 0..<100{
let model = PageModel(title: "Title \(i + 1)", subTitle: "SubTitle \(i + 1)")
let page = sb?.instantiateViewController(withIdentifier: "PageID") as! Page
page.data = model
page.pageIndex = i
page.delegate = viewController
list.append(page)
}
self.delegate = self
self.dataSource = self
setViewControllers([list[0]], direction: .forward, animated: true, completion: nil)
self.updateCurrentPageLabel(index: 0)
}
func movePage(index: Int){
let currentIndex = self.viewControllers![0] as! Page
self.updateCurrentPageLabel(index: index)
setViewControllers([list[index]], direction: index > currentIndex.pageIndex ? .forward : .reverse, animated: true)
}
func getCurrentPageIndex() -> Int{
return (self.viewControllers![0] as! Page).pageIndex
}
func updateCurrentPageLabel(index: Int){
(self.parent as? ViewController)?.currentListingLabel.text = "\(index + 1) of \(list.count)"
}
}
extension PageViewController: UIPageViewControllerDelegate{
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
let currentIndex = (self.viewControllers![0] as! Page).pageIndex
self.updateCurrentPageLabel(index: currentIndex)
}
}
extension PageViewController: UIPageViewControllerDataSource{
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let index = (viewController as! Page).pageIndex
if index > 0 {
return list[index-1]
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let index = (viewController as! Page).pageIndex
if index < list.count-1 {
return list[index+1]
}
return nil
}
}
第
页import UIKit
struct PageModel {
var title: String
var subTitle: String
}
class Page: PageViewBase {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var subTitleLabel: UILabel!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var btnWorking: UIButton!
var data: PageModel?
override func viewDidLoad() {
super.viewDidLoad()
setupTags()
setupActions()
setupData()
}
func setupData(){
if let data = data{
self.titleLabel.text = data.title
self.subTitleLabel.text = data.subTitle
imageView.image = #imageLiteral(resourceName: "car")
}
}
enum buttonTags: Int{
case working = 1
}
func setupTags(){
btnWorking.tag = buttonTags.working.rawValue
}
func setupActions(){
btnWorking.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
}
@objc func didSelect(_ sender: UIView){
if let tag = buttonTags.init(rawValue: sender.tag){
switch tag{
case .working:
delegate?.didReceive(withMessage: "wokring button clicked of index \(pageIndex)")
}
}
}
}
ViewController // 主控制器
import UIKit
protocol CallBack {
func didReceive(withMessage message: String)
}
class ViewController: UIViewController {
@IBOutlet weak var containerView: UIView!
@IBOutlet weak var btnCall: UIButton!
@IBOutlet weak var btnMessage: UIButton!
@IBOutlet weak var btnNext: UIButton!
@IBOutlet weak var btnBack: UIButton!
@IBOutlet weak var currentListingLabel: UILabel!
var pageController: PageViewController?
override func viewDidLoad() {
super.viewDidLoad()
setupTags()
setupActions()
setupContainerView()
}
enum buttonTags: Int{
case call = 1
case message
case next
case back
}
func setupTags(){
btnCall.tag = buttonTags.call.rawValue
btnMessage.tag = buttonTags.message.rawValue
btnNext.tag = buttonTags.next.rawValue
btnBack.tag = buttonTags.back.rawValue
}
func setupActions(){
btnCall.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
btnMessage.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
btnNext.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
btnBack.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
}
@objc func didSelect(_ sender: UIView){
if let tag = buttonTags.init(rawValue: sender.tag){
switch tag{
case .call:
print("Call button called for index \(pageController?.getCurrentPageIndex() ?? 0)")
case .message:
print("message button called for index \(pageController?.getCurrentPageIndex() ?? 0)")
case .next:
if let p = pageController{
let currentIndex = p.getCurrentPageIndex()
if currentIndex < p.list.count - 1{
p.movePage(index: currentIndex + 1)
}
}
case .back:
if let p = pageController{
let currentIndex = p.getCurrentPageIndex()
if currentIndex > 0{
p.movePage(index: currentIndex - 1)
}
}
}
}
}
func setupContainerView(){
let sb = UIStoryboard(name: "Main", bundle: nil)
pageController = sb.instantiateViewController(withIdentifier: "PageViewControllerID") as? PageViewController
pageController?.viewController = self
addViewIntoParentViewController(vc: pageController)
}
func addViewIntoParentViewController(vc: UIViewController?){
if let vc = vc{
for v in self.containerView.subviews{
v.removeFromSuperview()
}
self.containerView.addSubview(vc.view)
self.containerView.translatesAutoresizingMaskIntoConstraints = false
vc.view.translatesAutoresizingMaskIntoConstraints = false
addChildViewController(vc)
NSLayoutConstraint.activate([
vc.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
vc.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
vc.view.topAnchor.constraint(equalTo: containerView.topAnchor),
vc.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])
vc.didMove(toParentViewController: self)
}
}
}
extension ViewController: CallBack{
func didReceive(withMessage message: String) {
print("message: \(message)")
}
}
PageViewBase
import UIKit
class PageViewBase: UIViewController {
var pageIndex = -1
var delegate: CallBack?
}