状态恢复工作但随后在 viewDidLoad 中无效

Posted

技术标签:

【中文标题】状态恢复工作但随后在 viewDidLoad 中无效【英文标题】:state restoration working but then nullified in viewDidLoad 【发布时间】:2017-08-06 16:33:28 【问题描述】:

注释代码已更新以包含 cmets 中详述的修复,但这里是原始问题文本:

状态恢复在下面基于代码的 ViewController 上工作,但随后通过第二次调用 viewDidLoad 将其“撤消”。我的问题是:我该如何避免这种情况? 在decodeRestorableState 处有一个断点,我可以看到它确实恢复了selectedGroupselectedType 两个参数,但随后它再次通过viewDidLoad 并且这些参数被重置为nil,因此恢复无效。没有情节提要:如果您将此类与空的 ViewController 相关联,它将起作用(我仔细检查了这一点——也有一些按钮资产,但功能不需要它们)。我还在底部添加了启用状态恢复所需的 AppDelegate 方法。

import UIKit

class CodeStackVC2: UIViewController, FoodCellDel 

  let fruit = ["Apple", "Orange", "Plum", "Qiwi", "Banana"]
  let veg = ["Lettuce", "Carrot", "Celery", "Onion", "Brocolli"]
  let meat = ["Beef", "Chicken", "Ham", "Lamb"]
  let bread = ["Wheat", "Muffin", "Rye", "Pita"]
  var foods = [[String]]()
  let group = ["Fruit","Vegetable","Meat","Bread"]
  var sView = UIStackView()
  let cellId = "cellId"
  var selectedGroup : Int?
  var selectedType : Int?

  override func viewDidLoad() 
    super.viewDidLoad()
    restorationIdentifier = "CodeStackVC2"
    foods = [fruit, veg, meat, bread]
    setupViews()
    displaySelections()
  

  override func viewDidAppear(_ animated: Bool) 
    super.viewDidAppear(animated)
    guard let index = selectedGroup, let type = selectedType else  return 
    pageControl.currentPage = index
    let indexPath = IndexPath(item: index, section: 0)
    cView.scrollToItem(at: indexPath, at: UICollectionViewScrollPosition(), animated: true)
    cView.reloadItems(at: [indexPath])
    guard let cell = cView.cellForItem(at: indexPath) as? FoodCell else  return 
    cell.pickerView.selectRow(type, inComponent: 0, animated: true)
  

  //State restoration encodes parameters in this func
  override func encodeRestorableState(with coder: NSCoder) 
    if let theGroup = selectedGroup,
      let theType = selectedType 
      coder.encode(theGroup, forKey: "theGroup")
      coder.encode(theType, forKey: "theType")
    
    super.encodeRestorableState(with: coder)
  

  override func decodeRestorableState(with coder: NSCoder) 
    selectedGroup = coder.decodeInteger(forKey: "theGroup")
    selectedType = coder.decodeInteger(forKey: "theType")
    super.decodeRestorableState(with: coder)
  

  override func applicationFinishedRestoringState() 
    //displaySelections()
  

  //MARK: Views
  lazy var cView: UICollectionView = 
    let layout = UICollectionViewFlowLayout()
    layout.scrollDirection = .horizontal
    layout.minimumLineSpacing = 0
    layout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
    layout.itemSize = CGSize(width: self.view.frame.width, height: 120)
    let cRect = CGRect(x: 0, y: 0, width: self.view.frame.width, height: 120)
    let cv = UICollectionView(frame: cRect, collectionViewLayout: layout)
    cv.backgroundColor = UIColor.lightGray
    cv.isPagingEnabled = true
    cv.dataSource = self
    cv.delegate = self
    cv.isUserInteractionEnabled = true
    return cv
  ()

  lazy var pageControl: UIPageControl = 
    let pageC = UIPageControl()
    pageC.numberOfPages = self.foods.count
    pageC.pageIndicatorTintColor = UIColor.darkGray
    pageC.currentPageIndicatorTintColor = UIColor.white
    pageC.backgroundColor = .black
    pageC.addTarget(self, action: #selector(changePage(sender:)), for: UIControlEvents.valueChanged)
    return pageC
  ()

  var textView: UITextView = 
    let tView = UITextView()
    tView.font = UIFont.systemFont(ofSize: 40)
    tView.textColor = .white
    tView.backgroundColor = UIColor.lightGray
    return tView
  ()

  func makeButton(_ tag:Int) -> UIButton
    let newButton = UIButton(type: .system)
    let img = UIImage(named: group[tag])?.withRenderingMode(.alwaysTemplate)
    newButton.setImage(img, for: .normal)
    newButton.tag = tag // used in handleButton()
    newButton.contentMode = .scaleAspectFit
    newButton.addTarget(self, action: #selector(handleButton(sender:)), for: .touchUpInside)
    newButton.isUserInteractionEnabled = true
    newButton.backgroundColor = .clear
    return newButton
  
  //Make a 4-item vertical stackView containing
  //cView,pageView,subStackof 4-item horiz buttons, textView
  func setupViews()
    view.backgroundColor = .lightGray
    cView.register(FoodCell.self, forCellWithReuseIdentifier: cellId)
    //generate an array of buttons
    var buttons = [UIButton]()
    for i in 0...foods.count-1 
      buttons += [makeButton(i)]
    
    let subStackView = UIStackView(arrangedSubviews: buttons)
    subStackView.axis = .horizontal
    subStackView.distribution = .fillEqually
    subStackView.alignment = .center
    subStackView.spacing = 20
    //set up the stackView
    let stackView = UIStackView(arrangedSubviews: [cView,pageControl,subStackView,textView])
    stackView.axis = .vertical
    stackView.distribution = .fill
    stackView.alignment = .fill
    stackView.spacing = 5
    //Add the stackView using AutoLayout
    view.addSubview(stackView)
    stackView.translatesAutoresizingMaskIntoConstraints = false
    stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 5).isActive = true
    stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
    stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
    stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

    cView.translatesAutoresizingMaskIntoConstraints = false
    textView.translatesAutoresizingMaskIntoConstraints = false
    cView.heightAnchor.constraint(equalTo: textView.heightAnchor, multiplier: 0.5).isActive = true
  

  // selected item returned from pickerView
  func pickerSelection(_ foodType: Int) 
    selectedType = foodType
    displaySelections()
  

  func displaySelections() 
    if let theGroup = selectedGroup,
      let theType = selectedType 
      textView.text = "\n \n Group: \(group[theGroup]) \n \n FoodType: \(foods[theGroup][theType])"
    
  

  // 3 User Actions: Button, Page, Scroll
  func handleButton(sender: UIButton) 
    pageControl.currentPage = sender.tag
    let x = CGFloat(sender.tag) * cView.frame.size.width
    cView.setContentOffset(CGPoint(x:x, y:0), animated: true)
  

  func changePage(sender: AnyObject) -> () 
    let x = CGFloat(pageControl.currentPage) * cView.frame.size.width
    cView.setContentOffset(CGPoint(x:x, y:0), animated: true)
  

  func scrollViewDidScroll(_ scrollView: UIScrollView) 
    let index = Int(cView.contentOffset.x / view.bounds.width)
    pageControl.currentPage = Int(index) //change PageControl indicator
    selectedGroup = Int(index)
    let indexPath = IndexPath(item: index, section: 0)
    guard let cell = cView.cellForItem(at: indexPath) as? FoodCell else  return 
    selectedType =  cell.pickerView.selectedRow(inComponent: 0)
    displaySelections()
  

  //this causes cView to be recalculated when device rotates
  override func viewWillLayoutSubviews() 
    super.viewWillLayoutSubviews()
    cView.collectionViewLayout.invalidateLayout()
  

//MARK: cView extension
extension CodeStackVC2: UICollectionViewDataSource, UICollectionViewDelegate,UICollectionViewDelegateFlowLayout 
  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
    return foods.count
  

  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! FoodCell
    cell.foodType = foods[indexPath.item]
    cell.delegate = self
    return cell
  

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize 
    return CGSize(width: view.frame.width, height: textView.frame.height * 0.4)
  


// *********************
protocol FoodCellDel 
  func pickerSelection(_ food:Int)


class FoodCell:UICollectionViewCell, UIPickerViewDelegate, UIPickerViewDataSource 

  var delegate: FoodCellDel?
  var foodType: [String]? 
    didSet 
      pickerView.reloadComponent(0)
      //pickerView.selectRow(0, inComponent: 0, animated: true)
    
  

  lazy var pickerView: UIPickerView = 
    let pView = UIPickerView()
    pView.frame = CGRect(x:0,y:0,width:Int(pView.bounds.width), height:Int(pView.bounds.height))
    pView.delegate = self
    pView.dataSource = self
    pView.backgroundColor = .lightGray
    return pView
  ()

  override init(frame: CGRect) 
    super.init(frame: frame)
    setupViews()
  

  func setupViews() 
    backgroundColor = .clear
    addSubview(pickerView)
    addConstraintsWithFormat("H:|[v0]|", views: pickerView)
    addConstraintsWithFormat("V:|[v0]|", views: pickerView)
  
  required init?(coder aDecoder: NSCoder) 
    fatalError("init(coder:) has not been implemented")
  

  func numberOfComponents(in pickerView: UIPickerView) -> Int 
    return 1
  

  func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int 
    if let count = foodType?.count 
      return count
     else 
      return 0
    
  

  func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView 
    let pickerLabel = UILabel()
    pickerLabel.font = UIFont.systemFont(ofSize: 15)
    pickerLabel.textAlignment = .center
    pickerLabel.adjustsFontSizeToFitWidth = true
    if let foodItem = foodType?[row] 
      pickerLabel.text = foodItem
      pickerLabel.textColor = .white
      return pickerLabel
     else 
      print("chap = nil in viewForRow")
      return UIView()
    
  

  func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) 
    if let actualDelegate = delegate 
      actualDelegate.pickerSelection(row)
    
  



extension UIView 
  func addConstraintsWithFormat(_ format: String, views: UIView...) 
    var viewsDictionary = [String: UIView]()
    for (index, view) in views.enumerated() 
      let key = "v\(index)"
      view.translatesAutoresizingMaskIntoConstraints = false
      viewsDictionary[key] = view
    
    addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
  

以下是 AppDelegate 中的函数:

  //====if set true, these 2 funcs enable state restoration
  func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool 
    return true
  
  func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool 
    return true
  

  func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool 
    //replace the storyboard by making our own window
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.makeKeyAndVisible()
    //this defines the entry point for our app
    window?.rootViewController = CodeStackVC2()
    return true
  

【问题讨论】:

我没有解决任何问题,但是:我在这一行遇到编译器错误:addConstraintsWithFormat("H:|[v0]|", views: pickerView) 和下一行。我不得不将其更改为addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[v0]|", options: [], metrics: nil, views: ["v0" : pickerView]))。我看到this 图像。 1.请更新您的代码。 2. 这个图片是你所期望的吗? 3.即使经过所有这些更改,我也会在调试器中收到一些警告。我想知道你是否也得到它们并忽略它们?!我得到“无法同时满足约束”和“将尝试通过打破约束来恢复”。查看错误image。有关该警告的解决方案,请参阅here。 我很抱歉:我忘了包括一个扩展。它现在已包含在内,我刚刚在一个新项目中进行了测试,它可以正常工作(除了一些按钮资产,对于功能来说是不必要的)。所以,现在代码应该可以工作了,状态恢复问题可以通过 1)在 Xcode 的模拟器中运行来证明,2)选择食物组和/或食物类型(通过滑动和使用选择器),3)回到主页(shift-cmd-H),4)单击停止按钮停止运行,最后 5)再次运行以查看状态未恢复(或如上所说,已恢复,然后恢复正常)无效) 【参考方案1】:

如果viewDidLoad 被调用了两次,那是因为你的视图控制器被创建了两次。

您没有说您是如何创建视图控制器的,但我怀疑您的问题是视图控制器首先由情节提要或在应用程序委托中创建,然后因为您设置了恢复类而第二次创建。

如果你的视图控制器不是由正常的应用加载序列创建的,你只需要设置一个恢复类(否则一个恢复标识符就足够了)。尝试删除设置恢复类的viewDidLoad 中的行,我想你会看到viewDidLoad 被调用一次,然后是decodeRestorableState

更新:确认您正在应用程序委托中创建视图控制器,因此您不需要使用恢复类。这解决了 viewDidLoad 被调用两次的问题。

您希望在应用委托中的willFinishLaunchingWithOptions 中创建初始根视图控制器,因为它在状态恢复发生之前被调用。

恢复 selectedGroup 和 selectedType 值后的最后一个问题是更新 UI 元素(页面控件、集合视图)等以使用恢复的值

【讨论】:

制作viewController的代码现在在我的问题的最后。我根本没有使用故事板 - 事实上,根据您的回复,我什至删除了 Main.storyboard。当我删除恢复类时,viewDidLoad 只调用一次,如果我在 AppDelegate 中使用 willFinishLaunchingWithOptions 而不是 didFinishLaunchingWithOptions,它也会正确恢复 decodeRestorableState 中的参数。然而,尽管如此,状态实际上并没有恢复!如果我使用 didFinishLaunchingWithOptions,它甚至不会在 decodeRestorableState 处停止。我就是无法恢复状态! 所以至少我们已经解决了 viewDidLoad 被调用两次的问题 :-) 你想在 willFinishLaunchingWithOptions 中创建视图控制器——在状态恢复发生之前调用。我不清楚的是您要保存和恢复的状态是什么?您还需要在状态恢复后刷新用户界面(viewWillAppear 是一个好地方) 完成我上面的评论代码已成功恢复 selectedGroup 和 selectedType 的值,但您需要更新 UI 以使用恢复的值。您有一个复杂的控制器,您可能想要拆分它,但至少在恢复完成后,您需要设置页面控件、滚动集合视图等。 现在一切正常,除了你的最后一个“等”。多于!也就是说,除了在 collectionView 中找到的 pickerView 之外,所有状态都已正确恢复。我将刷新代码放入 viewDidAppear 中,因为它在 viewWillAppear 中不起作用。在我更新的代码中,但是 viewDidAppear 中的第二个保护语句阻止了 pickerView 刷新,我不知道为什么。欢迎任何帮助。非常感谢!你赢得了赏金,但我也感谢 CloakedEddy!我很惊讶 Swift 中关于这个主题的在线帮助很少 - 让我想知道人们是否真的使用状态恢复? 更新:根据下面的帖子解决了剩余的小问题(请参阅我的基于 swift 的答案版本)。不漂亮,但它奏效了。 ***.com/questions/21469459/…【参考方案2】:

在我六年的 ios 编程生涯中,我不记得曾见过 iOS 在同一个视图控制器上两次调用 viewDidLoad()。因此,您很可能两次实例化 CodeStackVC2 :)

据我所知,您正在didFinishLaunchingWithOptions 中以编程方式创建视图层次结构。但是,状态恢复是在调用此委托方法之前调用的。因此,iOS 向视图控制器的恢复类请求一个新的视图控制器实例,然后执行设置基本层次结构的代码,创建一个新的视图控制器。

尝试将您的代码从 didFinishLaunchingWithOptions 移动到 willFinishLaunchingWithOptions:(在任何状态恢复之前调用)。然后,由于 iOS 尝试恢复的视图控制器已经存在,它不会使用来自 UIViewControllerRestoration 协议的长名称调用该方法,而是在该视图控制器上调用 decodeRestorableState(with coder:)

如果您需要更深入的解释,请尝试useyourloaf 或当然Apple docs - 我发现两者对于理解 Apple 实施背后的概念非常有用。虽然我必须承认,在我自己理解之前,我读了几遍。

【讨论】:

我尝试了你的要求,但状态恢复仍然不起作用。在我的问题中查看我的 AppDelegate 中的 didFinishLaunchingWithOptions 代码。我在 viewDidLoad 和 decodeRestorableState 的最后 处都有断点,当我运行并通过 5 个步骤(在我的 8 月 6 日 19:21 评论中描述)来测试状态恢复时,它会中断如下:viewDidLoad-decodeRestorableState-viewDidLoad。但是,当我按照您的建议将代码放入 willFinishLaunchingWithOptions 时(参见注释代码),它现在中断如下:viewDidLoad-viewDidLoad-decodeRestorableState(请参见对 kharrison 的评论) 因此,即使您在willFinish 中设置了起始视图层次结构,State Restoration 仍然会为您创建一个新的 VC。我忘记了这一点,但是您提供的恢复类 优先于 现有的视图控制器,并且无论匹配的现有视图控制器如何,都被要求提供一个新的类(如 Apple 文档中所述 ? )。看看删除恢复类后会发生什么。 我确实尝试过删除恢复类 - 正如 kharrison 建议的那样 - 并在上面对他的评论中写到了它。

以上是关于状态恢复工作但随后在 viewDidLoad 中无效的主要内容,如果未能解决你的问题,请参考以下文章

状态恢复iOS时播放声音

[react] 在react中无状态组件有什么运用场景

使用 WatchOS 应用程序时未调用 viewDidLoad

Service Fabric 中无状态服务的服务解析器

如果不在 viewDidLoad 中,我应该在哪里进行初始网络调用(例如,最初填充提要)?

3D Touch 和状态恢复的问题