Passing data between UIViewControllers controlled by UITabBarController in Swift 5 - swift

I've got a TabBarViewController controlling 5 UIViewControllers.
I'm trying to pass data (a user object in particular) from one UIViewController.
What I'm currently doing is to set the user property in the TabBar Controller from within one of the UIViewControllers (HomeScreen) like so:
class HomeViewController: UIViewController {
let apiService = APIService()
var user: User?
var parentController: TabBarController?
override func viewDidLoad() {
super.viewDidLoad()
let token = UserDefaults.standard.string(forKey: "authtoken")!
self.apiService.getUserFromAuthtoken(token: token, completion: {result in
switch result {
case .success(let user):
self.user = user
self.parentController = self.tabBarController as? TabBarController
self.parentController?.user = user
print("EMAIL-HOME-SCREEN: ", user.email)
case .failure(let error):
print("An error occured \(error.localizedDescription)")
}
})
}
}
Then, after navigation, I'm trying to access the TabBarView's user property from another UIViewController (CoopOverview) like so:
class CoopOverviewViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var CampaignBrandSliderBackground: UIView!
#IBOutlet weak var CampaignCollectionView: UICollectionView!
#IBOutlet weak var CampaignBrandSlider: UISegmentedControl!
var user: User?
var parentController: TabBarController?
#IBOutlet weak var foo: UIView!
let campaignImages: [UIImage] = [UIImage(named: "icon-black")!,UIImage(named: "icon-white")!,UIImage(named: "icon-black")!,UIImage(named: "icon-white")!]
override func viewDidLoad() {
super.viewDidLoad()
self.parentController = self.tabBarController as? TabBarController
self.user = self.parentController?.user
print("EMAIL-COOP-SCREEN: ", user!.email)
...
}
Is this a good approach or is there a more elegant way to solve this?

I would discourage the tight coupling between the TabBarController and each of its children. The children should not be relying on their parent’s internal implementation details.
Why’s that a problem? What if you later change the parent to a page view controller later? Or some other custom container view controller? You don’t want to have to go into all these children to fix that. Children should rely upon abstractions and not rely upon implementation details of their parent.
There are two solutions:
Avoid tight coupling of classes via protocol. For example,
you might have a UserProvider or UserDataSource protocol that exposes a property or method to retrieve the current user from the source of truth;
the tab bar controller (or it’s view model) can conform to that protocol;
the children then use this protocol, not UITabBarController type, to retrieve the User; and
when the tab bar controller selects a child, it can supply a reference to itself as this protocol type.
Now, you have the basic structure that your original pattern had, but without the tight coupling between our classes.
Going a step further, use dependency injection, where the parent injects whatever the child needs.
So, rather than having the children reach back into the parent (via a protocol) to retrieve the User, the other approach is to have a User property in all the children and have the parent inject it into the relevant children.
So you might have a protocol for children that have a settable User property. Then the tab bar controller would, for any children that conform to that protocol, supply a copy of the User. Again, using protocols keeps these loosely coupled.
Both of these patterns avoid tight coupling between our classes.

Related

Private IBOutlets Swift

I know that our IBOutlets should be private, but for example if I have IBOutlets in TableViewCell, how should I access them from another ViewController? Here is the example why I'm asking this kind of question:
class BookTableViewCell: UITableViewCell {
#IBOutlet weak private var bookTitle: UILabel!
}
if I assign to the IBOutlet that it should be private, I got an error in another ViewController while I'm accessing the cell property: 'bookTitle' is inaccessible due to 'private' protection level
If I understand your question correctly, you are supposing the #IBOutlet properties should be marked as private all the time... Well it's not true. But also accessing the properties directly is not safe at all. You see the ViewControllers, TableViewCells and these objects use Implicit unwrapping on optional IBOutlets for reason... You don't need to init ViewController when using storyboards or just when using them somewhere in code... The other way - just imagine you are creating VC programmatically and you are passing all the labels to the initializer... It would blow your head... Instead of this, you come with this in storyboard:
#IBOutlet var myLabel: UILabel!
this is cool, you don't need to have that on init, it will just be there waiting to be set somewhere before accessing it's value... Interface builder will handle for you the initialization just before ViewDidLoad, so the label won't be nil after that time... again before AwakeFromNib method goes in the UITableViewCell subclass, when you would try to access your bookTitle label property, it would crash since it would be nil... This is the tricky part about why this should be private... Otherwise when you know that the VC is 100% on the scene allocated there's no need to be shy and make everything private...
When you for example work in prepare(for segue:) method, you SHOULD NEVER ACCESS THE #IBOutlets. Since they are not allocated and even if they were, they would get overwritten by some internal calls in push/present/ whatever functions...
Okay that's cool.. so what to do now?
When using UITableViewCell subclass, you can safely access the IBOutlets (ONLY IF YOU USE STORYBOARD AND THE CELL IS WITHIN YOUR TABLEVIEW❗️)
and change their values... you see
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// We shouldn't return just some constructor with UITableViewCell, but who cares for this purposes...
guard let cell = tableView.dequeueReusableCell(withIdentifier: "bookTableViewCell", for: indexPath) else { return UITableViewCell() }
cell.bookTitle.text = "any given text" // This should work ok because of interface builder...
}
The above case should work in MVC pattern, not MVVM or other patterns where you don't use storyboards with tableViewControllers and embed cells too much... (because of registering cells, but that's other article...)
I will give you few pointers, how you can setup the values in the cell/ViewController without touching the actual values and make this safe... Also good practice (safety) is to make the IBOutlets optional to be 100% Safe, but it's not necessary and honestly it would be strange approach to this problem:
ViewControllers:
class SomeVC: UIViewController {
// This solution should be effective when those labels could be marked weak too...
// Always access weak variables NOT DIRECTLY but with safe unwrap...
#IBOutlet var titleLabel: UILabel?
#IBOutlet var subtitleLabel: UILabel?
var myCustomTitle: String?
var myCustomSubtitle: String?
func setup(with dataSource: SomeVCDataSource ) {
guard let titleLabel = titleLabel, let subtitleLabel = subtitleLabel else { return }
// Now the values are safely unwrapped and nothing can crash...
titleLabel.text = dataSource.title
subtitleLabel.text = dataSource.subtitle
}
// WHen using prepare for segue, use this:
override func viewDidLoad() {
super.viewDidLoad()
titleLabel.text = myCustomTitle
subtitleLabel.text = myCustomSubtitle
}
}
struct SomeVCDataSource {
var title: String
var subtitle: String
}
The next problem could be this:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destinationVC = segue.destination as? SomeVC else { return }
let datasource = SomeVCDataSource(title: "Foo", subtitle: "Bar")
// This sets up cool labels... but the labels are Nil before the segue occurs and even after that, so the guard in setup(with dataSource:) will fail and return...
destinationVC.setup(with: datasource)
// So instead of this you should set the properties myCustomTitle and myCustomSubtitle to values you want and then in viewDidLoad set the values
destinationVC.myCustomTitle = "Foo"
destinationVC.myCustomSubtitle = "Bar"
}
You see, you don' need to set your IBOutlets to private since you never know how you will use them If you need any more examples or something is not clear to you, ask as you want... Wish you happy coding and deep learning!
You should expose only what you need.
For example you can set and get only the text property in the cell.
class BookTableViewCell: UITableViewCell {
#IBOutlet weak private var bookTitleLabel: UILabel!
var bookTitle: String? {
set {
bookTitleLabel.text = newValue
}
get {
return bookTitleLabel.text
}
}
}
And then, wherever you need:
cell.bookTitle = "It"
Now outer objects do not have access to bookTitleLabel but are able to change it's text content.
What i usually do is configure method which receives data object and privately sets all it's outlets features.
I haven't come across making IBOutlets private to be common, for cells at least. If you want to do so, provide a configure method within your cell that is not private, which you can pass values to, that you want to assign to your outlets. The function within your cell could look like this:
func configure(with bookTitle: String) {
bookTitle.text = bookTitle
}
EDIT: Such a function can be useful for the future, when you change your cell and add new outlets. You can then add parameters to your configure function to handle those. You will get compiler errors everywhere, where you use that function, which allows you to setup your cell correctly wherever you use it. That is helpful in a big project that reuses cells in different places.

What's the best way to pass an object to an NSViewController on application launch?

In my AppDelegate's applicationDidFinishLaunching I need to create an object using data read from disk, and then pass this object to the initial view controller for display. What would be the best way to do this?
Right now I'm loading the storyboard programatically like so:
func applicationDidFinishLaunching(_ aNotification: Notification) {
importantThing = ImportantThing()
importantThing.load(url: URL(fileURLWithPath: "..."))
let storyboard = NSStoryboard(name: "Main", bundle: nil)
myWindowController = storyboard.instantiateController(withIdentifier: "MyWindowController") as! NSWindowController
(myWindowController.contentViewController as? MyViewController)?.importantThing = importantThing
myWindowController.showWindow(self)
}
But this feels clunky. For one, the property is being set after viewDidLoad, so now view setup is weird.
There must be a better way to do this. If possible, I would like to not resort to using a singleton, because I actually need to set up a few interconnected objects (two objects with important state that have references to each other, but it doesn't make sense for either to contain the other). What would be a good way to solve this?
What you're doing in the app delegate is correct. As for what you should do in the view controller, Apple's Master-Detail app template shows you the correct pattern (I've added a few comments):
// the interface
#IBOutlet weak var detailDescriptionLabel: UILabel!
// the property
var detailItem: NSDate? {
didSet {
self.configureView()
}
}
func configureView() {
// check _both_ the property _and_ the interface
if let detail = self.detailItem { // property set?
if let label = self.detailDescriptionLabel { // interface exists?
label.text = detail.description
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// at this point, its _certain_ that the interface exists
self.configureView()
}
If you think about it, you'll see that the interface is updated correctly regardless of the order of events — that is, regardless of whether viewDidLoad or the setting of the property comes first. Just follow that pattern.

Change #IBOutlet from a subview

I'm trying to enable or disable an #IBOutlet UIButton Item of a toolbar from a UIView.
The button should get disabled when the array that I'm using in EraseView.Swift is empty.
I tried creating an instance of the view controller but it gives me the error (found nil while unwrapping):
in EraseView:
class EraseView: UIView {
...
let editViewController = EditImageViewController()
//array has item
editViewController.undoEraseButton.enabled = true //here I get the error
...
}
I tried to put a global Bool that changed the value using it in EditImageViewController but it doesn't work:
var enableUndoButton = false
class EditImageViewController: UIViewController {
#IBOutlet weak var undoEraseButton: UIBarButtonItem!
viewDidLoad() {
undoEraseButton.enabled = enableUndoButton
}
}
class EraseView: UIView {
...
//array has item
enableUndoButton = true //here I get the error
...
}
I know it's simple but I can't let it work. Here's the situation:
The root of the problem is the line that says:
let editViewController = EditImageViewController()
The EditImageViewController() says "ignore what the storyboard has already instantiated for me, but rather instantiate another view controller with no outlets hooked up and use that." Clearly, that's not what you want.
You need to provide some way for the EraseView to inform the existing view controller whether there was some change to its "is empty" state. And, ideally, you want to do this in a way that keeps these two classes loosely coupled. The EraseView should only be informing the view controller of the change of the "is empty" state, and the view controller should initiate the updating of the other subviews (i.e. the button). A view really shouldn't be updating another view's outlets.
There are two ways you might do that:
Closure:
You can give the EraseView a optional closure that it will call when it toggles from "empty" and "not empty":
var emptyStateChanged: ((Bool) -> ())?
Then it can call this when the state changes. E.g., when you delete the last item in the view, the EraseView can call that closure:
emptyStateChanged?(true)
Finally, for that to actually do anything, the view controller should supply the actual closure to enable and disable the button upon the state change:
override func viewDidLoad() {
super.viewDidLoad()
eraseView.emptyStateChanged = { [unowned self] isEmpty in
self.undoEraseButton.enabled = !isEmpty
}
}
Note, I used unowned to avoid strong reference cycle.
Delegate-protocol pattern:
So you might define a protocol to do that:
protocol EraseViewDelegate : class {
func eraseViewIsEmpty(empty: Bool)
}
Then give the EraseView a delegate property:
weak var delegate: EraseViewDelegate?
Note, that's weak to avoid strong reference cycles. (And that's also why I defined the protocol to be a class protocol, so that I could make it weak here.)
The EraseView would then call this delegate when the the view's "is empty" status changes. For example, when it becomes empty, it would inform its delegate accordingly:
delegate?.eraseViewIsEmpty(true)
Then, again, for this all to work, the view controller should (a) declare that is conforms to the protocol; (b) specify itself as the delegate of the EraseView; and (c) implement the eraseViewIsEmpty method, e.g.:
class EditImageViewController: UIViewController, EraseViewDelegate {
#IBOutlet weak var undoEraseButton: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
eraseView.delegate = self
}
func eraseViewIsEmpty(empty: Bool) {
undoEraseButton.enabled = !empty
}
}
Both of these patterns keep the two classes loosely coupled, but allow the EraseView to inform its view controller of some event. It also eliminates the need for any global.
There are other approaches that could solve this problem, too, (e.g. notifications, KVN, etc.) but hopefully this illustrates the basic idea. Views should inform their view controller of any key events, and the view controller should take care of the updating of the other views.

How to subclass custom UIViewController in Swift?

I'd like to create a reusable view controller UsersViewControllerBase.
UsersViewControllerBase extends UIViewController, and implements two delegates (UITableViewDelegate, UITableViewDataSource), and has two views (UITableView, UISegmentedControl)
The goal is to inherit the implementation of the UsersViewControllerBase and customise the segmented items of segmented control in UsersViewController class.
class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource{
#IBOutlet weak var segmentedControl: UISegmentedControl!
#IBOutlet weak var tableView: UITableView!
//implementation of delegates
}
class UsersViewController: UsersViewControllerBase {
}
The UsersViewControllerBase is present in the storyboard and all outlets are connected, the identifier is specified.
The question is how can I init the UsersViewController to inherit all the views and functionality of UsersViewControllerBase
When I create the instance of UsersViewControllerBase everything works
let usersViewControllerBase = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewControllerBase") as? UsersViewControllerBase
But when I create the instance of UsersViewController I get nil outlets
(I created a simple UIViewController and assigned the UsersViewController class to it in the storyboard )
let usersViewController = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewController") as? UsersViewController
It looks like views are not inherited.
I would expect init method in UsersViewControllerBase that gets controller with views and outlets from storyboard:
class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource{
#IBOutlet weak var segmentedControl: UISegmentedControl!
#IBOutlet weak var tableView: UITableView!
init(){
let usersViewControllerBase = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewControllerBase") as? UsersViewControllerBase
self = usersViewControllerBase //but that doesn't compile
}
}
And I would init UsersViewController:
let usersViewController = UsersViewController()
But unfortunately that doesn't work
When you instantiate a view controller via instantiateViewControllerWithIdentifier, the process is essentially as follows:
it finds a scene with that identifier;
it determines the base class for that scene; and
it returns an instance of that class.
And then, when you first access the view, it will:
create the view hierarchy as outlined in that storyboard scene; and
hook up the outlets.
(The process is actually more complicated than that, but I'm trying to reduce it to the key elements in this workflow.)
The implication of this workflow is that the outlets and the base class are determined by the unique storyboard identifier you pass to instantiateViewControllerWithIdentifier. So for every subclass of your base class, you need a separate storyboard scene and have hooked up the outlets to that particular subclass.
There is an approach that will accomplish what you've requested, though. Rather than using storyboard scene for the view controller, you can instead have the view controller implement loadView (not to be confused with viewDidLoad) and have it programmatically create the view hierarchy needed by the view controller class. Apple used to have a nice introduction to this process in their View Controller Programming Guide for iOS, but have since retired that discussion, but it can still be found in their legacy documentation.
Having said that, I personally would not be compelled to go back to the old world of programmatically created views unless there was a very compelling case for that. I might be more inclined to abandon the view controller subclass approach, and adopt something like a single class (which means I'm back in the world of storyboards) and then pass it some identifier that dictates the behavior I want from that particular instance of that scene. If you want to keep some OO elegance about this, you might instantiate custom classes for the data source and delegate based upon some property that you set in this view controller class.
I'd be more inclined to go down this road if you needed truly dynamic view controller behavior, rather than programmatically created view hierarchies. Or, even simpler, go ahead and adopt your original view controller subclassing approach and just accept that you'll need separate scenes in the storyboard for each subclass.
So, you have your base class:
class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var segmentedControl: UISegmentedControl!
#IBOutlet weak var tableView: UITableView!
//implementation of delegates
}
[A] And your subclass:
class UsersViewController: UsersViewControllerBase { var text = "Hello!" }
[B] A protocol that your subclass will be extending:
protocol SomeProtocol {
var text: String? { get set }
}
[C] And some class to handle your data. For example, a singleton class:
class MyDataManager {
static let shared = MyDataManager()
var text: String?
private init() {}
func cleanup() {
text = nil
}
}
[D] And your subclass:
class UsersViewController: UsersViewControllerBase {
deinit {
// Revert
object_setClass(self, UsersViewControllerBase.self)
MyDataManager.shared.cleanup()
}
}
extension UsersViewController: SomeProtocol {
var text: String? {
get {
return MyDataManager.shared.text
}
set {
MyDataManager.shared.text = newValue
}
}
}
To properly use the subclass, you need to do (something like) this:
class TestViewController: UIViewController {
...
func doSomething() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
//Instantiate as base
let usersViewController = storyboard.instantiateViewControllerWithIdentifier("UsersViewControllerBase") as! UsersViewControllerBase
//Replace the class with the desired subclass
object_setClass(usersViewController, UsersViewController.self)
//But you also need to access the property 'text', so:
let subclassObject = usersViewController as! UsersViewController
subclassObject.text = "Hello! World."
//Use UsersViewController object as desired. For example:
navigationController?.pushViewController(subclassObject, animated: true)
}
}
EDIT:
As pointed out by #VyachaslavGerchicov, the original answer doesn't work all the time so the section marked as [A] was crossed out. As explained by an answer here:
object_setClass in Swift
... setClass cannot add instance variables to an object that has already been created.
[B], [C], and [D] were added as a work around. Another option to [C] is to make it a private inner class of UsersViewController so that only it has access to that singleton.
The problem is that you created a scene in the storyboard, but you didn't give the view controller's view any subviews or connect any outlets, so the interface is blank.
If your goal is to reuse a collection of views and subviews in connection with instances of several different view controller classes, the simplest way, if you don't want to create them in code, is to put them in a .xib file and load it in code after the view controller's own view-loading process (e.g. in viewDidLoad).
But if the goal is merely to "customise the segmented items of segmented control" in different instances of this view controller, the simplest approach is to have one view controller class and one corresponding interface design, and perform the customization in code. However, you could load just that segmented control from its own .xib in each case, if it's important to you design it visually.

Swift Delegation

I'm having trouble wrapping my head around delegation in Swift. After reading some guides, I was able to set it up delegation between two ViewControllers, but I'm not understanding how it works. In my first view controller, I have a a label that displays what has been entered in the second view controller which contains a text field and a button (that returns to the first view controller). Here is the code for the first view controller:
#IBOutlet weak var labelText: UILabel!
func userDidEnterInformation(info: String) {
labelText.text = info;
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if(segue.identifier == "transition"){
let secondVC: SecondViewController = segue.destinationViewController as! SecondViewController;
secondVC.delegate = self;
}
}
Here's the code for the second view controller:
protocol DataEnteredDelegate{
func userDidEnterInformation(info: String);
}
#IBOutlet weak var userText: UITextField!
var delegate: DataEnteredDelegate? = nil;
#IBAction func buttonPressed(sender: AnyObject) {
let information = userText.text!;
delegate!.userDidEnterInformation(information);
self.navigationController?.popToRootViewControllerAnimated(true);
}
My understanding is that in the text inside the text field gets stored in the information constant, then the userDidEnterInformation method from the protocol is called, with the method being defined inside the first view controller. This method then changes the label inside the first view controller. The thing is, I'm not sure what is happening in the prepareForSegue function. Specifically, I'm not sure what's the purpose of secondVC.delegate = self.
I would appreciate any sort of clarity on delegation.
The diagram is simple but can help you understand what's going on.
FirstViewController must conform to the DataEnteredDelegate protocol you have defined (see Sumit's answer). When using secondVC.delegate = self, you are saying that for the segue transition with the destination being a SecondViewController, the attribute delegate of that SecondViewController instance will be set to this instance of FirstViewController, thus delegating things from SecondViewController to your FirstViewController as made possible by the DataEnteredDelegate protocol.
The protocol you created in second viewcontroller is an Interface. You must implement your first view controller with the DataEnteredDelegate protocol.
class FirstViewController:UIViewController, DataEnteredDelegate{
func userDidEnterInformation(info: String) {
//stub
}
}
If the delegate of the second VC is not set in prepareForSegue() it remains nil. The second VC is then unable to call the first VC.
On a side note, if the delegate is nil your code will crash because delegate! is trying to unwrap an optional binding with the value of nil. It's better to first unwrap the delegate variable:
if let handler = delegate {
handler.userDidEnterInformation(information)
}
Alternatively, you could use Swift's Optional Chaining, calling userDidEnterInformation only if delegate is not nil.
delegate?.userDidEnterInformation(information);
In addition it is recommended to declare the delegate weak, to prevent retain cycles:
weak var delegate: DataEnteredDelegate?
Delegates and Protocols
Do not try to figure out how the dictionary definition of “delegate” fits with the concept of delegation in Swift. It doesn't.
Delegation in Swift is an agreement between two players—a sensing object and a requesting object. The “delegate” is the “requesting object.” Just think “asker” or “requester” every time you see “delegate” and it will make a lot more sense. Here is their agreement...
The Sensing Object (Second View Controller):
I have data from some event that took place. I will publish instructions (a protocol) on how you may access that data. If you want it, you must do three things.
You must declare in your class type that your class abides by my protocol.
You must write the functions that I describe in my protocol. I don't care what those functions do but the function type must match what I publish.
In YOUR code, you must set MY “delegate” (think “asker”) property to point to you. {secondVC.delegate = self} That way I can call one of YOUR functions to deliver the data.
After that, when I get some data, I will call one of the functions in your object that I told you to write. My call to your function will contain the data you are looking for as one of the arguments. {delegate!.userDidEnterInformation(information)} Note: delegate! (asker!) is YOU.
The Delegate (Requesting) Object (First View Controller):
O.K. You've got a deal.