Present a Modal from the View Model of a UITableViewController - ios

I have a UITableViewController, that as a ViewModel class. I am trying to build out my application using the MVVM pattern.
My tableView has a cell that display an image, that image has a gesture recogniser that calls a method in the view model on press.
At this point I would like to present a ViewController modally, with some embedded content.
However my TableView cell conforms to UITableViewCell so I cannot call present from here.
My ViewModel does not conform to anything, so I cannot call present from there either.
How can I trigger a modal to appear, from within a UITableViewCell?

You have couple of options but I will cover solution with delegate.
The idea is to define protocol and property of that protocol in MyViewModel and make MyViewController conforming to it.
Here is how the MyViewModel could look like:
protocol MyViewModelDelegate: class {
func didTapOnCell()
}
class MyViewModel {
// Please note the delegate is weak and optional
weak var delegate: MyViewModelDelegate?
// This function handle gesture recognizer taps
#objc func handleImageViewTap() {
delegate?.didTapOnCell()
}
// Here is the rest of the ViewModel class...
}
Then in the MyViewController you set viewModel's delegate property to self and conforms to the protocol function (I'm assuming view controller references the view model instance).
class MyViewController: UITableViewController {
func setup() {
// ...
// When MyViewModel is initialised, set the delegate property to self
myViewModel.delegate = self
}
}
extension MyViewController: ViewModelDelegate {
func didTapOnCell() {
// ...
// Allocate instance of anotherViewController here and present it
self.present(anotherViewController, animated: true, completion: .none)
}
}
This way you can let know MyViewController something happened in MyViewModel and act accordingly.
Please note it's necessary to make delegate property optional to avoid retain cycles.

Add a UIWindow extension
extension UIWindow {
static var top: UIViewController? {
get {
return topViewController()
}
}
static var root: UIViewController? {
get {
return UIApplication.shared.delegate?.window??.rootViewController
}
}
static func topViewController(from viewController: UIViewController? = UIWindow.root) -> UIViewController? {
if let tabBarViewController = viewController as? UITabBarController {
return topViewController(from: tabBarViewController.selectedViewController)
} else if let navigationController = viewController as? UINavigationController {
return topViewController(from: navigationController.visibleViewController)
} else if let presentedViewController = viewController?.presentedViewController {
return topViewController(from: presentedViewController)
} else {
return viewController
}
}
}
than call this from anywhere like:
guard let topController = UIWindow.top else { return } // UIWindow.root
let youVC = theStoryboard.instantiateViewController(withIdentifier: "YourViewController") as! YourViewController
youVC.modalTransitionStyle = .crossDissolve
youVC.modalPresentationStyle = .overCurrentContext
topController.present(youVC, animated: true, completion: nil)

Related

Why don't work about using delegate to set property value in Swift

I try to use delegate to reset my ViewControllerA (HomePage) property "type" value when I logout.
But I set breakpoint and my delegate function work success.
When I login again, and print my property "type" in ViewWillAppear. It's also cache old value before I logout.
Please tell me what's wrong with me.
Thanks.
class ViewControllerA: UIViewController, CustomDelegate {
enum Type: Int {
case book = 0
case pen
}
var tmpType: Type?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
printBBLog("tmpType: \(tmpType)") //before I logout the value is "pen",and I login again the value is "pen".
}
func clearType() {
printBBLog("clear")
self.tmpType = nil
printBBLog("\(self.tmpType)")
}
#objc func bookBtnClicked(sender: UIButton) {
self.tmpType = .book
}
#objc func penBtnClicked(sender: UIButton) {
self.tmpType = .pen
}
}
class ViewControllerB: UIViewController {
var delegate: CustomDelegate?
func doLogout() {
let vc = ViewControllerA()
self.delegate = vc
self.delegate?.clearType()
}
}
You are creating a new instance of your ViewControllerA. since you are using the UITabBarController you can access you ViewControllerA from your ViewControllerB and assign the delegate. after that you will get your desired result. for reference please check below code.
class ViewControllerB: UIViewController {
var delegate: CustomDelegate?
func doLogout() {
let viewControllers = self.tabBarController?.viewControllers
if let vc1 = viewControllers[0] as? ViewControllerA {
self.delegate = vc1
self.delegate?.clearType()
}
}
}
if you are using UINavigationController inside the UITabBarcontroller then use:
if let vc1 = ((self.tabBarController?.viewControllers?[0] as? UINavigationController)?.viewControllers[0] as? ViewControllerA)

How to pass data between two ViewController in UIPageViewController

I have two UIViewControllers, A and B, I connect them within a UIPageViewController:
Here is how it looks in the Storyboard:
I don't know how to pass data to B from A.
Well assume you have some class (which you should have provided) like:
class MyModel {
var dataFromFirstController: Any?
var dataFromSecondController: Any?
var sharedData: Any?
}
Now you need a subclass of page view controller which is the one that controls the data so override view did load to create a model:
var myModel: MyModel!
override func viewDidLoad() {
super.viewDidLoad()
self.myModel = MyModel()
}
Now when you generate or fetch view controllers you simply assign the same model to them:
func getFirstViewController() -> UIViewController {
let controller = MyFirstController.generate()
controller.myModel = self.myModel
return controller
}
func getSecondViewController() -> UIViewController {
let controller = MySecondController.generate()
controller.myModel = self.myModel
return controller
}
Now all 3 view controllers share the same model. This is probably the easiest way of doing it but there are very many ways. The cleanest is probably using delegates which would report back to page controller that would then report back to given view controllers.
I had some difficulty finding a solution to this and came up with something myself using delegation. Suggestions are welcome
In the ViewController sending the data, define a delegate as follows:
protocol FirstVCDelegate {
func foo(someData: String)
}
class FirstViewController: UIViewController {
var delegate: FirstVCDelegate?
....
func someMethod() {
delegate?.foo("first VC")
}
}
In the PageViewController set up your View Controllers as follows:
class PageViewController: UIPageViewController, ... {
var myViewControllers = [UIViewController]()
override func viewDidLoad() {
let firstVC = storyboard?.instantiateViewController(withIdentifier: "FirstViewController") as! FirstViewController
let secondVC = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
firstVC.delegate = secondVC
myViewControllers.append(firstVC)
myViewControllers.append(secondVC)
}
// MARK: - PageVC Delegate / Datasource
....
and finally, the receiving ViewController implements the delegate as follows:
class SecondViewController: UIViewController, FirstVCDelegate {
....
func foo(data: String) { // This method is triggered from FirstViewController's delegate?.foo("first VC")
print(data) // "first VC" will be printed
}
}
Good luck,
Aaron

Get the parentViewController instance

I have two classes: MyViewController: UIViewController and TheView: UIView. Inside the MyViewController i have declared and object var theView: TheView and a boolean flag like var flag: Bool = false. How do I get the flag value from inside TheView class without passing if from MyViewController? I have tried methods like if let vc = self.parentViewController as? MyViewController { } but unsuccessfully.
The fact that it's difficult to get a reference to a view's associated controller should be a hint that it's bad practice. You would be better off passing the parent view controller into the view's initialiser and storing it as a weak reference.
class TheView: UIView {
weak var parentViewController: MyViewController?
init(parentViewController: MyViewController) {
self.parentViewController = parentViewController
}
}
// in MyViewController...
let view = TheView(parentViewController: self)
The best way is to declare a variable in TheView that references the parent and set this when you instantiate TheView:
var parentController: MyViewController?
Then when you instantiate theView you can attach the parent:
// Instantiate your view either view storyboard or code and then:
self.theView.parentController = self
get parentViewController from view then use this extension.
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
Get a UIView parentViewController like that.
class TheView: UIView {
func teset(){
if let vc = self.parentViewController as? ViewController {
//Sccess block
}
}
}
your ParentViewController like.
class ViewController: UITableViewController {
var theView: TheView = TheView()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(theView)
}

Attempt to present vc whose view is not in the window hierarchy [duplicate]

This question already has answers here:
Attempt to present * on * whose view is not in the window hierarchy
(7 answers)
Closed 5 years ago.
I'm trying to open file in thread and here is my code:
DispatchQueue.main.async(execute: { () -> Void in
var documentsURL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)).appendPathComponent(“File.pdf")
self.docController = UIDocumentInteractionController.init(url: documentsURL as URL)
self.docController?.delegate = self as? UIDocumentInteractionControllerDelegate
self.docController?.presentPreview(animated: true)
self.docController?.presentOpenInMenu(from: CGRect.zero, in: self.view, animated: true)
})
when move to main screen this warning is displayed and file not open
Warning: Attempt to present <_UIDocumentActivityViewController: 0x...> on <HCM.PrintVacationDecisionVC: 0x...> whose view is not in the window hierarchy!
Any help to solve this problem?
Add extention given bellow to your application and use it any where you want to present any view controller, it works for me hope it helps you.
//MARK: - UIApplication Extension
extension UIApplication {
class func topViewController(viewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = viewController as? UINavigationController {
return topViewController(viewController: nav.visibleViewController)
}
if let tab = viewController as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(viewController: selected)
}
}
if let presented = viewController?.presentedViewController {
return topViewController(viewController: presented)
}
return viewController
}
}
And Present it by following code:
UIApplication.topViewController()?.present(vc, animated: true, completion: nil)
If you are attempting to present a modal view controller within the viewDidLoad method, can try to move this call to the viewDidAppear: method.
You need to find top view controller
From
https://stackoverflow.com/a/26859650/4601900
extension UIViewController {
func topMostViewController() -> UIViewController {
// Handling Modal views
if let presentedViewController = self.presentedViewController {
return presentedViewController.topMostViewController()
}
// Handling UIViewController's added as subviews to some other views.
else {
for view in self.view.subviews
{
// Key property which most of us are unaware of / rarely use.
if let subViewController = view.nextResponder() {
if subViewController is UIViewController {
let viewController = subViewController as UIViewController
return viewController.topMostViewController()
}
}
}
return self
}
}
}
extension UITabBarController {
override func topMostViewController() -> UIViewController {
return self.selectedViewController!.topMostViewController()
}
}
extension UINavigationController {
override func topMostViewController() -> UIViewController {
return self.visibleViewController.topMostViewController()
}
}
How to use
UIApplication.sharedApplication().keyWindow!.rootViewController!.topMostViewController()

How to send data back by popViewControllerAnimated for Swift?

I need to send some data back from secondView to First View by popView.
How can i send back the data by popViewControllerAnimated?
Thanks!
You can pass data back using delegate
Create protocol in ChildViewController
Create delegate variable in ChildViewController
Extend ChildViewController protocol in MainViewController
Give reference to ChildViewController of MainViewController when navigate
Define delegate Method in MainViewController
Then you can call delegate method from ChildViewController
Example
In ChildViewController: Write code below...
protocol ChildViewControllerDelegate
{
func childViewControllerResponse(parameter)
}
class ChildViewController:UIViewController
{
var delegate: ChildViewControllerDelegate?
....
}
In MainViewController
// extend `delegate`
class MainViewController:UIViewController,ChildViewControllerDelegate
{
// Define Delegate Method
func childViewControllerResponse(parameter)
{
.... // self.parameter = parameter
}
}
There are two options:
A) with Segue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
{
let goNext = segue.destinationViewController as ChildViewController
goNext.delegate = self
}
B) without Segue
let goNext = storyboard?.instantiateViewControllerWithIdentifier("childView") as ChildViewController
goNext.delegate = self
self.navigationController?.pushViewController(goNext, animated: true)
Method Call
self.delegate?.childViewControllerResponse(parameter)
If you want to send data by popping, you'd do something like:
func goToFirstViewController() {
let a = self.navigationController.viewControllers[0] as A
a.data = "data"
self.navigationController.popToRootViewControllerAnimated(true)
}
Extending Dheeraj's answer in case your ViewController is not first VC in the stack, here is the solution:
func popViewController() {
guard let myVC = self.navigationController?.viewControllers.first({ $0 is MyViewController }) else { return }
myVC.data = "data"
self.navigationController?.popViewController(animated: true)
}
However, this solution will break if you have 2 or more than 2 MyViewController in the stack. So, use wisely.
Answer given here is a little complex, quite simple to just use UINavigationControllerDelegate
class FirstNavigationController: UIViewController {
var value: String?
}
class SecondNavigationController: UIViewController, UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
guard let vc = navigationController.topViewController as? FirstNavigationController else { return }
vc.value = "Hello again"
}
}
self.navigationController?.popViewController(animated: true)
let vc = self.navigationController?.viewControllers.last as! MainViewController
vc.textfield.text = "test"
this popviewcontroller solutions

Resources