dimiss modal and return to presented childViewController in containerView - swift

I am having a bit of an issue with dismissing a modal view presented from a childviewController in a container view. I have a UINavigationController as the rootViewController (MainNavigationController), and present a modal from one of the childViewControllers from the selectedSegmentIndex 1 (secondViewController). The modal is presented fine, but when I dismiss the modal to go back to the secondViewController(a subclass of HomeController) it returns me back to selectedIndex 0, so not the selectedIndex 1 childViewController it was presented from. I would like the modal to dismiss and return the user back to childViewController it was presented from (the secondViewController) and not return back to selectedIndex 0. Thanks in advance!
// NavigationConroller as rootViewController
class MainNavigationController: UINavigationController {
var segmentedController: UISegmentedControl!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let vc1 = TravelersFeedVC()
let vc2 = ProfileVC()
if isLoggedIn() {
// assume user is logged in
let homeController = HomeController()
viewControllers = [homeController]
homeController.firstViewController = vc1
homeController.secondViewController = vc2
} else {
perform(#selector(showLoginController), with: nil, afterDelay: 0.01)
}
}
fileprivate func isLoggedIn() -> Bool {
return UserDefaults.standard.isLoggedIn()
}
func showLoginController() {
let loginController = LoginController()
present(loginController, animated: true, completion: {
// perhaps do something here later
})
}
}
// HomeController as parentViewController
class HomeController: UIViewController, FBSDKLoginButtonDelegate {
// child view controllers to put inside content view
var firstViewController: TravelersFeedVC?
var secondViewController: ProfileVC?
private var activeViewController: UIViewController? {
didSet {
removeInactiveViewController(inactiveViewController: oldValue)
updateActiveViewController()
}
}
private func removeInactiveViewController(inactiveViewController: UIViewController?) {
if let inActiveVC = inactiveViewController {
// call before removing child view controller's view from hierarchy
inActiveVC.willMove(toParentViewController: nil)
inActiveVC.view.removeFromSuperview()
// call after removing child view controller's view from hierarchy
inActiveVC.removeFromParentViewController()
}
}
private func updateActiveViewController() {
if let activeVC = activeViewController {
// call before adding child view controller's view as subview
addChildViewController(activeVC)
activeVC.view.frame = contentView.bounds
contentView.addSubview(activeVC.view)
// call before adding child view controller's view as subview
activeVC.didMove(toParentViewController: self)
}
}
// UI elements
lazy var contentView: UIView = {
let tv = UIView()
tv.backgroundColor = UIColor.purple
tv.translatesAutoresizingMaskIntoConstraints = false
tv.layer.masksToBounds = true
return tv
}()
var segmentedController: UISegmentedControl!
override func viewDidLoad() {
super.viewDidLoad()
activeViewController = firstViewController
checkIfUserIsLoggedIn()
view.addSubview(contentView)
setupProfileScreen()
let items = ["Travelers", "Me"]
segmentedController = UISegmentedControl(items: items)
navigationItem.titleView = segmentedController
segmentedController.tintColor = UIColor.black
segmentedController.selectedSegmentIndex = 0
// Add function to handle Value Changed events
segmentedController.addTarget(self, action: #selector(HomeController.segmentedValueChanged(_:)), for: .valueChanged)
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Sign Out", style: .plain, target: self, action: #selector(handleSignOut))
navigationItem.leftBarButtonItem?.tintColor = UIColor.black
}
// reference to collectionViewController
var travelersFeedVC: TravelersFeedVC!
func segmentedValueChanged(_ sender:UISegmentedControl!)
{
switch segmentedController.selectedSegmentIndex {
case 0:
activeViewController = firstViewController
case 1:
activeViewController = secondViewController
default: // Do nothing
break
}
}
// secondViewcontroller in containerView where modal is presented from
class ProfileVC: UIViewController {
// button to present modal
lazy var placesButton: UIButton = {
let customButton = UIButton(type: .system)
customButton.backgroundColor = UIColor.clear
// customButton.frame = CGRect(x: 150, y: 50, width: 120, height: self.view.frame.height)
customButton.setTitle("## of Places", for: .normal)
customButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
customButton.setTitleColor(.white, for: .normal)
customButton.addTarget(self, action: #selector(handleShowPlacesVC), for: .touchUpInside)
return customButton
}()
// function to call to present modal
func handleShowPlacesVC() {
let placesVC = PlacesTableVC()
let navigationController = UINavigationController(rootViewController: placesVC)
present(navigationController, animated: true, completion: nil)
}
// modal view to dismiss
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "back", style: .plain, target: self, action: #selector(handleCancel))
}
// dismiss modal view to return to secondViewController in childViewController containerView
func handleCancel() {
dismiss(animated: true, completion: nil)
}

When closing the modal dialog the viewDidAppear function in MainNavigationController is called. There you set a new homeController with it's childs. This will trigger a viewDidload in the HomeController with setting of firstViewController. Try to set a breakpoint there and you will see it.
I suggest to avoid content creation in viewDidAppear, use viewDidLoad instead.
Another hint: 'dismiss' is defined as: 'Dismisses the view controller that was presented modally by the view controller.' - If you open for instance an alert above your modal vc it closes the alert, not the modal view (self). A correct implementation has to call dismiss on the presenting controller (same controller that opened it): "presentingViewController?.dismiss()"
It works in your code because apple has implemented a fallback for the case that nothing is presented, but it's a trap that cause some headache sometime.

The chances are that although you're calling present from the child view controller, it isn't in fact handling the presentation. From the Apple docs:
The object on which you call this method may not always be the one that handles the presentation. Each presentation style has different rules governing its behavior. For example, a full-screen presentation must be made by a view controller that itself covers the entire screen. If the current view controller is unable to fulfill a request, it forwards the request up the view controller hierarchy to its nearest parent, which can then handle or forward the request.
Since you're keeping a reference of the active view controller, one solution may be to explicitly set the index upon dismissal.

Related

screen freezes if using modalPresentationStyle = .overCurrentContext swift 5

I am presenting a navigation controller with modalPresentationStyle as overCurrentContext. After dismissing controller screen freezes.
I am presenting a FirstViewController with NAvigationController.
let firstVC = FirstViewController.controller()
let nvc = UINavigationController(rootViewController: firstVC)
nvc.modalPresentationStyle = .overCurrentContext
present(nvc, animated: true)
Then inside FirstViewController, I am passing navigationController to push SecondViewController
override func viewDidLoad() {
super.viewDidLoad()
guard let nav = navigationController else { return }
showSecondViewController(parentController: nav)
}
func showSecondViewController(parentController: UINavigationController) {
let secondVC = SecondViewController.controller()
parentController.pushViewController(secondVC, animated: true)
}
Now first I am popping SecondViewcontroller on click action from SecondViewController
navigationController?.popViewController(animated: animated)
Then with some call back I am dismissing FirstViewController and NavigationControoler (nvc)
self.controller?.dismiss(animated: true)
self.nvc?.dismiss(animated: true)
Now after dismissing as above I am facing screen freeze issue.
I need help to resolve this issue. Please help. Why screen is freezing.
Please let me know if I am missing anything here?
Thanks
Did you see any errors in the console? I'm not clear about "some call back" as you mentioned above. Can you elaborate?
I created a small project to replicate your issue. The approach below works fine in my case
ViewController is the root view controller
FirstViewController is the first controller presented on top of ViewController
SecondViewController is pushed from the first view controller after the event "ViewDidLoad" happens in FirstViewController
I also created 1 onViewControllerDissmied callback in each ViewControllers (especially FirstViewController and SecondViewController)
In ViewController - I created a touch up inside event as below
#objc func onButtonClicked() {
let firstVC = FirstViewController()
nvc = UINavigationController(rootViewController: firstVC)
guard let nvc = nvc else { return }
nvc.modalPresentationStyle = .overCurrentContext
present(nvc, animated: true)
mycontroller = firstVC
firstVC.onViewControllerDimissed = { [weak self] in
self?.mycontroller?.dismiss(animated: true)
self?.nvc?.dismiss(animated: true)
}
}
FirstViewController
class FirstViewController: UIViewController {
var label: UILabel = {
let button = UILabel()
button.translatesAutoresizingMaskIntoConstraints = false
button.text = "First View Controller"
button.textColor = .white
return button
}()
var onViewControllerDimissed: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .purple
guard let nav = navigationController else { return }
showSecondViewController(parentController: nav)
}
func showSecondViewController(parentController: UINavigationController) {
let secondVC = SecondViewController()
parentController.pushViewController(secondVC, animated: true)
secondVC.onViewControllerDimissed = { [weak self] in
self?.onViewControllerDimissed?()
}
}
}
SecondViewController
class SecondViewController: UIViewController {
var label: UILabel = {
let button = UILabel()
button.translatesAutoresizingMaskIntoConstraints = false
button.text = "Second View Controller"
return button
}()
var onViewControllerDimissed: (() -> Void)?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: view)
let pnt: CGPoint = CGPoint(x: position.x, y: position.y)
if (view.bounds.contains(pnt)) {
onScreenTouch()
}
}
}
func onScreenTouch() {
navigationController?.popViewController(animated: true)
onViewControllerDimissed?()
}
}

Return from Hosting Controller to another controller from storyboard

I have a project with Storyboard but now I'm migrating to SwiftUI.
I have a Hosting Controller that control del SwiftUI segment, but now I need to return from this to another controller from Storyboard.
Basically I can do this: Storyboard (Controller 1) -> HostingController (SwiftUI).
So now y I need to return: HostingController (SwiftUI) -> Storyboard (Controller 2).
I like to use closures for this. I assume you are presenting the hosting controller like this?
let viewController = UIHostingController(rootView: ContentView())
self.present(viewController, animated: true)
You can add a closure dismissSelf, inside your SwiftUI View struct:
struct ContentView: View {
var dismissSelf: (() -> Void)?
var body: some View {
Button(action: {
dismissSelf?()
}) {
Text("Return")
}
}
}
This will call dismissSelf when the button is pressed. Now, you need to assign dismissSelf to a block of code that dismisses the UIHostingController. You can do it like this:
class ViewController: UIViewController {
#objc func buttonPressed() {
var viewController: UIViewController?
let contentView = ContentView {
/// set the closure (it's a trailing closure, so you don't need to put the `dismissSelf`)
viewController?.dismiss(animated: true, completion: nil)
}
viewController = UIHostingController(rootView: contentView)
if let vc = viewController {
self.present(vc, animated: true, completion: nil)
}
}
/// make the button
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 50, y: 50, width: 80, height: 40))
button.setTitle("Present", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
view.addSubview(button)
}
}
There's one thing that's kind of oof about this. viewController will never be nil, but to avoid a force unwrap I put in an if let.
Result:

How can I make sure that after declaring a delegate of a protocol in Swift 5.2, that delegate is not nil when it is called?

I am trying to clear a textfield in MainViewController from the DetailViewController. I have the following code in a Swift Playground.
import UIKit
import PlaygroundSupport
protocol DetailViewControllerDelegate: class {
func bar()
}
class DetailViewController: UIViewController {
var detailViewControllerDelegate: DetailViewControllerDelegate!
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let button = UIButton()
button.frame = CGRect(x: 100, y: 200, width: 180, height: 20)
button.setTitle("Hello World!", for: .normal)
button.backgroundColor = .blue
button.addTarget(self, action: #selector(handlePress), for: .touchUpInside)
view.addSubview(button)
self.view = view
}
#objc func handlePress() {
print("\(#function)")
if let vrvc = detailViewControllerDelegate {
vrvc.bar()
} else {
print("detailViewControllerDelegate is NIL")
}
}
}
class MainViewController : UIViewController, DetailViewControllerDelegate {
func bar() {
print("Inside Bar")
}
override func loadView() {
let detailViewController = DetailViewController()
detailViewController.detailViewControllerDelegate = self
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = DetailViewController()
When the button is pressed, detailViewController is nil. How can I make sure that detailViewController is NOT nil when the button is pressed?
You have two distinct DetailViewControllers:
override func loadView() {
let detailViewController = DetailViewController()
detailViewController now references a new instance of DetailViewController
detailViewController.detailViewControllerDelegate = self
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = DetailViewController()
liveView now references a new instance of DetailViewController which is different from the one above and in which you have not set detailViewControllerDelegate.
If you need to create a DetailViewController in one place and reference it in another you need to store a reference to it in a property that is accessible in both places.
That said, the second instance of DetailViewController is being created in a statement which looks like an attempt to test code in the Playground so maybe you just need to think about how you are testing.

UIPopoverPresentationController popover displays over the entire view

I am trying to create a rightBarButtonItem that appears throughout my app. When this barItem is clicked I want to show a modal popup using UIPopoverPresentationController. I have been able to get the button to show up on the barItem on all the views. However when i click on the button the xib takes over the entire view (including nav bar, see image below). Please see the class below:
class MyAppsNavigationController: UINavigationController, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.navigationBar.barTintColor = Colors.Red01.color()
self.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName : UIColor.white]
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
viewController.navigationItem.rightBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "Ellipsis"), style: .plain, target: self, action: #selector(displayMenu(sender:)))
}
func displayMenu(sender: UIBarButtonItem)
{
let filterVC = DropdownMenuController(nibName: "DropdownMenuController", bundle: nil)
let nav = UINavigationController(rootViewController: filterVC)
nav.modalPresentationStyle = UIModalPresentationStyle.popover
//nav.isNavigationBarHidden = true
nav.preferredContentSize = CGSize(width: 200, height: 300)
let popover = nav.popoverPresentationController! as UIPopoverPresentationController
popover.permittedArrowDirections = .up
popover.delegate = self
popover.barButtonItem = self.navigationItem.rightBarButtonItem
popover.sourceView = self.view;
var frame:CGRect = (sender.value(forKey: "view")! as AnyObject).frame
frame.origin.y = frame.origin.y+20
popover.sourceRect = frame
popover.delegate = self
self.present(nav, animated: true, completion: nil)
}
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
Result when clicked on the button:
When clicked the popup takes over entire view:
Any chance you're not using the right delegate method? I think this looks better:
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
Also, in this case sourceView and sourceRect is not needed: specifying a barButtonItem for the popover presentation controller is sufficient.
https://developer.apple.com/documentation/uikit/uipopoverpresentationcontroller/1622314-barbuttonitem

how to present a popover in custom cell in swift

For presenting popover I am following the below code.
func showPopOver() {
let secondStoryboard = UIStoryboard(name: "Second", bundle: nil)
viewObj = secondStoryboard.instantiateViewControllerWithIdentifier("ViewController") as! ViewController
viewObj.modalPresentationStyle = UIModalPresentationStyle.Popover
viewObj.preferredContentSize = CGSizeMake(400,500)
let popoverPresentationController = viewObj.popoverPresentationController
popoverPresentationController?.delegate = self
popoverPresentationController?.sourceView = self.view //walletButton
popoverPresentationController?.sourceRect = CGRectMake(0, button.frame.origin.y+100, 0, 0)
presentViewController(viewObj, animated: true, completion: nil)
}
//MARK:- UIPopoverPresentationControllerDelegate methods... starts
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle{
return UIModalPresentationStyle.None
}
func prepareForPopoverPresentation(popoverPresentationController: UIPopoverPresentationController) {
print("prepare for presentation")
}
func popoverPresentationControllerDidDismissPopover(popoverPresentationController: UIPopoverPresentationController) {
print("did dismiss")
}
func popoverPresentationControllerShouldDismissPopover(popoverPresentationController: UIPopoverPresentationController) -> Bool {
print("should dismiss")
return false
}
its working fine in normal view controller, but i need to show a popover view in custom tableview cell.
class flow like
|-UITableViewController
|-CustomCell:UITableViewCell
|-UIButton: button action-> here we need to show a popover with 3/4 frame value of the tableview frame.
whenever user tapped out of the popover view then dismiss the popover view.