Foreword:
The performance optimization based on UIKit seems to have reached a bottleneck. Whether it is using frame proxy snpakit, cache height, reducing layout levels, diff refreshing, compressing pictures, selecting appropriate queues, and selecting high-performance locks, it cannot satisfy the current large and complex project optimization. . The VC loads for a long time every time it is loaded and refreshed, or the collectionView freezes when it is refreshed, I am really at a loss. So, is there a more advanced optimization method than the above? The answer is yes, that is asynchronous drawing.
(For asynchronous drawing content, a better treatment is to choose mature works such as YYText or AsyncKit. This article is limited to the introduction, please do not use the examples directly in the production environment!)
Draw asynchronously:
UIButton
, UILabel
, UITableView
and other spaces are drawn synchronously on the main thread, that is to say, if these controls are complex in business, it may cause the interface to freeze. Fortunately, the system has opened a hole that allows us to draw by ourselves, of course, it is processed on the sub-thread and then returned to the main thread for display.
The general principle is that UIView
as a display class, it is actually responsible for event and touch transmission, and the real class is responsible for display CALayer
. layer
As long as the drawing on the child thread can be controlled , the asynchronous drawing operation is completed.
1. Principle:
Follow the sequence below:
- Inherited from
CALayer
creating an asynchronous Layer. Since the representation of Label is text and the representation of ImageView is image, it can be usedContext
to draw geometric information such as text and pictures. - Create and manage threads dedicated to asynchronous drawing.
- Every time the drawing for a certain control receives the drawing signal (such as setting text, color and other attributes), it will draw.
The rough steps are the above. However, the actual situation will be more complicated, the following is the introduction of each operation.
2. Queue pool:
Regarding the processing of sub-threads, choose here GCD
instead of other multi-threaded classes. The principle of selection is as follows: too many sub-threads continuously switching contexts will obviously bring performance loss, so you can choose an asynchronous serial queue to execute the drawing tasks serially to avoid the overhead caused by frequent context switching.
As for the number of queues, this can be based on the number of cores the processor works on (small cores are not counted). Each queue can roughly set an attribute of the current number of tasks to facilitate finding the lightest queue for the current drawing task to process.
In this way, every time a drawing task comes, one will be taken from the queue pool, and if there is no one, it will be created. When the drawing task is canceled, the number of current tasks in the current queue is given -1
.
code show as below:
Queue pool management class:
import Foundation
final class SGAsyncQueuePool {
public static let singleton: SGAsyncQueuePool = {
SGAsyncQueuePool() }()
private lazy var queues: Array<SGAsyncQueue> = {
Array<SGAsyncQueue>() }()
private lazy var maxQueueCount: Int = {
ProcessInfo.processInfo.activeProcessorCount > 2 ? ProcessInfo.processInfo.activeProcessorCount : 2
}()
/**
Get a serial queue with a balanced rule by `taskCount`.
- Note: The returned queue's sum is under the CPU active count forever.
*/
public func getTaskQueue() -> SGAsyncQueue {
// If the queues is doen't exist, and create a new async queue to do.
if queues.count < maxQueueCount {
let asyncQueue: SGAsyncQueue = SGAsyncQueue()
asyncQueue.taskCount = asyncQueue.taskCount + 1
queues.append(asyncQueue)
return asyncQueue
}
// Find the min task count in queues inside.
let queueMinTask: Int = queues.map {
$0.taskCount }.sorted {
$0 > $1 }.first ?? 0
// Find the queue that task count is min.
guard let asyncQueue: SGAsyncQueue = queues.filter({
$0.taskCount <= queueMinTask }).first else {
let asyncQueue: SGAsyncQueue = SGAsyncQueue()
asyncQueue.taskCount = asyncQueue.taskCount + 1
queues.append(asyncQueue)
return asyncQueue
}
asyncQueue.taskCount = asyncQueue.taskCount + 1
queues.append(asyncQueue)
return asyncQueue
}
/**
Indicate a queue to stop.
*/
public func stopTaskQueue(_ queue: SGAsyncQueue){
queue.taskCount = queue.taskCount - 1
if queue.taskCount <= 0 {
queue.taskCount = 0
}
}
}
Queue model:
final class SGAsyncQueue {
public var queue: DispatchQueue = {
dispatch_queue_serial_t(label: "com.sg.async_draw.queue", qos: .userInitiated) }()
public var taskCount: Int = 0
public var index: Int = 0
}
3. Affairs:
As mentioned above, draw every time a drawing signal comes. However, the drawing is done globally, that is to say, if the x value of the frame may be changed, the entire text content will be redrawn, which is a bit too wasteful of resources. Can these drawing signals be processed at the same time? The answer is the RunLoop cycle. This timing can be placed before the current RunLoop goes to sleep and when it exits.
Another situation is how to handle requests for the same drawing signal? That is to filter the weight, and only execute one. This can put the drawing task inside Set
instead of Array
inside.
Plot a model of task signals:
final fileprivate class AtomicTask: NSObject {
public var target: NSObject!
public var funcPtr: Selector!
init(target: NSObject!, funcPtr: Selector!) {
self.target = target
self.funcPtr = funcPtr
}
override var hash: Int {
target.hash + funcPtr.hashValue
}
}
You can see that the attributes are rewritten here hash
, and the hash of the signal host and the hash of the signal are added together to determine whether it is a repeated task (target is the signal host, funcPtr is the signal).
Register the callback at the specified timing in RunLoop.
final class SGALTranscation {
/** The task that need process in current runloop. */
private static var tasks: Set<AtomicTask> = {
Set<AtomicTask>() }()
/** Create a SGAsyncLayer Transcation task. */
public init (target: NSObject, funcPtr: Selector) {
SGALTranscation.tasks.insert(AtomicTask(target: target, funcPtr: funcPtr))
}
/** Listen the runloop's change, and execute callback handler to process task. */
private func initTask() {
DispatchQueue.once(token: "sg_async_layer_transcation") {
let runloop = CFRunLoopGetCurrent()
let activities = CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.exit.rawValue
let observer = CFRunLoopObserverCreateWithHandler(nil, activities, true, 0xFFFFFF) {
(ob, ac) in
guard SGALTranscation.tasks.count > 0 else {
return }
SGALTranscation.tasks.forEach {
$0.target.perform($0.funcPtr) }
SGALTranscation.tasks.removeAll()
}
CFRunLoopAddObserver(runloop, observer, .defaultMode)
}
}
/** Commit the draw task into runloop. */
public func commit(){
initTask()
}
}
extension DispatchQueue {
private static var _onceTokenDictionary: [String: String] = {
[: ] }()
/** Execute once safety. */
static func once(token: String, _ block: (() -> Void)){
defer {
objc_sync_exit(self) }
objc_sync_enter(self)
if _onceTokenDictionary[token] != nil {
return
}
_onceTokenDictionary[token] = token
block()
}
}
A little trick is used here. There is no oc dispatch_once
thread-safe method that executes only once in swift. Here, a dispatch_once
thread-safe method that is similar to only execute once is constructed with the enter and exit processing of objc_sync.
When the drawing class signals that it needs to draw, it passes to SGALTranscation
create a transaction and then commit()
. commit()
The method is actually a listener that puts the drawing task in Set
and then starts it RunLoop
. Since it is DispatchQueue.once()
a method, RunLoop
callbacks can be created and used with peace of mind.
4.Layer processing:
This is easy to understand, we handle most of the content of the underlying asynchronous drawing layer, and then just implement the drawing class.
import UIKit
import CoreGraphics
import QuartzCore
/**
Implements this protocol and override following methods.
*/
@objc protocol SGAsyncDelgate {
/**
Override this method to custome the async view.
- Parameter layer: A layer to present view, which is foudation of custome view.
- Parameter context: Paint.
- Parameter size: Layer size, type of CGSize.
- Parameter cancel: A boolean value that tell callback method the status it experienced.
*/
@objc func asyncDraw(layer: CALayer, in context: CGContext, size: CGSize, isCancel cancel: Bool)
}
class SGAsyncLayer: CALayer {
/**
A boolean value that indicate the layer ought to draw in async mode or sync mode. Sync mode is slow to draw in UI-Thread, and async mode is fast in special sub-thread to draw but the memory is bigger than sync mode. Default is `true`.
*/
public var isEnableAsyncDraw: Bool = true
/** Current status of operation in current runloop. */
private var isCancel: Bool = false
override func setNeedsDisplay() {
self.isCancel = true
super.setNeedsDisplay()
}
override func display() {
self.isCancel = false
// If the view could responsed the delegate, and executed async draw method.
if let delegate = self.delegate {
if delegate.responds(to: #selector(SGAsyncDelgate.asyncDraw(layer:in:size:isCancel:))) {
self.setDisplay(true)
} else {
super.display()
}
} else {
super.display()
}
}
}
extension SGAsyncLayer {
private func setDisplay(_ async: Bool){
guard let delegate = self.delegate as? SGAsyncDelgate else {
return }
// Get the task queue for async draw process.
let taskQueue: SGAsyncQueue = SGAsyncQueuePool.singleton.getTaskQueue()
if async {
taskQueue.queue.async {
// Decrease the queue task count.
if self.isCancel {
SGAsyncQueuePool.singleton.stopTaskQueue(taskQueue)
return
}
let size: CGSize = self.bounds.size
let scale: CGFloat = UIScreen.main.nativeScale
let opaque: Bool = self.isOpaque
UIGraphicsBeginImageContextWithOptions(size, opaque, scale)
guard let context: CGContext = UIGraphicsGetCurrentContext() else {
return }
if opaque {
context.saveGState()
context.setFillColor(self.backgroundColor ?? UIColor.white.cgColor)
context.setStrokeColor(UIColor.clear.cgColor)
context.addRect(CGRect(x: 0, y: 0, width: size.width, height: size.height))
context.fillPath()
context.restoreGState()
}
// Provide an async draw callback method for UIView.
delegate.asyncDraw(layer: self, in: context, size: size, isCancel: self.isCancel)
// Decrease the queue task count.
if self.isCancel {
SGAsyncQueuePool.singleton.stopTaskQueue(taskQueue)
return
}
guard let image: UIImage = UIGraphicsGetImageFromCurrentImageContext() else {
return }
UIGraphicsEndImageContext()
// End this process.
SGAsyncQueuePool.singleton.stopTaskQueue(taskQueue)
DispatchQueue.main.async {
self.contents = image.cgImage
}
}
} else {
SGAsyncQueuePool.singleton.stopTaskQueue(taskQueue)
}
}
}
The drawing agent at the top only needs to implement the drawing class, and then the drawing class will draw text and other content by itself according to the context
information .size
image
In the custom asynchronous drawing, Layer
rewrite display
the method to context
prepare the object, and then throw the proxy method out for the drawing class to implement. Finally, the asynchronous drawing Layer
gets the operated one and context
returns to the main thread contents
, and the content is displayed. .
5. View implementation class processing:
Using CoreText and other content to draw text will not go into details, just go to the code of Label:
import UIKit
class AsyncLabel: UIView, SGAsyncDelgate {
public var text: String = "" {
didSet {
SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
}
public var textColor: UIColor = .black {
didSet {
SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
}
public var font: UIFont = .systemFont(ofSize: 14) {
didSet {
SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
}
public var lineSpacing: CGFloat = 3 {
didSet {
SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
}
public var textAlignment: NSTextAlignment = .left {
didSet {
SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
}
public var lineBreakMode: NSLineBreakMode = .byTruncatingTail {
didSet {
SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
}
public var attributedText: NSAttributedString? {
didSet {
SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
}
public var size: CGSize {
get {
getTextSize() }
}
override class var layerClass: AnyClass {
SGAsyncLayer.self
}
@objc func drawTask(){
self.layer.setNeedsDisplay()
}
func asyncDraw(layer: CALayer, in context: CGContext, size: CGSize, isCancel cancel: Bool) {
if cancel {
return
}
let size: CGSize = layer.bounds.size
context.textMatrix = CGAffineTransformIdentity
context.translateBy(x: 0, y: size.height)
context.scaleBy(x: 1, y: -1)
let drawPath: CGMutablePath = CGMutablePath()
drawPath.addRect(CGRect(origin: .zero, size: size))
self.attributedText = self.generateAttributedString()
guard let attributedText = self.attributedText else {
return }
let ctfFrameSetter: CTFramesetter = CTFramesetterCreateWithAttributedString(attributedText)
let ctfFrame: CTFrame = CTFramesetterCreateFrame(ctfFrameSetter, CFRange(location: 0, length: attributedText.length), drawPath, nil)
CTFrameDraw(ctfFrame, context)
}
private func generateAttributedString() -> NSAttributedString {
let style: NSMutableParagraphStyle = NSMutableParagraphStyle()
style.lineSpacing = self.lineSpacing
style.alignment = self.textAlignment
style.lineBreakMode = self.lineBreakMode
style.paragraphSpacing = 5
let attributes: Dictionary<NSAttributedString.Key, Any> = [
NSAttributedString.Key.font: self.font,
NSAttributedString.Key.foregroundColor: self.textColor,
NSAttributedString.Key.backgroundColor: UIColor.clear,
NSAttributedString.Key.paragraphStyle: style,
]
return NSAttributedString(string: self.text, attributes: attributes)
}
private func getTextSize() -> CGSize {
guard let attributedText = self.attributedText else {
return .zero }
return attributedText.boundingRect(with: CGSize(width: self.frame.size.width, height: CGFLOAT_MAX),
context: nil).size
}
}
Code for ImageView:
import UIKit
class AsyncImageView: UIView, SGAsyncDelgate {
public var image: UIImage? {
didSet {
SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
}
public var quality: CGFloat = 0.9 {
didSet {
SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit() }
}
override class var layerClass: AnyClass {
SGAsyncLayer.self
}
@objc func drawTask() {
self.layer.setNeedsDisplay()
}
func asyncDraw(layer: CALayer, in context: CGContext, size: CGSize, isCancel cancel: Bool) {
if cancel {
return
}
let size: CGSize = layer.bounds.size
context.textMatrix = CGAffineTransformIdentity
context.translateBy(x: 0, y: size.height)
context.scaleBy(x: 1, y: -1)
guard let image = self.image else {
return }
guard let jpedData: Data = image.jpegData(compressionQuality: self.quality) else {
return }
guard let cgImage = UIImage(data: jpedData)?.cgImage else {
return }
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
}
}
How to use it? How to use UILabel usually, how to use UIIMageView how to use it here, no more details.
performance
When the drawing task is relatively small and light, using system controls such as UILabel is very fast. When the drawing tasks are many and complex, the speed of asynchronous drawing is highlighted. 500 UIImageView displaying 70k pictures is approx 120ms
, while AsyncImageView is 70ms
. But the price is that the memory usage is more than doubled (the memory usage drops to the normal level after the interface stops). This may be a reverse optimization on low-end devices, although the iPhone 14 Pro now has 6GB of memory. But the weak 1GB of the old iPhone 6 also needs to be considered how to deal with it. (Criticize a certain food delivery platform by name here. When I was in school, I opened the app and looked at a hotel when I was using iPhone6s Plus. WeChat was directly killed by the background. I will know why this happened below.)
Asynchronous underlying View processing:
Asynchronous underlying View processing is because I didn't think of how to accurately call this practice, so I will use this awkward name for the time being.
As mentioned above, you can customize the drawing AsyncLabel
, AsyncImageView
, AsyncSwitch
, AsyncButton
and other content, but many asynchronous drawing also have overhead, can you put them in an asynchronous View for processing? The answer is yes, and many companies use it.
A food delivery platform previously open-sourced a project called Graver, and then deleted the library. The general principle is the detailed version of YYText. This general idea is also used here to realize it.
1. Abstract proxy:
Here, objects such as Label, ImageView, and Button are abstracted as models for processing.
import UIKit
protocol NodeLayerDelegate: NSObject {
var contents: (Any & NSObject)? {
set get }
var backgroundColor: UIColor {
set get }
var frame: CGRect {
set get }
var hidden: Bool {
set get }
var alpha: CGFloat {
set get }
var superView: NodeLayerDelegate? {
get }
var paintSignal: Bool {
set get }
func setOnTapListener(_ listerner: (() -> Void)?)
func setOnClickListener(_ listerner: (() -> Void)?)
func didReceiveTapSignal()
func didReceiveClickSignal()
func removeFromSuperView()
func willLoadToSuperView()
func didLoadToSuperView()
func setNeedsDisplay()
}
2. Abstract drawing base class:
Create a low-level drawing base class, all ImageView, Label and other controls can be placed here to perform drawing. Of course, the abstract drawing base class here is based on UIView and then implements the asynchronous drawing agent in the previous chapter.
import UIKit
class NodeRootView: UIView, SGAsyncDelgate {
override class var layerClass: AnyClass {
SGAsyncLayer.self
}
public var subNodes: Array<NodeLayerDelegate> = {
Array<NodeLayerDelegate>() }()
}
// MARK: - Draw Node
extension NodeRootView {
@objc private func drawTask() {
self.layer.setNeedsDisplay()
}
public func addSubNode(_ node: NodeLayerDelegate) {
node.willLoadToSuperView()
self.subNodes.append(node)
SGALTranscation(target: self, funcPtr: #selector(drawTask)).commit()
}
func asyncDraw(layer: CALayer, in context: CGContext, size: CGSize, isCancel cancel: Bool) {
if cancel {
return
}
drawNodeImages(layer: layer, in: context, size: size)
drawNodeLabels(layer: layer, in: context, size: size)
drawNodeButtons(layer: layer, in: context, size: size)
}
private func drawNodeButtons(layer: CALayer, in context: CGContext, size: CGSize) {
let nodes: Array<NodeLayerDelegate> = self.subNodes.filter {
$0.isMember(of: NodeButton.self) }
let nodeButtons: Array<NodeButton> = nodes.map {
$0 as! NodeButton }
nodeButtons.forEach {
button in
let tempFrame: CGRect = CGRect(x: button.frame.minX,
y: layer.bounds.height - button.frame.maxY,
width: button.frame.width,
height: button.frame.height)
let drawPath: CGMutablePath = CGMutablePath()
drawPath.addRect(tempFrame)
UIColor(cgColor: button.backgroundColor.cgColor).setFill()
let bezierPath: UIBezierPath = UIBezierPath(roundedRect: tempFrame, cornerRadius: button.cornerRadius)
bezierPath.lineCapStyle = CGLineCap.round
bezierPath.lineJoinStyle = CGLineJoin.round
bezierPath.fill()
let style: NSMutableParagraphStyle = NSMutableParagraphStyle()
style.lineSpacing = 3
style.alignment = .center
style.lineBreakMode = .byTruncatingTail
style.paragraphSpacing = 5
let attributes: Dictionary<NSAttributedString.Key, Any> = [
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12),
NSAttributedString.Key.foregroundColor: button.textColor,
NSAttributedString.Key.backgroundColor: button.backgroundColor,
NSAttributedString.Key.paragraphStyle: style,
]
let attributedText: NSAttributedString = NSAttributedString(string: button.text, attributes: attributes)
let ctfFrameSetter: CTFramesetter = CTFramesetterCreateWithAttributedString(attributedText)
let cfAttributes: CFDictionary = [
kCTFrameProgressionAttributeName: CTFrameProgression.topToBottom.rawValue as CFNumber
] as CFDictionary
let ctfFrame: CTFrame = CTFramesetterCreateFrame(ctfFrameSetter, CFRange(location: 0, length: attributedText.length), drawPath, cfAttributes)
// let line = CTLineCreateWithAttributedString(attributedText)
// let offset = CTLineGetPenOffsetForFlush(line, 0.5, button.frame.width)
// var ascent: CGFloat = 0, descent: CGFloat = 0, leading: CGFloat = 0
// CTLineGetTypographicBounds(line, &ascent, &descent, &leading)
// let lineHeight = ascent + descent + leading
context.textPosition = CGPoint(x: button.frame.width, y: (button.frame.height - 10)/2.0)
// CTLineDraw(line, context)
CTFrameDraw(ctfFrame, context)
button.didLoadToSuperView()
}
}
private func drawNodeLabels(layer: CALayer, in context: CGContext, size: CGSize) {
let nodes: Array<NodeLayerDelegate> = self.subNodes.filter {
$0.isMember(of: NodeLabel.self) }
let nodeLabels: Array<NodeLabel> = nodes.map {
$0 as! NodeLabel }
nodeLabels.forEach {
label in
let tempFrame: CGRect = CGRect(x: label.frame.minX,
y: layer.bounds.height - label.frame.maxY,
width: label.frame.width,
height: label.frame.height)
let drawPath: CGMutablePath = CGMutablePath()
drawPath.addRect(tempFrame)
let style: NSMutableParagraphStyle = NSMutableParagraphStyle()
style.lineSpacing = 3
style.alignment = .left
style.lineBreakMode = .byTruncatingTail
style.paragraphSpacing = 5
let attributes: Dictionary<NSAttributedString.Key, Any> = [
NSAttributedString.Key.font: label.font,
NSAttributedString.Key.foregroundColor: label.textColor,
NSAttributedString.Key.backgroundColor: label.backgroundColor,
NSAttributedString.Key.paragraphStyle: style,
]
let attributedText: NSAttributedString = NSAttributedString(string: label.text, attributes: attributes)
let ctfFrameSetter: CTFramesetter = CTFramesetterCreateWithAttributedString(attributedText)
let ctfFrame: CTFrame = CTFramesetterCreateFrame(ctfFrameSetter, CFRange(location: 0, length: attributedText.length), drawPath, nil)
CTFrameDraw(ctfFrame, context)
label.didLoadToSuperView()
}
}
private func drawNodeImages(layer: CALayer, in context: CGContext, size: CGSize) {
let nodes: Array<NodeLayerDelegate> = self.subNodes.filter {
$0.isMember(of: NodeImageView.self) }
let nodeImageViews: Array<NodeLayerDelegate> = nodes.map {
$0 as! NodeImageView }
let size: CGSize = layer.bounds.size
context.textMatrix = CGAffineTransformIdentity
context.translateBy(x: 0, y: size.height)
context.scaleBy(x: 1, y: -1)
nodeImageViews.forEach {
if let image = $0.contents as? UIImage, let cgImage = image.cgImage {
context.draw(cgImage, in: $0.frame)
}
}
}
}
// MARK: - Touch Process
extension NodeRootView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch: UITouch = event?.touches(for: self)?.first else {
return }
let touchPoint: CGPoint = touch.location(in: self)
for node in self.subNodes {
let isInX: Bool = touchPoint.x >= node.frame.minX && touchPoint.x <= node.frame.maxX
let isInY: Bool = touchPoint.y >= node.frame.minY && touchPoint.y <= node.frame.maxY
if isInX && isInY {
node.didReceiveTapSignal()
break
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch: UITouch = event?.touches(for: self)?.first else {
return }
let touchPoint: CGPoint = touch.location(in: self)
for node in self.subNodes {
let isInX: Bool = touchPoint.x >= node.frame.minX && touchPoint.x <= node.frame.maxX
let isInY: Bool = touchPoint.y >= node.frame.minY && touchPoint.y <= node.frame.maxY
if isInX && isInY {
node.didReceiveClickSignal()
break
}
}
}
}
In drawTask
the method, different processing is performed according to different types of drawing objects, such as ImageView drawing image, Label drawing text, Button drawing text and processing gestures.
The idea of gesture processing here is: find the currently clicked point, then iterate the current subNodes to see if the point is within the frame of the node, and if so, call the gesture signal. Tap analogy UIControl.touchUp
, click analogy UIControl.touchUpInside
.
3. View implementation class:
Take ImageView
for example:
import UIKit
class NodeImageView: NSObject, NodeLayerDelegate {
var contents: (Any & NSObject)?
var backgroundColor: UIColor = .white
var frame: CGRect = .zero
var hidden: Bool = false
var alpha: CGFloat = 1.0
var superView: NodeLayerDelegate?
var paintSignal: Bool = false
private var didReceiveTapBlock: (() -> Void)?
private var didReceiveClickBlock: (() -> Void)?
func setOnTapListener(_ listerner: (() -> Void)?) {
didReceiveTapBlock = {
listerner?()
}
}
func setOnClickListener(_ listerner: (() -> Void)?) {
didReceiveClickBlock = {
listerner?()
}
}
func didReceiveTapSignal() {
didReceiveTapBlock?()
}
func didReceiveClickSignal() {
didReceiveClickBlock?()
}
func removeFromSuperView() {
}
func willLoadToSuperView() {
}
func didLoadToSuperView() {
}
func setNeedsDisplay() {
}
}
use
This time is a little different from the previous chapter.
class NodeCell: UITableViewCell {
lazy var nodeView: NodeRootView = {
let view = NodeRootView()
view.frame = CGRect(x: 0, y: 100, width: kSCREEN_WIDTH, height: 100)
return view
}()
lazy var nodeLabel: NodeLabel = {
let label = NodeLabel()
label.text = "Node Label"
label.frame = CGRect(x: 118, y: 10, width: 100, height: 20)
return label
}()
lazy var nodeTitle: NodeLabel = {
let label = NodeLabel()
label.text = "Taylor Swift - <1989> land to Music."
label.frame = CGRect(x: 118, y: 100 - 10 - 20, width: 200, height: 20)
return label
}()
lazy var nodeImageView: NodeImageView = {
let imageView = NodeImageView()
imageView.frame = CGRect(x: 10, y: 10, width: 80, height: 80)
imageView.contents = UIImage(named: "taylor")
imageView.setOnClickListener {
Log.debug("click node imageView")
}
return imageView
}()
lazy var nodeButton: NodeButton = {
let button = NodeButton()
button.text = "Buy"
button.backgroundColor = .orange
button.textColor = .white
button.frame = CGRect(x: kSCREEN_WIDTH - 60, y: 65, width: 40, height: 19)
button.setOnClickListener {
Log.debug("Buy")
}
return button
}()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
self.contentView.addSubview(nodeView)
self.nodeView.addSubNode(nodeLabel)
self.nodeView.addSubNode(nodeImageView)
self.nodeView.addSubNode(nodeTitle)
self.nodeView.addSubNode(nodeButton)
}
}
Since NodeImageView
wait is not UIView
a subclass of , so we can't addSubview
(), we can only use our base class to addSubNode
().
It can be seen that the layout level here is very shallow, which is very suitable for Caton optimization.
However, the optimization of this idea is not a panacea, and it consumes more memory than the asynchronous drawing of a single control in the previous chapter. Moreover, functions such as Label and ImageView must be consistent with the system, otherwise a more complicated business requirement will directly veto this thing. Such as animation and snapkit can't be used, and can only be processed with static content.