Best practice to change current view from a class - ios

I'm working with Swift 3 and I'd like to change my view from a function in my class when login succeed.
I've got a LoginViewController which contains this function:
static let sharedInstance = LoginViewController()
//...
func showNextView() {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
guard let eventVC = storyboard.instantiateViewController(
withIdentifier: "EventsTVC") as? EventsTableViewController else {
assert(false, "Misnamed view controller")
return
}
self.present(eventVC, animated: false, completion: nil)
}
In my class APIManager, I call this function inside my asynchronous method using Alamofire:
func processAuth(_ data: [String: String]) {
print("-- auth process started")
//... defining vars
Alamofire.request(tokenPath, method: .post, parameters: tokenParams, encoding: JSONEncoding.default, headers: tokenHeader)
.responseJSON { response in
guard response.result.error == nil else {
print(response.result.error!)
return
}
guard let value = response.result.value else {
print("No string received in response when swapping data for token")
return
}
guard let result = value as? [String: Any] else {
print("No data received or data not JSON")
return
}
// -- HERE IS MY CALL
LoginViewController.sharedInstance.showNextView()
print("-- auth process ended")
}
}
My console returns this error message:
-- auth process started 2017-03-18 20:38:14.078043 Warning: Attempt to present on
whose view is not in the window
hierarchy!
-- auth process ended
I think it's not the best practice to change my view when my asynchronous method has ended.
I don't know what I've got to do. Currently, this is the process:
User opens the app and the LoginViewController is displayed, if no token is saved (Facebook Login)
In the case where it has to login, a button "Login with Facebook" is displayed
When login succeed, I send Facebook data in my processAuth() function in my APIManager class
When my API returns me the token, I saved it and change the view to EventsTVC
I put in bold where the problem is. And I would like to know if it's the best practice in my case. If so, how to avoid my error message?
I hope I made myself understood. Thanks for your help!

What actually happens is that your singleton instance LoginViewController wants to present itself while not being in the view hierarchy. Let me explain it thoroughly:
class LoginViewController: UIViewController {
static let sharedInstance = LoginViewController()
func showNextView() {
...
// presentation call
self.present(eventVC, animated: false, completion: nil)
}
In this function you are calling present() from your singleton instance on itself. You have to call it from a view which is (preferably) on top of the view hierarchy stack. The solution would probably be not using a singleton on a VC in the first place. You should be instantiating and presenting it from the VC that is currently on the screen. Hope this helps!

Related

In Cognito on iOS, handling new password required doesn't ever reach didCompleteNewPasswordStepWithError

I'm trying to implement functionality to respond to FORCE_CHANGE_PASSWORD on my iOS app that uses AWS Cognito. I used this Stack Overflow question which references this sample code. Right now, my code opens a view controller like it's supposed to; however, once on that view controller, I can't get it to do anything. In the sample code, it seems that when you want to submit the password change request you call .set on an instance of AWSTaskCompletionSource<AWSCognitoIdentityNewPasswordRequiredDetails>, yet when I do this, the protocol function didCompleteNewPasswordStepWithError is never called. Interestingly, the other protocol function getNewPasswordDetails is called quickly after viewDidLoad and I can't tell why. I believe this shouldn't be called until the user has entered their new password, etc and should be in response to .set but I could be wrong.
My code is pretty identical to the sample code and that SO post, so I'm not sure what's going wrong here.
My relevant AppDelegate code is here:
extension AppDelegate: AWSCognitoIdentityInteractiveAuthenticationDelegate {
func startNewPasswordRequired() -> AWSCognitoIdentityNewPasswordRequired {
//assume we are presenting from login vc cuz where else would we be presenting that from
DispatchQueue.main.async {
let presentVC = UIApplication.shared.keyWindow?.visibleViewController
TransitionHelperFunctions.presentResetPasswordViewController(viewController: presentVC!)
print(1)
}
var vcToReturn: ResetPasswordViewController?
returnVC { (vc) in
vcToReturn = vc
print(2)
}
print(3)
return vcToReturn!
}
//put this into its own func so we can call it on main thread
func returnVC(completion: #escaping (ResetPasswordViewController) -> () ) {
DispatchQueue.main.sync {
let storyboard = UIStoryboard(name: "ResetPassword", bundle: nil)
let resetVC = storyboard.instantiateViewController(withIdentifier: "ResetPasswordViewController") as? ResetPasswordViewController
completion(resetVC!)
}
}
}
My relevant ResetPasswordViewController code is here:
class ResetPasswordViewController: UIViewController, UITextFieldDelegate {
#IBAction func resetButtonPressed(_ sender: Any) {
var userAttributes: [String:String] = [:]
userAttributes["given_name"] = firstNameField.text!
userAttributes["family_name"] = lastNameField.text!
let details = AWSCognitoIdentityNewPasswordRequiredDetails(proposedPassword: self.passwordTextField.text!, userAttributes: userAttributes)
self.newPasswordCompletion?.set(result: details)
}
}
extension ResetPasswordViewController: AWSCognitoIdentityNewPasswordRequired {
func getNewPasswordDetails(_ newPasswordRequiredInput: AWSCognitoIdentityNewPasswordRequiredInput, newPasswordRequiredCompletionSource: AWSTaskCompletionSource<AWSCognitoIdentityNewPasswordRequiredDetails>) {
self.newPasswordCompletion = newPasswordRequiredCompletionSource
}
func didCompleteNewPasswordStepWithError(_ error: Error?) {
DispatchQueue.main.async {
if let error = error as? NSError {
print("error")
print(error)
} else {
// Handle success, in my case simply dismiss the view controller
SCLAlertViewHelperFunctions.displaySuccessAlertView(timeoutValue: 5.0, title: "Success", subTitle: "You can now login with your new passowrd", colorStyle: Constants.UIntColors.emeraldColor, colorTextButton: Constants.UIntColors.whiteColor)
self.dismiss(animated: true, completion: nil)
}
}
}
}
Thank you so much for your help in advance and let me know if you need any more information.

Does anyone have good idea to handle the deeplink with the id which is supposed to show the single page?

Does anyone have good idea to handle the deeplink?
I want to push a single page view which needs id from the HomeViewcontroller(or anything is ok) to the single page with the id that I get from the deeplink.
My current situation is that I could get the deeplink and the id inside of that in AppDelegate file by the way like below.
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([Any]?) -> Void) -> Bool {
if let incomingURL = userActivity.webpageURL {
let linkHandled = DynamicLinks.dynamicLinks()!.handleUniversalLink(incomingURL) { [weak self](dynamiclink, error) in
guard let strongSelf = self else { return }
if let dynamiclink = dynamiclink, let _ = dynamiclink.url {
strongSelf.handleIncomingDynamicLink(dynamicLink: dynamiclink)
}
}
return linkHandled
}
return false
}
func handleIncomingDynamicLink(dynamicLink: DynamicLink) {
guard let pathComponents = dynamicLink.url?.pathComponents else {
return
}
if pathComponents.count > 1 {
for (i, value) in pathComponents.enumerated() {
if i == 1 {
// define whether the deeplink is for topic or post
UserDefaults.standard.set(value, forKey: "deepLinkType")
print(value)
} else if i == 2 {
UserDefaults.standard.set(value, forKey: "deepLinkId")
print(value)
}
}
}
}
And then viewDidAppear in the HomeViewController
if (self.isViewLoaded && (self.view.window != nil)) {
let us = UserDefaults.standard
if let deepLinkType = us.string(forKey: "deepLinkType"), let deepLinkId = us.string(forKey: "deepLinkId"){
us.removeObject(forKey: "deepLinkType")
us.removeObject(forKey: "deepLinkId")
if deepLinkType == "topic" {
let storyboard = UIStoryboard(name: "Topic", bundle: nil)
let nextVC = storyboard.instantiateViewController(withIdentifier: "SingleKukaiVC") as! TopicViewController
nextVC.topicKey = deepLinkId
self.navigationController?.pushViewController(nextVC, animated: true)
} else if deepLinkType == "post" {
}
}
}
this works fine when the app is neither in foreground nor background I mean if it's not instanced. However, while the app is instanced, this doesn't work because viewDidAppear is not going to be read. Or even the HomeViewController itself is not might be called if user had opened another view.
So my question is that what is the best way to handle the deeplink which has id for the single page? I appreciate some examples.
Thanks in advance.
Don't write your code to push Topic view controller in the HomeViewController.
Inside the App Delegate's handleIncomingDynamicLink method, get the top most view controller, then create the view controller (as in your code) and then push it from the top most view controller.
Your code to create Topic View Controller:
let storyboard = UIStoryboard(name: "Topic", bundle: nil)
let nextVC = storyboard.instantiateViewController(withIdentifier: "SingleKukaiVC") as! TopicViewController
nextVC.topicKey = deepLinkId
Check URL to see how to fetch top most view controller
Two ways you could handle it:
For the ViewController that you want presented upon handling of the deeplink set an initializer that takes info contained in the deeplink. And set whatever you need to set to handle that info(you seem to have done that). Now in your handleIncomingDynamicLink() instantiate the ViewController we mentioned and make it to be presented. How you are going to make it present itself depends on the navigation logic that you have set.
AppDelegate receives link->Handles it->Instatiates VC and presents it->Does things accordingly
In your handleIncomingDynamicLink() use NotificationCenter to post a notification containing the info. In your ViewController add an observer for that notification and define whatever you need to be done when the notification is received.
AppDelegate receives link->Handles it->Fires notification->VC listens notifications->Does things accordingly

Managed Object Context is nil for some reason in iOS

I'm using Alamofire to submit a request to an endpoint using Swift. I parse the JSON objects that I receive from the response using the Codable protocol, and then try to insert the objects into Core Data using Managed Object Subclasses. However, when I do this, I keep receiving an error saying that my parent Managed Object Context (MOC) is nil. This doesn't make sense to me because I set the MOC via Dependency Injection from the AppDelegate, and confirm that it has a value by printing out it's value to the console in the viewDidLoad() method.
Here is my relevant code:
I set my MOC here:
class ViewController: UIViewController {
var managedObjectContext: NSManagedObjectContext! {
didSet {
print("moc set")
}
}
override func viewDidLoad() {
super.viewDidLoad()
print(managedObjectContext)
}
///
func registerUser(userID: String, password: String) {
let parameters: [String: Any] = ["email": userID, "password": password, "domain_id": 1]
let headers: HTTPHeaders = ["Accept": "application/json"]
Alamofire.request(registerURL, method: .patch, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseJSON { response in
switch response.result {
case .success:
if let value = response.result.value {
print(value)
let jsonDecoder = JSONDecoder()
do {
let jsonData = try jsonDecoder.decode(JSONData.self, from: response.data!)
print(jsonData.data.userName)
print(jsonData.data.identifier)
print(self.managedObjectContext)
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = self.managedObjectContext
let user = UserLogin(context: privateContext)
user.userName = jsonData.data.userName
user.domainID = Int16(jsonData.data.identifier)
user.password = "blah"
do {
try privateContext.save()
try privateContext.parent?.save()
} catch let saveErr {
print("Failed to save user", saveErr)
}
} catch let jsonDecodeErr{
print("Failed to decode", jsonDecodeErr)
}
}
case .failure(let error):
print(error)
}
}
}
The specific error message I'm getting is:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Parent NSManagedObjectContext must not be nil.'
I realize that Alamofire is download the data on a background thread, which is why I use a child context, but I'm not sure why the parent is nil.
Here is the setup code for my Managed Object Context:
class AppDelegate: UIResponder, UIApplicationDelegate {
var persistentContainer: NSPersistentContainer!
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
createContainer { container in
self.persistentContainer = container
let storyboard = self.window?.rootViewController?.storyboard
guard let vc = storyboard?.instantiateViewController(withIdentifier: "RootViewController") as? ViewController else { fatalError("Cannot instantiate root view controller")}
vc.managedObjectContext = container.viewContext
self.window?.rootViewController = vc
}
return true
}
func createContainer(completion: #escaping(NSPersistentContainer) -> ()) {
let container = NSPersistentContainer(name: "Test")
container.loadPersistentStores { _, error in
guard error == nil else { fatalError("Failed to load store: \(error!)") }
DispatchQueue.main.async { completion(container) }
}
}
Can anyone see what it is I'm doing wrong?
I don't see anything immediately "wrong" so lets debug this a bit.
Put a breakpoint in applicationDidFinish... right after the guard.
Put a breakpoint at the creation of the privateContext.
Which fires first?
Where is the registerUser function? In a view controller? I hope not :)
the breakpoint right after my guard statement fires first. And yes, my registerUser function is indeed inside a ViewController.
Putting network code in view controllers is a code smell. View Controllers have one job, manage their views. Data gathering belongs in a persistence controller; for example, extending your NSPersistentContainer and putting data collection code there.
However, that is not the issue here, just a code smell.
Next test.
Is your persistent container and/or viewContext being passed into your view controller and bring retained?
Is your view controller being destroyed before the block fires?
To test this, I would put an assertion before Alamofire.request and crash if the context is nil:
NSAssert(self.managedObjectContext != nil, #"Main context is nil in the view controller");
I would also put that same line of code just before:
privateContext.parent = self.managedObjectContext
Run again. What happens?
I ran the test as you described, and I get the error: Thread 1: Assertion failed: Main context is nil in the view controller
Which assertion crashed? (probably should change the text a bit...)
If it is the first one then your view controller is not receiving the viewContext.
If it is the second one then the viewContext is going back to nil before the block executes.
Change your assumptions accordingly.
discovered something that is relevant here: If I place a button to call the registerUser() function at the user's discretion instead of calling it directly from the viewDidLoad() method, there is no crash, the code runs fine, and the MOC has a value
That leads me down the theory that your registerUser() was being called before your viewDidLoad(). You can test that by putting a break point in both and see which one fires first. If your registerUser() fires first, look at the stack and see what is calling it.
If it fires after your viewDidLoad() then put a breakpoint on the context property and see what is setting it back to nil.
So if I remove that line, how do I set the MOC property on my RootViewController via Dependency Injection?
The line right before it is the clue here.
let storyboard = self.window?.rootViewController?.storyboard
Here you are getting a reference to the storyboard from the rootViewController which is already instantiated and associated with the window of your application.
Therefore you could change the logic to:
(self.window?.rootViewController as? ViewController).managedObjectContext = container.viewContext
Although I would clean that up and put some nil logic around it :)
The problem I realize is that the MOC in the RootViewController is being used BEFORE the MOC is returned from the closure, and set in the AppDelegate. What do I do here?
This is a common synchronous (UI) vs. asynchronous (persistence) issue. Ideally your UI should wait until the persistence loads. If you load the persistent stores and then finish the UI after the stores have loaded it will resolve this issue.
Without a migration we are generally talking ms here rather than seconds.
BUT ... You want the same code to handle the UI loading whether it is milliseconds or seconds. How you solve that is up to you (design decision). One example is to continue the loading view until the persistence layer is ready and then transition over.
If you did that you could then subtly change the loading view if a migration is happening to inform the user as to why things are taking so long.

how can I instantiate a viewController with a containerView and it's containerView at the same time?

I want to instantiate a viewController with a container with the following:
let vc = self.storyboard?.instantiateViewController(withIdentifier: ContainerViewController") as? ContainerViewController
I also need a reference to the containerView so I try the following:
let vc2 = vc.childViewControllers[0] as! ChildViewController
The app crashes with a 'index 0 beyond bounds for empty NSArray'
How can I instantiate the containerViewController and it's childViewController at the same time prior to loading the containerViewController?
EDIT
The use case is for AWS Cognito to go to the signInViewController when the user is not authenticated. This code is in the appDelegate:
func startPasswordAuthentication() -> AWSCognitoIdentityPasswordAuthentication {
if self.containerViewController == nil {
self.containerViewController = self.storyboard?.instantiateViewController(withIdentifier: "ContainerViewController") as? ContainerViewController
}
if self.childViewController == nil {
self.childViewController = self.containerViewController!.childViewControllers[0] as! ChildViewController
}
DispatchQueue.main.async {
self.window?.rootViewController?.present(self.containerViewController!, animated: true, completion: nil)
}
return self.childViewController!
}
The reason I am instantiating the container and returning the child is that the return needs to conform to the protocol which only the child does. I suppose I can remove the container but it has functionality that I would have wanted.
Short answer: You can't. At the time you call instantiateViewController(), a view controller's view has not yet been loaded. You need to present it to the screen somehow and then look for it's child view once it's done being displayed.
We need more info about your use-case in order to help you.
EDIT:
Ok, several things:
If your startPasswordAuthentication() function is called on the main thread, there's no reason to use DispatchQueue.main.async for the present() call.
If, on the other hand, your startPasswordAuthentication() function is called on a background thread, the call to instantiateViewController() also belongs inside a DispatchQueue.main.async block so it's performed on the main thread. In fact you might just want to put the whole body of your startPasswordAuthentication() function inside a DispatchQueue.main.async block.
Next, there is no way that your containerViewController's child view controllers will be loaded after the call to instantiateViewController(withIdentifier:). That's not how it works. You should look for the child view in the completion block of your present call.
Next, you should not be reaching into your containerViewController's view hierarchy. You should add methods to that class that let you ask for the view you are looking for, and use those.
If you are trying to write your function to synchronously return a child view controller, you can't do that either. You need to rewrite your startPasswordAuthentication() function to take a completion handler, and pass the child view controller to the completion handler
So the code might be rewritten like this:
func startPasswordAuthentication(completion: #escaping (AWSCognitoIdentityPasswordAuthentication?)->void ) {
DispatchQueue.main.async { [weak self] in
guard strongSelf = self else {
completion(nil)
return
}
if self.containerViewController == nil {
self.containerViewController = self.storyboard?.instantiateViewController(withIdentifier: "ContainerViewController") as? ContainerViewController
}
self.window?.rootViewController?.present(self.containerViewController!, animated: true, completion: {
if strongSelf == nil {
strongSelf.childViewController = self.containerViewController.getChildViewController()
}
completion(strongSelf.childViewController)
}
})
}
(That code was typed into the horrible SO editor, is totally untested, and is not meant to be copy/pasted. It likely contains errors that need to be fixed. It's only meant as a rough guide.)

unexpectedly found nil when refreshing UI of presentingViewController within dismissViewController callback

I'm trying to reload the UI of a view (MyMatches) thats presenting another view (BuyView). When BuyView is dismissed, I want to reload all of the views of MyMatches. However, when I try to do this within the completion of "dismissViewController", I run into an "unexpectedly found nil" error on the line "let mmvc = self.presentingViewController? as! MyMatchesViewController". Does anyone know why this happens, or if there's an easier way to accomplish what I'm trying to do? Code posted below is found within BuyViewController:
func itemBought() {
print("Confirm tapped!")
BoughtController.globalController.sendSellerNotification(seller, match: match)
BoughtController.globalController.updateBuyer(self.item, buyer: LocalUser.user, match: self.match)
BoughtController.globalController.updateMarket(self.item, match: self.match)
BoughtController.globalController.updateSeller(self.item, seller: seller, soldPrice: self.match.matchedPrice)
self.cancel = false
if self.fromInfo == true {
self.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
}
else {
self.dismissViewControllerAnimated(true) {
let mmvc = self.presentingViewController as! MyMatchesViewController
mmvc.setupMatchesScrollContent()
}
}
}
Likely, the dismissViewControllerAnimated() block runs after the view controller has been dismissed and thus self.presentingViewController has changed? Perhaps. Safer to use:
else {
let mmvc = self.presentingViewController as! MyMatchesViewController
self.dismissViewControllerAnimated(true) {
mmvc.setupMatchesScrollContent()
}
}
where you 'capture' mmvc before using it in the trailing closure.

Resources