<译>iOS 8 Today Extension Tutorial

原文链接:iOS 8 Today Extension Tutorial

Ray提醒: 这篇文章可以说是一个精简版,它是从iOS8 by Tutirials这本书中摘录的一个章节。iOS8 by Tutirials是我们iOS8 Feast套餐中的一部分。通过这篇文章我们可以看到书中的内容是怎么样的。希望你喜欢!

iOS8介绍了一种新的概念 App Extensions:一种在操作系统上与其他应用共享应用程序功能的方式!

这些Extensions中有一种叫Today Extensions,又称Widgets。它可以让你在Notification Center中呈现信息。这是一种很好的方式向用户直接的提供他感兴趣的最新的信息。

在这个教程中,你会编写一个Today Extension用来渲染bitcoin.org中比特币基于美元的当前市场价格。

介绍比特币

如果你对比特币还不熟悉,那用一句很短的话来介绍的话就是比特币是一种还处于初期的数字加密货币。除了可以用来作P2P的交换和购买外,比特币交易还允许用户将比特币交易成另外一种数字加密货币,比如狗狗币和莱特币,以及像美金和欧元这样的常用货币。

介绍Crypticker项目

一旦你开始写一个Extension,你必须先需要一个载体应用(host app)来做扩展;看看Crypticker项目吧。

Crypticker是用来展示当前的比特币价格、当前价格与昨日价格的区别和历史价格图表的一个简单的App。图表包括30天的历史价格;滑动你的手指可以看到过去特定的某天的准确价格。

Extension会包含所有这些功能,除了点击图表可以看到特定某天的价格。Today Extension也有局限性,尤其是当它在滑动(swipe)的时候,Swipe Gesture总是在Notification Center中的TodayNotifications这两个sections里被触发,所以他没有真的做到提供了最好的或者最可靠的用户体验。

入门指南

我们从下载Crypticker起始项目入手。这个项目包含整个如上面描述的Crypticker app。我们的教程不会关注这个项目本身的开发,所以你愉快又惊讶的发现这个教程是如此的简洁。毕竟,你要写一个Extension,而不是整个app。 编译运行。请注意你需要一个可以工作的网络来链接web service以及拉取实时的价格。

(译者注:这个App还是Swift1.0的代码,可以根据Xcode的提示将代码修改成符合Swift1.1,主要是Swift1.1加入了可失败初始化功能)


App看起来非常像上面的屏幕截图;当然显示的数据是基于当前的比特币市场怎么样。触碰在底部附近的图表会绘制一条线以及显示遮天相关的价格。

BTC Widget

为了不熟悉的人更好的理解,BTC就是比特币的缩写。很像USD就代表美元。Today Extension会渲染出一个Crypticker主视图的缩小版本。

理论上,Crypticker有能力展示多种加密货币的价格,但是你的Extension是对BTC的特制版。因此,它的名字应该叫BTC Widget

1
Note: Extensions,就其本质而言,只有一个目的。如果你想要提供出你另一个数字加密货币的信息,比如狗狗币(Dogecoin),最好的方法是针对你的App打第二个widget的包或者适当的设计你的UI,也许就像股票Widget。

在这篇教程的结尾,你的Today Extension会看起来像这样:


添加一个Today Extension target

所有的Extention被包装成一个和他们载体工程隔离的binary。所以你需要为你的Crypticker工程添加一个Today Extentiontarget

在Xcode的Project Navigator中,选中Crypticker project,然后选择Editor\Add Target…来添加一个新的target。当模板选择器出现后,选择iOS\ Application Extension下的Today Extension。点击Next


设置Product NameBTC Widget,然后验证LanguageSwiftProjectCryptickerEmbed in Application也是Crypticker。点击Finish


当提示激活BTC Widget``scheme的时候.就像文本指示器一样,会为你创建另一个Xcode``scheme

恭喜!现在BTC Widget会展示在你的targets的列表中。


确保你选中了BTC Widgettarget,选择General标签,然后按下在Linked Frameworks and Libraries下的+按钮。


选中CryptoCurrencyKit.framework然后点击Add

CryptoCurrencyKit是一个在Crypticker app中使用的用来从网络上获取货币价格的自定义的framework。你很幸运,有个难以置信般善良和体贴的程序猿模块化了Crypticker的代码到framework,所以它可以在多个target下共享 :]

为了在载体工程和它的extensions质检共享代码,你必须使用自定义framework。如果你不这么做,你会发现你重复撰写了很多代码以及违反了软件工程中一个很重要的原则:DRY- or,Dont Repeat Yourself,我再说一次:Dont Repeat Yourself(不要重复发明轮子)

现在,你已经准备好开始实现extension了。

注意,现在有一个以你新的target命名的文件组在你项目的Navigator中,BTC Widget。这是Today Extension代码默认分组存放的地方。

展开这个文件组,你会看到这里有一个view controller,一个storyboard文件和一个Info.plist文件。它的target配置信息也告诉他去MainInterface.storyboard加载他的interfaceMainInterface.storyboard包含一个指定class为 TodayViewController.swiftViewController


你会发现有一些你期望看到的文件在Today Extension模板中却消失不见了。比如说app delegate。记住所有的Extension都运行在另外一个载体工程的内部,所以他们和传统的app的生命周期不一样。

本质上,Extension的生命周期是被映射在TodayViewController的生命周期上的。

打开MainInterface.storyboard。你会看到一个带着明亮的Hello World``label的深色的View。为了与通知中心的暗色调协调,当Today Extension有一个清晰透明的背景和一个明亮或者艳丽的文本时是最清晰易读的。

确保BTC Widgetscheme在Xcodetoolbar中被选中,然后编译运行。这时会出现一个窗口来问你哪一个app需要运行。Xcode正在问你哪个载体应用需要运行。选择Today。这会告诉你的iOS打开通知中心的Today视图。它会依次展开你的Widget


这也会附加Xcode的调试器到widget的进程中。


看你的插件,碉堡了,对不对?虽然这是一个超级刺激的东西,但widget也需要一点工作。是时候让它做一些有趣的事了。

1
2
Note:你可能遇到在运行Widget的时候控制台打印出很多Auto Layout的错误。这是一个Xcode模板的问题,会有希望Apple在将来解决他。不要担心,大胆的添加AutoLayout约束到你的interface上。
(译者注:个人感觉还是纯代码靠谱,清晰,一目了然,版本冲突成本低)

建立接口

打开MainInterface.storyboard然后删除那个label。在Size Inspector中设置view的高为150pts,宽为320pts。从Object Library拖出一个Button,两个Label和一个View到你的ViewControllerview上。

  • 将其中一个label放在左上角,在Attributes Inspector中设置他的Text$592.12ColorRed: 66, Green: 145,Blue: 211。这个label会用来显示当前的市场价格。
  • 将另外一个label放在你刚刚设置的label的右边,但在右边留出一个button的空间。在Attributes Inspector中设置他的Text+1.23ColorRed: 133, Green: 191, Blue: 371。这个会哦你过来显示昨天和当前的价格的不同。
  • 将button移动到视图的右上方。在Attributes Inspector中设置他的Imagecaret-notification-center,删除他的Title
  • 最后,将空的view放在两个label和button的下面,拉伸它直到它的底部和边缘和containing view吻合,设置他的Height98.在Attributes Inspector中设置他的BackgroundClear Color,然后在Identity Inspector中设置他的ClassJBLineChartView
1
Note: 当你在打字的时候Xcode会猜测你可能输入的一个叫 JBLineChartDotView 的class,确保你选择了JBLineChartView

现在视图和Outline看起来像这样:


1
Note: 为了本书的清晰度的目的展示在这里的View是白色背景的。你的视图实际上会和通知中心一样有一个深色的背景色。

不要担心页面的布局,你马上就会通过适当的定义布局来添加AutoLayout约束。

现在展开在Project NavigatorCrypticker组,选中Images.xcassets。在File Inspector中勾选BTC Widget将其添加到extension的target上。 这样Xcode就会将Images.xcassets从你的Cryptickertarget上添加到BTC Widget的target上;这是你的button使用的caret-notification-center图片存放的地方。如果你在载体app和widget上有重复的image assets,这是一个很好的共享方式。这可以通过不添加已经在使用的图片来减少App膨胀。


返回到MainInterface.storyboard,打开Assistant Editor。确保TodayViewController.swift是它的active file。将下面的代码添加到TodayViewController.swift的顶部:

1
import CryptoCurrencyKit

这是用来导入CryptoCurrencyKit framework。 接下来,你需要像这样更新他的类声明:

1
class TodayViewController: CurrencyDataViewController, NCWidgetProviding {

这会让TodayViewController成为CurrencyDataViewController的一个子类,确保他符合NCWidgetProviding协议。

CurrencyDataViewController是包含在CryptoCurrencyKit中也被Crypticker载体应用的第一个视图使用的Controller。由于Widget和App会通过UIViewController展示相似的信息,他会把可重用的组件放到一个superclass中,然后按不同的需求来做子类化。

NCWidgetProvidingWidget特有的接口。有两个接口的方法需要你来实现。

按住Ctrl,从IB的Button拖动鼠标到类声明下。在弹出的对话框中确保Connection设置为OutletType设置为UIButton,在Name中输入toggleLineChartButton后点击Connect


然后还是按住Ctrl,这次从IB的Button拖动鼠标到类的底部。在弹出的对话框中改变Connection值为Action,设置TypeUIButton,在Name中输入toggleLineChart后点击Connect


TodayViewControllerCurrencyDataViewController的子类,CurrencyDataViewController有三个outletprice label, price change labelline chart view。你现在需要把他们联通起来。在Document Outline中按住Ctrl,从Today View Controller拖拽到price label(设置他的text为$592.12).在弹出框中选中priceLabel来建立连接。对另外一个label也做重复的操作,选中弹出框中的priceChangeLabel.最后对 Line Chart View做同样的操作,在弹出框中选中lineChartView


Auto Layout

为了你的Widget可以自适应,你就会需要对它设置Auto Layout的约束。iOS8有一个新的自适应布局的概念。大意是视图可以被设计成各自有一个单独的布局,并且这个布局可以在各种各样的屏幕大小上工作。视图被设计成可以在未来以及不知道材料的设备上都可以自适应。

其中一个你要添加的约束是用来显示和隐藏我们的图表以及用来帮助定义Widget整体的高度。通知中心会依赖于你定义的适当的高度来显示你的Widget。

选中$592.12这个label,然后选择Editor\Size to Fit Content。如果Size to Fit Content选项不在你的菜单中,取消选中这个label,然后再次选中重试一遍。Xcode有的时候会抽风。接下来,使用在storyboard画布下的Pin按钮。分别钉住TopLeading的空间为8和16。确保Constrain to margins处于关闭状态。


现在选中+1.23这个label,再次选中Editor\Size to Fit Content。然后使用Pin按钮,钉住TopTrailing空间都为8.


选中那个Button,使用Pin按钮,钉住他的TopTrailing两个的空间为0,以及他的Button空间为8。再钉住他的WidthHeight都为44.确保Constrain to margins处于关闭状态。


你需要减小Button的底部空间约束的优先级。选中button,然后打开Size Inspector, 定位到Bottom Space to:约束列表的约束,点击Edit然后改变他的Priority为250.

通过降低优先级你被Auto Layout系统允许打破他认为必要的约束。250是一个被设置给所有的约束优先级的默认以及必须的且碰巧小于1000的任意值。在折叠状态下这个约束需要被打破。通过设置不同优先级的约束你暗示系统当发生冲突的时候哪一个约束最先或者最后打破。


最后,选中Line Chart View。使用Pin按钮,钉住他的Leading, TrailingBottom三个空间为0以及他的高度为98。

Document Outline中选中视图控制器的View,然后选择Editor\Resolve Auto Layout Issues\All Views\Update Frames.这会通过更新视图的frame来匹配他们的约束,从而修复在画布上的任何Auto Layout的警告。如果Update Frames不可用,那么你的一切都狠完美,这是不必要的运行。

完成了你的所有的约束后,最后一步就是为Line Chart View的高度创建一个outlet的约束。在Document Outline中找到Line Chart View然后点击显示的三角形。

然后点击三角形,找到我们需要的高度约束。选中他,然后按住Ctrl,拖拽到 Assistant Editor,放在其他outlet的下面。在弹出框中确保Connection被设置为Outlet,在Name中输入lineChartHeightConstraint。点击Connect

实现TodayViewController.swift

现在接口已经确立了,而且所有的东西都已经连接上,在Standard Editor中打开 TodayViewController.swift。 你会发现你在与一个普通的UIViewController的子类打交道。很欣慰,对不对?一会儿后你会遇到NCWidgetProviding协议中的一个叫做widgetPerformUpdateWithCompletionHandler的方法。在这个教程的结尾你会学到更多与它相关的东西。

这个ViewController主要负责显示当前的价格,价格的区别,响应button的点击和在line chart中显示价格的历史。

TodayViewController的顶部定义一个属性用来跟踪线形图(line chart)是否可见:

1
var lineChartIsVisible = false

现在用下面的实现替换掉viewDidLoad()的模板代码:

1
2
3
4
5
6
7
8
9
10
override func viewDidLoad() {
  super.viewDidLoad()
  lineChartHeightConstraint.constant = 0

  lineChartView.delegate = self;
  lineChartView.dataSource = self;

  priceLabel.text = "--"
  priceChangeLabel.text = "--"
}

这个方法会做以下几件事情:

1.设置线形图的height约束的约束值为0,所以线形图默认是hidden的。
2.设置线形图的dataSource和delegate为self。
3.设置一些占位文案在两个label上。

还是在TodayViewController中,添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
override func viewDidAppear(animated: Bool) {
  super.viewDidAppear(animated)

  fetchPrices { error in
    if error == nil {
      self.updatePriceLabel()
      self.updatePriceChangeLabel()
      self.updatePriceHistoryLineChart()
    }
  }
}

其中的fetchPrices方法是在CurrencyDataViewController中定义的,他是一个可以异步调用block。这个方法会向我们在这个教程开始的时候提到的web-service发送一个请求来获取比特币的价格信息。

在方法的实现块中更新所有的label和线形图。更新方法也在父类中为你定义好了。他们通过fetchPrices对需要的值做一个简单的检索以及合理的格式化后用来显示。

由于Widget的设计,你还需要实现widgetMarginInsetsForProposedMarginInsets方法用来提供自定义的 margin insets,添加如下代码到TodayViewController中:

1
2
3
4
func widgetMarginInsetsForProposedMarginInsets
  (defaultMarginInsets: UIEdgeInsets) -> (UIEdgeInsets) {
    return UIEdgeInsetsZero
}

Widget默认有一个很大的left margin,这在Apple很多的默认Widget上是恨明显的。如果你想要填充整个通知中心的宽度,你必须实现这个方法并且返回UIEdgeInsetsZero,他会把所有的边的margin insets都转化为0。

现在是时候看看你到目前为止都有些啥了。选中BTC Widget这个Scheme。编译运行。当app运行后提示的时候选择Today

*如果通知中心没有显示,那么在屏幕的顶部用手指向下滑动来激活它。
*如果Widget没有在通知中心显示,你需要通过编辑菜单来添加它。在今日视图的底部你会看到编辑按钮。点击按钮展开所有安装在系统中的Widget。在这里你可以随心所欲的enable, disable以及re-order他们。如果BTC Widget没有处于enable状态,则enable他。

屌爆了,有木有!你的Widget现在已经在通知中心实时的显示比特币的价格。但是你可能已经意识到了一个问题。按钮不能工作以及看不到线形图。

接下来,你要为你添加的button实现toggleLineChart方法,用来展开widget的视图并且显示线形图。就像方法名说的一样,这个button就像一个转换键一样;他也会折叠视图从而隐藏图表。

用下面的代码替换空的toggleLineChart

1
2
3
4
5
6
7
8
9
10
11
12
13
@IBAction func toggleLineChart(sender: UIButton) {
  if lineChartIsVisible {
    lineChartHeightConstraint.constant = 0
    let transform = CGAffineTransformMakeRotation(0)
    toggleLineChartButton.transform = transform
    lineChartIsVisible = false
  } else {
    lineChartHeightConstraint.constant = 98
    let transform = CGAffineTransformMakeRotation(CGFloat(180.0 * M_PI/180.0))
    toggleLineChartButton.transform = transform
    lineChartIsVisible = true
  }
}

这个方法用来操纵线形图的高度约束,从来约束他的显示。它也对button有一个旋转变化,所有它可以准确的反应出图表的可见度。

约束更新后,你必须重新加载图表的数据,以便它在新的约束上重新绘制。

你会在viewDidLayoutSubviews上做这些。添加如下代码到TodayViewController

1
2
3
4
override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()
  updatePriceHistoryLineChart()
}

确保BTC Widget这个scheme被选中。编译运行。当App运行的时候出现提示就选择Today

在左图,你会看到当图表隐藏的时候widget是如何展示的。在右图,你会看到当图表打开时widget是如何展示的。不是很寒酸!

添加如下代码到TodayViewController,你会拥有一个快速更新的线颜色以及目光尖锐的widget:

1
2
3
4
5
override func lineChartView(lineChartView: JBLineChartView!,
  colorForLineAtLineIndex lineIndex: UInt) -> UIColor! {
    return UIColor(red: 0.17, green: 0.49,
      blue: 0.82, alpha: 1.0)
}

确保当前的scheme依旧选中,编译运行。当app运行的时候出现提示就选择Today

你最后要做的就是通过允许系统来创建一个快照,当屏幕关闭的时候添加对Widget更新视图的支持。系统会定期的帮助你的widget保持最新。

用下面的代码替换掉widgetPerformUpdateWithCompletionHandler现存的实现:

1
2
3
4
5
6
7
8
9
10
11
12
func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) {
  fetchPrices { error in
    if error == nil {
      self.updatePriceLabel()
      self.updatePriceChangeLabel()
      self.updatePriceHistoryLineChart()
      completionHandler(.NewData)
    } else {
      completionHandler(.NoData)
    }
  }
}

这个方法做以下几件事情:

*他通过调用fetchPrices来从网络服务中获取当前的价格数据。
*如果没有错误,接口就会更新数据。
*最后 - 就像NCWidgetProviding协议要求的一样 - 方法调用系统提供的带着.NewData枚举的block。
*发生错误的时候,block会带着.Failed枚举调用。这会告诉系统没有新的可用的数据,用现存的快照吧,亲!

你可以在这里下载最后的工程.

现在去干哈

iOS8通知中心是你自己个人的playground!Widget在一些其他的手机操作系统上早就已经有了,Apple最终提供给你一个创造他们的能力。

作为一个有进取心的开发者,你可能想要再看看你现在的app,思考怎么用widget去提升他们。利用widget的可能性更进一步想象新的app的idea。

如果你喜欢学习更多关于创建其他类型的iOS8的App扩展。看看我们的书 iOS 8 by Tutorials,这里你可以学习关于Photo Extensions, Share Extensions, Action Extensions以及更多的知识。

我们迫不及待的想看你能想出什么东西来了,希望你的Widget尽快的在我们的通知中心的顶部!