青芒 for Mac客户端开发笔记

4,587 阅读11分钟

关于轻芒和青芒

轻芒阅读是我每天都会打开的app,在它提供的两百多个channel中,我订阅了其中十几个channel,非常喜欢这个产品。这个产品的创始人是王俊煜,他曾经创办了一个更有名的公司——豌豆荚。关于轻芒和俊煜的更多介绍,请参照: 前沿科技的报道。虽然报道里强调了轻芒不是青芒,但是参照知了和知乎的关系,我还是决定把我的客户端取名为青芒。而且取名青芒的一大好处是logo好设计。做完原型的第二天,我就找了非著名设计师Joseph帮我设计了一枚logo,个人觉得还是很赞的。

非著名设计师Joseph设计的logo
非著名设计师Joseph设计的logo

非著名设计师Joseph设计的logo

为什么要做青芒

8月18日,我在github闲逛的时候发现了轻芒团队仍处于内测阶段的API文档,抱着试一试的态度,我给轻芒团队发送了内测申请的邮件,两天后得到了对方产品经理振辉的回复,同意了我参加内测的申请,在这表示感谢。收到邮件之后我兴奋异常,虽然之前也做了像微博、twitter、Instagram的客户端,但是这些产品早已有比我做的更好的产品上架到Apple Store了,估计除了我以外,没有人会用我开发的简陋版客户端。而轻芒则不同,目前还真没有mac和windows的客户端。

轻芒的用户通常对精神生活有丰富的追求,在macOS和windows中,我估计macOS用户会非常多,因此我选择了macOS平台,我认为这对我是一个机会,可能会有不少人试用这款软件。毕竟看到别人电脑或手机上运行着你开发的软件,还是会给人带来很大的成就感的。(偷笑)

据不完全统计,自从开源以来,已经有100多人下载了该软件。

青芒是怎么做的

知了是知乎日报的mac客户端,青芒跟知了有着相同的需求,即展示channel list、article list、article content三个部分,于是萌生了借鉴知了的念头,搜了一圈之后发现github上并没有类似的mac软件开源出来,于是做了决定:自己从头开始写,并且写完的第一件事就是将项目开源。我相信三段式布局的应用会适用很多场景,并且会给初学者们带来一定的启发。项目开源在我的github上,欢迎fork和star,特别感谢github开发者@jydemo、@immress对本项目提出的改进与code实现,效果图如下。

客户端视图(模仿的知了)
客户端视图(模仿的知了)

客户端视图(模仿的知了)

Notification Center视图(略丑)
Notification Center视图(略丑)

Notification Center视图(略丑)

TouchBar视图
TouchBar视图

TouchBar视图

轻芒的API遵循RPC风格,形如 域名/主体.操作,主体和操作都使用小写开头的驼峰命名法,总体上还是比较容易调用的。青芒目前用到了其中的category.list、category.get、article.list、article.get。经测试,发现内测版里提供的channel还非常有限,只有11个,所以目前选择将所有channel展示出来。

开发青芒过程中,使用到了如下技术:HTTP请求、json解析、NSSplitView、WebKit、NSTableView、多线程(GCD)、imageView、自定义NSTextField、NSScrollView、自定义Window、NSTouchBar、NCWidgetProviding等技术,下面我来详细介绍一下青芒实现的过程。

界面部分

macOS/ios界面的构建一直是值得争论的话题,大体上有三种可选方式:

  1. 纯代码手写
  2. Xib文件
  3. Stroyboard

纯手写代码

纯手写代码是极客的不二选择,对于多人协作工作是很好的选择,但是缺点主要是不能直观的看到效果、编码速度很慢,例如初始化一个自定义的Button可能就需要二十行代码,非常不利于阅读。

Xib文件

Xib解决了上面的两个问题,提升了开发效率。其实Xib就是XML格式的文件,在编译过程中,被编译成Nib文件,每个Nib文件跟对应的ViewController关联。Xib的缺点是:代码可能回覆盖UI的设计,而且每个视图都需要单个的Xib,视图间的跳转依然需要代码控制。

Stroyboard

为了解决了Xib的问题,Apple提供了故事板功能。StoryBoard可以看成将很多Xib集中到了一起,像讲述一个故事一样,清晰的看到每个ViewController之间的跳转关系,跳转可以不用写代码了。因此我选用了StoryBoard来构建青芒。

青芒的StoryBoard文件截图
青芒的StoryBoard文件截图

青芒的StoryBoard文件截图

从StroyBoard截图可以清晰的看到整个界面的布局,整个界面的布局是模仿的知乎日报Mac客户端(知了)做的,经典的三段式布局,NSSplitView可以将界面分成左右(或上下)的两部分。考虑到主题一列比较窄,而且主题和相应主题下的文章有强关联关系,因此借用SplitView将Overview Controller一分为二,这样三段式布局就算是完成了。

先从左边的OverView视图说起,这个视图里由两部分组成,第一个部分是一系列的主题按钮,点击之后,在右边的NSTableview里展示对应主题下的最新文章list。最新文章list点击之后,在右边的DetailView中用一个WebView展现文章的详情。

黑色的部分是TouchBar的视图,TouchBar是苹果在MacBook Pro 2016机型上加入的一个新的交互设备,围绕他的讨论有很多,我们有空可以单独聊一下,但是为了尊重这条价值4K的bar,我还是决定做了相应的适配,将主题通过按钮的形式放到了Bar上,Bar上的按钮和OverView的按钮是需要做联动功能的,在下文中会提到。

耐心地拖入相关控件、控制控件之间的相对位置,最初始的原型就算是完成了。当然,经过试用,还是可以发现优化的部分,比如Overview里的tableview并不是顶到window title的,这样用户拖拽那部分的时候,窗口依然可以移动,同样,WebView上方也是需要留白的。虽然只是个很小的细节,但真的很讨好用户。

自定义取代系统默认

界面完成后,就需要做功能了,但是等等,好像看起来不大对劲,为什么效果相比于知了差很多呢?下面的截图展现了青芒的第二版,相比于第一版,已经是把window的标题去掉了,但还是给人一种寨寨的、不够简洁的感觉。TableView中选中的颜色跟整体界面很不符。

第二版的青芒原型
第二版的青芒原型

第二版的青芒原型

为什么会出现这样的情况呢?因为一直到现在,我们都是采用的系统默认选项,没有设计人员的审美在里面。如何给用户带来私人订制的感觉呢?这就需要我们覆盖系统的默认行为和属性,具体来说,就是自定义子类,继承并覆盖父类中不符合开发者预期的部分。

从最后的结果来看,我们需要整个软件看起来背景是白色的,因此我们在每一个view加载的函数中,指定背景色为白色,使用

view.wantsLayer =true
self.view.layer?.backgroundColor=NSColor.white.cgColor

虽然所有视图背景色全部设为了白色,但是关闭、最大化、最小化按钮依然title上,而不是overview那部分,设置title为影藏,它们又不见了,让他们正确显示在overview中的做法是在window加载函数中加入:

self.window?.titleVisibility = .hidden
self.window?.titlebarAppearsTransparent =true
self.window?.styleMask.insert(.fullSizeContentView)

关于TableView中选中状态的背景色,可行的方法有两种。第一种是自定义Cell覆盖NSTableCellView,覆盖父类中的override var backgroundStyle:NSBackgroundStyle{}属性。第二种方法是github上的用户@jydemofork我的项目之后给我提的issue,自定义NSTableRowView,覆盖父类方法:

override func drawSelection(in dirtyRect:NSRect) {
    super.drawSelection(in: dirtyRect)
    var slectorRect =NSInsetRect(self.bounds,0,0)
    NSColor(calibratedWhite:0.92, alpha:1.0).setStroke()
    NSColor(calibratedWhite:0.92, alpha:1.0).setFill()
    var slectorPath =NSBezierPath(roundedRect: slectorRect, xRadius:0, yRadius:0)
    slectorPath.fill()
    slectorPath.stroke()
}

然后实现tableview的代理方法,

func tableView(_tableView:NSTableView, rowViewForRow row:Int) ->NSTableRowView? {
    let rowview = MyTableRowView(frame: .zero)
    return rowview
}

两个主视图之间的分隔条比较粗,总让人觉得不美,解决方法还是自定义。覆盖NSSplitView,覆盖属性

override vardividerThickness:CGFloat{
    get {return0.5}
}

如果用户愿意分隔条还是可以左右动的,想禁掉左右动的功能,实现NSSplitView的一个代理方法:

override func splitView(_ofDividerAtsplitView:NSSplitView, effectiveRect proposedEffectiveRect:NSRect, forDrawnRect drawnRect:NSRect, ofDividerAt dividerIndex:Int) ->NSRect{
    return NSRect.zero
}

经过这一章节,不难发现还是代码靠谱(😎)。经过上面的调整,界面看起来简洁、清爽了不少,可以以假乱真了。

希望这一章节让大家明白,想要做出看起来美的东西,一定要去大胆地替代系统的默认选项。而具体的做法通常是覆盖父类中的属性和方法,记得要将组件和自定义的类关联起来。

美化后的界面
美化后的界面

美化后的界面

一个坑

自定义TableView的Cell过程中,由于文章的标题通常是比较长的,因此用NSTextField无法放下,必须使用NSTextView,而NSTextView默认是可以上下左右滑动的,所以在文章列表中上下滑动的时候,每当滑动到TextView里,滑动事件就会白TextView捕获,TableView中的Scroll view没有机会捕获了。

解决的方法和上一节一样,通过覆盖cell里(请注意是Cell,不是tableview)的scroll view,重写hitTest方法

override func hitTest(_point:NSPoint) ->NSView? {
    return nil
}

告诉cell,这个滑动事件我不处理了,请交给别人处理吧。

NSTouchBar

为了赶时髦,应用内做了TouchBar和通知中心的内容。TouchBar需要注意的是NSWindow和NSViewController之间的联动。从NSWindow到NSViewController:

let myViewcontroller =self.window?.contentViewControlleras!mainViewController

相反的过程:

let mywindowController=NSApplication.shared().windows[0].windowControlleras?windowController

这样就可以做到在TouchBar中按了某个按钮,在主界面里也可以看到按钮被选中的效果,满足了一致性。

Notification Center视图

通知中心做的蛮丑的,真的是为了尝试一下TodayExtension的功能而已。模仿知了,目前功能只是展示了首页的文章列表,点击文章可以用系统默认浏览器打开原文。其实这也就够了,毕竟通知中心就是为了看个大概用的,谁也不会经常点开看。通知中心需要注意的是要自定义视图的高度,通过 self.preferredContentSize=CGSize(width:self.view.frame.size.width, height:xxx) 完成。 最后要注意的是用URLSession请求数据,可以防止UI卡顿,UI卡顿给用户带来的感觉非常糟糕。

func getData(with urlString:String,success:@escaping(Data?)->Void, failure: ((Error)->Void)? =nil) {

         guard let url =URL(string: urlString)else{

         return

}
    let task =URLSession.shared.dataTask(with: url) { (data:Data?, response:URLResponse?, error:Error?)in
    DispatchQueue.main.async{
    if let error = error {
                     failure?(error)
    }else{
                      success(data)
                 }
          }
      }
     task.resume()
}

文章具体内容视图

青芒团队提供的API里有web-content这一项,只要通过webview包装然后load一下就会将文字和图片展示出来,但是还是有一些细节需要考虑到的。比如有些图片是很大的,虽然限制了窗口不得小于一个值,但有些图片还是远超了这个大小。web-content是没有标题的,看起来会比较突兀,标题和标题需要你手动加上。图片过大时,窗口是可以左右滑动的,怎么把他限制不能滑动。字体如何跟其他部分视图的字体做到没有违和感,都是需要考虑的问题,这些用前端里的CSS样式可以控制,需要开发者有一定的前端开经验。由于本公众号重心在果教,因此这里就不具体展开叙述了,感兴趣的朋友可以参考我的github

青芒的宣传

做完青芒之后,考虑到整个项目是用最新的swift4写成,于是我在微博上@了swiftLanguage,博主是一个不大不小的V,关注者多是对swift感兴趣的或从业人员。短短两天,该微博获得了1万多次阅读,最后我欣喜的发现,王俊煜也给该微博点了赞,感谢俊昱的鼓励与肯定。

微博截图
微博截图

微博截图