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)
}
}
Related
Is there a way to avoid open variables when using segues (or not segues)?
Everybody saw code like this:
if segue.identifier == ListViewController.className()
{
guard let indexPath = tableView.indexPathForSelectedRow else { return }
let destinationVC = segue.destination as? ListViewController
var data: CategoryModel
data = filteredData[indexPath.row]
destinationVC?.passedData = data
}
}
But in ListViewController now we have a var that open for access.
class ListViewController: UIViewController
{
//MARK: - DataSource
var passedData: CategoryModel?
Maybe exist way to avoid this?
I was thinking about dependency injection with init(data: data), but how to initiate this vc right?
Edited.
Using segue it's not a main goal. Main is to make var private. If there exist nice way to not to use segues and push data private I will glad to know.
I was trying to use init() and navigationController?.pushViewController(ListViewController(data: data), animated: true)
but
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value on line:
self.tableView.register(ListTableViewCell.nib(), forCellReuseIdentifier: ListTableViewCell.identifier())
You can't actually make Interface builder use a custom init for your view controller, it will always use init?(coder:).
So the easiest way to pass data to your view controller is to use a non-private property.
But if you really don't want to use an internal or public var you can always try something with a Singleton or Notifications but I don't think it would be wise
You could do it like so
class ListViewController {
private var passedData: CategoryModel?
private init () {
}
public convenience init (passedData: CategoryModel?) {
self.init()
self.passedData = passedData
}
}
And in tableView(_:didSelectRowAt:) of your initial table view controller:
let data: CategoryModel = filteredData[indexPath.row]
let destinationVC = ListViewController(passedData: data)
self.present(destinationVC, animated: true, completion: nil)
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)
}
}
I was wondering, when passing data using prepareForSegue, can you pass data to a View Controller later in the app? For example on the first ViewController I have the user enter their name. It's not until the very end, so a few views later, do I need to display their name. Is there a way to pass their name without having to go to the end view right away?
Use a Coordinator.
It's really easy to decouple your ViewControllers:
instead of using segues give every ViewController a delegate
create a coordinator object (this object knows your screen flow, not your screens)
the coordinator creates the ViewControllers (it can use UIStoryboard instantiateViewController(withIdentifier:) so ViewController A does not have to know that ViewController B exists
instead of calling performSegue you just call your delegate and pass in the data
Benefits
Simple to use
Easy to reorder screens in a flow
Highly decoupled (easier testing)
Very nice for A/B testing
Scales a lot (you can have multiple coordinators, one for each flow)
Sample
Let's say you have 3 VCs, the first one asks for your name, the second for your age and the third displays the data. It would make no sense that AgeViewController knew that NameViewController existed, later on you may want to change their order or even merge them.
Name View Controller
protocol NameViewControllerDelegate: class {
func didInput(name: String)
}
class NameViewController: UIViewController {
weak var delegate: NameViewControllerDelegate?
#IBOutlet var nameTextField: UITextField!
//Unimportant stuff ommited
#IBAction func submitName(sender: Any) {
guard let name = nameTextField.text else {
// Do something, it's up to you what
return
}
delegate?.didInput(name: name)
}
}
Age View Controller
protocol AgeViewControllerDelegate: class {
func didInput(age: Int)
}
class AgeViewController: UIViewController {
weak var delegate: AgeViewControllerDelegate?
#IBOutlet var ageTextField: UITextField!
//Unimportant stuff ommited
#IBAction func submitAge(sender: Any) {
guard let ageString = ageTextField.text,
let age = Int(ageString) else {
// Do something, it's up to you what
return
}
delegate?.didInput(age: age)
}
}
Displayer View Controller
class DisplayerViewController: UIViewController {
var age: Int?
var name: String?
}
Coordinator
class Coordinator {
var age: Int?
var name: String?
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
fileprivate lazy var storyboard: UIStoryboard = {
return UIStoryboard(name: "MyStoryboard", bundle: nil)
}()
//This works if you name your screns after their classes
fileprivate func viewController<T: UIViewController>(withType type: T.Type) -> T {
return storyboard.instantiateViewController(withIdentifier: String(describing: type(of: type))) as! T
}
func start() -> UIViewController {
let viewController = self.viewController(withType: NameViewController.self)
viewController.delegate = self
navigationController.viewControllers = [viewController]
return viewController
}
}
Coordinator + Name View Controller Delegate
extension Coordinator: NameViewControllerDelegate {
func didInput(name: String){
self.name = name
let viewController = self.viewController(withType: AgeViewController.self)
viewController.delegate = self
navigationController.pushViewController(viewController, animated: true)
}
}
Coordinator + Age View Controller Delegate
extension Coordinator: AgeViewControllerDelegate {
func didInput(age: Int) {
self.age = age
let viewController = self.viewController(withType: DisplayerViewController.self)
viewController.age = age
viewController.name = name
navigationController.pushViewController(viewController, animated: true)
}
}
Not really. You can pass view by view the item but it's not a proper way of doing things.
I suggest you to have a Static Manager or this kind of stuff to store the information globally in your app to retrieve it later
All the solution are pretty good. Possible you can try the below model also
1. DataModel class
1.1 Should be singleton class
1.2 Declare value
Step 1 : ViewCOntroller-one
1 Create the Sharedinstance of singleton class
1.1 Assign the value
Step 3 :ViewController-two
1 Create the Sharedinstance of singleton class
1.1 Get the value
I have a rails api set up and one test user. I can login/logout view the api.
I have a tab app set up, and I have a LoginViewController set up to authenticate into that. My view controller (basically) looks like the below code. This doesnt work. However, the second block of code does work
I am guessing I am calling something wrong.. The first block executes and then crashes with a *** Assertion failure in -[UIKeyboardTaskQueuewaitUntilAllTasksAreFinished].. I've been spinning my wheels for hours.. tried looking into segues.. etc. Anything would help!!!
//DOES NOT WORK //
class LoginViewController: UIViewController {
#IBOutlet weak var email: UITextField!
#IBOutlet weak var password: UITextField!
#IBAction func Login(sender: AnyObject) {
func switchToTabs() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let tabBarController = storyboard.instantiateViewControllerWithIdentifier("TabBarController") as! UITabBarController
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window?.rootViewController = tabBarController
}
let authCall = PostData()
var authToken = ""
authCall.email = email.text!
authCall.password = password.text!
authCall.forData { jsonString in
authToken = jsonString
if(authToken != "") {
switchToTabs()
}
}
}
// SAME CLASS THAT DOES WORK//
class LoginViewController: UIViewController {
#IBOutlet weak var email: UITextField!
#IBOutlet weak var password: UITextField!
#IBAction func Login(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let tabBarController = storyboard.instantiateViewControllerWithIdentifier("TabBarController") as! UITabBarController
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window?.rootViewController = tabBarController
}
My question is, why can I seemingly navigate to a new view when I don't use my api, but when I do receive my auth token.. I can't log in.
Please excuse the ugly code.. this is my first day with swift
Everytime you every do anything that changed something about the UI inside of a closure or anything that could even possibly be off the main queue, enclose it in this statement.
dispatch_async(dispatch_get_main_queue()) {
//the code that handles UI
}
This includes all code that segues inside a closure like the one you have
authCall.forData { jsonString in
authToken = jsonString
if(authToken != "") {
switchToTabs()
}
}
If you write it like so it would work
authCall.forData {
jsonString in
authToken = jsonString
if(authToken != "") {
dispatch_async(dispatch_get_main_queue()) {
switchToTabs()
}
}
}
then call your segue in switchToTabs()
If you arn't sure what queue you are on, it won't hurt to do it. As you become more familiar with scenarios that take you off the main queue, you will realize when to use it. Anytime you are in a closure and dealing with UI its a safe bet that you could leave. Obviously anything asynchronous would cause it as well. Just place your performSegueWithIdentifier inside of it and it should work.
If you make a segue on the storyboard from the view controller attatched to LoginViewController, then you click the segue you can give it an identifier. Then you just call performSegueWithIdentifier("identifierName", sender: nil) and none of the other code in switchToTabs() is necessary.
If you want to continue down the route you are taking to summon the View Controller, then I think what you are missing is these lines.
Use this below your class outside of it.
private extension UIStoryboard {
class func mainStoryboard() -> UIStoryboard { return UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()) }
class func tabBarController() -> UITabBarController? {
return mainStoryboard().instantiateViewControllerWithIdentifier("TabBarController") as? UITabBarController
}
}
Then change your switchToTabs() function out for this
func switchToTabs() {
let tabBarController = UIStoryboard.tabBarController()
view.addSubview(tabBarController!.view)
//line below can be altered for different frames
//tabBarController!.frame = self.frame
addChildViewController(tabBarController!)
tabBarController!.didMoveToParentViewController(self)
//line below is another thing worth looking into but doesn't really correlate with this
//self.navigationController!.pushViewController(tabBarController!, animated: true)
}
I added in a couple comments for you to look into if you like. I believe this will do the job, but keep in mind that the direction you are headed doesn't remove the previous screen from memory. It is technically still sitting behind your new screen. I wouldn't ever recommend this for what you are trying to do, but it might not hurt if you are building something simplistic.
Question:
How might one write a custom segue that would allow you to embed view controllers from a different storyboard?
Context:
I am trying to write a custom segue with which I can link from one storyboard to another. A good article on atomicobject.com illustrates how to create a segue that originates from a button / event etc. Translated into swift, and allowing for non UINavigationControllers, the code looks like:
public class SegueToStoryboard : UIStoryboardSegue {
private class func viewControllerInStoryBoard(identifier:String, bundle:NSBundle? = nil)
-> UIViewController?
{
let boardScene = split(identifier, { $0 == "." }, maxSplit: Int.max, allowEmptySlices: false)
switch boardScene.count {
case 2:
let sb = UIStoryboard(name: boardScene[0], bundle: bundle)
return sb.instantiateViewControllerWithIdentifier(boardScene[1]) as? UIViewController
case 1:
let sb = UIStoryboard(name: boardScene[0], bundle: bundle)
return sb.instantiateInitialViewController() as? UIViewController
default:
return nil
}
}
override init(identifier: String!,
source: UIViewController,
destination ignore: UIViewController) {
let target = SegueToStoryboard.viewControllerInStoryBoard(identifier, bundle: nil)
super.init(identifier: identifier, source: source,
destination:target != nil ? target! : ignore)
}
public override func perform() {
let source = self.sourceViewController as UIViewController
let dest = self.destinationViewController as UIViewController
source.addChildViewController(dest)
dest.didMoveToParentViewController(source)
source.view.addSubview(dest.view)
// source.navigationController?.pushViewController(dest, animated: true)
}
}
Problem:
The problem that I am having with both their Obj-C and the above Swift code is that when you try to use the via a container view (with semantics of an embed segue - starting with an embed segue, deleting the segue, and then use the above custom segue), it crashes before ever calling the segue code with the following method-not-found error:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<UIStoryboardSegueTemplate 0x7ffc8432a4f0>
setValue:forUndefinedKey:]: this class is not key value
coding-compliant for the key containerView.'
I have tried to inspect the address listed but get no meaningful results. I do the see the bold statement that it expecting the containerView but don't know how one might isolate, satisfy, and/or work around this problem.
Summary:
My end goal is to embed view controllers defined in separate storyboards to facilitate collaboration and testing without having to write additional code (a non invasive solution). Does anyone have any insight into how to accomplish this greater task? I could fall back to hybrid approach of calling performSegue, but would like to find a single, contained, and complete solution. The above code gets there for event driven (buttons etc) segues, but not with the embed segue.
Any input is appreciated, thanks in advance.
Your approach works fine for custom segues to push / display modally other view controllers but not for embed segues. The reason for this is that the "Embed" segue is not a subclass of UIStoryboardSegue but inherits from UIStoryboardSegueTemplate, which is a private API.
Unfortunately I couldn't find a better way to achieve what you want than with the hybrid approach.
My way is to link the containerView and delete the viewDidLoad segue from it. and manually call the segue on viewdidLoad
public protocol EmbeddingContainerView {
var containerView: UIView! { get set }
}
public class CoreSegue: UIStoryboardSegue {
public static func instantiateViewControllerWithIdentifier(identifier: String) -> UIViewController {
let storyboard = UIStoryboard(name: "Core", bundle: NSBundle(forClass: self))
let controller = storyboard.instantiateViewControllerWithIdentifier(identifier) as! UIViewController
return controller
}
var isPresent = false
var isEmbed = false
override init!(identifier: String?, source: UIViewController, destination: UIViewController) {
if var identifier = identifier {
if identifier.hasPrefix("present ") {
isPresent = true
identifier = identifier.substringFromIndex(advance(identifier.startIndex, count("present ")))
}
if identifier.hasPrefix("embed ") {
isEmbed = true
identifier = identifier.substringFromIndex(advance(identifier.startIndex, count("embed ")))
}
let controller = CoreSegue.instantiateViewControllerWithIdentifier(identifier)
super.init(identifier: identifier, source: source, destination: controller)
} else {
super.init(identifier: identifier, source: source, destination: destination)
}
}
public override func perform() {
if let source = sourceViewController as? UIViewController, dest = destinationViewController as? UIViewController {
if isPresent {
let nav = UINavigationController(rootViewController: dest)
nav.navigationBarHidden = true // you might not need this line
source.presentViewController(nav, animated: true, completion: nil)
} else if isEmbed {
if let contentView = (source as? EmbeddingContainerView)?.containerView {
source.addChildViewController(dest)
contentView.addSubview(dest.view)
dest.view.fullDimension() // which comes from one of my lib
}
} else {
source.navigationController?.pushViewController(destinationViewController as! UIViewController, animated: true)
}
}
}
}
and later in your code:
class MeViewController: UIViewController, EmbeddingContainerView {
#IBOutlet weak var containerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
performSegueWithIdentifier("embed Bookings", sender: nil)
}
}