NotificationCenter observer clearing after presenting modal - ios

My app is playing a video and I want to trigger an action when the video ends. The screen (A) is embedded in a navigation controller, and if I trigger a push (to B) and then come back (to A), the action (in A) still takes place based on the observer. There is also an option in my screen that triggers a modal (to C) which then gets dismissed to go back (to A). When I come back from the modal (C), however, the observer (in A) is gone.
Here's my code for screen A's view controller:
ScreenAViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do more stuff
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.avPlayer.currentItem, queue: .main) { _ in
// Do stuff
}
}
}
Here's the code that trigger the modal to screen C:
#IBAction func triggerModal(_ sender: UIButton) {
avPlayer.pause()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let screenCViewController = storyboard.instantiateViewController(withIdentifier: "ScreenC") as! ScreenCViewController
present(screenCViewController, animated: true)
}
And finally here's the line that dismisses screen C:
dismiss(animated: true, completion: nil)

addObserver returns an observer object. You are ignoring this and not retaining it. Therefore it goes out of scope and dies and the observation ends.

Related

If I present a ViewController programmatically without using a Navigation Controller, does the new VC "replace" the old one, or does it stack on top?

I'm new to iOS.
I have an app where the path through the app can vary depending on the configuration I fetch from an API. Because of this, I don't use segues because I would need to create a segue from each ViewController (VC) to EVERY other VC. It creates a mess of segues that I don't want. So Instead I navigate from screen to screen like this:
func navigate(to viewController: String) {
let storyboard = UIStoryboard(name: K.mainStoryBoard, bundle: nil)
let nextVC = storyboard.instantiateViewController(identifier: viewController)
self.present(nextVC, animated: true, completion: nil)
}
My question is this: If I would have embedded my VCs in a NavigationController I know it would have created a stack. When I get to the last screen I would call func popToRootViewController(animated: Bool) -> [UIViewController]? and start from the beginning. However, I don't use a NavigationController. So, does that mean that when I present the next VC it replaces the previous one or does it stack on top of the previous one? I'm trying to prevent memory leaks so I want to make sure I don't keep stacking the VCs on top of each other until my app runs out of memory and crashes.
Thanks in advance
Edit
So, in my final VC I created an unwind segue. And I call it like this: performSegue(withIdentifier: "unwindToMain", sender: self)
and In my first VC (the initial VC in my app) I write this:
#IBAction func unwind( _ seg: UIStoryboardSegue) {
}
Everything works fine the first trip through the app. The last VC unwinds back to the fist VC. The problem is now that when I try to run through the app again (starting from VC 1 and then going to the next one) I now get this error:
MyApp[71199:4203602] [Presentation] Attempt to present <MyApp.DOBViewController: 0x1038760c0> on <MyApp.ThankYouViewController: 0x112560c30> (from <MyApp.ThankYouViewController: 0x112560c30>) whose view is not in the window hierarchy.
To make sense of this, DOBViewController would be the second VC I want to go to from the MainVC. ThankYouViewController is my last VC. It looks as if it isn't completely removed from the stack. Can anyone tell me what's going on?
Here is a very simple, basic example...
The controllers are setup in Storyboard, each with a single button, connected to the corresponding #IBAction.
The DOBViewController has its Storyboard ID set to "dobVC".
The ThankYouViewController has its Storyboard ID set to "tyVC".
MainVC is embedded in a navigation controller (in Storyboard) and the navigation controller is set to Initial View Controller:
class MainVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.setNavigationBarHidden(true, animated: false)
}
#IBAction func pushToDOB(_ sender: Any) {
if let vc = storyboard?.instantiateViewController(withIdentifier: "dobVC") as? DOBViewController {
navigationController?.pushViewController(vc, animated: true)
}
}
}
class DOBViewController: UIViewController {
#IBAction func pushToTY(_ sender: Any) {
if let vc = storyboard?.instantiateViewController(withIdentifier: "tyVC") as? ThankYouViewController {
navigationController?.pushViewController(vc, animated: true)
}
}
}
class ThankYouViewController: UIViewController {
#IBAction func popToRoot(_ sender: Any) {
navigationController?.popToRootViewController(animated: true)
}
}
does that mean that when I present the next VC it replaces the previous one or does it stack on top of the previous one?
The new one stacks on top of the previous one.
When you present a view controller, like
self.present(nextVC, animated: true, completion: nil)
The one you called .present on (self in this case) becomes presentingViewController for the nextVC instance.
The one you presented (nextVC in this case) becomes presentedViewController for the self instance.

Swift tableView.reloadData() not working inside completion handler

I have two view controllers, in controller 1 I have a table view and I am presenting the controller 2 like so:
#IBAction func addPOItemButtonPressed(_ sender: Any) {
let itemViewController = self.storyboard?.instantiateViewController(withIdentifier: "LPOsItemController") as! LPOsItemController
itemViewController.RequestID = self.RequestID
let navigationViewController = UINavigationController(rootViewController: itemViewController)
self.present(navigationViewController, animated: true, completion: nil)
}
When I dismiss controller 2 I am calling a method in controller 1 like so:
In Controller 1:
NotificationCenter.default.addObserver(self, selector: #selector(self.refreshDetails(notification:)), name:NSNotification.Name(rawValue: "refreshDetails"), object: nil)
#objc func refreshDetails(notification: NSNotification){
spLPOHeaderGet() { result in
self.tableView.reloadData()
}
}
In Controller 2
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "refreshDetails"), object: nil)
dismiss(animated: true, completion: nil)
My problem is when I call the method in Controller 1, the table view does not reload the data, sometimes it does and sometimes it doesn't. The reload call is in a completion handler. Can someone tell me what I am doing wrong?
Thanks,
Posting to notification for this situation is not a good idea. You can use the delegate to communicate from ViewController 2 to controller 1.
if you want to identify an issue in the current implementation try to put a breakpoint on
self.tableView.reloadData()
and check this method is called. and if it is called does tableview is in memory when this called. There is a chance when the completion handler is called this tableView object is deallocted.

Dismiss two UIViewController at once without animation

I have a stack of UIViewControllers like A -> B -> C. I want to go back to controller A from C. I'm doing it with below code:
DispatchQueue.global(qos: .background).sync {
// Background Thread
DispatchQueue.main.async {
self.presentingViewController?.presentingViewController?.dismiss(animated: false, completion: {
})}
}
It works but controller B seen on screen although I set animated to false. How can I dismiss two UIViewControllers without showing the middle one (B)?
P.S: I can't just directly dismiss from root controller and also I can't use UINavigationController
I searched the community but can't find anything about the animation.
Dismiss more than one view controller simultaneously
Try this.
self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)
Created a sample storyboard like this
The yellow view controller is type of ViewController and the button action is as follows
#IBAction func Pressed(_ sender: Any) {
self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)
}
Output
I've created example for dismissing B controller before showing C controller. You can try it.
let bController = ViewController()
let cController = ViewController()
aController.present(bController, animated: true) {
DispatchQueue.main.asyncAfter(wallDeadline: .now()+2, execute: {
let presentingVC = bController.presentingViewController
bController.dismiss(animated: false, completion: {
presentingVC?.present(cController, animated: true, completion: nil)
})
})
}
But on my opinion solution with using navigation controller would be the best for the case. For example you can put just B controller into navigation controller -> present the navController onto A controller -> then show C inside the navController -> then dismiss from C controller whole navController -> And you will see A controller again. Think about the solution too.
Another solution
I've checked another solution.
Here extension which should solve your problem.
extension UIViewController {
func dissmissViewController(toViewController: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
self.dismiss(animated: flag, completion: completion)
self.view.window?.insertSubview(toViewController.view, at: 0)
dissmissAllPresentedControllers(from: toViewController)
if toViewController.presentedViewController != self {
toViewController.presentedViewController?.dismiss(animated: false, completion: nil)
}
}
private func dissmissAllPresentedControllers(from rootController: UIViewController) {
if let controller = rootController.presentedViewController, controller != self {
controller.view.isHidden = true
dissmissAllPresentedControllers(from: controller)
}
}
}
Usage
let rootController = self.presentingViewController!.presentingViewController! //Pointer to controller which should be shown after you dismiss current controller
self.dissmissViewController(toViewController: rootController, animated: true)
// All previous controllers will be dismissed too,
// but you will not see them because I hide them and add to window of current view.
But the solution I think may not cover all your cases. And potentially there can be a problem if your controllers are not shown on whole screen, all something like that, because when I simulate that transition I don't consider the fact, so you need to fit the extension maybe to your particular case.

UIPageViewController stacking ViewControllers when segueing to home page

I have two storyboards, a main then a second storyboard which houses a UIPageViewController. Inside the second storyboard, an exit button allows a user to return to the home screen which is on the main storyboard.
When I attempt to segue back to the home screen, the page controller views are stacking. I have tried to use:
self.navigationController?.popViewController
Along with some other methods to deallocate the UIPageViewController sets. Nothing seems to work? How do I fix this?
You can see in the image below where the controllers are marked by (3).
Memory Graph Image
Below are a few of my attempts. I am using a notification to invoke a method on the root controller after the user confirms via a yes/no custom modal.
func attempt1() {
self.viewControllerList.forEach {
index in
index.removeFromParentViewController()
index.dismiss(animated: false, completion: nil)
index.navigationController?.popViewController(animated: false)
}
self.performSegue(withIdentifier: "unwindHome", sender: self)
}
func attempt2() {
self.childViewControllers.forEach {
c in
print("CHILD...>", c)
c.removeFromParentViewController()
c.dismiss(animated: false, completion: nil)
}
}
func attempt3() {
(view.window?.rootViewController as? UIPageViewController)?.dismiss(animated: true, completion: nil)
self.dismiss(animated: false, completion: {
self.viewControllerList.forEach {
i in
i.navigationController?.popViewController(animated: true)
}
})
self.performSegue(withIdentifier: "unwindHome", sender: self)
}
Here were my issues:
On the first storyboard, my nav controller was not set to initial.
After setting it to initial, i then linked from the main SB to the second SB
Delegates var's were not set to weak
RxSwift was not being disposed of properly.
I was at first doing pub/sub via Variables as a Global. When moving the observer into the root of my UIPageViewController, upon page breakdown the observers were disposed of causing the onComplete callback to hit.

How to move through Views without reload them again if I return back Swift

I'll give an example of what I want so it's not so confusing:
Example:
Let's say that I have a map that adds every time that my user scrolls 3 annotations dynamically. Now I have a button under the map and when I press it I go to another viewController do what I want and get back to the viewController with the map, now I want to find all the annotations that my map had and not reload the view at all.
I used to use this function that I made to move between viewControllers:
func move(identifier: String , viewController : UIViewController) -> Void {
let mstoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let vc: UIViewController = mstoryboard.instantiateViewControllerWithIdentifier(identifier)
viewController.presentViewController(vc, animated: true, completion: nil)
}
I also tried this:
let vc = self.storyboard?.instantiateViewControllerWithIdentifier("view") as? MyViewcontroller
self.presentViewController(vc!, animated: true, completion: nil)
These two when I use them the viewcontroller that appears is calling viewDidload so its like it appeared for the first time.
Another example is the tabBarViewController if you notice when you navigate through tabs nothing reloads (only function that is called is viewDidAppear )
EDIT
test file
The problem is caused by the fact that the map controller gets deallocated when navigating back to the other controller, and another one is created when you want to move again to the map screen.
What you need is to hold on onto the same controller instance, and present that one. Keeping a strong reference in the presenting controller would suffice.
class PresentingController {
// making the property lazy will result in the getter code
// being executed only when asked the first time
lazy var mapController = { () -> UIViewController in
let mstoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
return mstoryboard.instantiateViewControllerWithIdentifier("mapControllerIdentifier")
}()
func moveToMap() {
// simply use the mapController property
// the property reference will make sure the controller won't
// get deallocated, so every time you navigate to that screen
// you'll get the same controller
presentViewController(mapController, animated: true, completion: nil)
}
}
According to the same project you posted, you instantiate a new UIViewController when going from view 2 back to view 1 and that is why your viewDidLoad gets called again and your entire map view is reloaded.
In your sample project, instead of
lazy var mapController2 = { () -> UIViewController in
let mstoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
return mstoryboard.instantiateViewController(withIdentifier: "first")
}
You should just dismiss your view 2 on the button press.
#IBAction func butto(_ sender: AnyObject) {
//Your initial code
//PresentingController().moveToMap(self, flag: 1)
self.dismiss(animated: true, completion: nil)
}
When you present a new UIViewController, the older UIViewController is not removed from memory, it is just hidden behind the new UIViewController. So whenever you wish to go back to a UIViewController with the previous state maintained, all you need to do is close the new UIViewController
However, if you are doing some tasks that you performed on your second UIViewController that you wish to be reflected in your initial UIViewController, you will have to setup closures to update your initial UIViewController.

Resources