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