Way to pass data to another vc with segues without open var - ios

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)

Related

Transferring an object from a Realm database to another UITabBar controller

Tell me, please, I'm trying to solve the problem of transferring an instance of a class to another controller using the Realm database.
I have a main controller that stores objects according to the model the following data:
class Route: Object {
#objc dynamic var routeImage: Data?
#objc dynamic var routeName: String?
#objc dynamic var numberOfPersons = 0.0
#objc dynamic var dateOfDeparture: String?
#objc dynamic var dateOfArrival: String?
let placeToVisit = List<Place>()
let person = List<Person>()
}
In the controller to which I need to transfer this data, I created
var currentRoute: Route!
In the Storyboard, I specified the identifier "showDetail" from the controller cell to the UITabBar, and in the main controller, I created a method:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
guard let indexPath = tableView.indexPathForSelectedRow else {return}
let newPlaceVC = segue.destination as! InformationViewController
newPlaceVC.currentRoute = routes[indexPath.row]
}
}
Error I got:
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
Could not cast value of type 'UITabBarController' (0x111ed8b10) to 'Organizer_Tourist.InformationViewController' (0x108dd0a70).
2019-10-07 14:30:35.626853+0800 Organizer Tourist[5467:2618892] Could not cast value of type 'UITabBarController' (0x111ed8b10) to 'Organizer_Tourist.InformationViewController' (0x108dd0a70).
(lldb)
But it is not valid, the application crashes by tap on the cell. I suppose this would work if there was not a tabBar, but a regular table, view controllers. I was looking for solutions and all I came across was implementation through singleton. Now I have a lot of questions, but will this really be the right decision? People say this violates the "modularity" of the application and carries its own problems. The question is how is this done through singleton? What to consider, where to start? Which method is worth editing?
Error said what is happening:
Could not cast value of type 'UITabBarController'
You are trying to cast segue.destination to typo InformationViewController which is not.
If you embed your InformationViewController in UITabBarController so you need to cast to your UITabBarController rather than InformationViewController.
Try something like this:
if segue.identifier == "showDetail" {
guard let indexPath = tableView.indexPathForSelectedRow else { return }
let tabBarController = segue.destination as! UITabBarController
UserSelectedRoute.shared.selectedRoute = routes[indexPath.row]
}
If you want to pass current selected route to InformationViewController you can create singleton object which will be hold current route
final class UserSelectedRoute {
private init() { }
static var shared = UserSelectedRoute()
var selectedRoute: Route?
}
And then in your InformationViewController you can have something like:
var currentRoute = UserSelectedRoute.shared.selectedRoute
Hope this will help you!

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)
}
}

Using prepareForSegue to pass data to ViewController later in app

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

Can't pass data to another view controller in when preparing for segue

I am trying to pass an object to another view controller but I get an error when I try to set the property of the view controller object.
The error is pretty informative. It says the class ViewController has no public or internal property called data. You'll have to declare a property called data in class ViewController.
class ViewController: UIViewController {
var data: String?
}
the class you have that is named ViewController needs to have a public variable named data.
Your ViewController class could look something like this:
class ViewController: UIViewController {
// This is your public accessible variable you can set during a seque
var data: String?
override func loadView() {
super.loadView()
print(self.data)
}
}
Also, your prepareForSegue function can be simplified like this
if let displayTodoVC = segue.destinationViewController as? ViewController {
displayTodoVC.data = "Hello World"
}
The ViewController is obviously missing a string variable named data.
class ViewController : UIViewController {
var data: String? // Make sure you have this defined in your view controller.
}
I would also suggest that you use a conditional unwrapping of the destinationViewController in your prepareForSegue.
prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let viewController = segue.destinationViewController as? ViewController {
viewController.data = "Hello World"
}
}
For future posts, please refrain from posting images of code. You should include code as text in your questions.
Happy coding :)
The Basics

Custom segue to a different storyboard

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)
}
}

Resources