Xcode插件: MMNavigatorFont

前言


上周听了@makezl的插件开发直播介绍之后 萌生了写个插件的想法 目的就是为了解决一直以来让我很纠结的一个东西

Xcode的文件管理窗口的字体不等宽的问题
也就是这个东西

字体不等宽很难受有木有? 以前尝试过用TinkerTool 但是问题多多

趁着这周有时间 所以花了点时间做了个插件MMNavigatorFont来解决这个问题

插件效果大概是这个样子

如何开发插件 这里就不介绍了 喵神的入门文章已经很好了
下面介绍一下开发过程中遇到的几个问题以及解决办法

问题


问题1 如何找到需要修改的对象


如图 很明显我是要修改图中每一个控件的字体 但是我如何找到它呢?

首先想到的是监控所有的NSNotification 找出需要的通知

1
2
3
4
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(allNotitication:)
name:nil
object:nil];

但是找了半天 都没找到 这时候我想 要是有一款跟Reveal功能类似 可以查看OS X上的App结构的工具就好了 不过没找到(有知道的朋友可以推荐一下)

这时我想起了chisel 除了调试iOS的应用 也可以调试OS X的应用 试了一下 果然可以

获取pviews命令打印出来的结构文本 搜索对应的关键字 就能找到我所需的view class了 这里我需要的就是这个DVTTableCellViewOneLine 再根据Xcode的头文件就可以知道 DVTTableCellViewOneLine是基于DVTTableCellView的 而DVTTableCellView中有如下两个成员

1
2
3
4
5
6
7
8
9
@interface DVTTableCellView : NSTableCellView
{
...
...
DVTTableCellViewTextField *_titleTextField;
DVTTableCellViewTextField *_subtitleTextField;
...
...
}

这就是我们需要修改字体的NSTextFiled

问题2 如何选择字体

因为不熟悉Cocoa 所以我也在网上一番搜索以及请教了@剑指人心以后 得到了如下的代码

1
2
3
4
5
6
7
8
9
10
11
- (void)actionChoose
{
[[NSFontManager sharedFontManager] setDelegate:self];
[[NSFontManager sharedFontManager] setTarget:self];
[[NSFontManager sharedFontManager] orderFrontFontPanel:nil];
}

- (void)changeFont:(id)sender
{
self.selectedFont = [sender convertFont:self.selectedFont];
}

但是运行以后却有问题 字体选择框是弹出来了 但是始终获取不到选择的字体 而且changeFont中的sender(即[NSFontManager sharedFontManager])不为nil

经过一番尝试之后发现 必须为NSFontManager指定一个初始字体才可以

1
[[NSFontManager sharedFontManager] setSelectedFont:self.selectedFont?:[NSFont systemFontOfSize:13] isMultiple:NO];

不过这里我仍有一个疑问
在10.11中 NSFontManager的delegate已经被声明为deprecated了 但是我查了官方文档 也没有找到替代的东西 是否有同学知道如何在10.11中正确的使用NSFontManager呢 :)

问题3 如何设置勾子

在cocoa中挂勾子肯定也是要用到Runtime的 这里我学习BBUFullIssueNavigator直接使用Aspects来hook

Aspects使用起来很简单 比如我在尝试了几次之后 发现在DVTTableCellViewOneLineawakeFromNib方法执行之后对字体进行替换是最好的 那么只需要这样写即可

1
2
3
4
[objc_getClass("DVTTableCellViewOneLine") aspect_hookSelector:@selector(awakeFromNib)
withOptions:AspectPositionAfter
usingBlock:fontBlock
error:nil];

问题4 如何立即预览修改字体的效果

因为钩子是挂在awakeFromNib上的 所以当初始化完成之后 便无法再修改字体了 所以当字体发生变化的时候 需要遍历所有的DVTTableCellViewOneLine并修改其中的字体

这倒不是难事 关键在于如何保存包含这些DVTTableCellViewOneLine的容器 不然每次都要遍历整个Xcode的窗口 效率也未免太低了

经过对结构的观察 发现IDENavigatorOutlineView是比较合适保存的 但是IDENavigatorOutlineView会因为切换而重新生成 不能保存强引用 所以这里我定义了一个weak的NSView来保存它

1
@property (nonatomic, weak)   NSView *outlineView;

并且在viewDidMoveToSuperview中hook住

1
2
3
4
5

[objc_getClass("IDENavigatorOutlineView") aspect_hookSelector:@selector(viewDidMoveToSuperview)
withOptions:AspectPositionAfter
usingBlock:controlBarBlock
error:nil];

这样 在修改了字体之后 只要递归遍历其subviews并修改对应字体就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)refreshFont
{
if ( self.outlineView )
{
[self refreshFontInView:self.outlineView];
}
}

- (void)refreshFontInView:(NSView*)view
{
for ( NSView *v in view.subviews )
{
[self refreshFontInView:v];
}

if ( [view isKindOfClass:NSClassFromString(@"DVTTableCellViewOneLine")] )
{
[self applyFont:view];
}
}

问题5 如何还原默认字体

为了怕用户不喜欢修改过的字体 所以我设置了一个启用状态 当用户禁用的时候 会将所有字体还原成默认字体 这里就需要记录一下默认字体 很简单 我用Category为NSView添加了两个property

1
@interface NSView (MMNavigatorFont)

@property (nonatomic, strong) NSFont *originalTitleFont;
@property (nonatomic, strong) NSFont *originalSubtitleFont;

@end

然后在的hook函数中记录一下默认字体即可

1
2
3
4
5
6
7
8
9
NSView *view = info.instance;
if ( !view.originalTitleFont )
{
NSTextField *titleTextFiled = [view valueForKey:@"_titleTextField"];
NSTextField *subtitleTextFiled = [view valueForKey:@"_subtitleTextField"];

view.originalTitleFont = titleTextFiled.font;
view.originalSubtitleFont = subtitleTextFiled.font;
}

好了 至此一个功能完整的插件就完成了 如果你感兴趣 赶紧用一下吧 现在用alcatraz可以搜索得到了

小结


一个功能简单的Xcode插件就这么诞生了 历时一天半的样子 虽然整个过程中也都是在摸石头过河 不过因为cocoa和cocoa touch开发起来确实有很多相似的地方 所以开发起来也不是非常的困难(也是因为功能简单的原因啦) 以后可能还会根据我自己的需求来开发更多的插件 XD

有同学说能不能改变Xcode的颜色 比如像我用的主题Monokai一样弄个暗色的主题 其实你完全可以自己开发一个插件来做这个事情 说不定还会火哦~