iosswiftstate-restoration

state restoration working but then nullified in viewDidLoad


Note code has been updated to incorporate the fixes detailed in the comments, but here is the original question text:

State restoration works on the code-based ViewController below, but then it is "undone" by a second call to viewDidLoad. My question is: how do I avoid that? With a breakpoint at decodeRestorableState I can see that it does in fact restore the 2 parameters selectedGroup and selectedType but then it goes through viewDidLoad again and those parameters are reset to nil so the restoration is of no effect. There's no storyboard: if you associated this class with an empty ViewController it will work (I double checked this -- there are some button assets too, but they aren't needed for function). I've also included at the bottom the AppDelegate methods needed to enable state restoration.

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

Here are the functions in 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
  }

Solution

  • If viewDidLoad is being called twice it will because your view controller is being created twice.

    You do not say how you are creating the view controller but I suspect your problem is that the view controller is being created first by a storyboard or in the app delegate and then a second time because you have set a restoration class.

    You only need to set a restoration class if your view controller is not being created by the normal app load sequence (a restoration identifier is enough otherwise). Try removing the line in viewDidLoad where you set a restoration class and I think you will see viewDidLoad is called once followed by decodeRestorableState.

    Update: Confirmed you are creating the view controller in the app delegate so you do not need to use a restoration class. That fixes the problem with viewDidLoad being called twice.

    You want to do the initial root view controller creation in willFinishLaunchingWithOptions in the app delegate as that is called before state restoration takes place.

    The final issue once you have the selectedGroup and selectedType values restored is to update the UI elements (page control, collection view), etc to use the restored values