AMScrollingNavbar: 创建一个Cocoapod

这周 Matt Thompson在NSHipster上发布了一篇有意思的文章,关于管理工作,基本上是公共服务的责任和道德。尽管我已经尽力在开源组织里变的更积极了,我发现可以用这个机会写一篇文章,关于我在写一个新的iOS库时遵循的过程。我会描述我在AMScrollingNavbar上的工作作为例子。
AVScrollingNavbar是一个简单的iOS库,当用户在滑动app内容的时候,可以把UINavigationBar滑出去。你可以在 Google Chrome、Instagram或Facebook app里看到这个行为。我想在我做的app里模仿它们,Fancy Pixel,但找不到谁已经做了我想要的工作。何不做一些开源工作呢?

写代码

像这样的例子里,写代码更多的是一系列的试验和出错,所以我没有用测试驱动开发(TDD),用了更抽象的驱动方式。这意味着我只是打开 Xcode,创建一个新项目,开始摆弄SDK。这是这个工作里最棒的部分。
UINavigationBar滑出去的方式相当简单,我只需要给带有内容的UIScrollView添加一个UIGestureRecognizer
self.panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
[self.panGesture setMaximumNumberOfTouches:1];
[self.panGesture setDelegate:self];
[self.scrollableView addGestureRecognizer:self.panGesture];
但只有这个还不行,需要复写一个UIGestureRecognizerDelegate的代理方法:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
在这个方法里返回YES会允许scrollview和手势识别器(gesture recognizer)同时一起工作。酷。现在是时候拉UINavigationBar的大腿了。这是完成这一步的代码抽象:
- (void)scrollWithDelta:(CGFloat)delta
{
CGRect frame;

  frame = self.navigationController.navigationBar.frame;
  
  if (frame.origin.y - delta < -self.deltaLimit) {
      delta = frame.origin.y + self.deltaLimit;
  }
      
  frame.origin.y = MAX(-self.deltaLimit, frame.origin.y - delta);
  self.navigationController.navigationBar.frame = frame;
      
  [self updateSizingWithDelta:delta];
}

可以看到UINavigationBar有它自己的框架,你可以轻易修改。一旦得到了跟随scroll view的navigation bar,就需要扩大或减少scroll view的框架来利用剩下的屏幕资源。一大块代码来了:
- (void)updateSizingWithDelta:(CGFloat)delta
{
// 在这一点 navigation bar 已经被放到了正确的位置上,它会是其它视图尺寸的参考点
CGRect frame = self.navigationController.navigationBar.frame;

    [self updateNavbarAlpha:delta];

    // 移动和扩展(或缩小)给定的scrollview的父视图
    frame = self.scrollableView.superview.frame;
    frame.origin.y -= delta;
  frame.size.height += delta;
  self.scrollableView.superview.frame = frame;

  frame = self.scrollableView.frame;
  frame.size.height = self.scrollableView.superview.frame.size.height -frame.origin.y;
    
    // 如果 scroll view 是 UIWebView,我们需要调整它的 scrollview 的高度
    if ([self.scrollableView isKindOfClass:[UIWebView class]]) {
        ((UIWebView*)self.scrollableView).scrollView.frame = frame;
    } else {
        self.scrollableView.frame = frame;
    }

    // 保持 view 的滑动位置固定直到 navbar 走了
    if ([self.scrollableView isKindOfClass:[UIScrollView class]]) {
        [(UIScrollView*)self.scrollableView setContentOffset:CGPointMake(((UIScrollView*)self.scrollableView).contentOffset.x, ((UIScrollView*)self.scrollableView).contentOffset.y - delta)];
    } else if ([self.scrollableView isKindOfClass:[UIWebView class]]) {
        [((UIWebView*)self.scrollableView).scrollView setContentOffset:CGPointMake(((UIWebView*)self.scrollableView).scrollView.contentOffset.x, ((UIWebView*)self.scrollableView).scrollView.contentOffset.y - delta)];
    }
  }

很容易读,有点难写。scrollview的高度增加了滑动的delta,它的origin平移上去或下来相同的高度。用一点判断检查scroll view是不是普通的UIScrollView(这包括table view)或UIWebView。后者有它自己的scroll view,所以我需要额外调整。
view尺寸调节完成后,最后一步是淡出navigation item。我用了一种老司机方式,因为我不能设置每个 nav item 的 alpha 频道,我发现我可以强加一个带有相同的barTintColor的覆盖 view,来回改变它的 alpha:
- (void)updateNavbarAlpha:(CGFloat)delta
{
CGRect frame = self.navigationController.navigationBar.frame;

  // 改变navbr上的每个item的alpha频道。覆盖会出现,其它对象会消失,反之亦然
  float alpha = (frame.origin.y + self.deltaLimit) / frame.size.height;
  [self.overlay setAlpha:1 - alpha];
  [self.navigationItem.leftBarButtonItems enumerateObjectsUsingBlock:^(UIBarButtonItem* obj, NSUInteger idx, BOOL *stop) {
      obj.customView.alpha = alpha;
  }];
  [self.navigationItem.rightBarButtonItems enumerateObjectsUsingBlock:^(UIBarButtonItem* obj, NSUInteger idx, BOOL *stop) {
      obj.customView.alpha = alpha;
  }];
  self.navigationItem.titleView.alpha = alpha;
  self.navigationController.navigationBar.tintColor = [self.navigationController.navigationBar.tintColor colorWithAlphaComponent:alpha];
}

这样就起作用了。

准备相机

一旦样本运行的如我所想,我就步入了重构阶段。这一步里,我拿下所有代码移动到一个可复用的对象里。这个例子里我选择了UIViewController可以被其他开发者继承。我评估了 category 可能性的价值,但因为我用了很多实例变量,我偏向于第一种方式(不是associatedObject技巧的爱好者)。
待办清单上的最后一项是样本项目的重构。我们已经完成了。

写文档

一个开源项目,文档也要一样好。它帮助其他开发者最开始的实现,最重要的是帮助他们决定是否使用你的库。某种程度上它是营销,但没有市场那一部分。
我喜欢花时间用苹果文档注释的方式来添加注释。这在Xcode里会提供全面的快速指南,也会在 Cocoapods Pages 里生成一个苹果风格的文档。可以看到语法写起来很简单,也很易懂:
/** 滑动初始化方法
*
* 在一个普通的 UIView 上允许滑动。
*
* @param scrollableView 滑动发生的UIView。
/
- (void)followScrollView:(UIView
)scrollableView;
现在是时候把所有工作 push 到 Github 上了,这需要一个好的 README 文件。这个 README 应该有一个关于库的快速描述,设置说明,库的文档或链接,可能的话还有一张截图。

截图

遵照原则“展示,不要说”(show, don’t tell),截图作用很棒,但GIF甚至会更好。运行你的样例来创建一个GIF动画就像馅饼一样简单,使用LICEcap。不要管它诡异的名字和低分辨率的图标,LICEcap是个非常宝贵的工具,用起来很简单也非常全能,只需要匹配屏幕适合的部分,就是你希望录制在窗口里的,点击record,就好了。


你可能希望改变输出的GIF的大小,如果你捕获了屏幕的大部分,或者你在视网膜屏上工作。ImageMagick可以解救你:
convert big.gif -coalesce temp.gif
convert -size 960x640 temp.gif -resize 480x320 small.gif

Travis CI

Travis CI是一个持续性整合工具,用来创建和测试管理在 Github 上的项目。在一个开源项目里启用 Travis 是一个很棒的方式来确保项目编译好。对于你的新发行很酷(如果最后一个 commit 破坏了什么会提前知道)甚至对于 pull request 来说更棒,因为 Travis 告诉你一个 pull request 是否编译的没有错误,直接在 PR 的 github 页面里。
要开始使用 Travis,看这里,用 Github 登录,在控制面板里的列表中启用你的项目。
在 iOS 启用 Travis 真的很简单,只需要两步:

配置 build scheme

打开样例项目,或者你希望建立的项目组件,并且在 Manage Schemes 面板里,确保你的项目 scheme 是Shared

添加 .travis.yml

在你的项目根目录添加一个 .travis.yml文件。你会写一个简单的脚本,会编译你的样例项目:
language: objective-c
install:
- cd ScrollingNavbarDemo
script: xctool -project ScrollingNavbarDemo.xcodeproj -scheme 'ScrollingNavbarDemo' -configuration Release -sdk iphonesimulator7.0 -arch i386 build
install阶段只是更改目录包含的项目,然后我们信任的xctool编译了项目。不要太简单。
一旦你 push 了最新的发布,项目会被添加到 travis 编译队列,你会收到一封关于结果的 email。你也可以实时看到脚本运行,很酷。

Cocoapods

没有必要重申 Cocoapods 很棒。在项目里配置一个新的库花掉的宝贵时间(几乎)没了。创建一个 pod 是几分钟的事,但同时给组织提供了巨大的价值。你会在 cocoapods 网站上找到一个相当全面的指南。这是我的 podspec 看起来的样子:

Pod::Spec.new do |s|
  s.name         = "AMScrollingNavbar"
  s.version      = "0.5"
  s.summary      = "Scrollable UINavigationBar that follows the scrolling of a UIScrollView. Similiar to Chrome for iOS7"
  s.homepage     = "https://github.com/andreamazz/AMScrollingNavbar"
  s.license      = { :type => 'MIT', :file => 'LICENSE' }
  s.author       = { "Andrea Mazzini" => "andrea.mazzini@gmail.com" }
  s.source       = { :git => "https://github.com/andreamazz/AMScrollingNavbar.git", :tag => '0.5' }
  s.platform     = :ios, '5.0'
  s.source_files = 'AMScrollingNavbar', '*.{h,m}'
  s.requires_arc = true
end

一旦 spec 被 merge 到 Spec repo里,一个新的 pod 诞生了,伴随它的文档页面也被创建了。

Cocoacontrols

Cocoacontrols是我最喜欢逛新的库的地方。一旦我的 pod 好了并且感觉 README 也足够全面了,我提交我的控件(control)到cocoacontrols的审核队列。这是一个绝佳的方式来得到来自其他开发者的注意,也是绝佳的方式来发现新的控件。非常感谢Aaron, Marine 和 Bob

维持代码

一旦放生了代码,是时候维持代码了。这意味着回答关于它的使用的问题,修复可能的问题和merge一直欢迎的 pull request。有事它可以成为自己的工作,但它是一个高回报任务,因为它让你有机会进行更多的实验和大量改进你的代码。
我用的唯一工具是 github 的 Issue部分。我尝试过一次养成用 Waffle追踪问题的习惯,但最后我也从没有有效的使用它。
AMScrollingNavbar 被很好地接受了,现在(对我来说)在Github上有相当数量的观察者。我要感谢所有的贡献者,帮助提高这个库,介绍新的特色和修复我的过失。

推荐阅读更多精彩内容