03 10月

使用Swift语言自定义Pull To Refresh控件

文 / gabriel theodoropoulos:拥有20年的编程经验,2010年下半年以来,一直专注于iOS开发,长期使用多种语言在不同平台实现软件架构解决方案。

原文链接: http://www.appcoda.com/custom-pull-to-refresh/

全世界有非常多的应用程序。要开发出新的应用,并且能够吸引用户的注意力,使用应用能够脱颖而出。毫无疑问,有人会说使简单应用出类拔萃的秘笈是开发小组(开发人员和设计人员)所使用的个性化技艺,用在大多数开发人员不做处理的小细节处。其中之一就是从本文题目中可见一斑的pull-to-refresh控件。通过本文我会展示使该控件改头换面的方法。

你知道,pull-to-refresh控件是活动指示器(经常带有短小的信息),数据加载过程中,会出现在表视图的顶部。此时的表视图还未完成刷新。实际上,pull-to-refresh控件与“Please, wait…”信息提示类似,当用户等待获取和显示新的内容时出现。于此最知名的应用就是电子邮箱。向底部拖拽邮件视图,邮件列表会得到刷新。该控件自iOS 6.0引入,之后在不计其数的应用软件中频繁地被使用。

pull-to-refresh-featured-1024x533

如果想要在应用中使用pull-to-refresh控件的话,就要查找如何去用的相关信息,也一定会找到simon的文章,文章很好地诠释了你想知道的一切。而本文要说说pull-to-refresh控件的另外一面,那就是如何自定义一个pull-to-refresh控件。这样一来,你就能够在这个不起眼儿但比较重要的细节上添加不同的风格,给应用赋予不同的视角。

长话短说,接下来看看添加自定义内容和动画的技巧,来使用任何你想要添加的对象“取代”默认的pull-to-refresh控件。要注意,下面的内容是展示你要跟着做的步骤,实际的自定义内容完全由你自己决定,更确切地说,由你的想象力决定。开始吧,用不了多久,就能创建出自定义的pull-to-refresh控件了!

示例应用概览

下面的动画就是本文中自定义的pull-to-refresh控件:

t38_1_final_sample

可以看到,表视图里面有一些模拟数据。我们的目的不是真地要从服务器获取数据,而是关注刷新的过程。活动指示器是不会出现的,取而代之的是与刷新过程持续时长相同的“APPCODA”动画。

想要知道模拟刷新的动作什么时候结束的话,我只能告诉你这里使用了计时器(NSTimer)对象。四秒钟后,该对象会结束刷新动作,将自定义的pull-to-refresh控件隐藏起来。示例中的四秒钟是随机选取的时间间隔,这当然是为了在本文中演示自定义控件的方法。很多情况下数据刷新时间都比这要短(特别是有高速英特网连接的情况下)。因此,在真实的应用中不要为了给用户头脑中留下印记而使用计时器来显示自定义的pull-to-refresh控件。要知道,特别是如果应用很棒,被用户经常使用的情况下,用户会很多机会看到这个控件。

正如你所看到的,要开发一个极其简单的项目。但没有必要从头开始,和往常一样,可以先下载一个初始项目。这是在故事板(storyboard)中所作的界面设计,还有一个Interface Builder文件,叫做RefreshContents.xib。在界面中我加入了自定义的内容,用来取代普通pull-to-refresh控件来显示的内容。实际上,该自定义控件包含了七个标签(UILabel)对象。在一起组成了“APPCODA”字样。还有一个自定义视图(UIView)作为这些标签的容器。所以必要对字体格式都做好了,正确设置了约束。随后要做的就是在视图控制器中加载这些控件,并用适当的方式处理它们。

所以,先下载初始项目,用Xcode打开它。

默认的Pull-To-Refresh控件

对示例应用要做的第一件事就是显示表视图中的模拟数据。下载的初始项目中已经包含了叫做tblDemo的IBOutlet属性,用来连接故事板中的表视图。因此需要编写表视图所需的代理(delegate)方法和数据源(datasource)方法。但是,这样做之前需要指定表视图中要显示的数据。在ViewController.swift文件中,在类内添加下列代码行:

  1. var dataArray: Array<String> = [“One”“Two”“Three”“Four”“Five”]

现在,如下所示,通过添加UITableViewDelegate和UITableViewDataSource协议修改类的第一行:

  1. class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource

然后要将ViewController的类实例作为表视图的代理和数据源:

  1. override func viewDidLoad() {
  2.     …
  3.     tblDemo.delegate = self
  4.     tblDemo.dataSource = self
  5. }

现在添加表视图的方法,用来显示模拟数据:

  1. func numberOfSectionsInTableView(tableView: UITableView) -> Int {
  2.     return 1
  3. }
  4. func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  5.     return dataArray.count
  6. }
  7. func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  8.     let cell = tableView.dequeueReusableCellWithIdentifier(“idCell”, forIndexPath: indexPath) as! UITableViewCell
  9.     cell.textLabel!.text = dataArray[indexPath.row]
  10.     return cell
  11. }
  12. func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
  13.     return 60.0
  14. }

好啦!这些都没有啥难度,运行应用后就会看到表视图显示“One, Two, …”。

t38_2_tableview_data

来看看如何显示和使用默认情况下的pull-to-refresh控件吧。我们现在这种情况是ViewController或其他什么类的子类,总之不是UITable ViewController的子类。pull-to-refresh控件必须作为表视图的子类添加进来(参见Simon的相关文章,作为UITableViewController的子类添加进来的做法)。首先在类的开始处声明refreshControl。

  1. var refreshControl: UIRefreshControl!

别忘了,尽管refreshControl由一组特别的控件组成,但声明和使用的方法与其他属性和对象别无两样。所以上面的做法很正常。

首先在viewDidLoad方法中初始化refreshControl,然后将它添加到表视图当中。

  1. override func viewDidLoad() {
  2.     …
  3.     refreshControl = UIRefreshControl()
  4.     tblDemo.addSubview(refreshControl)
  5. }

再一次运行应用,当表视图被拖拽到底部的时候,你会发现旋转的图标出现了。但不要指望这个控件再隐藏回去,这个功能不是自动产生的。必须要明确地结束刷新动作,但这是稍后要做的事情。目前的亮点是刷新指示运行良好。

t38_3_default_refresh

提示一下,这个带有刷新功能的控件,其背景色和控件颜色都可以修改。例如,下面的两行代码运行后的效果就是底色是红色,旋转图标是黄色。

  1. override func viewDidLoad() {
  2.     …
  3.     refreshControl = UIRefreshControl()
  4.     refreshControl.backgroundColor = UIColor.redColor()
  5.     refreshControl.tintColor = UIColor.yellowColor()
  6.     tblDemo.addSubview(refreshControl)
  7. }

t38_4_red_yellow_refresh

自定义控件的内容

自定义pull-to-refresh控件背后的关键点是给控件本身添加自己想要的任何额外内容,所添加的内容将作为子视图。示例程序中所谓的额外内容就是RefreshContents.xib文件。更具体地说,Interface Builder文件的内容是这个样子的:

t38_5_refresh_contents

如你所见,视图对象包含了依次七个标签。每个标签与“APPCODA”字母相对应。

接下来要做的事情非常简单:通过编程的方式将.xib文件的内容赋值给属性。更确切地说,将视图对象复制给UIView属性,所有标签都会被依次放到一个数组当中。这样做就可以把这些视图做成任何我们想要的动画效果。

现在深入到细节。首先,在类的开始处添加下列两行声明语句:

  1. var customView: UIView!
  2. var labelsArray: Array<UILabel> = []

有了上面新声明的两个属性,我们来创建一个新的自定义方法,用来加载.xib文件所有的内容:

  1. func loadCustomRefreshContents() {
  2.     let refreshContents = NSBundle.mainBundle().loadNibNamed(“RefreshContents”, owner: self, options: nil)
  3. }

我们会继续改进上面的自定义函数。下一步要做的是给customView分配上面代码加载的视图对象。注意,像上面的这种方法,从一个外部.xib文件中获取子视图,获得的是包含所有这些子视图的数组。在这里,数组只包含自定义的视图对象,也就是作为自定义视图子视图的那些标签,而不是.xib中自定义视图之外单独存在的视图对象。还要注意下面的几行代码,使自定义视图的框架与带有刷新功能旧有控件的框架要相吻合。

  1. func loadCustomRefreshContents() {
  2.     …
  3.     customView = refreshContents[0] as! UIView
  4.     customView.frame = refreshControl.bounds
  5. }

拖动表视图后刷新动作被触发,上面代码中的最后一行使自定义控件尺寸的变化与已知的约束相一致。

现在将所有的标签加载到labelsArray数组中。也许你已经注意到了,RefreshContents.xib文件中的每个标签都被分配了一个标号。从左侧开始,标号范围从1到7。我们将会利用这些标号单独访问每个标签。

  1. func loadCustomRefreshContents() {
  2.     …
  3.     for var i=0; i<customView.subviews.count; ++i {
  4.         labelsArray.append(customView.viewWithTag(i + 1) as! UILabel)
  5.     }
  6. }

最后,把自定义视图作为刷新控件的子视图添加进来:

  1. func loadCustomRefreshContents() {
  2.     …
  3.     refreshControl.addSubview(customView)
  4. }

搞定!还有一件事就是调用上面的函数,当然是在viewDidLoad方法中调用:

  1. override func viewDidLoad() {
  2.     …
  3.     loadCustomRefreshContents()
  4. }

最后要做一些重要的且必须要做的修改。在viewDidLoad方法中,将刷新控件的背景色和控件颜色设置成透明色。下面是修改后最终的代码。

  1. override func viewDidLoad() {
  2.     super.viewDidLoad()
  3.     // Do any additional setup after loading the view, typically from a nib.
  4.     tblDemo.delegate = self
  5.     tblDemo.dataSource = self
  6.     refreshControl = UIRefreshControl()
  7.     refreshControl.backgroundColor = UIColor.clearColor()
  8.     refreshControl.tintColor = UIColor.clearColor()
  9.     tblDemo.addSubview(refreshControl)
  10.     loadCustomRefreshContents()
  11. }

测试一下应用,拉动刷新时,可以看到带有标签的自定义视图取代了默认情况下的图标。当然还没有动画效果,下面我们就要实现动画效果。

t38_6_custom_contents

初始化自定义动画

这就是最终要实现的动画效果:

t38_7_refresh_animation

如果仔细观察,你会发现整个动画过程由两个子过程组成:

  • 从第一个开始,每个标签略微旋转(45度),与此同时,标签文本的颜色发生变化。下一个标签开始旋转前,当前旋转的标签状态复原。
  • 所有标签旋转过程完成后恢复原状,然后一起按比例放大,再按比例缩小。

我们把每个部分的动画作为单独的自定义函数来实现,尽量保持简单易懂。动手之前,要声明一些随后要用到的新属性。

  1. var isAnimating = false
  2. var currentColorIndex = 0
  3. var currentLabelIndex = 0

下面对每个属性做简单的介绍:

  • isAnimating标志动画过程是否发生。使用该属性用来告知是否能够开始一个新的动画过程(很明显,当一个动画开始后,我们不希望开始第二个动画)。
  • currentColorIndex属性会被用在实现的另一个自定义函数中。该函数拥有一个表示颜色(控件文本的颜色)的数组,这个属性表示下一个会被用到的标签文本颜色。
  • currentLabelIndex属性代表动画效果的第一个子过程里标签的索引。这样不仅可以确定下一个要旋转和着色的标签,还可以确定动画效果的第二个子过程(按比例放大)何时应该开始。

现在来处理动画效果的第一部分。使用一个叫做animateRefreshStep1()的函数完全实现了该功能:

  1. func animateRefreshStep1() {
  2.     isAnimating = true
  3.     UIView.animateWithDuration(0.1, delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: { () -> Void in
  4.         self.labelsArray[self.currentLabelIndex].transform = CGAffineTransformMakeRotation(CGFloat(M_PI_4))
  5.         self.labelsArray[self.currentLabelIndex].textColor = self.getNextColor()
  6.         }, completion: { (finished) -> Void in
  7.             UIView.animateWithDuration(0.05, delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: { () -> Void in
  8.                 self.labelsArray[self.currentLabelIndex].transform = CGAffineTransformIdentity
  9.                 self.labelsArray[self.currentLabelIndex].textColor = UIColor.blackColor()
  10.                 }, completion: { (finished) -> Void in
  11.                     ++self.currentLabelIndex
  12.                     if self.currentLabelIndex < self.labelsArray.count {
  13.                         self.animateRefreshStep1()
  14.                     }
  15.                     else {
  16.                         self.animateRefreshStep2()
  17.                     }
  18.             })
  19.     })
  20. }

我们来说说上面这段代码的核心部分。一开始,isAnimating标志被置为true,因此认定没有要开始的动画过程。随后会看到判断过程。接下来你会注意到,有两个产生动画效果的代码块。第二个代码块在第一个代码块执行结束的时候开始。其中的原因有两点:

  • 我们在第一个代码块中对当前标签执行旋转和更改标签文本颜色的操作(参见关于currentLabelIndex属性的描述)。
  • 动画效果的子过程结束时,想要将标签的状态复原,这个过程要优雅平缓,而不能显得突兀不自然。很明显,第二段起到动画效果的代码块起了作用。

在产生动画效果的代码块内,completion handler(完成处理程序)检查currentLabelIndex属性值是否有效。如果有效,就再次重复调用相同的函数。这样下一个标签就产生了动画效果。另外,所有标签都执行完动画过程后,就调用动画过程的第二个子过程的自定义方法(animateRefreshStep2())。

你肯定注意到了getNextColor()函数(在第一个动画效果代码块中)。之前说过,通过这个函数会得到当前带有动画效果控件的文本颜色。一会儿再看这部分。

我们现在解决动画效果第二个子过程,实现animateRefreshStep2()函数:

  1. func animateRefreshStep2() {
  2.     UIView.animateWithDuration(0.35, delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: { () -> Void in
  3.         self.labelsArray[0].transform = CGAffineTransformMakeScale(1.5, 1.5)
  4.         self.labelsArray[1].transform = CGAffineTransformMakeScale(1.5, 1.5)
  5.         self.labelsArray[2].transform = CGAffineTransformMakeScale(1.5, 1.5)
  6.         self.labelsArray[3].transform = CGAffineTransformMakeScale(1.5, 1.5)
  7.         self.labelsArray[4].transform = CGAffineTransformMakeScale(1.5, 1.5)
  8.         self.labelsArray[5].transform = CGAffineTransformMakeScale(1.5, 1.5)
  9.         self.labelsArray[6].transform = CGAffineTransformMakeScale(1.5, 1.5)
  10.         }, completion: { (finished) -> Void in
  11.             UIView.animateWithDuration(0.25, delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: { () -> Void in
  12.                 self.labelsArray[0].transform = CGAffineTransformIdentity
  13.                 self.labelsArray[1].transform = CGAffineTransformIdentity
  14.                 self.labelsArray[2].transform = CGAffineTransformIdentity
  15.                 self.labelsArray[3].transform = CGAffineTransformIdentity
  16.                 self.labelsArray[4].transform = CGAffineTransformIdentity
  17.                 self.labelsArray[5].transform = CGAffineTransformIdentity
  18.                 self.labelsArray[6].transform = CGAffineTransformIdentity
  19.                 }, completion: { (finished) -> Void in
  20.                     if self.refreshControl.refreshing {
  21.                         self.currentLabelIndex = 0
  22.                         self.animateRefreshStep1()
  23.                     }
  24.                     else {
  25.                         self.isAnimating = false
  26.                         self.currentLabelIndex = 0
  27.                         for var i=0; i<self.labelsArray.count; ++i {
  28.                             self.labelsArray[i].textColor = UIColor.blackColor()
  29.                             self.labelsArray[i].transform = CGAffineTransformIdentity
  30.                         }
  31.                     }
  32.             })
  33.     })
  34. }

我们使用了两个产生动画效果的代码块。在第一个代码块中,等比例放大了所有标签。请注意,这里无法使用循环结构完成任务(例如:for语句)。循环结构的执行过程与动画过程无关,这样在所有标签等比例放大执行完毕前循环结构早就执行结束了。

在程序执行“完成处理”部分,所有标签都完成了初始化处理。因此,这些标签再一次回到了初始状态。动画效果代码块内部的“完成处理”部分使用了if语句,刷新过程还在进行中,我们就做好了重新开始整个动画的准备。通过给currentLabelIndex属性简单地设置初始值(0)就能完成这个任务,调用第一个自定义方法来执行动画。下一步我们再来处理刷新结束后的事情。但是如果刷新结束了,可以通过修改isAnimating标志表示不再执行任何动画,而通过给所有的属性(和视图)赋初值来参与动画过程。这样一来,动画过程要在下一次拉动表视图的时候重新开始。

问题出来了,自定义动画应该在哪里开始呢?如果你仔细观察过上面的动画,就会发现每次表视图拖动完成,动画就开始一次。从编程的角度说,表视图是滚动视图(scroll view)的子类,我们所关心的代理方法是scrollViewDidEndDecelerating(_:)。每次表视图滚动停止时,该方法都会被调用。这个方法起初会检查刷新过程是否在进行。就我们这里的情况而言,就是要检查isAnimating标志的值。如果没有动画在进行中,就要调用之前实现的第一个函数来做初始化。就像下面这样的代码:

  1. func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
  2.     if refreshControl.refreshing {
  3.         if !isAnimating {
  4.             animateRefreshStep1()
  5.         }
  6.     }
  7. }

值得注意的是,上述滚动视图代理方法的使用并非强制性的,这取决于自定义pull-to-refresh控件的代码逻辑。也可以考虑其他代理方法,比如scrollViewDidScroll(_:)。

还落一部分没有说,那就是getNextColor()函数的实现:

  1. func getNextColor() -> UIColor {
  2.     var colorsArray: Array<UIColor> = [UIColor.magentaColor(), UIColor.brownColor(), UIColor.yellowColor(), UIColor.redColor(), UIColor.greenColor(), UIColor.blueColor(), UIColor.orangeColor()]
  3.     if currentColorIndex == colorsArray.count {
  4.         currentColorIndex = 0
  5.     }
  6.     let returnColor = colorsArray[currentColorIndex]
  7.     ++currentColorIndex
  8.     return returnColor
  9. }

这非常简单!首先将一些预定义的颜色(顺序完全随机)放到数组当中。然后确认currentColorIndex属性的值。如果不具有有效的值,就要赋初值(0)。使用currentColorIndex表示颜色,然后累加其值,因此,再次getNextColor()的时候后就不会得到相同的颜色。函数最后将选定的颜色返回。

现在可以再次尝试运行应用。拖动后刷新,会看到动画效果。当然,刷新控件并不会消失。这一部分还没有实现。随便怎么去用,动画的任何一部分都可以修改。

自定义动画之外的事情

为pull-to-refresh控件创建自定义动画很有趣,但不要忘了,用户刷新不是光为了观看控件有多好看,用户需要获得新的内容。这也是自定义pull-to-refresh控件时你的主旨思想。因此,完成了上面这些内容以后,下一步就要实现真正获取数据的过程。

显然,本文并未涉及获取任何数据的操作,也没有对表视图更新内容的操作。但是,这些都无法阻止我们实施之前所描述的逻辑。所以要继续完成其余的任务,那就是创建一个新的自定义函数,名为doSomething()(蛮滑稽的名字,是不是觉得是个啥也干不了的函数)。我们要在该函数中初始化一个计时器(NSTimer)对象。4秒钟的时间间隔后,就会发出刷新过程结束的信号。在真实的应用软件中,没有必要这样做。当获取数据的过程结束时,刷新过程也就结束了。

首先,回到类定义的开始,(最后一次)声明一个对象:

  1. var timer: NSTimer!

现在来“do something”:

  1. func doSomething() {
  2.     timer = NSTimer.scheduledTimerWithTimeInterval(4.0, target: self, selector: “endOfWork”, userInfo: nil, repeats: true)
  3. }

4秒钟已经足够了,这样可以不止一次地看到动画效果。在上面的方法中可以看到仅有的一行代码,预设的时间间隔过后endOfWord()就会被调用。这样就会停止刷新过程,使计时器失效:

  1. func endOfWork() {
  2.     refreshControl.endRefreshing()
  3.     timer.invalidate()
  4.     timer = nil
  5. }

这个时候我们几乎大功告成了。唯一要做的就是调用doSomething()函数,要在动画过程开始前调用。因此需要再次修改滚动视图的代理方法:

  1. func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
  2.     if refreshControl.refreshing {
  3.         if !isAnimating {
  4.             doSomething()
  5.             animateRefreshStep1()
  6.         }
  7.     }
  8. }

示例程序做好了,这回再体验一把!

t38_1_final_sample

总结

你也看到了,自定义一个pull-to-refresh控件一点儿都不难。只是把好的创意用图形表现出来,仅此而已。正如我在最后的部分所讲的,主旨在于真实数据的获取过程,而不是炫耀所创建的可视化效果。还要注意的是数据一旦更新完毕,就不要惦记着延长时间而阻止刷新控件隐藏。这样做会导致你不愿意看到的用户体验,很糟糕。如果应用软件对用户非常有帮助的话,就会有很多机会让用户领略软件的自定义效果,所以不要试图更新完毕后还阻止刷新控件隐藏。文中示例程序所自定义的内容比较简单,但足以说明问题。你知道,这是编程方面的问题,可以接受很多自定义的内容和即兴创作。当然,最终的应用软件甚至可能会彼此各不相同。自定义的pull-to-refresh控件要与不同的应用相匹配。总之,本文就此告一段落,真心希望能对你有所帮助。能够帮助你愉快地开发出自定义的pull-to-refresh控件!

作为参考,可以从这里下载整个项目

http://www.csdn.net/article/2015-09-11/2825686

30 8月

苹果官方博客:从SDK详说Swift代码的改变

文 / Apple Inc.     原文链接:Apple Developer’s Blog

Xcode 6.3中,我们为Objective-C添加了新语言特性nullability注解。这个特性给Objective-C提供了表达API使用null和non-null统一的方式。相当于Swift语言中的Optional类型。Xcode 7继续为Objective-C与Swift之间更加自然地通信引入轻量级的泛型模型。泛型使得这两种语言能够可靠地融合并分享带有特定类型元素的集合。

这些特性对任何使用Swift和Objective-C这两种语言编程的人来说都比较有用。但应用程序开发人员每天使用的Objective-C代码占更大部分:这就是组成Apple SDK的那些框架。为了提升Swift和Objective-C的使用体验,全公司的范围内,我们在SDK的头文件中都提供了这样的信息。Xcode 7中,你会发现几乎所有常见框架都指定了其API的nullability注解,包括其集合类型的元素。这使得Swift代码的样子发生了变化。

变化前:

  1. class UIView : UIResponder {
  2.     init!(frame: CGRect)
  3.     var superview: UIView! { get }
  4.     var subviews: [AnyObject]! { get }
  5.     var window: UIWindow! { get }
  6.     // …
  7.     func isDescendantOfView(view: UIView!) -> Bool
  8.     func viewWithTag(tag: Int) -> UIView!
  9.     // …
  10.     var constraints: [AnyObject]! { get }
  11.     // …
  12. }

变化后:

  1. class UIView : UIResponder {
  2.     init(frame: CGRect)
  3.     var superview: UIView? { get }
  4.     var subviews: [UIView] { get }
  5.     var window: UIWindow? { get }
  6.     // …
  7.     func isDescendantOfView(view: UIView) -> Bool
  8.     func viewWithTag(tag: Int) -> UIView?
  9.     // …
  10.     var constraints: [NSLayoutConstraint] { get }
  11.     // …
  12. }

最后一个疑问是Xcode 7中将代码转换成Swift 2的工具,在Xcode的Edit菜单下,Convert > To Latest Swift Syntax。通过必要的编辑,该工具会将一个使用Swift 1.2编写的项目转换成合法的Swift 2.0代码。这些改变源于改进后的头文件信息。例如,正在重写的一个方法,其参数和结果类型更加精确,方法转换的过程中将会被更新与之匹配。

在今年的WWDC的Swift and Objective-C Interoperability视频14:30处,对Objective-C所作的改进做了详尽的描述。注意,该视频使用的是Xcode 6.3中的__nullable语法,并非更新的Xcode 7中用的_Nullable。了解更多nullability注解的信息,可以看看Nullability and Objective-C。了解Swift 2.0和Objective-C中轻量级泛型更多信息,可以看看Xcode 7 Release Notes

http://www.csdn.net/article/2015-08-17/2825473-swift-er-sdk

09 8月

Stripe使用指南:Swift开发中的信用卡支付

文 / Ziad Tamim:资深iOS开发人员,创业公司移动战略咨询顾问。自App Store上线以来,编写超过80款应用程序。目前,他经营着一家叫做TAMIN LAB的移动开发工作室。

原文链接:http://www.appcoda.com/ios-stripe-payment-integration/

在这篇文章中,我们来谈谈Stripe的集成。Stripe提供了程序库来接受在线及移动应用支付行为,是功能最为强大的程序库之一。如果你计划在iOS应用中出售产品,或者在寻找一种支付解决方案,那么Stripe应该是你的首选。

很多iOS开发人员问我,为什么要选择Stripe而不是应用内购买(In-App Purchase)。根据苹果公司的规定,出售数字化内容需要使用应用内购买,像电子游戏附带的游戏级别,应用给予用户的虚拟物品。对于像衣服这样的实物,则允许使用像Stripe 这样的其他支付方案。所以,在这篇文章中,我会简要介绍Stripe,并解释它是如何工作的,告诉你如何使用Stripe的API构建一个示例,用来接受信用卡的支付行为。
stripe-integration-tutorial

Stripe为何物?

Stripe为开发人员构建,提供友好的API,使商家能够立即接受并管理移动支付行为。而不必为开设商业账户,设立信用卡网关而费心费力。有了Stripe,你可以轻松地实现应用的信用卡支付功能,甚至是重复付款。

Stripe意在使在线/移动交易唾手可得。你很快就会看到,在方便易用的API顶端,Stripe甚至精简了注册流程,集成过程就是这么简单!这就是该公司为什么能够如此迅速地扩张,名列2015十大创业公司行列的原因。

我们的应用程序

为了避免从头构建应用浪费时间,把注意力集中在学习Stripe上,我创建了项目模版。首先,下载项目并运行一下。

在本文中,我们将会创建一个叫做“Donate”的示例,演示如何使用Stripe接受信用卡付款。

iOS-Simulator-Screen-Shot-Jun-9-2015-5.35.27-PM-576x1024

编辑注:此应用为示例程序,根据苹果公司的App Store Review Guidelines,如果应用带有捐款功能,应用本身应当免费,必须通过网站的Safari页面或SMS服务完成公益款项的筹集。

应用的业务周期及Stripe的工作原理

深入实现之前,我们先看看应用如何与Stripe通信,如何处理事务。简而言之,用户发送支付请求的时候,应用如何工作:

1. 应用首先收集支付信息,包括电子邮件、信用卡号码、有效期、CVC(Card Validation Code)和支付表单上的支付金额。然后通过调用Stripe的API将这些信息发送给自己的服务端。

2. 如果支付信息有效,那么Stripe就会返回一个令牌(token)。此时,信用卡还没有被扣费。随后你会使用令牌执行实际的交易。

3. 现在应用获得了令牌。下一步是将其发送到本地服务器来完成扣费的过程。出于安全原因,Stripe需要通过服务器来进行扣费,而不是移动设备本身。不过不用担心,Stripe为我们提供了服务代码,可以轻松完成扣费过程。我们会在后面的小节讨论。

4. 你的本地服务器发送令牌到Stripe来执行实际的扣费操作。无论交易成功与否,Stripe都会返回结果。

5. 最后,本地服务器将有关交易结果的响应返回给应用。

stripe-flow

注:返回的响应是JSON格式的,所以如果不熟悉JSON解析的话,可以查看这个教程

创建一个Stripe测试帐号

创建一个Stripe测试帐号非常简单,可以在几秒钟内完成。Stripe可以简化整个注册过程,注册一个帐号不需要填写任何信息。我真的很喜欢这种精简的流程。如果你正在为客户开发应用,或者仅打算探索一下Stripe的API,这样的流程再好不过了。

第一步是去Stripe的网站,点击sign up按钮:

stripe-signup

你将被引导到注册页面创建一个新的帐号,在这里可以填写个人信息。但正如我之前提到的,Stripe精简了注册流程,可以在这里简单地单击“skip this step”继续。你将能够通过一个以测试为目的的临时帐号访问Stripe的Dashboard页面。

Screen-Shot-2015-06-09-at-9.05.20-PM-e1433890510184

一旦完成这个过程,Stripe的Dashboard页面就会呈现在你面前:

stripe-dashboard

接下来,点击菜单中的“Your Account”,点击“Account Settings”,然后点击“API Keys”选项卡,会显示用于应用开发的Key。

Screen-Shot-2015-06-10-at-12.05.31-AM

这就是Stripe的配置过程。从现在开始,你就可以使用Stripe的API来测试交易了。以后还可以回到Dashboard页面检查交易是否被成功处理了。

使用CocoaPods添加Stripe库

在Xcode工程使用Stripe之前,我们必须将库引入作为一个依赖项。有几个方法可以做到这一点。在示例中,我们将使用CocoaPods,它是一个著名的Swift项目依赖管理工具。就像这个示例,很多项目都依赖于第三方库工作。CocoaPods是一种工具,有助于开发人员管理所需的依赖库,确保这些库是最新的。

如果还没有安装CocoaPods,可以在终端执行下面的命令安装:

sudo gem install cocoapods

只要有耐心,只要等待几分钟的时间就能完成安装。安装完成后,你可以看到“”gems installed这样的字眼儿。

Screen-Shot-2015-06-10-at-4.09.48-PM

接下来,在Xcode工程根目录下创建一个名为“Podfile”的文件,使用你喜欢的文本编辑器打开它,把下面两行复制进来:

pod 'Stripe', '~> 4.0'
pod 'AFNetworking', '~> 2.5.4'

Podfile告诉CocoaPods我们想使用哪个库。我们这里需要是Stripe 4.0和AFNetworking 2.5.4。

现在,退出Xcode,打开Terminal,更改Xcode的根目录。根目录是“Podfile”所在目录。接下来,在Terminal键入:

pod install

CocoaPods将寻找“Podfile”,尝试安装我们所设置的依赖项。下载和安装的过程可能要持续几分钟。完成后,结果应该是这样的:

Screen-Shot-2015-06-10-at-4.28.31-PM

CocoaPods下载完毕,在Xcode项目中引入了Stripe和AFNetworking。然而,从现在起,我们不会再使用原始应用项目了。CocoaPods生成了另一个叫做Donate.xcworkspace的工作区。这个工作区包含了原始项目和CocoaPods管理的项目。

现在打开Xcode中新的工作区(也就是Donate.xcworkspace),你应该注意到一共有两个项目:Pods和Donate。

cocoapod-projects

在Swift项目中使用Objective-C

Stripe的库是用Objective-C写的,而我们的项目使用Swift。不进行适当的配置,这两种语言无法一起工作。通过建立桥接头文件(briddging header),任何Objective-C的库,项目和类都可以在Swift项目中使用。从技术角度来说,这样的关联会把头文件从Objective-C转换成Swift。

通过以下步骤可以创建一个桥接头文件:

  1. 在Donate路径中添加一个新的头文件。对准项目导航器中的Donate右击,然后点击“New File…”。
  2. 选择iOS -> Source分类,选择“Header File”模版,然后点击“Next”。
  3. 将类命名为“Donate-Bridging-Header.h”,点击继续,进行下一步并保存文件。
  4. 接下来,去“Building Settings”,找到“Objective-C Bridging Header”,将值设置为“Donate/Donate-Bridging-Header.h”。

Screen-Shot-2015-06-10-at-4.37.23-PM-1024x312

完成后,打开Donate-Bridging-Header.h文件,编写如下代码:

#import <Stripe/Stripe.h>
#import <AFNetworking/AFNetworking.h>

太棒了!终于完成了项目的配置。我们现在已经做好准备,进入示例程序的实现阶段。

设置API Key

你要做的第一件事就是配置Stripe的API Key。在Stripe的Dashboard页面,你可以点击“Account Settings” -> “API keys”来找到你的Key。现在,我们将使用用于测试的Key,这样你就可以不用真实的信用卡也可以测试交易过程了。

打开AppDelegate.swift文件,在里面插入一行代码(第3行):

  1. func application(application: UIApplication, did-FinishLaunchingWithOptions launchOptions: [NSOb-ject: AnyObject]?) -> Bool {
  2.      // Stripe Configuration
  3.      Stripe.setDefaultPublishableKey(“pk_test_IeR8DmaKtT6Gi5W7vvySoCiO”)
  4.      return true
  5. }

这里使用的是我们用于测试的Test Publishable Key,别忘了你要改成自己的。

注意:以后在实际应用程序中,你可以将Test Publishable Key换成Live Publishable Key。

收集信用卡信息

要进行交易,应用要有一个收集用户信息的环节。Stripe建议通过三种方法收集付款信息:

  • 通过Apple Pay访问用户所存储的支付信息
  • 通过Stripe内置的支付表单组件,PaymentKit
  • 通过构建自己的支付表单

在本文中,我们选择第三种方式,构建我们自己的支付表单。Stripe至少需要收集信用卡号和其有效期。但我也建议要保护CVC的安全,防止欺诈性交易,并获得用户的信任。你可能想要获得另一条信息是电子邮件地址,这样你就可以留有捐款者的记录以备后续沟通。

如果你用的是项目模版,我已经在其中建立了支付接口来收集用户的支付细节。所有的文本字段都与ViewController.swift文件中对应的outlet关联了。

donate-app-storyboard

现在,打开ViewController.swift,更新donate方法:

  1. @IBAction func donate(sender: AnyObject) {
  2.         // Initiate the card
  3.         var stripCard = STPCard()
  4.         // Split the expiration date to extract Month & Year
  5.         if self.expireDateTextField.text.isEmpty == false {
  6.             let expirationDate = self.expireDateTextField.text.componentsSeparatedByString(“/”)
  7.             let expMonth = UInt(expirationDate[0].toInt()!)
  8.             let expYear = UInt(expirationDate[1].toInt()!)
  9.             // Send the card info to Strip to get the token
  10.             stripCard.number = self.cardNumberTextField.text
  11.             stripCard.cvc = self.cvcTextField.text
  12.             stripCard.expMonth = expMonth
  13.             stripCard.expYear = expYear
  14.         }
  15. }

当用户点击Donate按钮时,该动作方法将被触发。我们首先初始化一个STPCard对象,并进行赋值。

下一步,在同样的方法中添加如下代码:

  1. var underlyingError: NSError?
  2. strip-Card.validateCardReturningError(&underlyingError)
  3.    if underlyingError != nil {
  4.         self.spinner.stopAnimating()
  5.         self.handleError(underlyingError!)
  6.    return
  7. }

STPCard类有非常方便的方法validateCardReturningError,我们就不用实现自己的验证逻辑了。类将收集到的信用卡信息发送给服务器,如果卡片信息是无效的,就会返回一个错误(error)。

像上面一样,将下面的代码添加到统一的方法中:

  1. STPAPICli-ent.sharedClient().createTokenWithCard(stripCard, completion: { (token, error) -> Void in
  2.             if error != nil {
  3.                 self.handleError(error!)
  4.                 return
  5.             }
  6.             self.postStripeToken(token!)
  7.         })

一旦确认信用卡有效,我们就调用STPAPIClient类的createTokenWithCard方法,通过安全的HTTPS请求发送信用卡数据。如果请求成功,Stripe将返回一个令牌。

这个时候用户的信用卡还没发生扣费。Stripe仅仅给了你一个一次性的令牌。稍后你将这个令牌发送到本地服务器去执行实际的扣费。

如果无法获取令牌,应用会简单地向用户显示一个错误信息。还是在同一个文件中,ViewController类中需要插入如下代码:

  1. func handleError(error: NSError) {
  2.         UIAlertView(title: “Please Try Again”,
  3.             message: error.localizedDescription,
  4.             delegate: nil,
  5.             cancelButtonTitle: “OK”).show()
  6.     }

建立本地服务器

本文刚开始的时候提到过,用户的信用卡扣费实际发生在我们自己的服务器上。出于安全原因,Stripe不直接从应用程序对信用卡进行扣费。Stripe只是生成一个令牌。应用将这个令牌传递给本地服务器进而进行实际的扣费。

所以在实现应用程序这一步之前,让我们停留片刻,建立自己的本地服务器来处理付款。为了创建自己的服务端,我们将使用PHP作为主要的编程语言,并利用Stripe提供的库。不用担心,你不必是一位Web开发的行家,跟着所描述的步骤走,就能配置处理Stripe支付所需要的服务端。

首先,下载XAMPP (for OS X) v5.6.8 (or up),在Mac上安装。接下来,下载这个压缩文件并解压缩。找到XAMPP的安装目录,通常安装在“Applications”文件夹。打开“Applications/XAMPP/htdocs”,拷贝压缩后的文件夹。

xampp-donate-htdoc

完成以后,返回XAMPP文件夹,打开“manager-osx”。在“Manage Servers”标签下点击“Start All”,服务就启动了:

xampp-manager

下一步就是去“Applications/XAMPP/htdocs/donate”,使用你喜欢的任意文本编辑器打开文件“payment.php”。

用Test Secret Key将方法中的参数替换掉,这在本文中已做描述:

\Stripe\Stripe::setApiKey("sk_test_qCTa2pcFZ9wG6jEvPGY7tLOK");

搞定了!你刚才已经完成了服务端的配置,简单吧?

给服务端发送令牌进行支付处理

现在,我们的本地服务器准备处理付款。我们回到Xcode,通过发起一个HTTP POST请求给本地服务器开始发送令牌。打开ViewController.swift,在文件中插入代码:

  1. func postStripeToken(token: STPToken) {
  2.         let URL = “http://localhost/donate/payment.php”
  3.         let params = [“stripeToken”: token.tokenId,
  4.             “amount”: self.amountTextField.text.toInt()!,
  5.             “currency”“usd”,
  6.             “description”: self.emailTextField.text]
  7.         let manager = AFHTTPRequestOperationManager()
  8.         manager.POST(URL, parameters: params, success: { (operation, responseObject) -> Void in
  9.             if let response = responseObject as? [String: String] {
  10.                 UIAlertView(title: response[“status”],
  11.                     message: response[“message”],
  12.                     delegate: nil,
  13.                     cancelButtonTitle: “OK”).show()
  14.             }
  15.             }) { (operation, error) -> Void in
  16.                 self.handleError(error!)
  17.         }
  18. }

由于我们在本地建立了服务端,那么就把URL设置为http://localhost/donate/payment.php。如果在远程主机中建立的服务端,请做相应的更改。

Stripe要求我们提交令牌来执行实际的扣费。外带付款额,币种和相关描述。你可以使用描述字段作为付款说明。在示例中,我们只是使用这个字段来存储用户的电子邮件。

一旦配置好参数,就使用AFHTTPRequestOperationManager提交一个异步的POST请求。这是AFNetworking提供的一个API。如果请求成功,我们就向用户显示支付响应。

这样就好啦!现在你可以运行应用程序并测试交易功能。出于测试目的,可以使用4242 4242 4242 4242这个信用卡号,还有任意的将来日期作为有效期(例如07/2019),包括CVC(例如222),这样可以模拟交易过程。

donate-stripe-success-300

为了进一步验证支付是否成功,可以看看Stripe的Dashboard:

Screen-Shot-2015-06-11-at-5.09.01-PM

总结

在本文中,我带着你学习了Stripe的基础知识。你应该学会了如何在应用中集成Stripe来接受信用卡的支付行为。为了进一步学习,我鼓励你去看看官方文档。Stripe提供了许多通过其API完成支付的方式,可以通过官方文档了解更多信息。

作为参考,可以从这里下载整个Donate项目,请使用Xcode 6.3(或以上)版本运行。别忘了把AppDelegate.swift文件中的Secret Key换成自己的。

http://www.csdn.net/article/2015-07-27/2825301-ios-stripe-payment-integration/1