Recently, I have been using React Native for cross-terminal development. As an iOS developer, I have encountered many problems during this period. How to correctly use React to render UI efficiently has always been a challenge. In order to understand the internal working principle of React, I tried to write a simple version of React by referring to some materials. Since the team is pushing Swift this year, I picked up Swift that I forgot before and wrote this Demo again.

React is probably the most valuable front-end technology to learn, and its excellent ideas and architecture are also worth exploring. I hope this Demo can help you understand the operation mechanism of React and broaden our horizons.

Let’s copy React and use Swift to implement a simple version of React.

The Demo download address: https://github.com/superzcj/swift-react-demo

React

React is a Javascript framework for building the user interface, which does nothing but render views.

React’s Virtual DOM mechanism makes UI rendering more efficient. When the interface changes, we can know what the Virtual DOM has changed, so that we can efficiently alter the DOM without redrawing the real DOM.

React is a one-way data stream that does not directly manipulate the DOM. It drives UI updates through the state (data) of the application and has predictability.

React component-based development makes it easier to reuse component code and test it.

Create Element

Following the React implementation, we create an Element tree to describe the UI we want to present. Element is not a real TREE of UI components; it is a virtual object.

We know that manipulating real UI components is expensive and affects performance by creating a virtual Element tree and storing it, creating a new virtual Element tree every time the state changes, comparing it to the old one and rendering the changed parts. This reduces the number of times you need to manipulate real UI components, reducing the burden of UI rendering.

Element is a tree structure, it consists of one node, each node contains two attributes: type: (string | ReactClass) and props: Object. If type is string, which represents a DOM node, and if type is class or function, which represents an Element node, the props might have a children attribute, which is the element node.

Each node corresponds to a UI component node. We create corresponding UI component nodes based on each Element node and set the properties one by one. For simplicity, we have made some changes in the Demo. Type represents the view type, such as UIView and UILabel, frame represents the size and position of the view, prop represents the properties of the view, and children represents the children of the view. The code is as follows:

class ComponentNode {
    var type: String! = nil
    var frame: CGRect! = nil
    var prop: Dictionary<String, Any>? = nil
    var children: [ComponentNode] = []
    
    init(type: String, frame: CGRect, prop: Dictionary<String, Any>, children: [ComponentNode]) {
        self.type = type
        self.frame = frame
        self.prop = prop
        self.children = children
    }
}
Copy the code

Element to render the UI

The next step is to render the Element as a real UI view. We defined the structure of the Element above.

let element = ComponentNode(type: "view", frame: timerFrame, prop: [:], children: [
            ComponentNode(type: "label", frame: textFrame, prop: ["text": "Current time:"], children: []),
            ComponentNode(type: "label", frame: textFrame2, prop: ["text": "\(formatter.string(from: time as Date))"], children: [])
            ])
Copy the code

Description of the UI

</Label> <Label>10:11:00</Label> </View>Copy the code

How is this step from Element to real UI implemented?

We use a createView function, the input parameter is a node ComponentNode, the output parameter is a UIView, the function determines the node type based on the type of ComponentNode, creates the corresponding real view, frame is the layout of the node, Prop store node properties, children put nested child nodes, generate real UI. The code is as follows:

    func createView(node: ComponentNode) -> UIView {
        
        switch node.type {
        case "view":
            letView = UIView(frame: node.frame) view.backgroundColor = UIColor(White: 0.0, alpha: 0.1)return view
            
        case "label":
            let view = UILabel(frame: node.frame)
            view.text = node.prop?["text"] as? String
            return view
            
        case .none:
            return UIView()
        case .some(_):
            return UIView()
        }
        
    }
Copy the code

Let’s put this code together,


class Component {
    public var hostView: UIView!
    public var element: ComponentNode!
    
    init() {
        self.element = self.render()
    }
    
    func renderComponent() {
        let new = self.render()
        self.element = new
        
        let uiView = createView(node: self.element)
        
        for subview in(hostView? .subviews)! { subview.removeFromSuperview() } hostView! .addSubview(uiView) } func render() -> ComponentNode {return ComponentNode(type: "view", frame: CGRect.zero, prop: [:], children: [])
    }
}
Copy the code

We’ve already written rendering from the data-driven UI. Based on this Component, let’s write a demo call to see if it works as expected.


let timerFrame = CGRect(x: 100, y: 100, width: 200, height: 65)
let textFrame = CGRect(x: 60, y: 10, width: 100, height: 20)
let textFrame2 = CGRect(x: 60, y: 30, width: 100, height: 20)

class TimerComponent: Component, ComponentProtocol {
    
    var time = NSDate()
    
    override func render() -> ComponentNode {
        
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm:ss"
        
        return ComponentNode(type: "view", frame: timerFrame, prop: [:], children: [
            ComponentNode(type: "label", frame: textFrame, prop: ["text": "Current time:"], children: []),
            ComponentNode(type: "label", frame: textFrame2, prop: ["text": "\(formatter.string(from: time as Date))"], children: [])
            ])
    }
}
Copy the code

Create a timer to refresh the page periodically and display the current time. The page hierarchy is also relatively simple, a bottom view with two labels on it to display “current time” and time respectively.

class ViewController: UIViewController {
    
    lazy var component: TimerComponent = {
        let component = TimerComponent()
        component.hostView = view
        return component
    }()

    @objc func tick() {
        self.component.time = NSDate()
        self.component.renderComponent()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(tick), userInfo: nil, repeats: true)}}Copy the code

Add this Component to our VC and the code will work as follows.

Diff

In the above code, we only implemented a Demo of a data-driven UI rendering, but we will extend the Demo and give it the diff algorithm to find and render the changed page elements.

First we add an attribute, currentElement, which holds the current node tree. Each time the data changes, the renderComponent function is called, which finds and compares the old and new node trees.

    public var parentView: UIView!
    private var currentElement: ComponentNode?

    func renderComponent() {
        let old = self.currentElement
        let new = self.render()
        let element = reconcile(old: old, new: new, parentView: self.parentView)
        self.currentElement = element
    }
Copy the code

Reconcile is an algorithm that compares the old node tree with the new one. If the old node tree is empty, it is the first rendering and the UI is initialized. Then check whether there is any update, including the type of node view, frame and prop. If there is any update, remove the view of the old node and re-render the new node and its child nodes. Finally, with respect to the child nodes, this function is recursively called to find the updated nodes and re-render.

func reconcile(old: ComponentNode? , new: ComponentNode? , parentView: UIView) -> ComponentNode? {// Render the first time, old is null, initialize the UIifold == nil { instantiate(node: new! , parentView: parentView)return new!
        }
        
        let oldNode = old!
        letnewNode = new! // Compare the old and new nodes, and re-render the new node and its children if there is an updateifoldNode.type ! = newNode.type || oldNode.frame ! = newNode.frame || oldNode.prop ! = newNode.prop {ifoldNode.view ! = nil { oldNode.view? .removeFromSuperview() oldNode.view = nil } instantiate(node: newNode, parentView: parentView)returnNewNode} // Compares child nodes with newNode.children = reconcileChildren(old: oldNode, new: newNode) newNode.view = oldNode.viewreturn newNode
    }
    
Copy the code

Instantiate functions render real views from the node tree, createView actually creates the view, and recursively calls themselves to instantiate the view.

    func instantiate(node: ComponentNode, parentView: UIView) {
        let newView = createView(node: node)
        for index in0.. <node.children.count { instantiate(node: node.children[index], parentView: newView) } parentView.addSubview(newView) node.view = newView }Copy the code

Loop through the child nodes to generate a new view

    func reconcileChildren(old: ComponentNode, new: ComponentNode) -> [ComponentNode] {
        var newChildInstances: [ComponentNode] = []
        for index in0.. <new.children.count {let oldChild = old.children[index]
            let newChild = new.children[index]
            let newChildInstance = reconcile(old: oldChild, new: newChild, parentView: old.view!)
            newChildInstances.append(newChildInstance!)
        }
        return newChildInstances
    }
Copy the code

Remove nodes

The above code does not take into account node deletion. We modify the code to remove the old view from the view hierarchy when new is empty. Filter empty items when the child node compares. The code is as follows:

func reconcile(old: ComponentNode? , new: ComponentNode? , parentView: UIView) -> ComponentNode? {// Render the first time, old is null, initialize the UIifold == nil { instantiate(node: new! , parentView: parentView)return new!
        } else if(new == nil) { old? .view? .removeFromSuperview()return nil
        }

        let oldNode = old!
        letnewNode = new! // Compare the old and new nodes, and re-render the new node and its children if there is an updateifoldNode.type ! = newNode.type || oldNode.frame ! = newNode.frame || oldNode.prop ! = newNode.prop {ifoldNode.view ! = nil { oldNode.view? .removeFromSuperview() oldNode.view = nil } instantiate(node: newNode, parentView: parentView)returnNewNode} // Compares child nodes with newNode.children = reconcileChildren(old: oldNode, new: newNode) newNode.view = oldNode.viewreturn newNode
    }
Copy the code
    func reconcileChildren(old: ComponentNode, new: ComponentNode) -> [ComponentNode] {
        var newChildInstances: [ComponentNode] = []
        let count = max(old.children.count, new.children.count)
        for index in0.. <count {let oldChild = old.children.count > index ? old.children[index] : nil
            let newChild = new.children.count > index ? new.children[index] : nil
            let newChildInstance = reconcile(old: oldChild, new: newChild, parentView: old.view!)
            if newChildInstance != nil {
                newChildInstances.append(newChildInstance!)
            }
        }
        return newChildInstances
    }
Copy the code

Finally, let’s change our Demo to show something different every time we refresh to see how it looks

class TimerComponent: Component {

    var time = NSDate()

    var flag = false

    override func render() -> ComponentNode {

        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm:ss"self.flag = ! self.flagif self.flag {
            return ComponentNode(type: "view", frame: timerFrame, prop: [:], children: [
                ComponentNode(type: "label", frame: textFrame, prop: ["text": "Current time:"], children: []),
                ComponentNode(type: "label", frame: textFrame2, prop: ["text": "\(formatter.string(from: time as Date))"], children: []),
                ComponentNode(type: "label", frame: textFrame3, prop: ["text": "\(formatter.string(from: time as Date))"], children: [])
                ])
        }
        return ComponentNode(type: "view", frame: timerFrame, prop: [:], children: [
            ComponentNode(type: "label", frame: textFrame, prop: ["text": "Current time:"], children: []),
            ComponentNode(type: "label", frame: textFrame2, prop: ["text": "\(formatter.string(from: time as Date))"], children: [])
            ])
    }
}
Copy the code

The Demo download address: https://github.com/superzcj/swift-react-demo

conclusion

In this Demo, we finished describing the UI view with Element, generating the UI from Element rendering, and driving UI changes with data as the center. React updates the UI automatically when the data changes, making it easier to manage and maintain.

In the process of data conversion to UI, according to the Diff algorithm, find out the real updated view and render, also greatly improve the rendering efficiency in the program.

Resources: How to build React Render step-by-step from scratch