Saving and restoring app state with native API - ios

I'm trying to implement saving/restoring state of application.
That code I have:
In AppDelegate I've added:
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
return true
}
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
return true
}
Then I have TabBarController implementation that conforms to restoring:
final class TabBarViewController: UITabBarController {
init() {
super.init(nibName: nil, bundle: nil)
restorationIdentifier = "ContainerVC"
restorationClass = type(of: self)
let vc1 = ViewController(with: .green)
let vc2 = ViewController(with: .red)
vc1.tabBarItem = .init(title: "green", image: nil, tag: 0)
vc2.tabBarItem = .init(title: "red", image: nil, tag: 1)
self.viewControllers = [vc1, vc2]
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension TabBarViewController: UIViewControllerRestoration {
static func viewController(withRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
let vc = TabBarViewController()
vc.selectedIndex = coder.decodeInteger(forKey: "index")
return vc
}
override func encodeRestorableState(with coder: NSCoder) {
coder.encode(self.selectedIndex, forKey: "index")
print("encoded")
super.encodeRestorableState(with: coder)
}
override func decodeRestorableState(with coder: NSCoder) {
self.selectedIndex = coder.decodeInteger(forKey: "index")
super.decodeRestorableState(with: coder)
}
}
Also TabBarViewController is a root ViewController of app window.
I want to, for example, i select second tab - after app terminated its restored and app launched again it should show second tab opened.
I think that encoding should be performed when app terminates. But it's not called. And Decoding not called too. What I make wrong? Thanks in advance!

Related

iOS settings page helper class

I am trying to build a settings page helper class in order to simplify the setup of a settings page.
The idea would be that the class handles saving the state to UserDefaults and setting the initial state of any UISwitch.
Setting up a switch would just be a matter of setting a new switch to a class of "UISettingsSwitch" and adding the name of it to the accessibility label (it's the only identifier available as far as i'm aware).
So far I have :
import Foundation
import UIKit
class SettingsUISwitch: UISwitch {
enum SettingsType {
case darkMode, sound
}
func ison(type: SettingsType ) -> Bool {
switch type {
case .darkMode:
return userDefaults.bool(forKey: "darkMode")
case .sound:
return userDefaults.bool(forKey: "sound")
}
}
let userDefaults = UserDefaults.standard
override init(frame: CGRect) {
super.init(frame: frame)
initSwitch()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSwitch()
}
deinit {
}
func initSwitch() {
addTarget(self, action: #selector(toggle), for: .valueChanged)
}
#objc func toggle(){
userDefaults.setValue(self.isOn, forKey: self.accessibilityLabel!)
}
}
Not an awful lot I know.
I can currently do :
if settingsSwitch.ison(type: .darkMode) {
print (settingsSwitch.ison(type: .darkMode))
print ("ON")
} else {
print ("OFF")
}
The accessibility label doesn't seem to be available in the init setup at any point, so setting up the initial state doesn't seem to be a possibility.
Is it possible to set the initial state of the UISwitch this way ?
Ideally , I'd like to expose : settingsSwitch.darkMode.ison as a boolean ... but I can't figure that one out. Thanks for any help
I managed to use the restoration identifier to do the setup for the switch but I'd still love to remove the cases and the repeated calls to userDefaults
import Foundation
import UIKit
class UISwitchSettings: UISwitch {
enum SettingsType: String, CaseIterable {
case darkMode = "darkMode"
case sound = "sound"
}
func ison(type: SettingsType ) -> Bool {
switch type {
case .darkMode:
return userDefaults.bool(forKey: "darkMode")
case .sound:
return userDefaults.bool(forKey: "sound")
}
}
let userDefaults = UserDefaults.standard
override init(frame: CGRect) {
super.init(frame: frame)
initSwitch()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSwitch()
}
deinit {
}
func initSwitch() {
if let key = self.restorationIdentifier {
// Logic needs changing if default switch is off
if userDefaults.bool(forKey: key) || userDefaults.object(forKey: key) == nil {
self.isOn = true
} else {
self.isOn = false
}
}
addTarget(self, action: #selector(toggle), for: .valueChanged)
}
#objc func toggle(){
userDefaults.setValue(self.isOn, forKey: self.restorationIdentifier!)
}
}

UIViewController with closure init

I try to create UIViewController:
class CategoriesVC: UIViewController {
let tableView = UITableView()
var completionHandler: (Category)->Void?
init(completionHandler: #escaping (Category)->Void) {
super.init()
self.completionHandler = completionHandler
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and I get this error:
Must call a designated initializer of the superclass 'UIViewController'
On this line:
super.init()
The error states clearly that you must call the designate init for UIViewController, which in this case is super.init(nibName:,bundle:).
Also, the completionHandler syntax is wrong, here's the fix:
class CategoriesVC: UIViewController {
let tableView = UITableView()
var completionHandler: ((Category)->Void)?
init(completionHandler: #escaping ((Category)->Void)) {
super.init(nibName: nil, bundle: nil)
self.completionHandler = completionHandler
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Saving Information Across ViewControllers

I would like to maintain my var removedIDs = [String]() even when I exit a viewController. I have checked off "Use Storyboard ID" for all Restoration IDs in my StoryBoard. Yet when I navigate away from my viewController, I still lose the contents of removedIDs.
In my AppDelegate, I have written:
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
return true
}
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
return true
}
And in my MainTextView, the controller that holds removedIds, I have the extension:
extension MainTextView {
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(removedIDs, forKey: "removed")
}
override func decodeRestorableState(with coder: NSCoder) {
func decodeRestorableState(with coder: NSCoder) {
super.decodeRestorableState(with: coder)
removedIDs = coder.decodeObject(forKey: "removed") as? [String] ?? []
}
}
}
I might add that the contents of removedIDs is filled through the following report function:
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let more = UITableViewRowAction(style: .default, title: "Report") { action, index in
self.removedIDs!.append(self.comments[indexPath.row].snap)
What piece of the restoration state process am I missing to allow Xcode to hold my IDs?
What you are trying to do is to save application state, while you really need to save some application data. To do that you can use UserDefaults.
For example something like this:
var removedIds: [String]? {
get { return UserDefaults.standard.value(forKey: "removedIds") as? [String] }
set {
if newValue != nil {
UserDefaults.standard.set(newValue, forKey: "removedIds")
}
else {
UserDefaults.standard.removeObject(forKey: "removedIds")
}
}
}
public func add(removedId: String) {
guard var list = removedIds else { // Nothing saved yet
removedIds = [removedId] // Save array with 1 item
return
}
list.append(removedId) // Add new item
removedIds = list // Save
}
And then you can:
Add an item to the list of stored IDs:
add(removedId: self.posts[indexPath.row].id)
You can also overwrite list:
removedIds = [self.posts[indexPath.row].id]
Get list of previously saved removed ids:
var x = removedIds
Removed all IDs from storage:
removedIds = nil

How does Codable fit in with iOS restoration process?

In AppDelegate.swift I have:
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
return true
}
And iOS will call my encodeRestorableState() & decodeRestorableState() class methods during state restoration.
How does Codable work with respect to state restoration? What does iOS call and how do I tie in my Codable structs and classes?
encodeRestorableState(with:) passes you an instance of NSCoder. Any variables you require to restore your state must be encoded here using encode(_:forKey:) with this coder and must therefore conform to Codable.
decodeRestorableState(with:) passes you this same Coder into the function body. You can access the properties in the decoder with the key you used when they were encoded and then set them to instance variables or otherwise use them to configure your controller.
e.g.
import UIKit
struct RestorationModel: Codable {
static let codingKey = "restorationModel"
var someStringINeed: String?
var someFlagINeed: Bool?
var someCustomThingINeed: CustomThing?
}
struct CustomThing: Codable {
let someOtherStringINeed = "another string"
}
class ViewController: UIViewController {
var someStringIDoNotNeed: String?
var someStringINeed: String?
var someFlagINeed: Bool?
var someCustomThingINeed: CustomThing?
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
let restorationModel = RestorationModel(someStringINeed: someStringINeed,
someFlagINeed: someFlagINeed,
someCustomThingINeed: someCustomThingINeed)
coder.encode(restorationModel, forKey: RestorationModel.codingKey)
}
override func decodeRestorableState(with coder: NSCoder) {
super.decodeRestorableState(with: coder)
guard let restorationModel = coder.decodeObject(forKey: RestorationModel.codingKey) as? RestorationModel else {
return
}
someStringINeed = restorationModel.someStringINeed
someFlagINeed = restorationModel.someFlagINeed
someCustomThingINeed = restorationModel.someCustomThingINeed
}
}

Custom init UIViewController query

I am hoping you can help me understand why the below code segment works and the other does not. I am wanting to create a custom initialiser for my UIViewController which has a custom nib file I have created.
My issue is that I want to understand why in the below code the references to newMember and facebookLogin are retained when I hit the viewDidLoad method but in the other segment of code they are not? Can anyone shed some light as to why this would be the case?
Working Code Block
class RegistrationFormViewController: MiOSBaseViewController
{
var newMember:Member!
var facebookLogin: Bool = false
init(member: Member, facebookLogin: Bool = false) {
self.newMember = member
self.facebookLogin = facebookLogin
super.init(nibName: "RegistrationFormViewController", bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
super.init(nibName: "RegistrationFormViewController", bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
let view = self.view as! RegistrationFormView
view.loadViewWith(member: newMember)
view.customNavBarView.backActionBlock = {
self.newMember.deleteEntity(MiOSDataContext.sharedInstance.managedObjectContext)
_ = self.navigationController?.popViewController(animated: true)
return
}
}
}
Broken Code Block
class RegistrationFormViewController: MiOSBaseViewController
{
var newMember:Member!
var facebookLogin: Bool = false
init(member: Member, facebookLogin: Bool = false) {
self.newMember = member
self.facebookLogin = facebookLogin
super.init(nibName: "RegistrationFormViewController", bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewDidLoad() {
super.viewDidLoad()
let view = self.view as! RegistrationFormView
view.loadViewWith(member: newMember)
view.customNavBarView.backActionBlock = {
self.newMember.deleteEntity(MiOSDataContext.sharedInstance.managedObjectContext)
_ = self.navigationController?.popViewController(animated: true)
return
}
}
}
Thanks,
Michael

Resources