XCode Live Rendering from Nib

Giới thiệu Tính năng Live Rendering được Apple giới thiệu ở WWDC14 cùng với Xcode 6. Trước đây khi ta kéo thả một custom view lên trên storyboard để design thì xcode chỉ có thể hiển thị 1 view trắng thay vì hiển thị giao diện của customview đó, giao diện custom view này chỉ được hiển thị khi run app do đó sẽ khó khăn khi thiết kế giao diện cho app. Theo Apple đưa ra thì tính năng live rendering này mục đích để dùng cho các giao diện UI được tạo ra bằng code, những phần giao diện trong code sẽ được thể hiện ngay trên giao diện storyboard thay vì phải build run mới xem được giao diện như trước đây. Điều này không thực sự thoả mãn được nhu cầu của các developer, khi mà trong dự án nhiều khi chúng ta mong muốn có thể sử dụng được cả các file Nibs để kéo thả trên các Storyboard khác nhau, và giao diện của các file Nibs này cũng được phản ánh lên storyboard ngay trong lúc design. Do đó trong bài này mình sẽ hướng dẫn cách để có thể tạo 1 file custom giao diện Nib có thể live rendering trên Storyboard .

Nibs Chúng ta sẽ tạo 1 file CustomView Xib gồm 1 UIImageView, 1 UILabel để sử dụng lại trong Storyboard như hình dưới:

Trước tiên ta sẽ tạo 2 files: CustomView.Swift, CustomView.Xib, đặt class cho View của CustomView.xib là CustomView.swift Kéo thả UIImageView, UILabel vào CustomView.Xib và layout cho nó, tạo IBOutlet Tạo hàm set giá trị cho UIImageView, UILabel :

import UIKit
class CustomView: UIView {
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    public var title: String = "" {
        didSet {
            self.titleLabel.text = title
        }
    }
    public var avatarImage: UIImage = UIImage() {
        didSet {
            self.imageView.image = self.avatarImage
        }
    }
}

Chúng ta cần 1 hàm load file nib trong code, ở đây ta sẽ sử dụng hàm NSBundle(forClass:) thay vì sử dụng hàm NSBundle.mainBundle()

let bundle = Bundle.init(for: type(of: self))
var view = bundle.loadNibNamed("CustomView", owner: nil, options: nil)?[0] as? CustomView

Đã có hàm load file nib, bây giờ ta cần kết nối file nib này với Interface Builder, khi Interface Builder khởi tạo view nó gọi các method sau lần lượt init(coder:) - được gọi khi khởi tạo file Nib awakeFromNib() - gọi mỗi lần 1 item trong file nib được load. Ta sẽ override hàm init(coder:) để load custom view của chúng ta từ file nib sau đó return nó. Trong trường hợp nếu view có số lượng subviews = 0 có nghĩ là CustomView.xib chưa được tạo ra

import UIKit

class CustomView: UIView {
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!

    public var title: String = "" {
        didSet {
            self.titleLabel.text = title
        }
    }

    public var avatarImage: UIImage = UIImage() {
        didSet {
            self.imageView.image = self.avatarImage
        }
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        if self.subviews.count == 0 {
            let bundle = Bundle.init(for: type(of: self))
            var view = bundle.loadNibNamed("CustomView", owner: nil, options: nil)?[0] as! CustomView
            view.frame = self.bounds
            view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            
            self.proxyView = view
            self.addSubview(self.proxyView!)
        }
    }
}

CustomView đã được load lên từ nib khi ta chạy app, tuy nhiên mục đích chúng ta đang cần bây giờ là có thể live rendering CustomView này trên storyboard, ta add thuộc tính @IBDesignable , @IBInspectable như code bên dưới

import UIKit

@IBDesignable
class CustomView: UIView {
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!

    @IBInspectable public var title: String = "" {
        didSet {
            self.titleLabel.text = title
        }
    }

    @IBInspectable public var avatarImage: UIImage = UIImage() {
        didSet {
            self.imageView.image = self.avatarImage
        }
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        if self.subviews.count == 0 {
            let bundle = Bundle.init(for: type(of: self))
            var view = bundle.loadNibNamed("CustomView", owner: nil, options: nil)?[0] as! CustomView
            view.frame = self.bounds
            view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            
            self.proxyView = view
            self.addSubview(self.proxyView!)
        }
    }
}

Live Rendering không khởi tạo file Nib bằng hàm init(coder:), mà nó sử dụng hàm init(frame:), do đó ta cần override lại hàm init(frame:)


init(frame: CGRect) {
    super.init(frame: frame)
    let bundle = Bundle.init(for: type(of: self))
    var view = bundle.loadNibNamed("CustomView", owner: nil, options: nil)?[0] as! CustomView
    view.frame = self.bounds
    view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    self.addSubview(view)
}

Cấu trúc CustomView của chúng ta sẽ như sau:

  • CustomView CustomView UIImageView UILabel

Để có thể làm việc với các public properties thì ta cần tạo reference tới CustomView được load

private var proxyView: CustomView?

Thay đổi hàm didSet như sau:

didSet {
    if let optionalView = self.proxyView {
        optionalView.titleLabel.text = title
    }
    else {
        self.titleLabel.text = title
    }
}

Toàn bộ file code như sau:

import UIKit

@IBDesignable

class CustomView: UIView {
    private var proxyView: CustomView?
    
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    
    @IBInspectable var title: String = "" {
        didSet {
            if let optionalView = self.proxyView {
                optionalView.titleLabel.text = title
            }
            else {
                self.titleLabel.text = title
            }
        }
    }
    
    @IBInspectable var image: UIImage = UIImage() {
        didSet {
            if let optionalView = self.proxyView {
                optionalView.imageView.image = image
            } else {
                self.imageView.image = image
            }
        }
    }
    
    override func awakeAfter(using aDecoder: NSCoder) -> Any? {
        return self
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        let bundle = Bundle.init(for: type(of: self))
        var view = bundle.loadNibNamed("CustomView", owner: nil, options: nil)?[0] as! CustomView
        view.frame = self.bounds
        view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        
        self.proxyView = view
        self.addSubview(self.proxyView!)
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        if self.subviews.count == 0 {
            let bundle = Bundle.init(for: type(of: self))
            var view = bundle.loadNibNamed("CustomView", owner: nil, options: nil)?[0] as! CustomView
            view.frame = self.bounds
            view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            
            self.proxyView = view
            self.addSubview(self.proxyView!)
        }

    }
}

Mở storyboard, bây giờ ta sẽ thấy CustomView có thể live rendering, ta có thể thay đổi ảnh, title bằng cách thay đổi các inspectable properties trên xcode. Storyboard hiển thị 2 live rending view : Notes: Khi làm việc với CustomView.xib Interface Builder sẽ tự động build để live render view này, điều này có nghĩa là bất cứ thấy đổi nào cũng sẽ bị delay 1 khoảng thời gian nào đó để build lại cái live rendering này, do đó khi trước khi ta thay đổi tới file nib này nên remove @IBDesignable trước, thay đổi xong lại đặt lại @IBDesignable