Xcode 懒加载生成插件

前言

关于 iOS 架构方面的总结,Casatwy写了一系列的文章。其中关于View代码结构的规定。大致意思是这样的:

pic1

不要在viewDidLoad里面初始化你的view然后再add,这样代码就很难看。在viewDidload里面只做addSubview的事情。属性的初始化交给getter去做。比如这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma mark - life cycle
- (void)viewDidLoad
{
[super viewDidLoad];

self.view.backgroundColor = [UIColor whiteColor];
[self.view addSubview:self.firstTableView];
[self.view addSubview:self.secondTableView];
[self.view addSubview:self.firstFilterLabel];
[self.view addSubview:self.secondFilterLabel];
[self.view addSubview:self.cleanButton];
[self.view addSubview:self.originImageView];
[self.view addSubview:self.processedImageView];
[self.view addSubview:self.activityIndicator];
[self.view addSubview:self.takeImageButton];
}

这样即便在属性非常多的情况下,还是能够保持代码整齐,view的初始化都交给getter去做了。总之就是尽量不要出现以下的情况:

1
2
3
4
5
6
7
8
9
10
11
- (void)viewDidLoad
{
[super viewDidLoad];

self.textLabel = [[UILabel alloc] init];
self.textLabel.textColor = [UIColor blackColor];
self.textLabel ... ...
self.textLabel ... ...
self.textLabel ... ...
[self.view addSubview:self.textLabel];
}

这种做法就不够干净,都扔到getter里面去就好了。

我一直在项目中实践这种做法。但是其中懒加载的代码写起来耗时、容易犯错且有迹可循的。所以萌生了使用插件生成这些代码的想法。

网上找到了两个现成的插件,Xcode -AutoLazyLoadAMEGetterMaker,但是和预期的效果有点不太一致,主要体现在:

  1. 对于常见的类型,比如UILabel,我希望生成一些常用属性属性模版。比如_label.font = ...
  2. 对于为空的判断,我个人习惯使用 == nil 而不是取非 :(!_label)

所以,我决定 造轮子

Creating a Source Editor Extension

得益于Apple提供的Xcode 插件API 过于简陋 简单明了,很容易就能实现一个插件。

参考 Creating a Source Editor Extension

首先打开 Xcode 新建一个 macOS 项目,然后添加一个 source editor extension target 到项目,

这里苹果提供了一个使代码倒叙的插件的实现 :

1
2
3
4
5
6
7
8
9
10
11
12
class SourceEditorCommand: NSObject, XCSourceEditorCommand {
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
// Retrieve the contents of the current source editor.
let lines = invocation.buffer.lines
// Reverse the order of the lines in a copy.
let updatedText = Array(lines.reversed())
lines.removeAllObjects()
lines.addObjects(from: updatedText)
// Signal to Xcode that the command has completed.
completionHandler(nil)
}
}

进入 XCSourceEditorCommandInvocation 的头文件,我们可以看到,其实只有一个关键的属性:

1
var buffer: XCSourceTextBuffer { get }

文档说明为 :

The buffer of source text upon which the command can operate.

即 可以操作的源文本缓冲区。

该属性又包含两个重要的,下面要用到的属性

1
2
3
var lines: NSMutableArray { get }

var selections: NSMutableArray { get }

通过它们,我们可以当前编辑器的所有代码,以及选中的行数。

整个插件的流程是这样的:

  1. 通过 selections 找到选中的代码,
  2. 找到选中代码的的属性的类名和属性名,
  3. 生成懒加载代码,
  4. 查到合适的位置插入到 lines 数组中。

实现如下:

1
2
3
4
5
6
7
8
//通过 selections 找到选中的代码
func getSelectLinesWith(_ selection:XCSourceTextRange,lines: [String]) -> [String] {
var result = [String]()
for i in selection.start.line ... selection.end.line {
result.append(lines[i])
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 找到选中代码的的属性的类名和属性名
func parseLineString(_ lineString: String) -> (className: String, propertyName: String)?{

let lineContent = lineString.replacingOccurrences(of:" ", with:"", options: .literal, range: nil)//去除空格

guard lineContent.hasPrefix("@property") else {
return nil
}

var className:String?
var propertyName:String?

let classNameRes = classNameRegex.matches(in: lineContent, options: .reportCompletion, range: NSRange(location: 0,length: lineContent.count))
if classNameRes.count != 0 {
className = (lineContent as NSString).substring(with: classNameRes.first!.range)
}
let propertyNameRes = propertyNameRegex.matches(in: lineContent, options: .reportCompletion, range: NSRange(location: 0,length: lineContent.count))
if propertyNameRes.count != 0 {
propertyName = (lineContent as NSString).substring(with: propertyNameRes.first!.range)
}
guard let classNameResult = className,let propertyNameResult = propertyName else {
return nil
}
// print("className = " + classNameResult!)
// print("propertyName = " + propertyNameResult!)
return(classNameResult,propertyNameResult)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//生成懒加载代码
func generateLazyLoadString(className: String, propertyName: String) -> String {


if let tempString = lazyLoadStringTemp[className]{
return tempString.replacingOccurrences(of: "propertyName", with: propertyName)
}


let otherTempString = #"""
-(ClassName *)propertyName{
if (_propertyName == nil) {
_propertyName = [ClassName new];
}
return _propertyName;
}
"""#
var result = otherTempString.replacingOccurrences(of: "ClassName", with: className)
result = result.replacingOccurrences(of: "propertyName", with: propertyName)
return result;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 查到合适的位置插入到  lines 数组中。
//Bool值 mark 表示是否写了 //MARK: Lazy Load 如果有,添加在其下方
func findInsertIndex(currentEndLine:Int,lines: [String]) -> (lineIndex: Int,mark: Bool) {
var flag = false //当查找到第二个 @end 时,返回其行数
for index in currentEndLine ..< lines.count {
let lineString = lines[index]

if lineString.contains("//MARK: Lazy Load"){
print("index = \(index)")
let resultIndex = index < lines.count - 1 ? index + 1 : index //插入在mark 下面
return (resultIndex,true)
}

if lineString.contains("@end"){
if flag{
return (index,false)
}else{
flag = true
}
}
}
return (lines.count - 1,false)
}

以上即插件的核心代码。

我在项目中添加了一个 LazyLoadTemp.plist 文件, 用户储存常见类型的模版。

key对应类型名,value 为模版文件。

生成懒加载代码的方法逻辑为:LazyLoadTemp.plist 中存在的类型,使用 LazyLoadTemp.plist 中的代码,替换属性名。 LazyLoadTemp.plist没有的类型,使用通用模版。

UILabel 为例:

其模版为 :

1
2
3
4
5
6
7
8
9
10
- (UILabel *)propertyName{
if (_propertyName == nil) {
_propertyName = [UILabel new];
// _propertyName.text = @"<#labelText#>";
// _propertyName.textColor = <#[UIColor redColor]#>;
// _propertyName.font = [UIFont systemFontOfSize:<#17#>];
// _propertyName.numberOfLines = <#0#>;
}
return _propertyName;
}

常用的属性只需要打开注释简单修改。

Complete

完整的代码开源在Github

lazyload

End

为什么会用Swift写一个生成Objective-C代码的插件?

因为官方的文档Demo都只有Swift版本,拥抱Swift刻不容缓。

参考内容

  1. 正则表达式30分钟入门教程