present a ViewController from a Swift class derived from an NSObject? - ios

This project was written in Objective C and a bridging header and Swift files were added so they can be used at run time. When the app starts, Initializer called in Mock API client is printed in the debugger. Is it possible to present a ViewController from the initializer?
Xcode Error:
Value of type 'MockApiClient' has no member 'present'
//MockApiclient.Swift
import Foundation
class MockApiClient: NSObject
{
override init ()
{
print("Initializer called in Mock API client")
if isLevelOneCompleted == false
{
print("It's false")
let yourVC = ViewController()
self.present(yourVC, animated: true, completion: nil)
} else
{
print("It's true")
}
}
var isLevelOneCompleted = false
#objc func executeRequest()
{
print("The execute request has been called")
isLevelOneCompleted = true
if isLevelOneCompleted {
print("It's true")
} else {
//do this
}
}
}
Update - ViewController.m
// prints "The execute request has been called" from the debugger window
- (void)viewDidLoad {
[super viewDidLoad];
MockApiClient *client = [MockApiClient new];
[client executeRequest];
}

You can't call present(_:animated:completion) because it is a method of UIViewController, not NSObject.
Why not pass a viewController reference to the MockApiClient to present on instead like so. Be sure to check Leaks or Allocations on instruments to avoid the client retaining the controller.
class MockApiClient: NSObject {
var referencedViewController: UIViewController?
override init() {
let presentableViewController = ViewController()
referencedViewController.present(presentableViewController, animated: true, completion: nil)
}
deinit {
referencedViewController = nil
}
}
let apiClient = MockApiClient()
apiClient.referencedViewController = // The view controller you want to present on

Assuming you're using UIKit, you'll have to present the view controller from the nearest available attached view controller. If you know for certain that no other view controllers would currently be presented then you can safely present from the root view controller:
UIApplication.shared.keyWindow?.rootViewController?.present(someViewController, animated: true, completion: nil)
This concept of attached and unattached/detached view controllers is never officially explained but the infamous UIKit warning of presenting view controllers on detached view controllers is real. And the workaround is finding the nearest available attached view controller, which at first (when nothing is currently being presented) is the root view controller (of the window). To then present an additional view controller (while one is currently being presented), you'd have to present from that presented view controller or its nearest parent view controller if it has children (i.e. if you presented a navigation view controller).
If you subclass UIViewController, you can add this functionality into it to make life easier:
class CustomViewController: UIViewController {
var nearestAvailablePresenter: UIViewController {
if appDelegate.rootViewController.presentedViewController == nil {
return appDelegate.rootViewController
} else if let parent = parent {
return parent
} else {
return self
}
}
}
Then when you wish to present, you can simply do it through this computed property:
nearestAvailablePresenter.present(someViewController, animated: true, completion: nil)

Related

iOS New-Contact-Style Segue in Swift

I am trying to emulate the iOS contacts segueing between two view controllers.
I have a simple Person class given by:
class Person {
var name = ""
}
and a UIViewController that contains an array of Person, which is embedded in a UINavigationController:
class PeopleViewController: UIViewController {
var people = [Person]()
var selectedPerson: Person?
switch segueIdentifier(for: segue) {
case .showPerson:
guard let vc = segue.destination as? PersonViewController else { fatalError("!") }
vc.person = selectedPerson
}
}
This controller uses a Show segue to PersonViewController to display the selectedPerson:
class PersonViewController: UIViewController {
var person: Person!
}
PeopleViewController can also add a new Person to the array of Person. The NewPersonViewController is presented modally, however:
class NewPersonViewController: UIViewController {
var person: Person?
}
If a new Person is added, I want NewPersonViewController to dismiss but show the new Person in the PersonViewController that is part of the navigation stack. My best guess for doing this is:
extension NewPersonViewController {
func addNewPerson() {
weak var pvc = self.presentingViewController as! UINavigationController
if let cvc = pvc?.childViewControllers.first as? PeopleViewController {
self.dismiss(animated: false, completion: {
cvc.selectedPerson = self.person
cvc.performSegue(withIdentifier: .showPerson, sender: nil)
}
}
}
}
However, (1) I'm not too happy about forcing the downcast to UINavigationController as I would have expected self.presentingViewController to be of type PeopleViewController? And (2), is there a memory leak in the closure as I've used weak var pvc = self.presentingViewController for pvc but not for cvc? Or, finally (3) is there a better way of doing this?
Many thanks for any help, suggestions etc.
(1) I'm not too happy about forcing the downcast to UINavigationController as I would have expected self.presentingViewController to be of type PeopleViewController?
There is nothing wrong in downcasting. I would definitely remove force unwrapping.
(2), is there a memory leak in the closure as I've used weak var pvc = self.presentingViewController for pvc but not for cvc?
I think, there is none.
(3) is there a better way of doing this?
You can present newly added contact from NewContactVC. When you about to dismiss, call dismiss on presentingVC.
// NewPersonViewController.swift
func addNewPerson() {
// New person is added
// Show PeopleViewController modally
}
Note: Using presentingViewController this way will dismiss top two/one Modal(s). You will see only top view controller getting dismissed.
If you can't determine how many modals going to be, you should look-into different solution or possibly redesigning navigation flow.
// PeopleViewController.swift
func dismiss() {
if let presentingVC = self.presentingViewController?.presentingViewController {
presentingVC.dismiss(animated: true, completion: nil)
} else {
self.dismiss(animated: true, completion: nil)
}
}

How to push user to ViewController from non UIView class [duplicate]

This question already has answers here:
How to launch a ViewController from a Non ViewController class?
(7 answers)
Closed 5 years ago.
I would like to know how can I push user back to specific ViewController from regular swift class without being non UIView Class
Example
class nonUI {
function Push() {
//How to send user back to specific VC here?
}
}
This is a generic method you can use with in the class or outside the class for push if required else it will pop if the instance of view controller is in the stack:
func pushIfRequired(className:AnyClass) {
if (UIViewController.self != className) {
print("Your pushed class must be child of UIViewController")
return
}
let storyboard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var isPopDone = false
let mainNavigation = UIApplication.shared.delegate?.window??.rootViewController as? UINavigationController
let viewControllers = mainNavigation!.viewControllers
for vc in viewControllers {
if (type(of: vc) == className) {
mainNavigation?.popToViewController(vc, animated: true)
isPopDone = true
break
}
}
if isPopDone == false{
let instanceSignUp = storyboard.instantiateViewController(withIdentifier: NSStringFromClass(className)) // Identifier must be same name as class
mainNavigation?.pushViewController(instanceSignUp, animated: true)
}
}
USES
pushIfRequired(className: SignupVC.self)
You could also utilise the NotificationCenter to achieve a loosely coupled way to "request a view controller"; if you will.
For example, create a custom UINavigationController that observes for the custom Notification and upon receiving one, looks for the requested UIViewController and pops back to it.
class MyNavigationController : UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(forName: NSNotification.Name("RequestViewController"), object: nil, queue: OperationQueue.main) { [unowned self] (note) in
guard let targetType = note.object as? UIViewController.Type else {
print("Please provide the type of the VC to display as an `object` for the notification")
return
}
// Find the first VC of the requested type
if let targetVC = self.viewControllers.first(where: { $0.isMember(of: targetType) }) {
self.popToViewController(targetVC, animated: true)
}
else {
// do what needs to be done? Maybe instantiate a new object and push it?
}
}
}
}
Then in the object you want to go back to a specific ViewController, post the notification.
#IBAction func showViewController(_ sender: Any) {
NotificationCenter.default.post(Notification(name: NSNotification.Name("RequestViewController"), object: ViewController2.self))
}
Now, it's also fairly easy to adopt this method for other presentation-styles.
Instead of using the NotificationCenter, you could also muster up a Mediator to achieve the loose coupling.
You can't. UIViewController and its subclass only can handle navigate between screen.
In your case, need pass link (variable) to navigation controller in custom class.
Like:
class nonUI {
var navigationController: UINavigationController?
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
function Push() {
//How to send user back to specific VC here?
navigationController?.popViewController(animated: true)
}
}

Dismissing a modal view but keeping the data

I'm trying to dismiss a modal view and return back to the view controller that was "sent" from, while keeping the data that was entered in the modal view. If I understand correctly I need to use delegates/protocols for this but I'm having a lot of trouble understanding how to actually implement it in this situation.
Basically a user can call a modal view to enter some information in text fields, and when they hit save this function is called:
func handleSave() {
guard let newProductUrl = NSURL(string: urlTextField.text!) else {
print("error getting text from product url field")
return
}
guard let newProductName = self.nameTextField.text else {
print("error getting text from product name field")
return
}
guard let newProductImage = self.logoTextField.text else {
print("error getting text from product logo field")
return
}
// Call save function in view controller to save new product to core data
self.productController?.save(name: newProductName, url: newProductUrl as URL, image: newProductImage)
// Present reloaded view controller with new product added
let cc = UINavigationController()
let pController = ProductController()
productController = pController
cc.viewControllers = [pController]
present(cc, animated: true, completion: nil)
}
Which calls the self.productController?.save function to save the newly entered values into core data, and reloads the productController table view with the new product.
However the issue I'm running into, is that the productController table view is dynamically set depending on some other factors, so I just want to dismiss the modal view once the user has entered the data, and return back to the page the modal view was called from.
EDIT: attempt at understanding how to implement the delegate -
ProductController is the parent class that the user gets to the modal view from:
protocol ProductControllerDelegate: class {
func getData(sender: ProductController)
}
class ProductController: UITableViewController, NSFetchedResultsControllerDelegate, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
weak var delegate:ProductControllerDelegate?
}
func getData(sender: ProductController) {
}
And AddProductController is the modally presented controller where the user enters in the data then handleSave is called and I want to dismiss and return to the ProductController tableview it was called from:
class AddProductController: UIViewController, ProductControllerDelegate {
override func viewDidDisappear(_ animated: Bool) {
// error on this line
getData(sender: productController)
}
If the sole purpose of your protocol is to return the final state of the view controller its usually easier and clearer to use an unwind segue instead of a protocol.
Steps:
1) In the parent VC you make a #IBAction unwind(segue: UIStoryboardSegue) method
2) In the storyboard of the presented ViewController you control drag from either the control you want to trigger the exit or from the yellow view controller itself(if performing the segue in code) on to the orange exit icon.
your code should look like:
#IBAction func unwind(segue: UIStoryboardSegue) {
if let source = segue.source as? MyModalViewController {
mydata = source.data
source.dismiss(animated: true, completion: nil)
}
}
see apple documentation
Edit here is the hacky way to trigger and unwind from code without storyboard; I do not endorse doing this:
guard let navigationController = navigationController,
let presenter = navigationController.viewControllers[navigationController.viewControllers.count - 2] as? MyParentViewController else {
return
}
presenter.unwind(UIStoryboardSegue(identifier: String(describing: self), source: self, destination: presenter))
Basically you need to create a delegate into this modal view.
Let's say you have ParentViewController which creates this Modal View Controller. ParentViewController must implement the delegate method, let´s say retrieveData(someData).
On the Modal View Controller, you can use the method viewWillDisappear() to trigger the delegate method which the data you want to pass to the parent:
delegate.retrieveData(someData)
If you have issues understanding how to implement a delegate you can check this link

Pushing Navigation Controller From Other Class in Swift

I have a Data Manager class that handles some JSON activities. When one action is completed, I want to push to the Navigation controller.
The FirstViewController calls a function on the DataManager Class
DataManager Class
class func extract_json(data:NSData) -> Bool {
//Success
FirstViewController().pushToValueView()
}
FirstViewController Class
func pushToValueView() {
println("Push to Value View")
navigationController?.pushViewController(ValueViewController(), animated: true)
}
In this example, println() is called but no push occurs. If I call pushToValueView() within FirstViewController e.g. self.pushToValueView() (For debug purposes), a push does occur.
Try calling this function on an existing instance of FirstViewController.
I think that in this example you try to push view controller to FirstViewController which is deallocated after exiting the method scope. That is why view controller doesn't appear
Unwrap the optional. Edit: You shouldn't call your vc from a model class btw, thats bad MVC, I'd use an observers rather and get notified when the closure is complete
if let nav = self.navigationController {
println("Push to Value View")
nav.pushViewController(ValueViewController(), animated: true)
}
You can modify your extract_json function so that it accepts a closure to be executed once the data has been extracted. The closure will contain the code to push the new view controller and it will execute in the context of the calling rather than called object:
In DataManager.swift -
func extractJSON(data:NSData, completion:() -> Void) {
//Do whatever
completion()
}
In FirstViewController.swift -
func getDataAndSegue() {
extractJSON(someData, completion:{
navigationController?.pushViewController(ValueViewController(), animated: true)
}
)
}

Retrieve var value from another class

i have those two classes and i want to retrieve a value from one to another:
class SearchUserViewController:UIViewController{
var selectedUser: String!
#IBAction func btn_accept(sender: AnyObject) {
selectedUser = "Norolimba"
self.dismissViewControllerAnimated(true, completion: nil)
}
}
I'm saving the value to "selectedUser" var, then i want to check the value from this class:
class CalendarViewController: UIViewController {
override func viewDidAppear(animated: Bool) {
let vc : SearchUserViewController! = self.storyboard.instantiateViewControllerWithIdentifier("searchView") as SearchUserViewController
println("\(vc.selectedUser)")
if vc.selectedUser == nil {
self.requestData("team")
}else{
self.requestData("user")
}
}
}
But when i println the value "vc.selectedUser" the value is nil. So what can i do here to catch it from the other class and don't get a nil value?
searchView is here:
Hope you can help me.
Thanks
When you use instantiateViewControllerWithIdentifier(), you're not accessing the view controller that was being displayed on the screen, nor are you accessing the controller that has potentially been automatically instantiated by Interface Builder.
What you're doing is instantiating (hence the name) a new instance of that controller. So the instance variable selectedUser of that new instance is going to be nil.
What you should do is probably provide a callback to your SearchUserViewController when you display it, so that it can notify the view that presented it when a user is picked.
Alternatively, you can use the parentViewController property of UIViewController in cases where you (the view controller) are being presented modally to access the view controller that presented you. So in your SearchUserViewController, when it's being dismissed, it can access self.parentViewController, which should be a CalendarViewController, and call a method or set a property.
(But for the modal controller to assume who its parent is, is a bit of a code smell. I recommend using a callback or delegate of some sort.)
Edit: An example of using the completion callback:
class CalendarViewController : UIViewController {
public func displayUserSelectionController() {
let suvc : SearchUserViewController = ... (how are you displaying it?) ...
self.presentViewController(suvc, animated:true, completion: {
// The SUVC is hiding
if let user = suvc.selectedUser {
self.requestData("team")
} else {
self.requestData("user")
}
})
}
...
}

Resources