восстановление состояния работает, но затем аннулируется в viewDidLoad

Код примечания был обновлен, чтобы включить исправления, описанные в комментариях, но вот исходный текст вопроса:

Восстановление состояния работает с представленным ниже ViewController на основе кода, но затем оно «отменяется» вторым вызовом viewDidLoad. У меня вопрос: как этого избежать? С точкой останова в decodeRestorableState я вижу, что он действительно восстанавливает 2 параметра selectedGroup и selectedType, но затем он снова проходит через viewDidLoad, и эти параметры сбрасываются до нуля, поэтому восстановление не имеет никакого эффекта. Там нет раскадровки: если вы связали этот класс с пустым 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
  }

person Tony M    schedule 06.08.2017    source источник
comment
Я ничего не решил, кроме: я получаю ошибку компилятора в этой строке: addConstraintsWithFormat("H:|[v0]|", views: pickerView) и в следующей строке. Мне пришлось изменить его на addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[v0]|", options: [], metrics: nil, views: ["v0" : pickerView])). И я вижу это изображение. 1. обновите свой код. 2. Соответствует ли это изображение тому, что вы ожидаете?   -  person Honey    schedule 06.08.2017
comment
3. Даже после всех этих изменений я получаю некоторые предупреждения в отладчике. Мне интересно, вы их тоже получаете и игнорируете?! Я получаю Невозможно одновременно удовлетворить ограничения и Попытаюсь восстановиться, нарушив ограничение. См. ошибки на изображении. Решение по этому предупреждению см. здесь .   -  person Honey    schedule 06.08.2017
comment
Мои извинения: я забыл включить расширение. Теперь он включен, и я только что проверил его в новом проекте, и он сработал (за исключением некоторых кнопок, которые не нужны для работы). Итак, теперь код должен работать, и проблема восстановления состояния может быть продемонстрирована 1) запуском в симуляторе Xcode, 2) выбором группы продуктов и/или типа продуктов (путем пролистывания и использования средств выбора), 3) переход на главную (shift-cmd-H), 4) нажатие кнопки «Стоп», чтобы остановить его работу, и, наконец, 5) запуск снова, чтобы увидеть, что состояние не восстановлено (или, как сказано выше, IS восстановлено, а затем это восстановление аннулировано)   -  person Tony M    schedule 06.08.2017


Ответы (2)


Если viewDidLoad вызывается дважды, это произойдет, потому что ваш контроллер представления создается дважды.

Вы не говорите, как вы создаете контроллер представления, но я подозреваю, что ваша проблема заключается в том, что контроллер представления создается сначала раскадровкой или в делегате приложения, а затем во второй раз, потому что вы установили класс восстановления.

Вам нужно только установить класс восстановления, если ваш контроллер представления не создается обычной последовательностью загрузки приложения (в противном случае достаточно идентификатора восстановления). Попробуйте удалить строку в viewDidLoad, где вы устанавливаете класс восстановления, и я думаю, вы увидите, что viewDidLoad вызывается один раз, а затем decodeRestorableState.

Обновление: подтверждено, что вы создаете контроллер представления в делегате приложения, поэтому вам не нужно использовать класс восстановления. Это устраняет проблему с двойным вызовом viewDidLoad.

Вы хотите выполнить начальное создание контроллера корневого представления в willFinishLaunchingWithOptions в делегате приложения, поскольку это вызывается до того, как произойдет восстановление состояния.

Последней проблемой после восстановления значений selectedGroup и selectedType является обновление элементов пользовательского интерфейса (управление страницей, представление коллекции) и т. д., чтобы использовать восстановленные значения.

person kharrison    schedule 09.08.2017
comment
Код для создания viewController теперь находится в конце моего вопроса. Я вообще не использую раскадровки — на самом деле, основываясь на вашем ответе, я даже удалил Main.storyboard. Когда я удаляю класс восстановления, viewDidLoad вызывается только один раз, и он также правильно восстанавливает параметры в decodeRestorableState, ЕСЛИ я использую willFinishLaunchingWithOptions в AppDelegate вместо didFinishLaunchingWithOptions. Однако, несмотря на это, государство фактически не восстанавливается! И если я использую didFinishLaunchingWithOptions, он даже не останавливается на decodeRestorableState. Я просто не могу восстановить состояние! - person Tony M; 10.08.2017
comment
Так что, по крайней мере, мы исправили проблему с двойным вызовом viewDidLoad :-) Вы хотите создать свой контроллер представления в willFinishLaunchingWithOptions, который вызывается перед восстановлением состояния. Что мне не ясно, так это то, какое состояние вы пытаетесь сохранить и восстановить? Вы также захотите обновить пользовательский интерфейс после восстановления состояния (viewWillAppear — хорошее место для этого). - person kharrison; 10.08.2017
comment
Чтобы закончить мой комментарий выше, код успешно восстанавливает значения для selectedGroup и selectedType, но затем вам нужно обновить пользовательский интерфейс, чтобы использовать восстановленные значения. У вас есть сложный контроллер, который вы, возможно, захотите разбить, но, по крайней мере, после завершения восстановления вам нужно установить управление страницей, прокрутить представление коллекции и т. д. - person kharrison; 10.08.2017
comment
Теперь все работает, кроме вашего последнего и т. д. выше! То есть все состояния восстанавливаются правильно, за исключением pickerView, найденного в collectionView. Я поместил код обновления в viewDidAppear, потому что он не работал в viewWillAppear. В моем обновленном коде второй оператор защиты в viewDidAppear препятствует обновлению pickerView, и я не могу понять, почему. Любая помощь приветствуется. Большое спасибо! Вы заработали награду, но я также благодарю CloakedEddy! Я удивлен, как мало помощи в Интернете по этой теме в Swift - заставляет меня задуматься, действительно ли люди используют восстановление состояния? - person Tony M; 11.08.2017
comment
Обновление: оставшаяся незначительная проблема решена на основе приведенного ниже сообщения (см. Там мою быструю версию ответа). Не красиво, но сработало. stackoverflow.com/questions/21469459/ - person Tony M; 11.08.2017
comment
Отлично, рад, что ты разобрался - person kharrison; 11.08.2017

За шесть лет программирования для iOS я не помню, чтобы iOS дважды вызывала viewDidLoad() на одном и том же контроллере представления. Так что, скорее всего, вы дважды создаете экземпляр CodeStackVC2 :)

Насколько я могу судить, вы программно создаете иерархию представлений в didFinishLaunchingWithOptions. Однако восстановление состояния вызывается до вызова этого метода делегата. Итак, iOS запрашивает у класса восстановления контроллера представления новый экземпляр контроллера представления, и после этого выполняется ваш код, устанавливающий базовую иерархию, создавая новый контроллер представления.

Попробуйте переместить код с didFinishLaunchingWithOptions на willFinishLaunchingWithOptions: (который вызывается перед любым восстановлением состояния). Затем, поскольку контроллер представления, который iOS пытается восстановить, уже существует, он не будет вызывать этот метод с длинным именем из протокола UIViewControllerRestoration, а вместо этого вызовет decodeRestorableState(with coder:) на этом контроллере представления.

Если вам нужно более подробное объяснение, попробуйте useyourloaf или, конечно, документы Apple — я считаю, что оба очень полезны в понимании концепций реализации Apple. Хотя, должен признаться, мне потребовалось несколько прочтений, прежде чем я понял это сам.

person CloakedEddy    schedule 09.08.2017
comment
Я попробовал то, что вы просили, но восстановление состояния по-прежнему не работает. См. код didFinishLaunchingWithOptions из моего AppDelegate в моем вопросе. У меня есть точки останова в последних } как viewDidLoad, так и decodeRestorableState, и когда я запускаю и выполняю 5 шагов (описанных в моем комментарии от 6 августа в 19:21), чтобы проверить восстановление состояния, он ломается следующим образом: viewDidLoad-decodeRestorableState-viewDidLoad. Но когда, как вы предлагаете, я помещаю код в willFinishLaunchingWithOptions (см. прокомментированный код), он теперь ломается следующим образом: viewDidLoad-viewDidLoad-decodeRestorableState (см. комментарий к harrison) - person Tony M; 10.08.2017
comment
Таким образом, даже когда вы устанавливаете начальную иерархию представлений в willFinish, State Restoration по-прежнему создает для вас новый VC. Я забыл об этом, но указанный вами класс восстановления имеет приоритет над существующим контроллером представления, и запрашивается новый независимо от соответствующего существующего (как описано в документации Apple ????). Посмотрите, что происходит, когда вы удаляете класс восстановления. - person CloakedEddy; 10.08.2017
comment
Я попытался удалить класс восстановления - как посоветовал Харрисон - и написал об этом в моем комментарии к нему выше. - person Tony M; 10.08.2017