I am trying to setup a clean way of handling when a user goes offline, currently if a user is offline I am pushing a new screen to the top of the stack with a message to connect to the internet. This is working perfectly for the one screen it is setup on, but I want this code to work on any screen the user is on. I am trying to put this into the app delegate and have the code shared across all screens but it is not working. The end goal is if the user is on any screen in my app and loses connection, to show the offline view controller - currently this is only working on the home screen.
Also I do not want to use Alamo / Firebase or any other 3rd party to handle this if possible.
This is the home screen code and everything working as expected:
import UIKit
class ViewController: UIViewController, UIAlertViewDelegate {
let reachability = try! Reachability()
override func viewDidLoad() {
super.viewDidLoad()
// When network is unreachable, present offline view controller
reachability.whenUnreachable = { _ in
let vc = self.storyboard?.instantiateViewController(withIdentifier: "OfflineViewController")
vc?.modalPresentationStyle = .fullScreen
self.present(vc!, animated: true, completion: nil)
}
do {
try reachability.startNotifier()
} catch {
print("Unable to start notifier")
}
}
}
This is the code for the offline view controller - it is also working as expected
import UIKit
class OfflineViewController: UIViewController {
let reachability = try! Reachability()
override func viewDidLoad() {
super.viewDidLoad()
do {
try reachability.startNotifier()
} catch {
print("Unable to start notifier")
}
}
#IBAction func tapTryAgain(_ sender: Any) {
reachability.whenReachable = { reachability in
self.dismiss(animated: true, completion: nil)
}
}
}
Now when I try to put all the code in the app delegate, it does not work -- This the part I need help with - note that I am commenting out the reachability code from home screen when I try this out
import UIKit
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
let reachability = try! Reachability()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Note nothing happens when user goes offline with this code
reachability.whenUnreachable = { _ in
print("Not reachable")
let storyboard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let offlineVC = storyboard.instantiateViewController(withIdentifier: "OfflineViewController")
offlineVC.modalPresentationStyle = .fullScreen
let appDelegate = UIApplication.shared.delegate
appDelegate?.window??.addSubview(offlineVC.view)
appDelegate?.window??.bringSubviewToFront(offlineVC.view)
}
do {
try reachability.startNotifier()
} catch {
print("Unable to start notifier")
}
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
I suppose the problem is that you are trying to use window from AppDelegate, but it will always be nil, since your are using SceneDelegate. Just move this code to SceneDelegate, everything will work as it should then.
Related
This is a little long but it's not trivial and it takes a lot to demonstrate this issue.
I'm trying to figure out how to update a little sample app from iOS 12 to iOS 13. This sample app doesn't use any storyboards (other than the launch screen). It's a simple app that shows one view controller with a label that is updated by a timer. It uses state restoration so the counter starts from where it left off. I want to be able to support iOS 12 and iOS 13. In iOS 13 I want to update to the new scene architecture.
Under iOS 12 the app works just fine. On a fresh install the counter starts at 0 and goes up. Put the app in the background and then restart the app and the counter continues from where it left off. The state restoration all works.
Now I'm trying to get that working under iOS 13 using a scene. The problem I'm having is figuring out the correct way to initialize the scene's window and restore the navigation controller and the main view controller to the scene.
I've been through as much of the Apple documentation as I can find related to state restoration and scenes. I've watched WWDC videos related to windows and scenes (212 - Introducing Multiple Windows on iPad, 258 - Architecting Your App for Multiple Windows). But I seem to be missing a piece that puts it all together.
When I run the app under iOS 13, all of the expected delegate methods (both AppDelegate and SceneDelegate) are being called. The state restoration is restoring the nav controller and the main view controller but I can't figure out how to set the rootViewController of the scene's window since all of the UI state restoration is in the AppDelegate.
There also seems to be something related to an NSUserTask that should be used but I can't connect the dots.
The missing pieces seem to be in the willConnectTo method of SceneDelegate. I'm sure I also need some changes in stateRestorationActivity of SceneDelegate. There may also need to be changes in the AppDelegate. I doubt anything in ViewController needs to be changed.
To replicate what I'm doing, create a new iOS project with Xcode 11 (beta 4 at the moment) using the Single View App template. Set the Deployment Target to iOS 11 or 12.
Delete the main storyboard. Remove the two references in the Info.plist to Main (one at the top level and one deep inside the Application Scene Manifest. Update the 3 swift files as follows.
AppDelegate.swift:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("AppDelegate willFinishLaunchingWithOptions")
// This probably shouldn't be run under iOS 13?
self.window = UIWindow(frame: UIScreen.main.bounds)
return true
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print("AppDelegate didFinishLaunchingWithOptions")
if #available(iOS 13.0, *) {
// What needs to be here?
} else {
// If the root view controller wasn't restored, create a new one from scratch
if (self.window?.rootViewController == nil) {
let vc = ViewController()
let nc = UINavigationController(rootViewController: vc)
nc.restorationIdentifier = "RootNC"
self.window?.rootViewController = nc
}
self.window?.makeKeyAndVisible()
}
return true
}
func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
print("AppDelegate viewControllerWithRestorationIdentifierPath")
// If this is for the nav controller, restore it and set it as the window's root
if identifierComponents.first == "RootNC" {
let nc = UINavigationController()
nc.restorationIdentifier = "RootNC"
self.window?.rootViewController = nc
return nc
}
return nil
}
func application(_ application: UIApplication, willEncodeRestorableStateWith coder: NSCoder) {
print("AppDelegate willEncodeRestorableStateWith")
// Trigger saving of the root view controller
coder.encode(self.window?.rootViewController, forKey: "root")
}
func application(_ application: UIApplication, didDecodeRestorableStateWith coder: NSCoder) {
print("AppDelegate didDecodeRestorableStateWith")
}
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
print("AppDelegate shouldSaveApplicationState")
return true
}
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
print("AppDelegate shouldRestoreApplicationState")
return true
}
// The following four are not called in iOS 13
func applicationWillEnterForeground(_ application: UIApplication) {
print("AppDelegate applicationWillEnterForeground")
}
func applicationDidEnterBackground(_ application: UIApplication) {
print("AppDelegate applicationDidEnterBackground")
}
func applicationDidBecomeActive(_ application: UIApplication) {
print("AppDelegate applicationDidBecomeActive")
}
func applicationWillResignActive(_ application: UIApplication) {
print("AppDelegate applicationWillResignActive")
}
// MARK: UISceneSession Lifecycle
#available(iOS 13.0, *)
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
print("AppDelegate configurationForConnecting")
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
#available(iOS 13.0, *)
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
print("AppDelegate didDiscardSceneSessions")
}
}
SceneDelegate.swift:
import UIKit
#available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
print("SceneDelegate willConnectTo")
guard let winScene = (scene as? UIWindowScene) else { return }
// Got some of this from WWDC2109 video 258
window = UIWindow(windowScene: winScene)
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
// Now what? How to connect the UI restored in the AppDelegate to this window?
} else {
// Create the initial UI if there is nothing to restore
let vc = ViewController()
let nc = UINavigationController(rootViewController: vc)
nc.restorationIdentifier = "RootNC"
self.window?.rootViewController = nc
window?.makeKeyAndVisible()
}
}
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
print("SceneDelegate stateRestorationActivity")
// What should be done here?
let activity = NSUserActivity(activityType: "What?")
activity.persistentIdentifier = "huh?"
return activity
}
func scene(_ scene: UIScene, didUpdate userActivity: NSUserActivity) {
print("SceneDelegate didUpdate")
}
func sceneDidDisconnect(_ scene: UIScene) {
print("SceneDelegate sceneDidDisconnect")
}
func sceneDidBecomeActive(_ scene: UIScene) {
print("SceneDelegate sceneDidBecomeActive")
}
func sceneWillResignActive(_ scene: UIScene) {
print("SceneDelegate sceneWillResignActive")
}
func sceneWillEnterForeground(_ scene: UIScene) {
print("SceneDelegate sceneWillEnterForeground")
}
func sceneDidEnterBackground(_ scene: UIScene) {
print("SceneDelegate sceneDidEnterBackground")
}
}
ViewController.swift:
import UIKit
class ViewController: UIViewController, UIViewControllerRestoration {
var label: UILabel!
var count: Int = 0
var timer: Timer?
static func viewController(withRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
print("ViewController withRestorationIdentifierPath")
return ViewController()
}
override init(nibName nibNameOrNil: String? = nil, bundle nibBundleOrNil: Bundle? = nil) {
print("ViewController init")
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
restorationIdentifier = "ViewController"
restorationClass = ViewController.self
}
required init?(coder: NSCoder) {
print("ViewController init(coder)")
super.init(coder: coder)
}
override func viewDidLoad() {
print("ViewController viewDidLoad")
super.viewDidLoad()
view.backgroundColor = .green // be sure this vc is visible
label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "\(count)"
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
override func viewWillAppear(_ animated: Bool) {
print("ViewController viewWillAppear")
super.viewWillAppear(animated)
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (timer) in
self.count += 1
self.label.text = "\(self.count)"
})
}
override func viewDidDisappear(_ animated: Bool) {
print("ViewController viewDidDisappear")
super.viewDidDisappear(animated)
timer?.invalidate()
timer = nil
}
override func encodeRestorableState(with coder: NSCoder) {
print("ViewController encodeRestorableState")
super.encodeRestorableState(with: coder)
coder.encode(count, forKey: "count")
}
override func decodeRestorableState(with coder: NSCoder) {
print("ViewController decodeRestorableState")
super.decodeRestorableState(with: coder)
count = coder.decodeInteger(forKey: "count")
label.text = "\(count)"
}
}
Run this under iOS 11 or 12 and it works just fine.
You can run this under iOS 13 and on a fresh install of the app you get the UI. But any subsequent run of the app gives a black screen because the UI restored via state restoration isn't connected to the scene's window.
What am I missing? Is this just missing a line or two of code or is my entire approach to iOS 13 scene state restoration wrong?
Keep in mind that once I get this figured out the next step will be supporting multiple windows. So the solution should work for multiple scenes, not just one.
This, it seems to me, is the major flaw in the structure of the answers presented so far:
You would also want to chain calls to updateUserActivityState
That misses the whole point of updateUserActivityState, which is that it is called for you, automatically, for all view controllers whose userActivity is the same as the NSUserActivity returned by the scene delegate's stateRestorationActivity.
Thus, we automatically have a state-saving mechanism, and it remains only to devise a state-restoration mechanism to match. I will illustrate an entire architecture I've come up with.
NOTE: This discussion ignores multiple windows and it also ignores the original requirement of the question, that we be compatible with iOS 12 view controller-based state saving and restoration. My goal here is only to show how to do state saving and restoration in iOS 13 using NSUserActivity. However, only minor modifications are needed in order to fold this into a multiple-window app, so I think it answers the original question adequately.
Saving
Let's start with state-saving. This is entirely boilerplate. The scene delegate either creates the scene userActivity or passes the received restoration activity into it, and returns that as its own user activity:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = (scene as? UIWindowScene) else { return }
scene.userActivity =
session.stateRestorationActivity ??
NSUserActivity(activityType: "restoration")
}
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
return scene.userActivity
}
Every view controller must use its own viewDidAppear to share that user activity object. That way, its own updateUserActivityState will be called automatically when we go into the background, and it has a chance to contribute to the global pool of the user info:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.userActivity = self.view.window?.windowScene?.userActivity
}
// called automatically at saving time!
override func updateUserActivityState(_ activity: NSUserActivity) {
super.updateUserActivityState(activity)
// gather info into `info`
activity.addUserInfoEntries(from: info)
}
That's all! If every view controller does that, then every view controller that is alive at the time we go into background gets a chance to contribute to the user info of the user activity that will arrive next time we launch.
Restoration
This part is harder. The restoration info will arrive as session.stateRestorationActivity into the scene delegate. As the original question rightly asks: now what?
There's more than one way to skin this cat, and I've tried most of them and settled on this one. My rule is this:
Every view controller must have a restorationInfo property which is a dictionary. When any view controller is created during restoration, its creator (parent) must set that restorationInfo to the userInfo that arrived from session.stateRestorationActivity.
This userInfo must be copied out right at the start, because it will be wiped out from the saved activity the first time updateUserActivityState is called (that is the part that really drove me crazy working out this architecture).
The cool part is that if we do this right, the restorationInfo is set before viewDidLoad, and so the view controller can configure itself based on the info it put into the dictionary on saving.
Each view controller must also delete its own restorationInfo when it is done with it, lest it use it again during the app's lifetime. It must be used only the once, on launch.
So we must change our boilerplate:
var restorationInfo : [AnyHashable : Any]?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.userActivity = self.view.window?.windowScene?.userActivity
self.restorationInfo = nil
}
So now the only problem is the chain of how the restorationInfo of each view controller is set. The chain starts with the scene delegate, which is responsible for setting this property in the root view controller:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = (scene as? UIWindowScene) else { return }
scene.userActivity =
session.stateRestorationActivity ??
NSUserActivity(activityType: "restoration")
if let rvc = window?.rootViewController as? RootViewController {
rvc.restorationInfo = scene.userActivity?.userInfo
}
}
Each view controller is then responsible not only for configuring itself in its viewDidLoad based on the restorationInfo, but also for looking to see whether it was the parent / presenter of any further view controller. If so, it must create and present / push / whatever that view controller, making sure to pass on the restorationInfo before that child view controller's viewDidLoad runs.
If every view controller does this correctly, the whole interface and state will be restored!
A bit more of an example
Presume we have just two possible view controllers: RootViewController and PresentedViewController. Either RootViewController was presenting PresentedViewController at the time we were backgrounded, or it wasn't. Either way, that information has been written into the info dictionary.
So here is what RootViewController does:
var restorationInfo : [AnyHashable:Any]?
override func viewDidLoad() {
super.viewDidLoad()
// configure self, including any info from restoration info
}
// this is the earliest we have a window, so it's the earliest we can present
// if we are restoring the editing window
var didFirstWillLayout = false
override func viewWillLayoutSubviews() {
if didFirstWillLayout { return }
didFirstWillLayout = true
let key = PresentedViewController.editingRestorationKey
let info = self.restorationInfo
if let editing = info?[key] as? Bool, editing {
self.performSegue(withIdentifier: "PresentWithNoAnimation", sender: self)
}
}
// boilerplate
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.userActivity = self.view.window?.windowScene?.userActivity
self.restorationInfo = nil
}
// called automatically because we share this activity with the scene
override func updateUserActivityState(_ activity: NSUserActivity) {
super.updateUserActivityState(activity)
// express state as info dictionary
activity.addUserInfoEntries(from: info)
}
The cool part is that the PresentedViewController does exactly the same thing!
var restorationInfo : [AnyHashable : Any]?
static let editingRestorationKey = "editing"
override func viewDidLoad() {
super.viewDidLoad()
// configure self, including info from restoration info
}
// boilerplate
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.userActivity = self.view.window?.windowScene?.userActivity
self.restorationInfo = nil
}
override func updateUserActivityState(_ activity: NSUserActivity) {
super.updateUserActivityState(activity)
let key = Self.editingRestorationKey
activity.addUserInfoEntries(from: [key:true])
// and add any other state info as well
}
I think you can see that at this point it's only a matter of degree. If we have more view controllers to chain during the restoration process, they all work exactly the same way.
Final notes
As I said, this is not the only way to skin the restoration cat. But there are problems of timing and of distribution of responsibilities, and I think this is the most equitable approach.
In particular, I do not hold with the idea that the scene delegate should be responsible for the whole restoration of the interface. It would need to know too much about the details of how to initialize each view controller along the line, and there are serious timing issues that are difficult to overcome in a deterministic way. My approach sort of imitates the old view controller-based restoration, making each view controller responsible for its child in the same way it would normally be.
To support state restoration in iOS 13 you will need to encode enough state into the NSUserActivity:
Use this method to return an NSUserActivity object with information about your scene's data. Save enough information to be able to retrieve that data again after UIKit disconnects and then reconnects the scene. User activity objects are meant for recording what the user was doing, so you don't need to save the state of your scene's UI
The advantage of this approach is that it can make it easier to support handoff, since you are creating the code necessary to persist and restore state via user activities.
Unlike the previous state restoration approach where iOS recreated the view controller hierarchy for you, you are responsible for creating the view hierarchy for your scene in the scene delegate.
If you have multiple active scenes then your delegate will be called multiple times to save the state and multiple times to restore state; Nothing special is needed.
The changes I made to your code are:
AppDelegate.swift
Disable "legacy" state restoration on iOS 13 & later:
func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
if #available(iOS 13, *) {
} else {
print("AppDelegate viewControllerWithRestorationIdentifierPath")
// If this is for the nav controller, restore it and set it as the window's root
if identifierComponents.first == "RootNC" {
let nc = UINavigationController()
nc.restorationIdentifier = "RootNC"
self.window?.rootViewController = nc
return nc
}
}
return nil
}
func application(_ application: UIApplication, willEncodeRestorableStateWith coder: NSCoder) {
print("AppDelegate willEncodeRestorableStateWith")
if #available(iOS 13, *) {
} else {
// Trigger saving of the root view controller
coder.encode(self.window?.rootViewController, forKey: "root")
}
}
func application(_ application: UIApplication, didDecodeRestorableStateWith coder: NSCoder) {
print("AppDelegate didDecodeRestorableStateWith")
}
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
print("AppDelegate shouldSaveApplicationState")
if #available(iOS 13, *) {
return false
} else {
return true
}
}
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
print("AppDelegate shouldRestoreApplicationState")
if #available(iOS 13, *) {
return false
} else {
return true
}
}
SceneDelegate.swift
Create a user activity when required and use it to recreate the view controller. Note that you are responsible for creating the view hierarchy in both normal and restore cases.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
print("SceneDelegate willConnectTo")
guard let winScene = (scene as? UIWindowScene) else { return }
// Got some of this from WWDC2109 video 258
window = UIWindow(windowScene: winScene)
let vc = ViewController()
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
vc.continueFrom(activity: activity)
}
let nc = UINavigationController(rootViewController: vc)
nc.restorationIdentifier = "RootNC"
self.window?.rootViewController = nc
window?.makeKeyAndVisible()
}
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
print("SceneDelegate stateRestorationActivity")
if let nc = self.window?.rootViewController as? UINavigationController, let vc = nc.viewControllers.first as? ViewController {
return vc.continuationActivity
} else {
return nil
}
}
ViewController.swift
Add support for saving and loading from an NSUserActivity.
var continuationActivity: NSUserActivity {
let activity = NSUserActivity(activityType: "restoration")
activity.persistentIdentifier = UUID().uuidString
activity.addUserInfoEntries(from: ["Count":self.count])
return activity
}
func continueFrom(activity: NSUserActivity) {
let count = activity.userInfo?["Count"] as? Int ?? 0
self.count = count
}
Based on more research and very helpful suggestions from the answer by Paulw11 I have come up with an approach that works for iOS 13 and iOS 12 (and earlier) with no duplication of code and using the same approach for all versions of iOS.
Note that while the original question and this answer don't use storyboards, the solution would be essentially the same. The only differences is that with storyboards, the AppDelegate and SceneDelegate wouldn't need the code to create the window and root view controller. And of course the ViewController wouldn't need code to create its views.
The basic idea is to migrate the iOS 12 code to work the same as iOS 13. This means that the old state restoration is no longer used. NSUserTask is used to save and restore state. This approach has several benefits. It lets the same code work for all iOS versions, it gets you really close to supporting handoff with virtually no additional effort, and it lets you support multiple window scenes and full state restoration using the same basic code.
Here's the updated AppDelegate.swift:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("AppDelegate willFinishLaunchingWithOptions")
if #available(iOS 13.0, *) {
// no-op - UI created in scene delegate
} else {
self.window = UIWindow(frame: UIScreen.main.bounds)
let vc = ViewController()
let nc = UINavigationController(rootViewController: vc)
self.window?.rootViewController = nc
self.window?.makeKeyAndVisible()
}
return true
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print("AppDelegate didFinishLaunchingWithOptions")
return true
}
func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
print("AppDelegate viewControllerWithRestorationIdentifierPath")
return nil // We don't want any UI hierarchy saved
}
func application(_ application: UIApplication, willEncodeRestorableStateWith coder: NSCoder) {
print("AppDelegate willEncodeRestorableStateWith")
if #available(iOS 13.0, *) {
// no-op
} else {
// This is the important link for iOS 12 and earlier
// If some view in your app sets a user activity on its window,
// here we give the view hierarchy a chance to update the user
// activity with whatever state info it needs to record so it can
// later be restored to restore the app to its previous state.
if let activity = window?.userActivity {
activity.userInfo = [:]
((window?.rootViewController as? UINavigationController)?.viewControllers.first as? ViewController)?.updateUserActivityState(activity)
// Now save off the updated user activity
let wrap = NSUserActivityWrapper(activity)
coder.encode(wrap, forKey: "userActivity")
}
}
}
func application(_ application: UIApplication, didDecodeRestorableStateWith coder: NSCoder) {
print("AppDelegate didDecodeRestorableStateWith")
// If we find a stored user activity, load it and give it to the view
// hierarchy so the UI can be restored to its previous state
if let wrap = coder.decodeObject(forKey: "userActivity") as? NSUserActivityWrapper {
((window?.rootViewController as? UINavigationController)?.viewControllers.first as? ViewController)?.restoreUserActivityState(wrap.userActivity)
}
}
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
print("AppDelegate shouldSaveApplicationState")
if #available(iOS 13.0, *) {
return false
} else {
// Enabled just so we can persist the NSUserActivity if there is one
return true
}
}
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
print("AppDelegate shouldRestoreApplicationState")
if #available(iOS 13.0, *) {
return false
} else {
return true
}
}
// MARK: UISceneSession Lifecycle
#available(iOS 13.0, *)
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
print("AppDelegate configurationForConnecting")
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
#available(iOS 13.0, *)
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
print("AppDelegate didDiscardSceneSessions")
}
}
Under iOS 12 and earlier, the standard state restoration process is now only used to save/restore the NSUserActivity. It's not used to persist the view hierarchy any more.
Since NSUserActivity doesn't conform to NSCoding, a wrapper class is used.
NSUserActivityWrapper.swift:
import Foundation
class NSUserActivityWrapper: NSObject, NSCoding {
private (set) var userActivity: NSUserActivity
init(_ userActivity: NSUserActivity) {
self.userActivity = userActivity
}
required init?(coder: NSCoder) {
if let activityType = coder.decodeObject(forKey: "activityType") as? String {
userActivity = NSUserActivity(activityType: activityType)
userActivity.title = coder.decodeObject(forKey: "activityTitle") as? String
userActivity.userInfo = coder.decodeObject(forKey: "activityUserInfo") as? [AnyHashable: Any]
} else {
return nil;
}
}
func encode(with coder: NSCoder) {
coder.encode(userActivity.activityType, forKey: "activityType")
coder.encode(userActivity.title, forKey: "activityTitle")
coder.encode(userActivity.userInfo, forKey: "activityUserInfo")
}
}
Note that additional properties of NSUserActivity might be needed depending on your needs.
Here's the updated SceneDelegate.swift:
import UIKit
#available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
print("SceneDelegate willConnectTo")
guard let winScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: winScene)
let vc = ViewController()
let nc = UINavigationController(rootViewController: vc)
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
vc.restoreUserActivityState(activity)
}
self.window?.rootViewController = nc
window?.makeKeyAndVisible()
}
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
print("SceneDelegate stateRestorationActivity")
if let activity = window?.userActivity {
activity.userInfo = [:]
((window?.rootViewController as? UINavigationController)?.viewControllers.first as? ViewController)?.updateUserActivityState(activity)
return activity
}
return nil
}
}
And finally the updated ViewController.swift:
import UIKit
class ViewController: UIViewController {
var label: UILabel!
var count: Int = 0 {
didSet {
if let label = self.label {
label.text = "\(count)"
}
}
}
var timer: Timer?
override func viewDidLoad() {
print("ViewController viewDidLoad")
super.viewDidLoad()
view.backgroundColor = .green
label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "\(count)"
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
override func viewWillAppear(_ animated: Bool) {
print("ViewController viewWillAppear")
super.viewWillAppear(animated)
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (timer) in
self.count += 1
//self.userActivity?.needsSave = true
})
self.label.text = "\(count)"
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let act = NSUserActivity(activityType: "com.whatever.View")
act.title = "View"
self.view.window?.userActivity = act
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.view.window?.userActivity = nil
}
override func viewDidDisappear(_ animated: Bool) {
print("ViewController viewDidDisappear")
super.viewDidDisappear(animated)
timer?.invalidate()
timer = nil
}
override func updateUserActivityState(_ activity: NSUserActivity) {
print("ViewController updateUserActivityState")
super.updateUserActivityState(activity)
activity.addUserInfoEntries(from: ["count": count])
}
override func restoreUserActivityState(_ activity: NSUserActivity) {
print("ViewController restoreUserActivityState")
super.restoreUserActivityState(activity)
count = activity.userInfo?["count"] as? Int ?? 0
}
}
Note that all code related to the old state restoration has been removed. It has been replaced with the use of NSUserActivity.
In a real app, you would store all kinds of other details in the user activity needed to fully restore the app state on relaunch or to support handoff. Or store minimal data needed to launch a new window scene.
You would also want to chain calls to updateUserActivityState and restoreUserActivityState to any child views as needed in a real app.
On 6th Sept 2019 Apple released this sample app that demonstrates iOS 13 state restoration with backwards compatibility with iOS 12.
From Readme.md
The sample supports two different state preservation approaches. In iOS 13 and later, apps save state for each window scene using NSUserActivity objects. In iOS 12 and earlier, apps preserve the state of their user interface by saving and restoring the configuration of view controllers.
The Readme convers in detail how it works - the basic trick is that on iOS 12 it encodes the Activity Object (available in iOS 12 for another purpose) in the old encodeRestorableState method.
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
let encodedActivity = NSUserActivityEncoder(detailUserActivity)
coder.encode(encodedActivity, forKey: DetailViewController.restoreActivityKey)
}
And on iOS 13 it implements the missing automatic view controller hierarchy restoration using the configure method of the SceneDelegate.
func configure(window: UIWindow?, with activity: NSUserActivity) -> Bool {
if let detailViewController = DetailViewController.loadFromStoryboard() {
if let navigationController = window?.rootViewController as? UINavigationController {
navigationController.pushViewController(detailViewController, animated: false)
detailViewController.restoreUserActivityState(activity)
return true
}
}
return false
}
Lastly, the Readme includes testing advice but I'd like to add if you launch the Xcode 10.2 simulator first, e.g. iPhone 8 Plus and then launch Xcode 11 you will have the iPhone 8 Plus (12.4) as an option and you can experience the backwards-compatible behaviour. I also like to use these user defaults, the second allows the restoration archive to survive crashes:
[NSUserDefaults.standardUserDefaults setBool:YES forKey:#"UIStateRestorationDebugLogging"];
[NSUserDefaults.standardUserDefaults setBool:YES forKey:#"UIStateRestorationDeveloperMode"];
Hi I was wondering if there was a way to display a push notification with the user's approval on specific days. I currently have a toggle for Push Notifications, and when this toggle is called, it should display a picker view for when the user would like these notifications to come. For example, the user could choose the notifications to come weekly, daily, monthly(this would be really nice), or yearly. Also, having the user able to choose which days it comes specifically would be a nice touch. I am not sure as to how I could do this. I would appreciate any guidance or help throughout this process.
Background: I am making an app for my high school's newspaper and would like to remind the user to check out the app. Notifying the user on the first of every month would be nice because our school releases a new issue out on the first of every month.
Here are is a screenshot of what my app looks like now.
Here is my current AppDelegate.swift code as well for reference:
//
// AppDelegate.swift
// TheGunnOracleApp
//
// Created by Rajat Khare on 11/22/16.
// Copyright © 2016 RDKhare. All rights reserved.
//
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if(!UserDefaults.standard.bool(forKey: "HasLaunchedOnce"))
{
UserDefaults.standard.set(true, forKey: "HasLaunchedOnce")
UserDefaults.standard.synchronize()
}
else{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
self.window!.rootViewController = storyboard.instantiateViewController(withIdentifier: "StartAfterFirst")
self.window?.makeKeyAndVisible()
}
if Reachability.checkIntenetRechable() == false {
let alert = UIAlertController(title: "No Internet Connection", message: "The Internet connection appears to be offline. Please connect to Wi-Fi or use Cellular Data to continue", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
self.window?.rootViewController?.present(alert, animated: true, completion: nil)
}
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: #escaping (Bool) -> Void) {
if shortcutItem.type == "com.rdkhare.Home"{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let frontNavigationController = storyboard.instantiateViewController(withIdentifier: "VCHome")
let rearNavigationController = storyboard.instantiateViewController(withIdentifier: "menuVC")
let mainRevealController : SWRevealViewController = SWRevealViewController(rearViewController: rearNavigationController, frontViewController: frontNavigationController)
self.window?.rootViewController? = mainRevealController
// let homeVC = sb.instantiateViewController(withIdentifier: "homePage1") as! WebViewHome
// let root = UIApplication.shared.keyWindow?.rootViewController
// let navController = UINavigationController(rootViewController: homeVC)
// root?.present(navController, animated: true, completion: {
// completionHandler(true)
// })
}
}
}
As I wrote in the title of this question - I went through this tutorial https://developers.google.com/identity/sign-in/ios/sign-in and now I'm able to log in the user to my app based on his google credentials.
The way I do it so far is that I have a ViewController.swift class with the following code:
class ViewController: UIViewController, GIDSignInUIDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let background = CAGradientLayer().greenBlue()
background.frame = self.view.bounds
self.view.layer.insertSublayer(background, atIndex: 0)
//GIDSignIn.sharedInstance().uiDelegate = self
// Uncomment to automatically sign in the user.
//GIDSignIn.sharedInstance().signInSilently()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
GIDSignIn.sharedInstance().uiDelegate = self
GIDSignIn.sharedInstance().signInSilently()
}
#IBAction func didTapSignOut(sender: AnyObject) {
GIDSignIn.sharedInstance().signOut()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func signInWillDispatch(signIn: GIDSignIn!, error: NSError!) {
print("Nothing!")
}
// Present a view that prompts the user to sign in with Google
func signIn(signIn: GIDSignIn!,
presentViewController viewController: UIViewController!) {
self.presentViewController(viewController, animated: true, completion: nil)
}
// Dismiss the "Sign in with Google" view
func signIn(signIn: GIDSignIn!,
dismissViewController viewController: UIViewController!) {
self.dismissViewControllerAnimated(true, completion: nil)
}
}
and in my AppDelegate.swift class I have:
class AppDelegate: UIResponder, UIApplicationDelegate, GIDSignInDelegate {
var window: UIWindow?
func application(application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Initialize sign-in
var configureError: NSError?
GGLContext.sharedInstance().configureWithError(&configureError)
assert(configureError == nil, "Error configuring Google services: \(configureError)")
GIDSignIn.sharedInstance().delegate = self
return true
}
// [START openurl]
func application(application: UIApplication,
openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool {
return GIDSignIn.sharedInstance().handleURL(url,
sourceApplication: sourceApplication,
annotation: annotation)
}
// [END openurl]
#available(iOS 9.0, *)
func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool {
return GIDSignIn.sharedInstance().handleURL(url,
sourceApplication: options[UIApplicationOpenURLOptionsSourceApplicationKey] as! String?,
annotation: options[UIApplicationOpenURLOptionsAnnotationKey])
}
// [START signin_handler]
func signIn(signIn: GIDSignIn!, didSignInForUser user: GIDGoogleUser!,
withError error: NSError!) {
if (error == nil) {
print("Signed in!")
} else {
print("\(error.localizedDescription)")
}
}
// [END signin_handler]
// [START disconnect_handler]
func signIn(signIn: GIDSignIn!, didDisconnectWithUser user:GIDGoogleUser!,
withError error: NSError!) {
// Perform any operations when the user disconnects from app here.
// [START_EXCLUDE]
NSNotificationCenter.defaultCenter().postNotificationName(
"ToggleAuthUINotification",
object: nil,
userInfo: ["statusText": "User has disconnected."])
// [END_EXCLUDE]
}
}
My storyboard looks as follows:
On the left I have a ViewController with marked google button (which is white, therefore it's not visible here - sorry!) and on the right I have a main TabController view that contains the whole logic of my app (so far it's quite empty):
class TabController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
let background = CAGradientLayer().greenBlue()
background.frame = self.view.bounds
self.view.layer.insertSublayer(background, atIndex: 0)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Now it works like this:
I run the app and see the google button to sign in. I sign in and when everything is validated - nothing changes, I'm not moving to the second screen (TabController). I just stay on this view and I can keep clicking the google button - nothing changes though because I'm already signed in.
I want to achieve a situation when the user opens my app and when he is not logged in - he sees the ViewController screen. When he logs in - he sees the TabController screen. And also when he already signed in before and opens my app - he immediately jumps to TabController and skips the ViewController page. How can I achieve it?
I suspect I have to mark on my storyboard my TabController to be the initial view controller, but what about the log in screen?
=====EDIT
following Mac Bellingrath answer I modified my function in appDelegate.swift class, now it looks like this:
func application(application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Initialize sign-in
var configureError: NSError?
GGLContext.sharedInstance().configureWithError(&configureError)
assert(configureError == nil, "Error configuring Google services: \(configureError)")
GIDSignIn.sharedInstance().delegate = self
/* check for user's token */
if GIDSignIn.sharedInstance().hasAuthInKeychain() {
/* Code to show your tab bar controller */
print("user is signed in")
let sb = UIStoryboard(name: "Main", bundle: nil)
if let tabBarVC = sb.instantiateViewControllerWithIdentifier("TabController") as? UITabBarController {
window!.rootViewController = tabBarVC
}
} else {
print("user is NOT signed in")
/* code to show your login VC */
let sb = UIStoryboard(name: "Main", bundle: nil)
if let tabBarVC = sb.instantiateViewControllerWithIdentifier("ViewController") as? ViewController {
window!.rootViewController = tabBarVC
}
}
return true
}
Now when I run the app and the user has been signed in before - I see the tabController view. If he wasn't signed in before - I see the ViewController view. It works almost like a charm, but I realized that no matter if I write if GIDSignIn.sharedInstance().hasAuthInKeychain() { or if !GIDSignIn.sharedInstance().hasAuthInKeychain() { it always prints out
the message user is signed in, so something is still not right...
According to Google's documentation, it appears that you can use GIDSignIn's instance method hasAuthInKeychain() to check whether the user has either currently signed in or has previous authentication saved in keychain.
So, in your AppDelegate's
func application(application: UIApplication,didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
You might write something like:
GIDSignIn.sharedInstance().delegate = self
/* check for user's token */
if GIDSignIn.sharedInstance().hasAuthInKeychain() {
/* Code to show your tab bar controller */
} else {
/* code to show your login VC */
}
for example if our user is signed in:
let sb = UIStoryboard(name: "Main", bundle: nil)
if let tabBarVC = sb.instantiateViewControllerWithIdentifier("MainTabBarVC") as? UITabBarController {
window.rootViewController = tabBarVC
}
*Substitute your UITabBarController subclass for UITabBarController.
Of course, there are many ways to change the flow. Check out storyboard reference and multiple storyboards.
let googleUser = GIDSignIn.sharedInstance().isCurrentUser
if googleUser != nil {
// Get new token from google and send to server
let strToken = googleUser.authentication.idToken
} else {
// present login screen here
presentLoginScreen()
}
Mac Bellingrath given perfect answer for verify user is logged in.
Alternate way to check user logged in.
Here is sample code in Objective-C you can convert code as per your requirement.
This sample code is just for your reference.
// The authentication object for the current user, or |nil| if there is currently no logged in user.
// Here you can get current user object
GIDGoogleUser *googleUser = [[GIDSignIn sharedInstance] currentUser];
if (googleUser) {
// Get new token from google and send to server
NSString *strToken = googleUser.authentication.idToken;
}
else {
// present login screen here
[self presentLoginScreen];
}
I have added 3D Touch on my app icon to show Quick Action menu. I think I should have it all set up correctly.
The problem is that when I am choosing one of the items in the Quick Action menu, the iPhone freezes for a few seconds before it opens up the application.
This is my AppDelegate.swift:
import UIKit
import Parse
#available(iOS 9.0, *)
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
Parse.setApplicationId("xx",
clientKey: "xx")
let currentInstallation: PFInstallation = PFInstallation.currentInstallation()
currentInstallation.badge = 0
currentInstallation.saveEventually()
return true
}
func applicationWillResignActive(application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
func applicationDidEnterBackground(application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
let rootNavigationViewController = window!.rootViewController as? UINavigationController
let rootViewController = rootNavigationViewController?.viewControllers.first as UIViewController?
rootNavigationViewController?.popToRootViewControllerAnimated(false)
if shortcutItem.type == "JEGHARALDRI" {
rootViewController?.performSegueWithIdentifier("JEGHARALDRISEGUE", sender: nil)
}
if shortcutItem.type == "PLING" {
rootViewController?.performSegueWithIdentifier("PLINGSEGUE", sender: nil)
}
if shortcutItem.type == "FLASKETUTENPEKERPÅ" {
rootViewController?.performSegueWithIdentifier("FLASKETUTENPEKERPÅSEGUE", sender: nil)
}
if shortcutItem.type == "KORTETTALER" {
rootViewController?.performSegueWithIdentifier("KORTETTALERSEGUE", sender: nil)
}
}
}
I think your app delegate should more looks like something like this
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
//MARK: - Properties
var window: UIWindow?
lazy var quickActionManager: QuickActionsManager = {
return QuickActionsManager()
}()
//MARK: - AppDelegate Methods
func application(application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
{
return self.setupQuickActions(launchOptions)
}
func application(application: UIApplication, performActionForShortcutItem
shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void)
{
completionHandler(self.quickActionManager.handleShortcut(shortcutItem))
}
//MARK: - Private Methods
private func setupQuickActions(launchOptions: [NSObject: AnyObject]?) -> Bool
{
guard let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey]
as? UIApplicationShortcutItem else { return false }
return self.quickActionManager.handleShortcut(shortcutItem)
}
}
And so then you get all the logic to handle a quick action in your quick action manager, which would looks something like this
//MARK: - Public Methods
func handleShortcut(shortcut: UIApplicationShortcutItem?) -> Bool
{
guard let shortcut = shortcut else { return false }
// Get the key of the shortcutItem
let key = self.shortKeyForType(shortcut.type)
// Check if that key is the key of a knowed viewController
guard let viewControllerKey = ViewControllerKeys(rawValue: key) else { return false }
// Try to show This View Controller
return self.showViewController(viewControllerKey)
}
Assuming you got an enum of viewController to display matching each quick actions.
I hope this answer your question, let me know if you got some more.
I've got three different .swift files, GameScene.swift, GameOverScene.swift, and StartScene.swift. When I open my app, GameScene shows up. How can I change it so that StartScene.swift is the first file that is displayed? I have been working all in code so a way to do this without the storyboard would be nice. Thanks for the help!
EDIT:
I feel like I've got to change something in GameViewController.swift so heres the code for that.
extension SKNode {
class func unarchiveFromFile(file : String) -> SKNode? {
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks") {
var sceneData = NSData(contentsOfFile: path, options: .DataReadingMappedIfSafe, error: nil)!
var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData)
archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene")
let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as! GameScene
archiver.finishDecoding()
return scene
} else {
return nil
}
}
}
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("pauseTimers:"), name:UIApplicationWillResignActiveNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("startTimers:"), name:UIApplicationDidBecomeActiveNotification, object: nil)
if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
// Configure the view.
let skView = self.view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.size = skView.bounds.size
scene.scaleMode = .AspectFill
skView.presentScene(scene)
}
}
func pauseTimers(notification : NSNotification) {
println("Observer method called")
timer.invalidate()
scoretimer.invalidate()
supertimer.invalidate()
}
func startTimers(notification : NSNotification) {
println("Observer method called")
timerRecreate = true
timerz = false
}
override func shouldAutorotate() -> Bool {
return false
}
override func supportedInterfaceOrientations() -> Int {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return Int(UIInterfaceOrientationMask.AllButUpsideDown.rawValue)
} else {
return Int(UIInterfaceOrientationMask.All.rawValue)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
I tried changing everything that said gamescene to StartScene but that didn't work.
Heres the list of files:
And heres my app delegate:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
func applicationDidEnterBackground(application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(application: UIApplication) {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
EDIT:
I tried implementing the second method in the post below but I got three errors.
With storyboard go into your app delegate and look for this method
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject :AnyObject]?) -> Bool {
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
var storyboard = UIStoryboard(name: "Main", bundle: nil)
var initialViewController = storyboard.instantiateViewControllerWithIdentifier("ViewController") as UIViewController
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
}
If you aren't using Storyboard
window: UIWindow?
var initialViewController :StartScene?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject :AnyObject]?) -> Bool {
initialViewController = StartScene(nibName:"StartScene",bundle:nil)
let frame = UIScreen.mainScreen().bounds
window = UIWindow(frame: frame)
window!.rootViewController = initialViewController
window!.makeKeyAndVisible()
...
}