Swift dismissing view with clunky animation - ios

I simply have a ViewController that I would like to dismiss. And this is my dismissAction:
#objc private func dismissView(){
self.dismiss(animated: true, completion: nil)
UserDefaultsService.shared.updateDataSourceArrayWithWishlist(wishlist: self.wishList)
let dataSourceArray = UserDefaultsService.shared.getDataSourceArray()
// update datasource array in MainVC
self.dismissWishlistDelegate?.dismissWishlistVC(dataArray: dataSourceArray, dropDownArray: self.dropOptions, shouldDeleteWithAnimation: false, wishlistToDelete: self.wishList)
}
Proble:
The dismiss animation is very clunky and not fluent at all. I found out that if I remove everything in the function but only call self.dismiss it is working perfectly fine. What is the issue here? Any idea on how I can fix this?

You can try to light-weight load in main thread by
DispatchQueue.global().async {
UserDefaultsService.shared.updateDataSourceArrayWithWishlist(wishlist: self.wishList)
}
And instead of let dataSourceArray = UserDefaultsService.shared.getDataSourceArray() use self.wishList directly in the last line

Related

UIViewController dismiss issue

I want to wait until dismiss animation completes but I don't want to use many blocks in my code, so I wrote this function in UIViewController extension (almost like this worked several years ago for me):
func dismissAnimated() {
var comleted: Bool = false
self.dismiss(animated: true) {
comleted = true
}
while !comleted {
RunLoop.current.run(mode: RunLoop.Mode.common, before: Date.distantFuture)
}
}
so now instead of:
viewController.dismiss(animated: true) {
// code after completion
}
I was supposed to write:
viewController.dismissAnimated()
// code after completion
But it doesn't dismiss view controller and doesn't enter into completion block.
I tried different RunLoop modes, tried different dates, tried inserting RunLoop.current.run into while condition, it didn't work. Any ideas how to accomplish this?
Edit:
And it worked on iOS 9 or something like this (may be with some code changes, because I can't find my source code). I start RunLoop.current.run to avoid blocking main thread. For instance, if I put completed = true in DispatchQue.main.asyncAfter , it will work, the issue is with dismiss
I tried again because I was curious and this solution actually works for me:
#objc private func dismissTapped() {
let dismissalTime = dismissAnimated()
print("Dismissal took: %ld", abs(dismissalTime))
}
private func dismissAnimated() -> TimeInterval {
let startDate = Date()
var completed = false
self.dismiss(animated: true) {
completed = true
}
while !completed {
RunLoop.current.run(mode: .default, before: .distantFuture)
}
return startDate.timeIntervalSinceNow
}
iOS 12.1.2 | Swift 4.2
It doesn't dismiss because your while !completed loop has stalled the main thread and the UI update happens on the main thread. What is wrong with running whatever code you need to run inside the dismiss completion closure?
self.dismiss(animated: true) {
runSomeCode()
}
If it's really just about not using blocks maybe this may be a solution?
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if self.navigationController?.isBeingDismissed ?? self.isBeingDismissed {
print("Dismissal completed...")
}
}

dismissing a view never calls completion function

When I dismiss a view it refuses to trigger the completion function and I don't know why.
Here I expect the print statement to execute but it never does.
#IBAction func tapBack(_ sender: Any) {
self.navigationController?.popViewController(animated: true)
self.dismiss(animated: true, completion: {
print("this should print")
})
}
Source Code
https://github.com/omenking/DismissCompletion
I also tried wrapping it in DispatchQueue.main.async but I had no luck.
Would you not be better off calling the function on the tap? Example:
func dismissController() {
self.navigationController?.popViewController(animated: true)
self.dismiss(animated: true, completion: nil)
}
And then on your button:
#IBAction func tapBack(_ sender: Any) {
dismissController()
print("this should print")
}
I've just downloaded your project and tried this and it prints.
First of all , I am assuming that since you are popping the view controller, you must have pushed it into the navigation Stack.
Secondly, if you did do it , then self.dismiss method will never get called because you are already popping the view controller before it.
If you want the completion block to work, you should present the ViewController instead of pushing it.Then, you can write your code in the completion block and it will execute.

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

Swift 3: make button present ViewController, how?

I would like my button to reshow the user my on boarding screen since it includes some important instructions to my app.
The first time I show my onboarding screen is after the first launch;
My current function looks like this:
#IBAction func anleitung(sender: RoundButtons) {
let appDelegate = AppDelegate()
appDelegate.window?.rootViewController?.present(OnboardingScreen(), animated: true, completion: nil)
}
This is actually like the 4th way I tried this, but, apparently, none of them worked. Do you know how to do that?
Thanks for having a look! Have a great day!
#IBAction func anleitung(sender: RoundButtons) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window?.rootViewController?.present(OnboardingScreen(), animated: true, completion: nil)
}

How to pass information back with view controllers

I have a view controller and one table View Controller. I go from VC One to VCTable. On VCTable, I select a(cell) data from type string that I store in value. When I press a cell, I would like to send that data back to VC One.
and show in button or label.
How to do this using Storyboards?
you should take a look at protocols / delegation:
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html
First Solution: Using CallBack
In your VCOne:
#IBAction func goToViewController2(sender: AnyObject) {
let vc2 = storyboard?.instantiateViewControllerWithIdentifier("ViewController2") as! ViewController2
vc2.callback = ({ string in
self.myString = string
})
presentViewController(vc2, animated: true, completion: nil)
}
in your VCTable:
create a callback variable:
var callback: ((String) -> Void)?
in your didSelectRowAtIndexPath method, send it to VCOne by:
callback?(textField.text!)
Second Solution: Using Reference
#IBAction func goToViewController2(sender: AnyObject) {
let vc2 = storyboard?.instantiateViewControllerWithIdentifier("ViewController2") as! ViewController2
vc2.vc1 = self
presentViewController(vc2, animated: true, completion: nil)
}
in your VCTable:
create this variable:
var vc1: ViewController?
in your didSelectRowAtIndexPath method, send it to VCOne by:
vc1?.myString = textField.text!
Third Solution: Using Delegate see the link as #Andre Slotta said.
FourthSolution: Using CenterNotification Googling for this :).
Hope this help :)

Resources