7

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

Latenec
  • 408
  • 6
  • 21
  • 3
    You should never access an outlet from a different view controller. If you think you need to, that’s the problem and something else is very wrong with your architecture. – matt Nov 15 '17 at 14:34
  • 1
    Why would you want to mark something private when you want to access it from other viewController? :D That sounds really funny to me. Why don't you create property accessable from another VC on which you call didSet { }and then from withing set the property to your label inside the viewController? – Dominik Bucher Nov 15 '17 at 14:37
  • @matt What do you mean? If I have TableViewController and TableViewCell, you want me to include all information in TableViewController that is related to the TableViewCell? I mean no extracts ? – Latenec Nov 15 '17 at 14:37
  • Methods can be public. Outlets should be private. Moreover, model is not view, so you would have no business setting the value of a cell's label. You should be telling the table view controller to change its _model_; the label is updated when the table is reloaded. You have completely failed to understand how table views work. – matt Nov 15 '17 at 15:41
  • @DominikBucher actually your answer helped me more and I think it is the best option from here. Could you please write it down as an answer and I will assign to that the right one. Thank you! – Latenec Nov 15 '17 at 23:38
  • Thank you, I was weiting down whole article with explanation but I gave up since maybe rhe accepted answer answers this okay... I will write this down :) – Dominik Bucher Nov 15 '17 at 23:42
  • updated, please write comments under my answer as you feel :) – Dominik Bucher Nov 16 '17 at 00:08

3 Answers3

11

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!

Bryan
  • 4,628
  • 3
  • 36
  • 62
Dominik Bucher
  • 2,120
  • 2
  • 16
  • 25
  • Thank you so much for explaining me with the deep details and examples! – Latenec Nov 16 '17 at 00:46
  • 2
    I asked this question because I wanted to understand for safety reasons if I should make the IBOutlets private. One developer returned my code back, because my IBOutlet wasn't private and he told me that I should use a methods, which can be used for passing the data. If I do like this I will avoid the fact that someone will access to the cell and change any of cells properties. – Latenec Nov 16 '17 at 00:47
  • @matt then write tests for the viewController and its expected behavior without accessing the labels and check their statuses – Dominik Bucher Mar 02 '19 at 14:38
  • Yes, that's a pity. You'd think `@testable` would overcome that one; I wonder if that's worth filing a bug report on? Being forced to violate good privacy practice in order to be testable is not very nice. – matt Mar 02 '19 at 15:02
6

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.

DanielH
  • 685
  • 5
  • 6
0

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.

erik_m_martens
  • 496
  • 3
  • 8