Swift: Change ViewController after Authentication from RESTful API - ios

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.

Related

iOS New-Contact-Style Segue in Swift

I am trying to emulate the iOS contacts segueing between two view controllers.
I have a simple Person class given by:
class Person {
var name = ""
}
and a UIViewController that contains an array of Person, which is embedded in a UINavigationController:
class PeopleViewController: UIViewController {
var people = [Person]()
var selectedPerson: Person?
switch segueIdentifier(for: segue) {
case .showPerson:
guard let vc = segue.destination as? PersonViewController else { fatalError("!") }
vc.person = selectedPerson
}
}
This controller uses a Show segue to PersonViewController to display the selectedPerson:
class PersonViewController: UIViewController {
var person: Person!
}
PeopleViewController can also add a new Person to the array of Person. The NewPersonViewController is presented modally, however:
class NewPersonViewController: UIViewController {
var person: Person?
}
If a new Person is added, I want NewPersonViewController to dismiss but show the new Person in the PersonViewController that is part of the navigation stack. My best guess for doing this is:
extension NewPersonViewController {
func addNewPerson() {
weak var pvc = self.presentingViewController as! UINavigationController
if let cvc = pvc?.childViewControllers.first as? PeopleViewController {
self.dismiss(animated: false, completion: {
cvc.selectedPerson = self.person
cvc.performSegue(withIdentifier: .showPerson, sender: nil)
}
}
}
}
However, (1) I'm not too happy about forcing the downcast to UINavigationController as I would have expected self.presentingViewController to be of type PeopleViewController? And (2), is there a memory leak in the closure as I've used weak var pvc = self.presentingViewController for pvc but not for cvc? Or, finally (3) is there a better way of doing this?
Many thanks for any help, suggestions etc.
(1) I'm not too happy about forcing the downcast to UINavigationController as I would have expected self.presentingViewController to be of type PeopleViewController?
There is nothing wrong in downcasting. I would definitely remove force unwrapping.
(2), is there a memory leak in the closure as I've used weak var pvc = self.presentingViewController for pvc but not for cvc?
I think, there is none.
(3) is there a better way of doing this?
You can present newly added contact from NewContactVC. When you about to dismiss, call dismiss on presentingVC.
// NewPersonViewController.swift
func addNewPerson() {
// New person is added
// Show PeopleViewController modally
}
Note: Using presentingViewController this way will dismiss top two/one Modal(s). You will see only top view controller getting dismissed.
If you can't determine how many modals going to be, you should look-into different solution or possibly redesigning navigation flow.
// PeopleViewController.swift
func dismiss() {
if let presentingVC = self.presentingViewController?.presentingViewController {
presentingVC.dismiss(animated: true, completion: nil)
} else {
self.dismiss(animated: true, completion: nil)
}
}

Cannot take values from other view controller Swift

I want to take user settings details from this view controller and read these details to the previous view controller. I have tried many different ways, but I cannot take values until I visit this view controller
I have tried first method from this page Pass Data Tutorial
This method is also not working. I think it is very simple, but I cannot figure out the right way to do it.
class SetConvViewController: UIViewController {
var engS = "engS"
#IBOutlet weak var swithEnglish: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
if let eng2 = defaults.value(forKey: engS)
{
swithEnglish.isOn = eng2 as! Bool
}
}
let defaults = UserDefaults.standard
#IBAction func switchEng(_ sender: UISwitch) {
defaults.set(sender.isOn, forKey: engS)
}
}
If I understand you correctly from this part - „but I cannot take values until I visit this view controller” - your problem lies with the fact, that until you visit your settings, there is no value for them in UserDefaults.
If you are reading them using getObject(forKey:) method, I’d recommend you to switch to using getBool(forKey:), since it will return false even if the value has not been set yet for that key ( docs )
Anyhow, if you want to set some default/initial values you can do so in your didFinishLaunching method in AppDelegate :
if UserDefaults.standard.object(forKey: „engS”) == nil {
// the value has not been set yet, assign a default value
}
I’ve also noticed in your code that you used value(forKey:) - you should not do that on UserDefaults - this is an excellent answer as to why - What is the difference between object(forKey:) and value(forKey:) in UserDefaults?.
On a side note, if you are using a class from iOS SDK for the first time, I highly recommend looking through its docs - they are well written and will provide you with general understanding as to what is possible.
I would recommend you to store this kind of data as a static field in some object to be able to read it from any place. e.g.
class AppController{
static var userDefaults = UserDefaults.standard
}
and then you can save it in your SetConvViewController like
#IBAction func switchEng(_ sender: UISwitch) {
AppController.userDefaults.set(sender.isOn, forKey: engS)
}
and after that you can just read it from any other view controller just by calling
AppController.userDefaults
Using segues you can set to any destination whether it be next vc or previous:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "PreviousVC" {
if let prevVC = segue.destination as? PreviousViewController {
//Your previous vc should have your storage variable.
prevVC.value = self.value
}
}
If you're presenting the view controller:
Destination vc:
//If using storyboard...
let destVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "DestinationViewController") as! DestinationViewController
destVC.value = self.value
self.present(destVC, animated: true, completion: nil)
Previous vc:
weak var prevVC = self.presentingViewController as? PreviousViewController
if let prevVC = prevVC {
prevVC.value = self.value
}

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

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.

Instantiating UIViewController in a test

I'm working in Swift 2, and would like to test functions within my view controller. I've made a dependency injection-like service which looks like this:
extension UIViewController: {
func getDbService() -> IDbService {
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
return DbService(context: context)
}
}
With this, I can set AppDelegate's context as a mocked one for test purposes. However, the problem arises when I try to instantiate a view controller. Here's the code:
class LoginViewController: UIViewController {
var token: String?
override func viewDidLoad() {
super.viewDidLoad()
let dbService = getDbService()
self.token = dbService.getToken()
//....do stuff with token
}
}
I instantiate the test like so:
class LoginViewControllerTests: XCTestCase {
func testTokenExists() {
let mockContext = MockContextUtils.getMockContext()
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.managedObjectContext = mockContext
let sut = LoginViewController()
let _ = sut.view // Apparently this renders the view; I set a breakpoint, viewDidLoad is called
XCTAssertNotNil(sut.token) // FAILS, BECAUSE APPDELEGATE CALLS APP DATABASE, AND NOT MOCK.
}
}
The reason this simple test fails is because LoginViewController has no idea what app delegate is. Is there a way to introduce that in the initialization phase?
So it appears that initializing the view controller with its default controller does not grab the app delegate which we set up in the test. Instead, I've initialized the view controller through the storyboard via so:
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let sut = storyboard.instantiateInitialViewController() as? LoginViewController
XCTAssertNotNil(sut)
XCTAssertNotNil(sut.view)
XCTAssertNotNil(sut.token)
which solves the problem.

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

Resources