Swift

Understanding how Xibs work in detail.

Have a good day everyone, hope you’re doing well in this pandemic.
In today article, I’m going to talk about Xibs in detail because I’ve seen many developers use it but not very understand the concept and how it works behind the scenes. So in this article we’re going to explore everything ok ?
As a disclaimer this post gonna be very long, so sit tight and have yourself a cup of coffee.

Before jump into code make sure that you’re opening the starter project by downloading it in the link I put in the description bellow and then you’re good to go.

https://github.com/LearnWithTung/XibsPractice

Overview

So what do we have in the project, let’s take a look at the CustomViews folder, I’ve created two different custom views which are very simple because we don’t need it to be complicated in this case, the first view called PlayerView and the second one is ResultView.

The important thing I want to mention is how those views were setting up.

With ResultView.xib I set its File’s Owner as ResultView class.

leave custom class empty
set File’s Owner as ResultView

Vice versa to the ResultView, I set PlayerView.xib I set Custom Class as PlayerView.

set Custom Class of PlayerView
Leave File’s Owner of PlayerView empty

Lastly open the Main.stoyboard you can see that I’ve added these two custom view into the view controller. Let’s run the app for the first time to see what happen.

Well, our custom views don’t show up as we…might expect but why is that ? Let me discuss in next sections.

Using File’s Owner

Let’s talk about ResultView first. I think the best way to explain is to make it work and then discuss how. So open ViewController.swift and add this code as result below and then run our application again.

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // MARK: - File's Owner
        // 1
        print("Before Operation")
        // 2
        let contentView = UINib(nibName: "ResultView", bundle: nil).instantiate(withOwner: resultView, options: nil).first as! UIView
        // 3
        resultView.addSubview(contentView)
        contentView.frame = resultView.bounds
    }

Here’s what we got so far:

It works now. So let’s discuss about the snippet code above. Basically at (1) which is when view’s controller is loaded we’ve already had the resultView instance but just the instance alone not including outlets or target actions that why at first we only see the purple view but not its content. You can check it by set a breakpoint at the print line bellow // 1. Then the reason why we see as above is all because of (2). So when you call:

UINib(nibName: "ResultView", bundle: nil).instantiate(withOwner: resultView, options: nil)

Two things gonna happen:

  1. The ResultView.xib will be unarchived and all top-levels will be loaded. Therefore we will have all outlets and targets now.
top-levels view will be loaded

2. By name resultView as an owner and set File’s Owner as ResultView in the xib file. The outlets and targets loaded above will be assigned to the resultView instance.

Therefore at this stage if you set a breakpoint and check you’ll see that correctsLabel is not nil anymore 🥳. Notice that I said “all top-level views” not just one that’s why instantiate(withOwner:options:) returns a list of Any. Step (3) is fairly simple to understand, isn’t it ? When we have Content View, we add it as a subview of the purple view

Experiment 1: Set the owner to nil

That is how it’s how to make it work but it isn’t satisfy me so let’s make some fun. Replace // 2 by this line and run our app again.

        let contentView = UINib(nibName: "ResultView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! UIView

What do you have ? a crash, right ? the system tells us that it cannot find any key value for the key correctsLabel.

Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[<NSObject 0x600000e28230> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key correctsLabel.’

terminating with uncaught exception of type NSException.

If we think about this crash description a little bit you’ll see that it makes sense, right ? As I explained above when ResultView.xib be unarchived it also check if we set File’s Owner or not. and in the case of ResultView.xib we do have set. So it conflicts with the line of code above (we set the owner to nil) that’s why the app crashes.

And of course if we try to assign any instance of any other type but ResultView we also get a crash.

Experiment 2: Cast contentView as ResultView

Next thing I want to try is cast the let variable to resultView and see what’s happen. So replace the line with this:

UINib(nibName: "ResultView", bundle: nil).instantiate(withOwner: self, options: nil).first as! ResultView

What do you think it would be ? Run the app and we got a crash with a message:

Could not cast value of type ‘UIView’ (0x7fff86f664a0) to ‘XibsPractice.ResultView’ (0x105c87158).

Your homework is find out why 😉 when you find out feel free to comment down bellow in the commend section.

Improvement

We can make our code a little bit cleaner by moving the code inside ResultView. Remove all the code we just added to viewDidLoad and in ResultView.swift we do this then we’ll be fine.

required init?(coder: NSCoder) {
        super.init(coder: coder)
        
        customInit()
    }
    
    private func customInit(){
        let contentView = UINib(nibName: "ResultView", bundle: nil).instantiate(withOwner: self, options: nil).first as! UIView
        addSubview(contentView)
        contentView.frame = self.bounds
    }

You also can create ResultView with frame by override init(frame:) function inside ResultView.swift like this:

override init(frame: CGRect) {
        super.init(frame: frame)
        
        customInit()
    }

And now you can create ResultView programmatically. Example:

ResultView(frame: .init(x: 0, y: 0, width: 100, height: 100))

Using Custom Class

Move on to the PlayerView, the view we’ve set custom class for it and leaved File’s owner empty. Try to do the same thing as we did with the ResultView. Add this code to PlayerView and run the app.

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        
        customInit()
    }
    
    private func customInit(){
        let contentView = UINib(nibName: "PlayerView", bundle: nil).instantiate(withOwner: self, options: nil).first as! UIView
        addSubview(contentView)
        contentView.frame = self.bounds
    }

Well, it crashes ! How ? If you open debug navigator you’ll notice that’s because the execution falls into a infinite run-loop.

That’s because when you add a view as child view in storyboard then init?(coder: NSCoder) function of the child view will be called and when the xib be unarchived the init?(coder: NSCoder) will be called again turns out it create a circle cause the crash:

Tip 1

Do not call instantiate xib from its init function

So what we can do is moving xib instantiate one level above which means call it in PlayerView’s parent view. So first, remove customInit() function in PlayerView and open ViewController and add this code:

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // MARK: - Custom Class
        print("Before Operation")
        let contentView = UINib(nibName: "PlayerView", bundle: nil).instantiate(withOwner: playerView, options: nil).first as! UIView
        playerView.addSubview(contentView)
        contentView.frame = playerView.bounds
        print("After Operation")
        
    }

Run the app:

It seems work well. But let’s put a breakpoint at the print("After Operation") and check we will see playerView’s outlets still are nil 🤔

That’s because we don’t set File’s Owner for PlayerView so what’s the point of setting custom class ? If you think a bit deeper you might notice that the line:

let contentView = UINib(nibName: "PlayerView", bundle: nil).instantiate(withOwner: playerView, options: nil).first as! UIView

not just returns us a view, moreover it’s a PlayView’s instance so replace that line with this:

let contentView = UINib(nibName: "PlayerView", bundle: nil).instantiate(withOwner: playerView, options: nil).first as! PlayerView

and then check contentView’s outlets you will get it.

Tip 2

If you only set custom class for your xib, you can only create it programmatically.

View’s size

One more thing I want to mention in this article is the view’s size when you load it. Open ViewController.swift and change View As device to iPhone 8 Plus which has screen size is (414 width, 736 height) then select iPhone 12 pro max or what ever simulator you want but not iPhone 8 Plus and then check the width of our custom views. It’s 414 points different to iPhone 12 pro max which is 428 points.

So be careful if you want to get frame of custom view loaded from xib you should get it in right place. Where ? Well, You can override viewDidLayoutSubviews .

But you might wondering why it can still be filled the purple view and the pink view. That’s because the xib automatically enable self sizing mechanism by default.

Conclusion

What a long article. I hope this will help you guys get fascinating insight into how xibs work and make you more confident when using it.

Please feel free to ask any questions for me and don’t forget to share to your friends if feel helpful. See you !

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *