28 6月

使用Swift构建带有地理定位功能的App

文/Eugene Trapeznikov:高级iOS开发工程师,他不断学习新技术。范围从移动开发到web敏捷实践(持续集成和行为驱动开发)。在过去的四年中,发布了超过10App,其中两款被Apple Store中的精品推荐(featured收录。

原文链接:http://www.appcoda.com/geo-targeting-ios/

地理定位是根据用户所处的地理位置,来显示不同的内容的一种方法。比如根据国家、地区、城市或者其他标准来定位。地理定位应用在很多地方。想象一下,一位客户正去造访你的竞争对手,我们可以给予他特别的优惠,把他吸引过来。再比如,如果用户在过去的几天里,跑了好几家汽车分销商,这就说明他想要买一辆新车。因此我们可以向他展示我们的汽车广告。这样有目标的广告投放总比随机投放要有效得多。

在这片文章中,我们会教你如何在iOS设备中实现地理定位,会向你介绍苹果标准库CLRegion。并且,我们还会教你如何测试不常见的功能特性。还会看到如何实现复杂的追踪逻辑。最后,我们还会教你如何创建自定义的区域,并向你解释什么情况下自定义的区域要优于CLRegion。你可以使用地理定位功能开发许多带有创新性定位功能的App

geotarget-demo

创建工程

假定要追踪用户去饭店。下面开始创建工程,创建以GeoTargeting命名的单视图应用。打开

Main.storyboard文件,在View上面拖拽一个Map Kit View。在ViewController.swift中为它创建@IBOutlet。编译的时候会报错,不过先不用管,storyboard就先这样,我们一起去看看ViewController.swift

我们要把MapKitCoreLocation的库加上去。并且在View Controller类中加上它俩的协议:

class ViewController: UIViewController, MKMapDelegate, CLLocationManagerDelegate {
    @IBOutlet weak var mapView: MKMapView!
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

接下来我们创建mapViewlocationManager实例:

//1. create locationManager

    let locationManager = CLLocationManager()

    override func viewDidLoad() {
        super.viewDidLoad()
        // 2. setup locationManager
        locationManager.delegate = self
        locationManager.distanceFilter = kCLLocationAccuracyNearestTenMeters
        locationManager.desiredAccuracy = kCLLocationAccuracyBest

        // 3. setup mapView
        mapView.delegate = self
        mapView.showsUserLocation = true
        mapView.userTrackingMode = .Follow

        // 4. setup test data
        setupData()

ViewController.swift文件中做了下面这些事情:

  1. 创建了locationManager实例,用来探测用户位置的变化。
  2. locationManager的代理指向了ViewController,这样就可以通过View Controller追踪用户的位置和区域范围了。另外,将定位服务的精准度设置成了最佳精准度(kCLLocationAccuracyBest)。
  3. 创建了mapView实例,设置了它的代理,这样就可以使mapView单独绘制图形。让mapView显示用户的位置,使我们能够对其进行追踪。这也让人更容易了解用户是否在某个区域内。也可以在Storyboard中设置MapView的代理,使用showsUserLocation属性显示用户的位置信息。用代码实现更直观。
  4. 创建了测试数据,创建的方法在后面实现。

接下来就要追踪用户的地理位置了。在这之前,我们需要检查是否有权限这样做。

    override func viewDidAppear(animated: Bool) {
        // 1. status is not determined
        if CLLocationManager.authorizationStatus() == .NotDetermined {
            locationManager.requestAlwaysAuthorization()
        }
        // 2. authorization were denied
        else if CLLocationManager.authorizationStatus() == .Denied {
            //showAlert("Location services were previously denied, please enable loaction services")
        }
        // 3. we do have authorization
        else if CLLocationManager.authorizationStatus() == .AuthorizedAlways {
            locationManager.startUpdatingLocation()
        }
    }

我们来仔细分析一下。首先,在viewDidAppear方法中检查了授权状态。用户在设置中将授权状态修改后我们还可以对授权状态再次检查一遍。

因此,可能出现下面几种情况:

  1. 如果用户从未选择或设置过授权状态,那么我们就要设置“Always”权限。在CLRegion使用状态监控,也只能这样设置。
  2. 如果定位权限之前被用户拒绝使用了。我们就要通知用户,允许使用地位服务,App会获得更佳的工作状态。这里使用了showAlert(title: String)方法。这也是UIAlertController来现实信息的便捷做法,还可以带有“Cancel”按钮。我们稍后会在几个地方用到该方法。
  3. 如果获得了定位权限,那么久可以使用startUpdatingLocation()方法了。

现在可以运行App了。你可能会问,并没有弹出授权定位的提示框呀?这需要打开…-Info.plist文件,在文件中添加一行,NSLocationAlwaysUsageDescription,然后把它作为key,添加value。例如:“Regions needs to always be able to access your location.”。当使用requestAlwaysAuthorization()方法时会用到上面的Plist信息。否则系统就会无视定位请求。这个键值对描述了系统使用定位功能的原因。

Plist里面添加了上述信息后,再次运行App就可以弹出设置的描述信息了。

当然,App中,对定位授权的情况可能有很多种,我们这里所选择的是最基本的一项。

接下来我们就要对地理位置进行监控了。

func setupData() {
        // 1. check if system can monitor regions
        if CLLocationManager.isMonitoringAvailableForClass(CLCircularRegion) {
            // 2. region data
            let title = "Lorrenzillo' s"
            let coordinate = CLLocationCoordinate2DMake(37.703026, -121.759735)
            let regionRadius = 300.0

            // 3. setup region
            let region = CLCircularRegion(center: CLLocationCoordinate2D(latitude: coordinate.latitude,longitude: coordinate.longitude), radius: regionRadius, identifier: title)

            // 4. setup annotation
            let restaurantAnnotation = MKPointAnnotation()
            restaurantAnnotation.coordinate = coordinate;
            restaurantAnnotation.title = "\(title)";
            self.mapView.addAnnotation(restaurantAnnotation)

            // 5. setup circle
            let circle = MKCircle(centerCoordinate: coordinate, radius: resionRadius)
            self.mapView.addOverlay(circle)
        }
        else {
            print("System can't track regions")
        }

        // 6. draw circle
        func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
            let circleRenderer = MKCircleRenderer(overlay: overlay)
            circleRenderer.strokeColor = UIColor.redColor()
            circleRenderer.lineWidth = 1.0
            return circleRenderer
        }
    }

我们一步步地来看,setupData()里面都做了哪些事情:

  1. 无论如何都要检查当前设备是否支持对制定区域进行位置监控。如果用户拒绝了来自系统的定位请求,那么isMonitoringAvailableForClass就会返回false,包括把后台应用刷新的功能禁掉,或者在飞行模式的情况下。
  2. 我们在这里创建了一个restaurant用来测试,本文中这样做无可厚非,但在实际项目中,你需要为被监控对象创建单独的类。
  3. 我们还创建了监控区域,使用restaurant的名称作为被监控区域的ID。由于这事探测用户所到区域的唯一方法,我们需要这样做。不要把强引用存入CLRegion,而可能只要记下区域的ID,以备后用。
  4. 为了视觉效果更好,我们在区域的中心位置添加了说明性的信息。
  5. 同样,我们在地图上也添加了圆环,表示区域的边界。
  6. 绘制圆环调用的是MKMapViewDelegate的代理方法。

这些都是工程里的准备工作,接下来,我们就要对区域进行监控了。

苹果的CLRegion

本文中,我们在对地理位置进行监控。苹果的CLRegion类是针对已知区域特定半径的圆形区域。所以只能使用CLRegion监控圆形区域。

    // 1. user enter region
    func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion) {
        showAlert("enter" \(region.identifier)")
    }
    // 2. user exit region
    func locationManager(manager: CLLocationManager, didExitRegion region: CLRegion) {
        showAlert("exit \(region.identifier)")
    }

添加这两个方法是为了通知用户进入或是离开了被监控的区域。使用简单的提醒方式只是为了方便查看。给圆形区域周边加个过渡区域,用户在边缘移动的时候,就不会收到大量进出边界的消息,这种情况下,给出进出消息的这两个方法不会被调用。

还要注意同时监控多个位置的情况。一个App最多只能监控20个位置,这些位置在用户所在位置的周边。当用户所在位置发生变化时,可以去掉较远的位置,把用户移动轨迹上的位置添加进来。如果监控的位置数量到达了上限,LocationManager会调用monitoringDidFailForRegion方法。这样做可以获得更好的用户体验。

刚才提到的两个方法,还有一些限制,都是需要注意的。

基本的方法都已经创建好了。现在你可以驾车去被监控的区域了吧!呵呵,开玩笑的,有一种便捷方法来测试功能。

如何测试

Xcode当中,有一种简便的方法,那就是使用GPX文件。在开发过程中,GPX是描述GPS的常见数据格式,是XML文件。看看再说:

<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">
    <wpt lat="37.702801" lon="-121.751862"></wpt>
    <wpt lat="37.702821" lon="-121.752321"></wpt>
    <wpt lat="37.702840" lon="-121.752780"></wpt>
</gpx>

这个GPX文件中,在我们所在区域附近有三个定位点。Xcode会逐一扫描并移动用户的位置。每秒移动一个位置。在Xcode中可以自行创建GPX文件。

再次运行App,在Xcode中打开“Debug Area”(界面下方),选择“Simulate Location”,选择刚刚添加的GPX文件。再看看模拟器,用户的位置应该发生了变化。

geotarget-simulate-location

几秒钟后,你就会看到“enter Lorrenzillo’s”这样的提示信息。然后再过几秒钟,会提示“exit Lorrenzillo’s”。所以,位置监控功能已经工作了,恭喜!

geotarget-demo-app-1

给被监控区域添加复杂的业务逻辑

对于一些App来说,监控进入或离开事件已经足够了。但如果需要更复杂的业务逻辑,又该如何呢?也许你会关注用户在某个区域的停留时间,或者在该区域内移动的平均速度。在我们的示例中,会检查用户是否在饭店中停留了足够长的时间,这意味着用户是否造访了饭店,我们可以从用户那里收集反馈信息。下面对代码来做一些改进:

    var monitoredRegions: Dictionary<String, NSDate> = [:]
    // 1. user enter region
    func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion) {
        //showAlert("enter" \(region.identifier)")

        // 2.1 Add entrance time
        monitoredRegions[region.identifier] = NSDate()
    }

    // 2. user exit region
    func locationManager(manager: CLLocationManager, didExitRegion region: CLRegion) {
        //showAlert("exit \(region.identifier)")

        // 2.2 Remove entrance time
        monitoredRegions.removeValueForKey(region.identifier)
    }

    // 3. Update resions logic
    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        updateRegions()
    }
  1. 在字典中存储用户进入监控区的时间。
  2. 在这里实现了时间的添加和删除功能。
  3. LocationManager的代理方法didUpdateLocations会帮助我们检查用户在监控区域内是否停留了足够的时间。
func updateRegions() {
        // 1. 
        let regionMaxVisiting = 10.0
        var regionsToDelete: [String] = []
        //2.
        for regionIdentifier in monitoredRegions.keys {
            //3.
            if NSDate().timeIntervalSinceDate(monitoredRegions[restorationIdentifier!]!) > regionMaxVisiting {
                //showAlert("Thanks for visiting our restaurant")    
                regionsToDelete.append((regionIdentifier))
            }
            // 4.
            for regionIdentifier in regionsToDelete {
  monitoredRegions.removeValueForKey(regionIdentifier)
            }
        }
    }
  1. 假定10秒钟对用户来说已经足够了。我们也需要变量来存储用户的位置,就是用户停留足够长时间的区域,将来会删除掉,因为我们已经达到了目的。
  2. 遍历所有当前被监控的区域。
  3. 如果用户在被监控区域停留了足够长的时间,我们就会向用户显示特定的消息,并把该区域标记为待删除区域。
  4. 删除所有待删除区域。

现在你就可以完全用updateRegions方法来实现任意复杂度的监控方法了。

自定义监控区域

令人遗憾的是,苹果的CLRegion有很多限制。其中之一就是用户如果对App设置了“While in Use”访问权限,你就无法监控用户所在位置了。我们都知道,很多用户担心它们手机的电池寿命,从而不想让App一直开启监控。真的很难向用户解释说,一直开启位置监控会有对他们有益处。这也是为什么有时你只在用户使用App的时候才监控用户的位置。我建议你创建自定义的区域类,像CLRegion那样带有类似接口和回调方法的类。这样就会容易理解自定义类的功能,就像其它开发人员所熟悉的CLRegion类一样。

protocol RegionProtocol {
    var coordinate: CLLocation {get}
    var radius: CLLocationDistance {get}
    var identifier: String {get}

    func updateRegion()
}
protocol RegionDelegateProtocol {
    func didEnterRegion()
    func didExitRegion()
}

你可以使用上面的协议,轻松地替换掉CLRegion

CLRegion另外一项限制是我们只能监控圆形区域。有时我们需要监控多边形(四边形、五边形等)或者椭圆形区域。这种情况依旧可以使用自定义的类。只要使用RegionDelegateProtocol中的didEnterRegion方法,检查是否进入了被监控区域即可。

另外,也没有必要立刻就向用户显示进出警告信息。我们要为今后的数据分析存储这些数据,这里有很多用例可以去实现。

http://geek.csdn.net/news/detail/79643

28 10月

Swift 2.1的新变化

文 / Russ Bishop:全能型程序员,使用C#,Objective-C和Swift语言编程,开发了奇特的应用Storm Sim Free。

原文链接:http://www.russbishop.net/swift-2-1

如果你觉得苹果的Swift语言没有赶上趟,那就要重新审视这一观点了!Xcode 7.1 Beta 2版本已经支持Swift 2.1了。我没有必要在此事无巨细一一列举,你随时可以自行查看其发布说明的。

互操作性和数据类型

  • 从C语言中引入的枚举类型自动遵循Equatable协议。所以用于样式匹配的开关语句(switch)目前不再强制去写扩展(extension)声明和操作符(operator)。
  • C语言中的非匿名联合体(union)被作为结构体(struct)引入。结构体中的每一个字段与联合体中的字段相对应。Swift中的结构体大概是用来维持一种能力的,即使用联合体给底层同一数据位(bit)投影不同的字节或字。但我还没验证过Swift结构体的域(field)是否会用同一个位进行存储。
  • C语言结构体中,相对鲜为人知的位域(bitfield)也被引入了Swift,能够使用了。
  • dispatch_block_t变回了@convention(block) () -> Void的样子,因此 dispatch_block_create又能正常工作了。

人欣喜的特性

  • 字符串可以按照字符串为单位进行插入操作。这是一直困扰我的地方。如今可以这样做了:”fancy \(thing ?? “”)”。
  • 如果只是文件中的私有内容被修改,则不会诱发与之依赖的其他文件重新编译。有些时候,这样做性能会得到大幅地提升。
  • 类型检查产生了“继续改进”模样的错误消息。我不会太多关注这一点,除非明显遇到了很常见的场景,那就是一个普通的闭包本身带有错误。这基本上是说你出错了,要么是超出了范围,要么是声明了没有用的内容;我已经习惯了剪切和粘贴,从而避免了这样的问题。我也会手动给闭包传递参数和返回类型。这部分是不错的!

协变性和逆变性

函数和闭包目前都具有协变性(covariance)和逆变性(contravariance)

这一奇特的性质表示可以通过Any -> Int这样的转换,得到String -> Any这样的结果。

换句话说,当返回类型的派生程度较目标类型大时,闭包的参数派生程度就较目标类型的小。想一想这是合理的……如果函数可以接受(Any, Any)类型的参数,那么它一定能够接受(String, Int)和(AnyObject, NSURLRequest)类型的参数。如果目标类型是AnyObject,那么UIButton肯定也没问题。

修复的问题

  • 当Objective-C中的块(block)传递给Swift中的方法时,会引起内存泄漏或崩溃,这个问题被修复了。
  • 在switch语句中使用as Type处理多种类型的转换不再引起内存泄漏。
  • 使用var或let一次声明多个全局变量不会再引起内存出错。
  • while let和while case语句不再使变量作用于其下的语句块。这样一来,可能会导致编译器崩溃,因此我担心对每个人来说这都是一种破坏。

已知的问题

  • 不带优化设置编译的情况下同时开启调试信息,这样做会导致编译器崩溃。解决方法是在非调试模式构建过程中,使用-gnone禁掉调试信息。

个人说明

紧张的日程外加生病,处理事情忙得不可开交。但我期待着恢复定期写博客的状态。熬了一段时间后,有几篇文章马上要完成了。一些是关于Swift的文章,另外一些是更泛泛的内容。

http://www.csdn.net/article/2015-10-05/2825846-swift-2

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

17 8月

iOS 9 Programming Fundamentals with Swift: Swift, Xcode, and Cocoa Basics 1st Edition

book1

内容简介:

通过坚实地把握iOS 9 开发的基础知识跻身于这一领域,内容包括Xcode 7,Cocoa Touch框架和苹果的Swift编程语言。通过最新的指南学习Swift语言中面向对象的概念,了解如何使用苹果开发工具,领会Cocoa如何提供iOS应用程序所需的底层功能。

  • 探索Swift面向对象的概念:变量和函数、作用域和命名空间、对象类型和实例
  • 熟悉Swift内置类型,如数字、字符串、区间、元组、可选类型、数组和字典
  • 学习如何声明、实例化和定制Swift的对象类型——枚举、结构和类
  • 了解Swift强大的功能特性,比如协议和泛型
  • 纵览Xcode项目的生命周期,从概念到产品
  • 使用nib及其编辑器和Interface Builder创建应用程序接口
  • 理解Cocoa的事件驱动模型及其主要的设计模式和特性
  • 弄清Swift如何与Cocoa的C API和Objective-C API通讯

作者简介:

Matt Neuburg从1968年起就使用电脑编程。14岁时作为高中俱乐部不折不扣的地下成员,每周都会在银行使用原始的电传打字机来为PDP-10电脑做分时处理。他偶尔也使用普林斯顿大学的IBM-360/67大型机,直到有一天不再使用穿孔卡片,他才沮丧地放弃了IBM-360/67。曾在美国斯沃斯摩尔学院(Swarthmore College)主修希腊语,1981年,从康奈尔大学(Cornell University)获得了博士学位,在大型主机上完成了他的博士论文(关于希腊悲剧之父Aeschylus)。他开始在许多著名的高等院校讲授古典语言、文学和文化,而大多数院校为了发表大量不大可能令人感兴趣的学术文章,对他的学识视而不见。与此同时,他获得了一台Apple IIc,再次无可救药地为之着迷。并于1990年换成了麦金塔电脑。他编写了一些教育软件和免费的工具软件,成为了在线杂志TidBITS早期的定期撰稿人。1995年,他离开了学术界去MacTech Magazine做编辑。他还是一位前沿的作者,著有The Definitive Guide和REALbasic: The Definitive Guide,由O’Reilly & Associates出版发行。

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

27 7月

Swift 2.0概览

文 / Russ Bishop:全能型程序员,使用C#,Objective-C和Swift语言编程,开发了奇特的应用Storm Sim Free。

原文链接:http://www.russbishop.net/swift-2-0

毫无疑问,Swift 2.0在2015全球开发者大会(Worldwide Developers Conference, WWDC 2015)上被发布的消息众人皆知。我会就该语言所发生的变化撰写一系列的文章,但目前我们先说说重点。

常规变化

  • 现在全局函数和独立(free-standing)函数都和方法一样,遵循同一个参数标签规则。不再使用#这样的语法来引用外部资源。
  • 你基本上可以使用 enum SomeEnum<T,U,V>来声明 multi-payload 风格的枚举,这样就能正常运行。这用来提示未完成的指令寄存器(IR)引发的错误。
  • 条件循环语句目前的语法是 repeat { } while(cond),不再使用 do 。
  • 关键字 do 目前用来引入一个新的作用域(这对新引进的错误处理和 defer 关键字很重要)。在 C 语言中你可以用大括号,但 Swift 里就要理解为闭包(closure)。所以使用关键字 do 可以任意引入作用域。
  • guard 语句块显式地声明你要恒成立的条件语句,恒成立时跳过整个guard 语句。这样做的好处是绑定在guard语句的变量在函数的其他部分也可用。这就避免了将所有的东西都围绕一条if语句嵌套使用来解析(unwrap)可选类型的变量。执行到函数中guard语句中的else部分,函数一定会退出并抛出异常。也可能会调用带有@noreturn标记的函数。
  • 文本注释(doc comments)换成了Markdown格式,与Playgrounds统一(Playgrounds注释格式源于功能有限的reStructured Text)。
  • 编译器对冗余的协议一致性,未被使用的绑定值以及可以设为常量的变量这些情况目前会给予警告或报错。
  • Swift语言的调用约定更加智能,能够理解 API 所发生的变化和 Swift 所给出的警告。并且还可以升级(但还不是那么完美,一定还漏掉了一些东西)。
  • find函数改名为indexOf,sort则变成了sortInPlace,sorted变成了sort。
  • String不再直接遵循序列类型(SequenceType),大概是为了避免一些新的可用协议扩展。目的是迫使你使用s.characters,s.utf8或s.utf16明确你想处理的unicode编码。
  • 允许对泛型添加公共扩展。
  • 非泛型类类型可以继承泛型类(强制类型参数固定)。
  • 便利的可失败构造器(failable initializer)可以先返回nil,而不必首先调用self.init。这是有利的一面,但指定了构造器在返回nil前仍要给所有字段初始化。所以此处还有改进的余地。

内部的可见性

这解决了单元测试中的一个较大的难点。以前的做法:

  • Swift文件包含在test target中。现在不同的模块中有重复的类的定义,出现无法将“X”转换为“X”这样非常可怕的错误,有时会无法执行特定的测试。
  • 在测试中引入引入主程序(main program)作为一个模块。现在一切都声明为public,所以对于测试来说都是可见的,有时候也包括应该声明为private的内部细节。

现在可以启用testability,它就像C#中的 InternalsVisibleTo。主应用程序目标模块的内部细节对测试模块可见。

  • 在对应用或框架的测试设置中,启用testability。
  • 在单元测试中,使用@testable import {ModuleName}。

这将导致测试忽略某些优化行为并保留稍后导入到测试模块中的那些内部符号。官方文档警告说,由于阻止了某些优化,因此这只适用于调试和测试版本。

模式匹配

switch语句的模式匹配(pattern matching)语法和“if let …, …. where”语法一直在推广。可以在任何控制流中使用逗号操作符和where条件语句。还可以使用新的case条件语句,例如:if case .Silly(let a) { }。还有一种用于Optional<T>的特殊形式:if case let a? = anOptional { }。

模式匹配在循环语句中也可以使用:for case let thing? in array { }。

这又是值得单独成文的另一个特性。

Objective-C的泛型和__kindof的用法

在关于Swift的帖子里谈论这个做甚?它的作用是使某些衔接更加清晰和简便。不求在这篇文章中面面具到,我会在单起一篇文章阐述它。

错误处理

这不是我们一贯所认识的异常,这是一个使函数提前返回Result<T, Error>的操作,单隐藏了所有提前返回的对象,也隐藏了错误解析(error unwrapping)过程等内容。

  1. let systemAttributes: [NSObject: AnyObject]?
  2. do {
  3.     systemAttributes = try NSFileManager.defaultManager().attributesOfFileSystemForPath(documentDirectoryPath.last!)
  4. catch _ {
  5.     systemAttributes = nil
  6. }

它完美地与Objective-C进行互操作,Swift语言中,将标记为throws的方法作为选择器。这是使用NSError的方法,-(BOOL or nullable type)someMethodTakingParam:(type)param error:(NSError **),这种样式会自动引入标记为throws的方法。

应该明白的是这并不像Java中已经被检查过的异常(checked exception)那样。Swift语言并不关心异常的类型,或者处理或者不处理。这又是值得单独成文的另一功能特性。

Defer关键字

关键字defer也很重要,因为它可以取代传统C风格的“if(err) goto cleanup”。获得资源后接着就是defer { release_resource() }。然后不管函数返回结果如何,获得的资源都将被清理。这也意味着资源的释放紧随获取资源之后。这看起来不起眼儿,实则很重要。

NS_OPTIONS和OptionSetType

位操作枚举(bitwise enumeration)与数组风格的语法相结合,而不使用管道符“ | ”按位操作,并且具有所有范围的集合操作功能。检查一下是否具有contains功能的标志,或能够执行像isSubsetOf和isDisjointWith等这样集合操作的其他功能。这是显著的改进,表达了不直接对位进行操作的意愿。

这种变化意味着位操作枚举实际上不再是枚举了。将这些位操作枚举声明为结构体,实现OptionSetType协议,提供rawValue属性。并且创建值作为结构体的静态成员。Swift便会搞定其余的一切,自动提供所有集合的操作。这是我希望将来看到的更加明了的语法内容。

协议扩展

协议如今可以被扩展了,包括与类型约束有关的通用协议。还可以自己提供协议的默认实现。

先前,你不能你说:“我要使用方法X来扩展CollectionType,但只有集合中的类型满足某些条件才可以”。现在,你可以这么做,并且很多像map,filter和sort这样的全局函数已经进行了扩展。

这样就解决了很多痛点,这也是值得单独成文的内容。同时,要看看WWDC的面向协议编程(Protocol Oriented Programming)了解一些细节。

API审计

大量的API已经进一步进行了审计而更合理。举几个例子:

  • UITableView的dequeueReusableCellWithIdentifier方法现在返回UITableViewCell?类型的对象。
  • UIKit的属性现在也被声明为了实际的属性。

用translatesAutoresizingMaskToConstraints = false 代替了setTranslatesAutoresizingMaskToConstrains(false)。

Availability属性

@available属性自Swift 1.2就存在了并且后续支持得很好。添加了一个新的陌生语法if#available(),为处理版本检查提供了支持。而不是插入你喜欢的方法。

遗憾的是你不能只声明一个属性 UISearchController并将target设置为iOS 7,然后只允许访问类中的属性。Swift希望整个类的定义都可以或者不可以。

也可以不再采用协议,除非支持target设置中所有的操作系统版本,除非将整个类标记为只在更新的操作系统版本可用。

这意味着使用if #available()存在单独的子类和对创建适当对象的保护。

尽管如此,我个人还是发现了一个Bug,应用在iOS4.0-4.1发生崩溃,由于编译器没有发出警告,方法只在iOS4.2才引入,因此我犹如与定时炸弹相伴。

C函数指针

Swift现在可以使用C函数指针,CFunctionPointer已不复存在。任何全局函数,嵌套函数和不捕获状态的闭包都可以作为一个C函数指针直接传递。你也可以调用来自C程序的函数。

你可以显示地使用新属性@convention(c),表示函数应该使用C调用约定,简单痛快!尽管我想不出在此对块(block)的支持有何用,作为所发生变化的一部分,@objc_block也被删掉了,使用@convention(block)取而代之。@convention(swift)默认支持所有函数和闭包。

这并不是编程语言所特有的。iOS 9含有不同版本的Swift标准库,并且在未来系统中将添加修正后的Swift标准库。结合新的App Thining技术,下载过程中苹果商店会将Swift标准库剥离出去的。我仍然在追根溯源地探求这究竟是如何工作的。

遗漏

明显的一个遗漏是处理异步代码。

苹果公司为我们提供了GCD,这是一个强大的基础类库,可以构建很多异步操作和并发原语。

然而,这些天我们做的每件事,构建用户接口和API都需要考虑异步性和并发性。我们把一个文件读操作锁定一段时间,对用户来说整个世界就都静止了。

这是个持续的痛点,不是多大的事儿,但如果经常性地每天重复,恐怕也是不行的。

C#和JavaScript都采用了async/await来为异步代码提供一流的语言支持。我想很多人都想知道,Swift会提供什么样的语法糖来帮助我们在实现异步操作方面确保正确性。我不知道在Swift 2.0发布的时间框架内是否会看到什么,但愿能有好的东西出现吧!

开放源码

宣布的内容中,反响最强烈的无疑是Swift开放源代码。苹果公司已经承诺在今年底前开放源码,我们也没有理由对此表示怀疑。与苹果公司编译器团队成员讨论过程中,他们看起来似乎对此由衷地兴奋,无论如何坚决要干成这件事(我有点小失望,他们没有打造出经典的苹果然后宣布开源,但我仍然对此消息发自内心地感到高兴)。

结论

Swift 2.0有很多令人喜爱之处。苹果公司的Swift团队向大家承诺他们会迅速行动。到目前为止这些承诺已经被兑现。成为苹果平台上的开发人员是一个激动人心的时刻。

http://www.csdn.net/article/2015-07-23/2825280-swift-2

19 7月

Swift 2.0中的错误处理

文 / Juan Pablo Claude:来自智利首都圣地亚哥,毕业于美国北卡罗莱纳大学教堂山分校(University of North Carolina at Chapel Hill),获化学博士学位,后入阿拉巴马大学伯明翰分校(University of Alabama at Birmingham)任教。2005年底作为Cocoa和Django框架程序开发人员加入Big Nerd Ranch。此前,有过DOS环境下C编程,Windows环境下使用C++编写数据分析应用程序的经历。

原文链接:https://www.bignerdranch.com/blog/error-handling-in-swift-2/

苹果公司在今年的全球开发者大会(Worldwide Developers Conference, WWDC)上宣布推出Swift2.0,该语言的首席架构师Chris Lattner表示,Swift2.0主要在语言基本语法、安全性和格式美观度这三方面进行了改进。除了这些新的功能特性,还有对语法的优化、修饰及美化,最后是Swift 1.x中最具影响力的错误处理机制。

这是因为你根本无法回避它。如果打算使用Swift 2.0的话,必须接受错误处理这样的机制,并且错误处理机制将改变Cocoa和Cocoa Touch框架中使用NSError与方法交互的方式。

历史一瞬:不起眼的开端

我们都知道,Swift语言作为Objective-C当前替代语言被推出,是OS X和iOS应用程序开发的“通用语”。在最初的版本中,Objective-C没有原生的异常处理机制。后来通过添加NSException类,还有 NS_DURING, NS_HANDLER和 NS_ENDHANDLER宏才有了异常处理。这种方案现在被称为“经典的异常处理”,还有这些宏都是基于setjmp()和longjmp()这两个C语言函数的。

异常捕获(exception-catching)看起来如下图所示,在NS_DURING和NS_HANDLER宏之间抛出的任何异常都将会导致在NS_HANDLER和NS_ENDHANDLER宏之间执行相应的代码。

  1. NS_DURING
  2.     // Call a dangerous method or function that raises an exception:
  3.     [obj someRiskyMethod];
  4. NS_HANDLER
  5.     NSLog(@“Oh no!”);
  6.     [anotherObj makeItRight];
  7. NS_ENDHANDLER

下面是立刻能触发抛出异常的方法(现在仍然可用):

  1. – (void)someRiskyMethod
  2. {
  3.     [NSException raise:@“Kablam”
  4.                 format:@“This method is not implemented yet. Do not call!”];
  5. }

可以想象,这种手工处理异常的方式戏弄的是早期Cocoa框架程序开发人员。但是这些程序员还不至于到这份儿上,因为他们很少使用这种方式。无论在Cocoa还是Cocoa Touch框架下,异常通常都被归为灾难性的,不可恢复的错误,比如程序员造成的错误。上面的-someRiskyMethod就是很好的例子,由于实现部分没有准备好而引发了异常。在Cocoa和Cocoa Touch框架中,可恢复的错误由稍后讨论的NSError类来处理。

原生的异常处理

我想由于Objective-C中的经典异常处理机制对应的手工处理方式让人感觉闹心,于是苹果公司在Mac OS X 10.3(2003年10月)中发布了原生的异常处理机制,彼时还没有iOS系统。这本质上是将C++的异常处理嫁接到了Objective-C。异常处理的结构目前看起来是这样的:

  1. @try {
  2.     [obj someRiskyMethod];
  3. }
  4. @catch (SomeClass *exception) {
  5.     // Handle the error.
  6.     // Can use the exception object to gather information.
  7. }
  8. @catch (SomeOtherClass *exception) {
  9.     // …
  10. }
  11. @catch (id allTheRest) {
  12.     // …
  13. }
  14. @finally {
  15.     // Code that is executed whether an exception is thrown or not.
  16.     // Use for cleanup.
  17. }

原生的异常处理使你有机会为每个异常类型指定不同@catch部分。无论@try结果如何,@finally都要执行其对应的代码。

尽管原生的异常处理如所预期的那样抛出一个NSException异常,但是最明确的方法还是“@throw <expression>;”语句。通常你抛出的是NSException实例,但说不定什么对象会被抛出。

NSError

尽管Objective-C原生与经典的异常处理有许多优点,但Cocoa和Cocoa Touch框架应用程序开发人员仍然很少使用异常,而是限制程序出现程序员所导致的不可恢复的错误。使用NSError类处理可恢复的错误,这种方法早于使用异常处理。Swift 1.x也继承了NSError的样式。

在Swift 1.x中,Cocoa和Cocoa Touch的方法和函数可能不会返回一个布尔类型的false或者nil来表示一个失败(failure)的对象。另外,NSErrorPointer对象会被当作一个参数返回特定的失败信息。下面是个典型的例子:

  1. // A local variable to store an error object if one comes back:
  2. var error: NSError?
  3. // success is a Bool:
  4. let success = someString.writeToURL(someURL,
  5.                                     atomically: true,
  6.                                     encoding: NSUTF8StringEncoding,
  7.                                     error: &error)
  8. if !success {
  9.     // Log information about the error:
  10.     println(“Error writing to URL: \(error!)”)
  11. }

程序员所导致的错误可以用Swift标准库(Swift Standard Library)函数fatalError(“Error message”)来标记,将其在控制台记录为错误消息并无条件中止执行。还可以使用assert(), assertionFailure(), precondition()和preconditionFailure()这些函数。

Swift第一次发布时,一些非苹果平台开发人员已经准备好了火把和干草叉。他们声称Swift不能算是“真正的语言”,因为它缺乏异常处理。但是,Cocoa和Cocoa Touch社区对此不予理睬,我们知道NSError和NSException那个时候就存在了。就我个人而言,我相信苹果公司仍然在思考实现错误和异常处理的正确方式。我还认为直到问题解决了,苹果公司才会公开Swift源码。这一切问题在Swift 2.0中全被扫清了。

Swift 2.0中的错误处理

在Swift 2.0中,如果想要抛出错误,那么抛出的对象必须符合ErrorType协议。可能正如你所愿,NSError就符合该协议。枚举在这里用来给错误进行分类。

  1. enum AwfulError: ErrorType {
  2.     case Bad
  3.     case Worse
  4.     case Terrible
  5. }

然后如果一个可能抛出一个或多个错误的函数或方法会被抛出关键字标记:

  1. func doDangerousStuff() throws -> SomeObject {
  2.     // If something bad happens throw the error:
  3.     throw AwfulError.Bad
  4.     // If something worse happens, throw another error: 
  5.     throw AwfulError.Worse
  6.     // If something terrible happens, you know what to do: 
  7.     throw AwfulError.Terrible
  8.     // If you made it here, you can return:
  9.     return SomeObject()
  10. }

为了捕获错误,新型的do-catch语句出现了:

  1. do {
  2.     let theResult = try obj.doDangerousStuff()
  3. }
  4. catch AwfulError.Bad {
  5.     // Deal with badness.
  6. }
  7. catch AwfulError.Worse {
  8.     // Deal with worseness.
  9. }
  10. catch AwfulError.Terrible {
  11.     // Deal with terribleness.
  12. }
  13. catch ErrorType {
  14.     // Unexpected error!
  15. }

这个do-catch语句和switch语句有一些相似之处,被捕获的错误详尽无遗,因此你可以使用这种样式来捕获抛出的错误。还要注意关键字try的使用。它是为了明确地标示抛出的代码行,因此当阅读代码的时候,你能够立刻找到错误在哪里。

关键字try的的变体是“try!”。这个关键字大概也适用于那些程序员导致的错误。如果使用“try!”标记一个被调用的抛出对象中的方法,你等于告诉编译器这个错误永远不会发生,并且你也不需要捕获它。如果该语句本身产生了错误(error),应用程序会停止执行,那么你就要开始调试了。

  1. let theResult = try! obj.doDangerousStuff()

与Cocoa和Cocoa Touch框架间的交互

现在的问题是,你如何在Swift 2.0中处理爷爷级的NSError API呢?苹果公司已经Swift 2.0中为统一代码行为作了大量工作,并且已经为未来写入Swift的框架准备方法。Cocoa和Cocoa Touch中可以产生NSError实例的方法和函数有苹果公司的签名( signature),可以自动转换为Swift新的错误处理方式。

例如,这个NSString的构造器( initializer)在Swift 1.x中就有以下签名:

  1. convenience init?(contentsOfFile path: String,
  2.                   encoding enc: UInt,
  3.                   error error: NSErrorPointer)

Swift 2.0中,签名被转换成:

  1. convenience init(contentsOfFile path: String,
  2.                  encoding enc: UInt) throws

注意:在Swift 2.0中,构造器不再被标记为failable,它并不需要NSErrorPointer来做参数,而是使用抛出异常的方式显式地指示潜在的失败。

下面的例子使用了这种新的签名:

  1. do {
  2.     let str = try NSString(contentsOfFile: “Foo.bar”,
  3.                            encoding: NSUTF8StringEncoding)
  4. }
  5. catch let error as NSError {
  6.     print(error.localizedDescription)
  7. }

注意错误是如何被捕获的,并且如何被转换成了一个NSError实例,这样你就可以获取与其相似API的信息了。事实上,任何ErrorType类型的实例都可以转换成NSError类型。

最后说说@finally

细心的读者可能已经注意到,Swift 2.0引入了一个新的do-catch语句,而不是do-catch-finally。不管是否捕捉到错误的情况下,你如何指定必须运行的代码呢?为此,现在可以使用defer语句,用来推迟代码块的执行直到当前的作用域结束。

  1. // Some scope:
  2. {
  3.     // Get some resource.
  4.     defer {
  5.         // Release resource.
  6.     }
  7.     // Do things with the resource.
  8.     // Possibly return early if an error occurs.
  9. // Deferred code is executed at the end of the scope.

Swift 2.0将Cocoa和Cocoa Touch的错误处理机制凝聚为具有现代风格的用法,这是一项伟大的工作,也会使许多程序员倍感亲切。统一行为是不错的定位,会使Swift语言和其所继承的框架逐步发展。

http://www.csdn.net/article/2015-07-01/2825095/2

17 6月

Swift语言那些鲜为人知的特性

文 / Russ Bishop:全能型程序员,使用C#,Objective-C和Swift语言编程,开发了奇特的应用Storm Sim Free。

原文链接:http://www.russbishop.net/more-swift-attributes

Swift语言有各种各样缺乏(或没有)文档记录的特性(attribute)放在那里等着被使用。让我们一起看看其中的一些特性:

@inline

这个特性为编译器提供了内联提示。有效的取值是__always和never。除非我认为必须要用这两个值,否则就不会使用它(特别是__always)。到目前为止与其相关的规则还不是很明确,在有限的测试下,它可以正常地工作,但还要视具体情况而定。

进一步的解释:尽管底层虚拟机(Low Level Virtual Machine, LLVM)有强制内联的概念,但我们目前还不知道这个@inline特性是否与其直接映射,也不知道是否存在大小方面的限制,但这将会导致编译器忽略这一点而跳过内联。理论上说应该是这样的,但我不保证一定是。

注意(当优化设置关闭时)在调试模式下的构建将忽略@inline。

@transparent

我最初并未将这个特性列出来。该特性会导致编译器在管道(pipeline)中更早地将函数内联。它用于“像+(Int, Int)这样非常原始的函数”,而“不应该用于独立函数”

甚至在没有优化设置的调试模式下@transparent特性函数就会被内联,所以在调用“1+1”这样的函数的时候并不会特别慢。另外这个特性与@inline(__always)非常类似。

@availability

这个特性可以用来标识某些函数只在某些平台或版本上可用。第一个参数是平台,可以用星号(*)代表一切可用,还可以是iOS或OSX。因为如果需要针对不同的平台,就要指定多个@availability属性。

如果需要表示该函数在某个给定的平台完全不可用时,可以将第二个参数置为unavailable。此外,还可以用introduced,deprecated和obsoleted来指定一个或是多个版本的组合:obsoleted意味着该项已经删除,deprecated仅仅表示如果使用就会给予警告。最后你可以设置message的值,如果该项被使用了就由编译器输出。下面是一些例子:

  1. @availability(*, unavailable)
  2. func foo() {}
  3. @availability(iOS, unavailable, message=“you can’t call this”)
  4. func foo2() {}
  5. @availability(OSX, introduced=10.4, deprecated=10.6, obsoleted=10.10)
  6. @availability(iOS, introduced=5.0, deprecated=7.0)
  7. func foo3() {}

@noreturn

正如该特性所描述的那样:编译器可以假定这个函数是一个永远循环运行的起点,例如while true { },或者假定是函数abort或者exit进程的情况。

评论者Marco Masser指出,如果调用另一个被标志为@noreturn的函数,那么编译器会忽略掉当前函数中缺失的返回值(missing return values),因为编译器理解程序的控制流。

@asmname

该属性给出了函数、方法或属性实现的符号名称。如果你已经知道对应的函数参数及其类型,那么就可以直接调用Swift的内部标准库函数,甚至不用头文件,也可以方便地调用C语言编写的函数:

  1. @asmname(“function”) func f()

@unsafe_no_objc_tagged_pointer

上面这个仍然是个谜,但我猜测它是在告诉Swift与Objective-C联系的时候不要使用tagged pointer

@semantics

这又是另一个谜。参数看起来像是array.mutate_unknown或array.init这样的字符串数组。想必这是要告诉编译器(或静态分析器)函数是如何工作的。

结论

谁还需要乏味老套的 @objc和@autoclosure呢?还是算了吧!

我今年会去参加苹果全球开发者大会(WWDC),你也一定要去呀!

http://www.csdn.net/article/2015-06-08/2824887-more-swift-attributes

09 6月

ARC中retain cycle揭秘

文 / Ignacio Nieto Carvajal:自由开发者,精通iOS和Mac OSX应用程序开发,还包括Web服务和UX/UI设计。

原文链接:http://digitalleaves.com/blog/2015/05/demystifying-retain-cycles-in-arc/

ARC中的retain cycle就像日本B级恐怖电影一样。开始使用Cocoa或Cocoa Touch做开发时,你甚至不会在意它的存在。直到有一天应用程序由于内存泄漏而出现了崩溃现象,你才意识到它们的存在,看到像幽灵一样的retain cycle无处不在。随着岁月流逝,你学会适应它们,发现它们,避免它们……但最终恐慌还在,无孔不入。

包括我在内,对于许多开发人员来说,ARC的最令人失望之处莫过于苹果公司让ARC来管理内存。不幸的是ARC没有循环引用检测器,因此很容易出现retain cycle现象,从而迫使开发人员在编码时要采取特殊的预防措施。

对于iOS开发人员来说,retain cycle是个难点。在网上有很多误导信息[1][2],人们所给出的这些错误信息和修复方法甚至会导致应用出现新的问题,甚至崩溃掉,基于这样的情况,本文中我会阐明主题,给读者一些启发。

相关理论一瞥

Cocoa框架内存管理可以追溯到MRR(Manual Retain Release),在MRR中,开发人员在创建对象的时候,要为每个内存中的对象声明所有权。并且,当不再需要该对象时,要放弃所有权。MRR通过引用计数系统来实现这种所有权机制。每个对象都被分配一个计数器指示被“拥有”了多少次,每次加一,释放对象的时候每次减一。当引用计数变成零的时候,该对象将不复存在。对于开发人员来说,不得不手动维护引用计数真的是很烦人的事情,于是苹果公司引入了自动引用计数(Automated Reference Counting, ARC)机制,免得开发人员手动添加保留(retain)和释放(release)指令,让他们专注于解决应用程序的问题。在ARC环境下,开发人员要将一个变量定义为“strong”或“weak”。使用weak的话应用程序中被声明的对象不会被retain,而使用strong声明的对象将会被retain,并且其引用计数加一。

为什么要在乎?

ARC的问题在于容易导致retain cycle,它发生在两个不同的对象间彼此包含强引用的时候。试想一个Book对象包含一系列的Page对象,每个Page对象有个属性指向该页所在的这本书。当你释放掉指向Book和Page的变量时,Book和Page之间还存在着强引用。因此,即使没有变量指向Book和Page了,Book和Page及其所占用的内存也不会被释放掉。

不幸之处在于并非所有retain cycle都很容易被发现。对象之间的传递关系(A引用B,B转而引用C,C引用A)会导致retain cycle。更糟糕的是Objective-C里的块(block)和Swift里的闭包(closure)都被认为是独立的内存对象。因此,任何在块或闭包内对像的引用都将会对其变量做retain操作。因此,如果对象仍然retain这个块的话,就会导致潜在的retain cycle发生。

retain cycle可以成为应用程序潜在的危害,导致内存消耗过高,性能低下和崩溃。但还没有来自于苹果公司的文档,针对retain cycle可能发生的不同场景,以及如何避免进行描述。这导致了一些误解并形成了不良的编程习惯。

用例场景

那么闲话少说,我们一起来分析一些场景,确定它们是否会导致retain cycle以及如何避免:

父子对象关系

这是retain cycle的典型例子。不幸的是这也是苹果公司唯一给出相关解决方案文档的例子。就是我上面描述的Book和Page对象的例子。这种情况的典型解决方案是把Child类里面的代表父类的变量定义成weak,这样就可以避免retain cycle。

  1. class Parent {
  2.    var name: String
  3.    var child: Child?
  4.    init(name: String) {
  5.       self.name = name
  6.    }
  7. }
  8. class Child {
  9.    var name: String
  10.    weak var parent: Parent!
  11.    init(name: String, parent: Parent) {
  12.       self.name = name
  13.       self.parent = parent
  14.    }
  15. }

在Swift语言中,代表父类的变量是个弱变量的事实迫使我们将其定义为可选类型。不使用可选类型的另一种做法是将父类型对象声明为“unowned”(意味着我们不会对变量声明进行内存管理或声明所有权)。然而在这种情况下,我们必须非常仔细地确保只有一个Child实例指向Parent,Parent就不能是nil,否则程序就会崩溃:

  1. class Parent {
  2.    var name: String
  3.    var child: Child?
  4.    init(name: String) {
  5.       self.name = name
  6.    }
  7. }
  8. class Child {
  9.    var name: String
  10.    unowned var parent: Parent
  11.    init(name: String, parent: Parent) {
  12.       self.name = name
  13.       self.parent = parent
  14.    }
  15. }
  16. var parent: Parent! = Parent(name: “John”)
  17. var child: Child! = Child(name: “Alan”, parent: parent)
  18. parent = nil
  19. child.parent <== possible crash here!

一般来说公认的做法是父对象必须拥有(强引用)其子对象,这些子对象对其父对象应该只保持一个弱引用。这同样适用于集合,集合必须拥有其所包含的对象。

包含在实例变量中的块和闭包

另一个经典的例子虽然不是那么直观,但正如我们之前所说的那样,闭包和块是独立的内存对象,并retain了它们所引用的对象。因此如果我们有一个包含闭包变量的类,这个变量又恰好引用了其所拥有对象的属性或方法,由于闭包通过创建一个强引用而“捕获”了自己,就会有retain cycle发生。

  1. class MyClass {
  2.    lazy var myClosureVar = {
  3.       self.doSomething()
  4.    }
  5. }

这种情况下的解决方法是将自身定义成“weak”版本,并且将此弱引用赋给闭包或块。在Objective-C语言中,要定义个新的变量:

 

  1. – (id) init() {
  2.    __weak MyClass * weakSelf = self;
  3.    self.myClosureVar = ^{
  4.       [weakSelf doSomething];
  5.    }
  6. }

而在Swift语言中,我们只需要指定“[weak self] in”作为闭包的启动参数:

  1. var myClosureVar = {
  2.    [weak self] in
  3.    self?.doSomething()
  4. }

这样一来,当闭包快执行完毕时,self变量不会被强制retain,因此会得到释放,打破循环。注意,当声明为weak时,self在闭包内是如何变成可选类型的。

GCD中的dispatch_async

与一贯的认识相反,dispatch_async本身并不会导致retain cycle。

  1. dispatch_async(queue, { () -> Void in
  2.    self.doSomething();
  3. });

在这里,闭包对self强引用,但是类(self)的实例对闭包没有任何强引用,因此一旦闭包结束,它将被释放,也不会有环出现,然而,有的时候会被(错误地)认为这种情况会导致retain cycle。一些开发人员甚至一针见血地指出将块或闭包内所有对“self”的引用都声明为weak:

  1. dispatch_async(queue, {
  2.    [weak self] in
  3.    self?.doSomething()
  4. })

在我看来,对每种情况都这样做并不是好的做法。我们假设某个对象启动了一个长时间的后台任务(比如从网络上下载一些东西),然后调用了一个“self”方法。如果对self传递一个弱引用的话,那么类会在闭包结束之前完成它的生命周期。因此,当调用 doSomething()的时候,类的实例已经不存在了,所以这个方法永远不会被执行。这种情况下,(苹果公司)建议的解决方案是对闭包内的弱引用(???)声明一个强引用:

  1. dispatch_async(queue, {
  2.    [weak self] in
  3.    if let strongSelf = self {
  4.       strongSelf.doSomething()
  5.    }
  6. })

我不仅发现语法冗长单调,缺乏直观性,甚至令人感到厌恶,而且使闭包作为独立的处理实体的打算也落空了。我认为要理解对象的生命周期,确切地明白什么时候应该为实例声明一个内部的weak版,还要知道对象存续期间会有哪些影响。但话又说回来,这正是我解决应用程序的问题时分散我注意力的地方,如果Cocoa框架中没有使用ARC的话,这些都是没有必要写的代码。

局部的闭包和块

没有引用或包含任何实例或类变量的函数局部闭包和块本身不会导致retain cycle。常见的例子就是UIView的animateWithDuration方法:

  1. func myMethod() {
  2.    …
  3.    UIView.animateWithDuration(0.5, animations: { () -> Void in
  4.       self.someOutlet.alpha = 1.0
  5.       self.someMethod()
  6.    })
  7. }

对于dispatch_async和其他GCD相关的方法,我们不用担心没有被类实例强引用的局部闭包和块,它们不会发生retain cycle。

代理方案

代理(delegation)是使用弱引用避免retain cycle的一个典型场景。将委托声明为weak一直是一种不错的做法(并且还算安全)。在Objective-C中:

  1. @property (nonatomic, weak) id <MyCustomDelegate> delegate;

Swift中:

  1. weak var delegate: MyCustomDelegate?

在大多数情况下,对象的代理实例化了该对象,或者被认为比对象存在的时间更长久(并且对代理方法做出反应)。因此,在一个设计得很好的类中,我们不会找到与对象声明周期相关的任何问题。

使用Instruments来调试retain cycle

不管我如何努力避免retain cycle,忘记去引入一个弱引用并意外地创建一个(多谢ARC!)这样的事情迟早还有发生。幸运的是XCode套件中的Instruments应用程序是很不错的工具,用于检测和定位retain cycle。一旦开发阶段结束,在提交苹果商店之前就分析(profile)你的应用是个不错的习惯。Instruments有很多模版用来分析应用的不同方面,但我们感兴趣的是“Leaks”选项。

一旦打开Instruments,你就应该启动应用程序并作一些交互操作,特别是在要测试的区域或试图控制器上。任何检测到的泄漏都将会在“Leaks”部分出现一条红线。辅助视图包含一个区域,Instruments用来显示发生泄漏处的堆栈追踪,用来找到问题所在,甚至使你可以直接定位到造成问题的代码。

http://www.csdn.net/article/2015-05-27/2824782-demystifying-retain-cycles-in-arc