How can I call a view controller function from an iOS AppDelegate? - ios

I am getting a call from a remote push notification, which includes a path to an image, which I want to display.
The AppDelegate is:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
// get url from notification payload (shortened version)
let url = alert["imgurl"] as? NSString
let vc = ViewController()
vc.loadImage(url as String) // cannot find the imageView inside
}
...and the view looks like this:
func loadImage(url:String) {
downloadImage(url, imgView: self.poolImage)
}
// http://stackoverflow.com/a/28942299/1011227
func getDataFromUrl(url:String, completion: ((data: NSData?) -> Void)) {
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { (data, response, error) in
completion(data: NSData(data: data!))
}.resume()
}
func downloadImage(url:String, imgView:UIImageView){
getDataFromUrl(url) { data in
dispatch_async(dispatch_get_main_queue()) {
imgView.image = UIImage(data: data!)
}
}
}
I'm getting an exception saying imgView is null (it is declared as #IBOutlet weak var poolImage: UIImageView! and I can display an image by calling loadImage("http://lorempixel.com/400/200/") from a button click.
I found a couple of related answers on stackoverflow (one is referenced above) but none work.

If you want to show a new view controller modally, then look into rach's answer further.
If you're ViewController is already on screen, you will need to update it to show the information you need.
How you do this will depend on your root view controller setup. Is your view controller embedded in an UINavigationController? If so then try this:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
if let rootViewController = window?.rootViewController as? UINavigationController {
if let viewController = rootViewController.viewControllers.first as? ViewController {
let url = alert["imgurl"] as? NSString
viewController.loadImage(url as String)
}
}
}
If it is not, then try this:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
if let rootViewController = window?.rootViewController as? ViewController {
let url = alert["imgurl"] as? NSString
rootViewController.loadImage(url as String)
}
}

I believe at the point of time where you are calling
let vc = ViewController()
it has yet to be instantiated. Since you are using #IBOutlet for your UIImageView, I am assuming that you actually have that UIViewController.
Maybe it could be corrected by this:
let vc = self.storyboard?.self.storyboard?.instantiateViewControllerWithIdentifier("ViewControllerStoryboardID")
vc.loadImage("imageUrl")
self.presentViewController(vc!, animated: true, completion: nil)
Let me know if it works. :)
EDIT: You could call an instance of storyboard via this method:
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
var storyboard = UIStoryboard(name: "Main", bundle: nil)
var vc = storyboard.instantiateViewControllerWithIdentifier("ViewController") as! UIViewController
vc.loadImage("imageUrl")
self.window?.rootViewController = vc
self.window?.makeKeyAndVisible()

By calling #IBOutlet weak var poolImage: UIImageView! you're passing the initialization of poolImage to the storyboard. Since you're initializing the view controller with let vc = ViewController(), vc will know that it is expected to have a reference to poolImage because you declared it in the class, but since you aren't actually initializing vc's view (and subviews) poolImage remains nil until the UI is loaded.
If you need to reference a UIImageView at the time of the initialization of vc, you'll have to manually instantiate it: let poolImage = UIImageView()
EDIT: Your ViewController implementation of poolImage currently looks like this:
class ViewController : UIViewController {
#IBOutlet weak var poolImage : UIImageView!
// rest of file
}
To access poolImage from AppDelegate via let vc = ViewController() you have to make the implementation look like this:
class ViewController : UIViewController {
/* ---EDIT--- add a placeholder where you would like
poolImage to be displayed in the storyboard */
#IBOutlet weak var poolImagePlaceholder : UIView!
var poolImage = UIImageView()
// rest of file
override func viewDidLoad() {
super.viewDidLoad()
/* ---EDIT--- make poolImage's frame identical to the
placeholder view's frame */
poolImage.frame = poolImagePlaceholder.frame
poolImagePlaceholder.addSubView(poolImage)
// then add auto layout constraints on poolImage here if necessary
}
}
That way it poolImage is available for reference when ViewController is created.

The reason I think is because the imageView may not have been initialized. Try the following solution.
Add another init method to your viewController which accepts a URLString. Once you receive a push notification, initialize your viewController and pass the imageURL to the new init method.
Inside the new initMethod, you can use the imageURL to download the image and set it to the imageView. Now your imageView should not be null and the error should not occur.

Related

View through IBOutlet or manually created is nil after views hierarchy is loaded

I am creating a macOS app on Xcode 9.2 and i can't figure out why after the views hierarchy is created in viewDidLoad(), i can't use anymore the IBOutlet which is referencing the view in the StoryBoard.
I have found similar question, where they suggest to save the view that i want to update, as a variable, when i am in viewDidLoad() so i can use it later; if so what is the advantage of using an IBOutlet?
Can someone pls explain how to update in this case the content of the PDFView outside viewDidLoad() without getting nil?
The AppDelegate
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
// MARK: - Properties
var filePath: URL?
var result: Int?
var fileObject: Data?
// MARK: - Outlets
#IBOutlet weak var openFileMenuItem: NSMenuItem!
// MARK: - App Life Cycle
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
// MARK: - My Functions
#IBAction func openFile(_ sender: NSMenuItem) {
//Get the window of the app
var window = NSApplication.shared.mainWindow!
// Create NSOpenPanel and setting properties
let panel = NSOpenPanel()
panel.title = "Select file"
panel.allowsMultipleSelection = false;
panel.canChooseDirectories = false;
panel.canCreateDirectories = false;
panel.canChooseFiles = true;
// Filtering file extension
let fileTypes: [String] = ["pdf"]
panel.allowedFileTypes = fileTypes
// Showing OpenPanel to the user for selecting the file
panel.beginSheetModal(for: window) {
(result) in
if result.rawValue == NSFileHandlingPanelOKButton {
// Getting url of the file
self.filePath = panel.urls[0]
print(self.filePath)
// Creating Data Object of the file for creating later the PDFDocument in the controller
do {
if let fileData = self.filePath {
self.fileObject = try Data.init(contentsOf: self.filePath!)
print("\(self.fileObject) ")
}
} catch let error as NSError {
print("Error with file Data Object: \(error)")
}
// Getting the mainViewController
let storyboard = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil)
let mainController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "mainViewControllerID")) as! MainViewController
// Appdelegate call function of mainViewController to update the content of PDFView
mainController.showPdfDocument(fileData: self.fileObject!)
}
}
}
}
The MainViewController:
import Foundation
import Quartz
class MainViewController: NSViewController {
// MARK: - Property
var pdfdocument: PDFDocument?
// MARK: - Outlets
#IBOutlet var mainView: NSView!
#IBOutlet var pdfView: PDFView!
// MARK: - App Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
// MARK: - My Functions
func showPdfDocument(fileData: Data) {
print("appdelegate calling showPdfDocument on mainViewController " )
pdfdocument = PDFDocument(data: fileData)
pdfView.document = pdfdocument
}
}
The Error I Am Getting:
fatal error: unexpectedly found nil while unwrapping an Optional value
I solved the problem with the current code in the AppDelegate. Basically i am using the contentViewController to update the PDFView but i don't know if this is a good practice because i should update the PDFView with its own controller and not with the one of the window.
in the openFile function of the AppleDelegate:
let storyboard = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil)
let mainViewController = NSApplication.shared.mainWindow?.contentViewController as! MainViewController
mainViewController.showPdfDocument(fileData: self.fileObject!)
Reading from the Apple documentation at this link (https://developer.apple.com/documentation/appkit/nswindow/1419615-contentviewcontroller) it says 'Directly assigning a contentView value clears out the root view controller.'. Should I programmatically assign my mainView to the contentView to automatically set my mainViewController as the initial controller?
I have also found that i can set my controller as the initial controller from IB as you can see form this image:
The ideal thing i want to do is to leave the contentViewController as the initial viewController (the default setting in IB) and then instantiate my mainViewController to update the pdfView. The problem is that with instantiation, the new viewController has everything to nil(IBOutlet, vars etc..). Is it the downcasting from contentViewController to mainViewController a good approach to achieve this task?

Swift: Change ViewController after Authentication from RESTful API

I have a rails api set up and one test user. I can login/logout view the api.
I have a tab app set up, and I have a LoginViewController set up to authenticate into that. My view controller (basically) looks like the below code. This doesnt work. However, the second block of code does work
I am guessing I am calling something wrong.. The first block executes and then crashes with a *** Assertion failure in -[UIKeyboardTaskQueuewaitUntilAllTasksAreFinished].. I've been spinning my wheels for hours.. tried looking into segues.. etc. Anything would help!!!
//DOES NOT WORK //
class LoginViewController: UIViewController {
#IBOutlet weak var email: UITextField!
#IBOutlet weak var password: UITextField!
#IBAction func Login(sender: AnyObject) {
func switchToTabs() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let tabBarController = storyboard.instantiateViewControllerWithIdentifier("TabBarController") as! UITabBarController
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window?.rootViewController = tabBarController
}
let authCall = PostData()
var authToken = ""
authCall.email = email.text!
authCall.password = password.text!
authCall.forData { jsonString in
authToken = jsonString
if(authToken != "") {
switchToTabs()
}
}
}
// SAME CLASS THAT DOES WORK//
class LoginViewController: UIViewController {
#IBOutlet weak var email: UITextField!
#IBOutlet weak var password: UITextField!
#IBAction func Login(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let tabBarController = storyboard.instantiateViewControllerWithIdentifier("TabBarController") as! UITabBarController
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window?.rootViewController = tabBarController
}
My question is, why can I seemingly navigate to a new view when I don't use my api, but when I do receive my auth token.. I can't log in.
Please excuse the ugly code.. this is my first day with swift
Everytime you every do anything that changed something about the UI inside of a closure or anything that could even possibly be off the main queue, enclose it in this statement.
dispatch_async(dispatch_get_main_queue()) {
//the code that handles UI
}
This includes all code that segues inside a closure like the one you have
authCall.forData { jsonString in
authToken = jsonString
if(authToken != "") {
switchToTabs()
}
}
If you write it like so it would work
authCall.forData {
jsonString in
authToken = jsonString
if(authToken != "") {
dispatch_async(dispatch_get_main_queue()) {
switchToTabs()
}
}
}
then call your segue in switchToTabs()
If you arn't sure what queue you are on, it won't hurt to do it. As you become more familiar with scenarios that take you off the main queue, you will realize when to use it. Anytime you are in a closure and dealing with UI its a safe bet that you could leave. Obviously anything asynchronous would cause it as well. Just place your performSegueWithIdentifier inside of it and it should work.
If you make a segue on the storyboard from the view controller attatched to LoginViewController, then you click the segue you can give it an identifier. Then you just call performSegueWithIdentifier("identifierName", sender: nil) and none of the other code in switchToTabs() is necessary.
If you want to continue down the route you are taking to summon the View Controller, then I think what you are missing is these lines.
Use this below your class outside of it.
private extension UIStoryboard {
class func mainStoryboard() -> UIStoryboard { return UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()) }
class func tabBarController() -> UITabBarController? {
return mainStoryboard().instantiateViewControllerWithIdentifier("TabBarController") as? UITabBarController
}
}
Then change your switchToTabs() function out for this
func switchToTabs() {
let tabBarController = UIStoryboard.tabBarController()
view.addSubview(tabBarController!.view)
//line below can be altered for different frames
//tabBarController!.frame = self.frame
addChildViewController(tabBarController!)
tabBarController!.didMoveToParentViewController(self)
//line below is another thing worth looking into but doesn't really correlate with this
//self.navigationController!.pushViewController(tabBarController!, animated: true)
}
I added in a couple comments for you to look into if you like. I believe this will do the job, but keep in mind that the direction you are headed doesn't remove the previous screen from memory. It is technically still sitting behind your new screen. I wouldn't ever recommend this for what you are trying to do, but it might not hurt if you are building something simplistic.

Pass messages and/or objects between different storyboards swift

I've two storyboards and need to pass messages and objects. I know how to do it in the same storyboard and with .xib files, but not with two different storyboards.
My code is:
var storyboard = UIStoryboard(name: "RecibosStoryboard", bundle: nil)
var controller = storyboard.instantiateViewControllerWithIdentifier("RecibosStoryboard") as! UINavigationController
self.presentViewController(controller, animated: true, completion: nil).
// If i do: var controller = storyboard.instantiateViewControllerWithIdentifier("RecibosStoryboard") as! = TableRecibosViewController -> fails ->cannot convert TablaRecibosViewController to UINavigationController
// If i do:
/* var controller = storyboard.instantiateViewControllerWithIdentifier("RecibosStoryboard") as! UINavigationController
let vartest: TablaRecibosTableViewController = TablaTablaRecibosTableViewController()
prueba.inicioPrueba(str, strPrueba2: str2) -> two objects are nill
self.presentViewController(controller, animated: true, completion: nil).
Two objects are nill*/
My second storyboard is "RecibosStoryboard" and only has 1 view who class is TablaRecibosViewController and has a contructor method:
class TablaRecibosTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tablaRecibos: UITableView!
var pruebaMansajes: String?
var pruebaMansajes2: String?
var arrayOfObjectsCellRecibos: [ObjectTableRecibos] = [ObjectTableRecibos] ()
override func viewDidLoad() {
super.viewDidLoad()
tablaRecibos.dataSource = self
tablaRecibos.delegate = self
println("Pruebas satisfactorias1 \(pruebaMansajes) \(pruebaMansajes2)")
}
func inicioPrueba(strprueba1:String, strPrueba2:String){
pruebaMansajes = strprueba1
pruebaMansajes2 = strPrueba2
}
When i execute the App or crash or print two objects = nil
I don't find the way to do. Thanks a lot.
The problem is the following "cannot convert TablaRecibosViewController to UINavigationController". Your TablaRecibosViewController is embedded inside a UINavigationController. Try the following:
if let recibosViewController = (storyboard.instantiateViewControllerWithIdentifier("RecibosStoryboard") as? UINavigationController).viewControllers.first as? TablaRecibosTableViewController {
recibosViewController.pruebaMensajes = "Hola mundo"
}
You were creating a new instance of TablaTablaRecibosTableViewController() which was not associated to the Storyboard, therefore the table view was nil and it crashed.

Custom segue to a different storyboard

Question:
How might one write a custom segue that would allow you to embed view controllers from a different storyboard?
Context:
I am trying to write a custom segue with which I can link from one storyboard to another. A good article on atomicobject.com illustrates how to create a segue that originates from a button / event etc. Translated into swift, and allowing for non UINavigationControllers, the code looks like:
public class SegueToStoryboard : UIStoryboardSegue {
private class func viewControllerInStoryBoard(identifier:String, bundle:NSBundle? = nil)
-> UIViewController?
{
let boardScene = split(identifier, { $0 == "." }, maxSplit: Int.max, allowEmptySlices: false)
switch boardScene.count {
case 2:
let sb = UIStoryboard(name: boardScene[0], bundle: bundle)
return sb.instantiateViewControllerWithIdentifier(boardScene[1]) as? UIViewController
case 1:
let sb = UIStoryboard(name: boardScene[0], bundle: bundle)
return sb.instantiateInitialViewController() as? UIViewController
default:
return nil
}
}
override init(identifier: String!,
source: UIViewController,
destination ignore: UIViewController) {
let target = SegueToStoryboard.viewControllerInStoryBoard(identifier, bundle: nil)
super.init(identifier: identifier, source: source,
destination:target != nil ? target! : ignore)
}
public override func perform() {
let source = self.sourceViewController as UIViewController
let dest = self.destinationViewController as UIViewController
source.addChildViewController(dest)
dest.didMoveToParentViewController(source)
source.view.addSubview(dest.view)
// source.navigationController?.pushViewController(dest, animated: true)
}
}
Problem:
The problem that I am having with both their Obj-C and the above Swift code is that when you try to use the via a container view (with semantics of an embed segue - starting with an embed segue, deleting the segue, and then use the above custom segue), it crashes before ever calling the segue code with the following method-not-found error:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<UIStoryboardSegueTemplate 0x7ffc8432a4f0>
setValue:forUndefinedKey:]: this class is not key value
coding-compliant for the key containerView.'
I have tried to inspect the address listed but get no meaningful results. I do the see the bold statement that it expecting the containerView but don't know how one might isolate, satisfy, and/or work around this problem.
Summary:
My end goal is to embed view controllers defined in separate storyboards to facilitate collaboration and testing without having to write additional code (a non invasive solution). Does anyone have any insight into how to accomplish this greater task? I could fall back to hybrid approach of calling performSegue, but would like to find a single, contained, and complete solution. The above code gets there for event driven (buttons etc) segues, but not with the embed segue.
Any input is appreciated, thanks in advance.
Your approach works fine for custom segues to push / display modally other view controllers but not for embed segues. The reason for this is that the "Embed" segue is not a subclass of UIStoryboardSegue but inherits from UIStoryboardSegueTemplate, which is a private API.
Unfortunately I couldn't find a better way to achieve what you want than with the hybrid approach.
My way is to link the containerView and delete the viewDidLoad segue from it. and manually call the segue on viewdidLoad
public protocol EmbeddingContainerView {
var containerView: UIView! { get set }
}
public class CoreSegue: UIStoryboardSegue {
public static func instantiateViewControllerWithIdentifier(identifier: String) -> UIViewController {
let storyboard = UIStoryboard(name: "Core", bundle: NSBundle(forClass: self))
let controller = storyboard.instantiateViewControllerWithIdentifier(identifier) as! UIViewController
return controller
}
var isPresent = false
var isEmbed = false
override init!(identifier: String?, source: UIViewController, destination: UIViewController) {
if var identifier = identifier {
if identifier.hasPrefix("present ") {
isPresent = true
identifier = identifier.substringFromIndex(advance(identifier.startIndex, count("present ")))
}
if identifier.hasPrefix("embed ") {
isEmbed = true
identifier = identifier.substringFromIndex(advance(identifier.startIndex, count("embed ")))
}
let controller = CoreSegue.instantiateViewControllerWithIdentifier(identifier)
super.init(identifier: identifier, source: source, destination: controller)
} else {
super.init(identifier: identifier, source: source, destination: destination)
}
}
public override func perform() {
if let source = sourceViewController as? UIViewController, dest = destinationViewController as? UIViewController {
if isPresent {
let nav = UINavigationController(rootViewController: dest)
nav.navigationBarHidden = true // you might not need this line
source.presentViewController(nav, animated: true, completion: nil)
} else if isEmbed {
if let contentView = (source as? EmbeddingContainerView)?.containerView {
source.addChildViewController(dest)
contentView.addSubview(dest.view)
dest.view.fullDimension() // which comes from one of my lib
}
} else {
source.navigationController?.pushViewController(destinationViewController as! UIViewController, animated: true)
}
}
}
}
and later in your code:
class MeViewController: UIViewController, EmbeddingContainerView {
#IBOutlet weak var containerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
performSegueWithIdentifier("embed Bookings", sender: nil)
}
}

Pushing ViewController Results In Cant Unwrap Optional.None

I have an issue in Swift. When I press a cell on my table I do a segue into another ViewController which has a UIWebView and loads a webpage. This works fine. The custom ViewController code looks like this:
class ViewController_ShowView : UIViewController
{
var showName: String?
var showUrl: String?
#IBOutlet var webView: UIWebView
override func viewDidLoad()
{
super.viewDidLoad()
self.title = showName
var requestURL = NSURL(string: showUrl)
var request = NSURLRequest(URL: requestURL)
webView.loadRequest(request)
}
}
However, I am trying to load the ViewController from the AppDelegate (when a user clicks on a UILocalNotification) and it crashes on the web view.loadRequest(request) line from above. Here is how I am attempting to call it:
var currentShowsViewController = viewController.topViewController as ViewController_CurrentShows
var viewControllerToPush = ViewController_ShowView()
viewControllerToPush.showName = "Test"
viewControllerToPush.showUrl = "http://www.google.com"
currentShowsViewController.navigationController.pushViewController(viewControllerToPush, animated: false)
This causes a crash with the error Cant Unwrap Optional.None
I am confused why it works from the perform segue method but not this way...
Problem caused by this line of code:
var viewControllerToPush = ViewController_ShowView()
You should instantiate view controller from its storyboard. Here is how you can do it:
let storyboard : UIStoryboard = UIStoryboard(name: "MainStoryboard", bundle: nil);
let viewControllerToPush : ViewController_ShowView
= storyboard.instantiateViewControllerWithIdentifier("showViewVC") as ViewController_ShowView;
Note that you should also open your storyboard and set the view controller's identifier to showViewVC.
Remarks:
The exception get thrown because #IBOutlet var webView: UIWebView does not get initialized. #IBOutlets in Swift are optionals thus you receive Cant Unwrap Optional.None.

Resources