前言
上周听了@makezl的插件开发直播介绍之后 萌生了写个插件的想法 目的就是为了解决一直以来让我很纠结的一个东西
Xcode的文件管理窗口的字体不等宽的问题
也就是这个东西
字体不等宽很难受有木有? 以前尝试过用 TinkerTool 但是问题多多
趁着这周有时间 所以花了点时间做了个插件 MMNavigatorFont 来解决这个问题
插件效果大概是这个样子
如何开发插件 这里就不介绍了 喵神的 入门文章 已经很好了
下面介绍一下开发过程中遇到的几个问题以及解决办法
问题
问题1 如何找到需要修改的对象
如图 很明显我是要修改图中每一个控件的字体 但是我如何找到它呢?
首先想到的是监控所有的NSNotification 找出需要的通知
[[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
中有如下两个成员
@interface DVTTableCellView : NSTableCellView
{
...
...
DVTTableCellViewTextField *_titleTextField;
DVTTableCellViewTextField *_subtitleTextField;
...
...
}
这就是我们需要修改字体的NSTextFiled
问题2 如何选择字体
因为不熟悉Cocoa 所以我也在网上一番搜索以及请教了@剑指人心以后 得到了如下的代码
- (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指定一个初始字体才可以
[[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使用起来很简单 比如我在尝试了几次之后 发现在 DVTTableCellViewOneLine
的 awakeFromNib
方法执行之后对字体进行替换是最好的 那么只需要这样写即可
[objc_getClass("DVTTableCellViewOneLine") aspect_hookSelector:@selector(awakeFromNib)
withOptions:AspectPositionAfter
usingBlock:fontBlock
error:nil];
问题4 如何立即预览修改字体的效果
因为钩子是挂在 awakeFromNib
上的 所以当初始化完成之后 便无法再修改字体了 所以当字体发生变化的时候 需要遍历所有的 DVTTableCellViewOneLine
并修改其中的字体
这倒不是难事 关键在于如何保存包含这些 DVTTableCellViewOneLine
的容器 不然每次都要遍历整个Xcode的窗口 效率也未免太低了
经过对结构的观察 发现 IDENavigatorOutlineView
是比较合适保存的 但是 IDENavigatorOutlineView
会因为切换而重新生成 不能保存强引用 所以这里我定义了一个weak的NSView来保存它
@property (nonatomic, weak) NSView *outlineView;
并且在 viewDidMoveToSuperview
中hook住
[objc_getClass("IDENavigatorOutlineView") aspect_hookSelector:@selector(viewDidMoveToSuperview)
withOptions:AspectPositionAfter
usingBlock:controlBarBlock
error:nil];
这样 在修改了字体之后 只要递归遍历其subviews并修改对应字体就好了
- (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
@interface NSView (MMNavigatorFont) @property (nonatomic, strong) NSFont *originalTitleFont; @property (nonatomic, strong) NSFont *originalSubtitleFont; @end
然后在的hook函数中记录一下默认字体即可
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 一样弄个暗色的主题 其实你完全可以自己开发一个插件来做这个事情 说不定还会火哦~