UIHostingController and navigationController - swift

Default swift project with view controller embedded in navigationController and pushing to next UIHostingController.
How to call navigationController?.popViewController from SimpleView ?
ViewController:
#IBOutlet weak var button: UIButton?
override func viewDidLoad() {
super.viewDidLoad()
button?.addTarget(self,
action: #selector(showNewSwiftUIController),
for: .touchUpInside)
}
#objc func showNewSwiftUIController() {
let vc = UIHostingController(rootView:SimpleView())
navigationController?.pushViewController(vc, animated: true)
}
SwiftUI:
struct SimpleView: View {
var body: some View {
Text("SimpleView")
.foregroundColor(.black)
}
}

First.
Have you tried?
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> // reference
Then on your body call:
self.presentationMode.wrappedValue.dismiss()
If that does not work:
I will suggest you either pass the navigation controller as an optional variable into the View as a reference and access from there.
Other option and maybe a healthier option is to pass a "callback" variable from the parent to the childView to dismiss the view (you need to save the Navigation as a variable to keep track)
Last option (Not recommended)
if var topController = UIApplication.shared.windows.first?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
topController.navigationController.popViewController(animated:true)
}

Related

How to HIDE View when tap on button swift?

In first view controller we have two buttons
if we tap on first view controller oneButn i need to hide onebutnContainerView in secondview controller
if we tap on first view controller secndButn i need to hide twobutnContainerView in secondview controller
in first view controller viewController.oneButnContainerView.isHidden = true getting error:
Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
first view controller code:
class firstViewController: UIViewController{
#IBAction func oneButn(_ sender: UIButton) {
self.view.endEditing(true)
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "NewZoomAddressViewController") as! NewZoomAddressViewController;
viewController.delegate = self
viewController.oneButnContainerView.isHidden = true
viewController.twobutnContainerView.isHidden = false
self.navigationController?.pushViewController(viewController, animated: true);
}
#IBAction func secndButn(_ sender: UIButton) {
self.view.endEditing(true)
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "NewZoomAddressViewController") as! NewZoomAddressViewController;
viewController.delegate = self
viewController.oneButnContainerView.isHidden = false
viewController.twobutnContainerView.isHidden = true
self.navigationController?.pushViewController(viewController, animated: true);
}
}
I have outlets for two views in Second view controller
#IBOutlet weak var oneButnContainerView: UIView!
#IBOutlet weak var twoButnContainerView: UIView!
how to hide seconviewcontroller view in firstviewcontroller
That's because you are trying to hide a View that wasn't initialized yet. As a rule of thumb, keep in mind that when you instantiate a viewController, you can only access its data not its views. You have 2 ways of fixing this:
Create 2 variables inside secondViewController:
var isOneButnContainerViewHidden: Bool = false
var isTwoButnContainerViewHidden: Bool = false
Assign a value to those 2 variables inside firstViewController:
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "NewZoomAddressViewController") as! NewZoomAddressViewController;
viewController.delegate = self
viewController.isOneButnContainerViewHidden= false
viewController.isTwoButnContainerViewHidden= true
self.navigationController?.pushViewController(viewController, animated: true);
Now inside your secondViewController's viewDidLoad or viewWillAppear, hide/show your buttonContainerViews based on the value of the 2 variables created:
oneButnContainerView.isHidden = isOneButnContainerViewHidden
twoButnContainerView.isHidden = isTwoButnContainerViewHidden
The second way involves forcing the viewController to layout its views by calling loadViewIfNeeded() on the secondViewController before accessing its views (in this case you are trying to hide/show the views).

How to pass data of one label(mainStoryBoard) to another label(of second storyboard)

I have a label1 in a view controller of main.storyboard
i have another label2 in view controller of storyboard named Second.storyboard
how can I pass data from one storyboard to another all solutions I can find are about passing data within the same storyboard.Help me with my question.
make a Global variable in Second View Controller
var labelText = ""
While Pushing to secondViewController from First Controller
let secondVC = secondViewController()
secondVC.labelText = label.text // Pass label text from first VC
self.navigationController?.pushViewController(secondVC, animated: true)
Create the instance of Storyboard with the relevant name while creating the instance of SecondViewController in FirsViewController, i.e
class FirsViewController: UIViewController {
#IBOutlet weak var label1: UILabel!
func pushVC2(_ sender: UIButton) {
if let controller = UIStoryboard(name: "Storyboard-2", bundle: nil).instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController { //here....
controller.str = "Your_Text_Here"
self.navigationController?.pushViewController(controller, animated: true)
}
}
}
Your SecondViewController looks like,
class SecondViewController: UIViewController {
#IBOutlet weak var label2: UILabel!
var str: String?
override func viewDidLoad() {
super.viewDidLoad()
self.label2.text = str
}
}

Is it possible to modify the properties of a subclass from a parent class in Swift 4?

Via a method or closure, perhaps?
I created a subclass view controller of my superclass/parent view controller and placed labels with placeholder text in that subclass view controller.
I want to set the labels' values to blank strings from the superclass/parent view controller, or, specifically, from an IBAction function that causes the subclass view controller to appear.
Here is the code, first from the parent class, then from the subclass...
'''
class ViewController: UIViewController {
#IBAction func leavingView(){
self.EntryViewController.entryDateLabel.text = ""
self.EntryViewController.entryLabel.text = ""
self.dismiss(animated: true, completion: nil)
}
'''
then from the subclass...
'''
class EntryViewController: ViewController {
#IBOutlet var entryDateLabel: UILabel!
#IBOutlet var entryLabel: UILabel!
}
'''
I have come up with 2 solutions to this problem, without having the parent view controller know about its subclass.
In the first example the parent sets properties on itself that the child listens to (via the didSet method, it then updates its view accordingly. However, this isn't ideal because the entryDate and entry string fields are useless on their own, almost redundant in the parent.
class ParentViewController: UIViewController {
var entryDate: String?
var entry: String?
#IBAction func leavingView(){
self.entryDate = ""
self.entry = ""
self.dismiss(animated: true, completion: nil)
}
}
class ChildViewController: ParentViewController {
#IBOutlet var entryDateLabel: UILabel!
#IBOutlet var entryLabel: UILabel!
override var entryDate: String? {
didSet {
guard isViewLoaded else {
return
}
entryDateLabel.text = entryDate
}
}
override var entry: String? {
didSet {
guard isViewLoaded else {
return
}
entryLabel.text = entry
}
}
}
In my opinion, the second solution is clearer and keeps implementation details more separate because you're using instructions or events to notify the child view controllers.
class ParentViewController: UIViewController {
#IBAction func leavingView(){
self.dismiss(animated: true, completion: didLeaveView)
}
func didLeaveView() { }
}
class ChildViewController: ParentViewController {
#IBOutlet var entryDateLabel: UILabel!
#IBOutlet var entryLabel: UILabel!
override func didLeaveView() {
entryDateLabel.text = ""
entryLabel.text = ""
}
}
Since your requirement is not that much clear I have created a demo for you and into that demo I have added child ContainerViewController into parent ViewController and from that parent view controller you can change UILabel text when you click on UIButton of parent ViewController and code will be for ViewController
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func btnFromParentViewTapped(_ sender: Any) {
//Here get the child of your parent view controller
if let containerView = self.children[0] as? ContainerViewController {
containerView.lblContainer.text = ""
}
}
}
and ContainerViewController code will be:
class ContainerViewController: UIViewController {
#IBOutlet weak var lblContainer: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
Don't need to add much here because you are accessing it from parent view.
And your result will be:
As you can see when I click on button which title says Change Container label text the label text from ContainerViewController set to empty string.
For more info check THIS demo project.

Delegate method is not being called

I have a splitview controller and the Master view has a button (Note) that pushes the Note view onto the Detail view. The Detail view makes used of navigation controller to help users navigate back and forth among multiple view controllers. Inside those view controllers, I have delegate methods that pushes Note view onto itself. This is what my UI look like :
The Note button works as I expected when the app is initially run. It still works when I tap one of the list elments and traverse to the views at the deeper level. However, it stops working when I go back to the very first view (which was working initially). I'm not sure what is causing this inconsistent behaviour and I appreciate much if you guys could help me figure this out.
This is excerpt of my code :
Master View
protocol ChildViewDelegate: class {
func updateView()
func pushOntoDetailViewNaviController(_ viewName: String)
}
class MasterViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate, GADBannerViewDelegate {
....
#IBAction func noteTapped(_ sender: UIBarButtonItem) {
pushOntoActiveNaviController("NoteGalleryView")
}
private func pushOntoActiveNaviController(_ viewName: String) {
guard let splitView = self.splitViewController else {
return
}
if splitView.viewControllers.count > 1 {
// Push view onto any active detailed view
self.delegate?.pushOntoDetailViewNaviController(viewName)
} else { //If active view is the master view (true for iphone), then push it onto the master view
if let vc = storyboard?.instantiateViewController(withIdentifier: viewName) {
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
....
}
Detail View
class DetailTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, GADBannerViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
masterViewController = (self.splitViewController?.viewControllers.first as! UINavigationController).topViewController as? MasterViewController
masterViewController?.delegate = self
}
extension DetailTableViewController: ChildViewDelegate {
func updateUI() {
....
}
func updateView() {
DispatchQueue.main.async {
self.tableView.reloadData()
self.updateUI()
}
}
func pushOntoDetailViewNaviController(_ viewName: String) {
if let vc = storyboard?.instantiateViewController(withIdentifier: viewName) {
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
In case somebody has the same issue, it's because I'm setting delegate variable inside viewDidLoad which is invoked when view is initially loaded. It isn't invoked when view appears again when user navigates back to the view from deeper view controllers. The delegate variable should have been set inside viewWillAppear.

dimiss modal and return to presented childViewController in containerView

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.