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

07 2月

使用Storyboard构建Navigation Controller和Table View

文 / Simon Ng:iOS程序员,Beginning iOS 9 Programming with Swift一书的作者,AppCoda.com创始人。

原文链接:http://www.appcoda.com/use-storyboards-to-build-navigation-controller-and-table-view/

到现在为止,如果一直跟着我们的教程学习,你应该有了对UITableView基本的了解。并且知道如何利用它构建简单的应用程序了。本周,我们要说说新东西,Storyboards。这是Xcode 4.2和iOS 5 SDK引入的最令人激动的功能特性之一。作为iOS开发人员,它使你的生活更简单,可以轻而易举地设计iOS应用程序的用户接口。

本文将向你展示如何使用Storyboards构建Navigation接口,并将它与UITableView集成起来。我们会尽力保持简单,集中精力解释概念。因此就没有好玩儿的界面和漂亮的图形。以后的文章会解决艺术性的问题。

好,我们开始吧!

什么是Navigation Controller?

我们开始编码前,像以往一样,我们对Navigation Controller和Storyboards做一个简单的介绍。

就像Table View,Navigation Controller是你在iOS应用程序中另一种最常见的UI元素。它提供了逐层向下的层次内容。看一看内置的Photos,YouTube 和Contacts应用,这些应用程序都使用了Navigation Controller来显示分层的内容。通常,大多数应用程序中的Table View和Navigation Controller是一起工作的。但这并不是说一定要两个一起使用。

Photos-App-Navigation-Controller

Storyboards概览

前面说过,Storyboard是Xcode 4.2以来引入的新功能特性。它为iOS开发者提供了创建和设计用户界面提供了全新的方式。对初学者来说,介绍Storyboard之前,创建导航界面和标签界面着实困难。每个界面都存储在单独的nib文件中。在这之上,还要编写代码讲所有的界面连接到一起,描述导航如何工作。

有了Storyboards,所有的界面都存储在一个文件中。这给了你一种应用程序可见的表现方式,向你展示了这些界面是如何连接在一起的。Xcode提供了内置的编辑器来编辑Storyboards的布局。通过简单的点击,你可以定义不同界面之间的转变(也就是所谓的segues)。这并不意味着不需要为用户界面编写代码。但Storyboards显著减少了编码量。下面的示例图片展示了Storyboards在Xcode中的样子。

Storyboards-Explained

Scene和Segues

使用Storyboards时,Scene和Segues是总出现的两条术语。在Storyboard中,scene指一个单独的视图控制器和它的视图。每一个scene都有一个dock,dock主要被用来在视图控制器和它的视图之间做action和outlet关联。

Segue位于两个scene之间,管理两个scene之间的转变(transition)。Push和Modal是转变的两种常见类型。

在Storyboards中创建Navigation Controller

我们现在动手创建Storyboards。在本文中,我们将构建一个使用UITableView和UINavigationController的简单示例应用。使用Table View显示菜谱。当用户选择任何一道菜时,应用程序就跳转到下一个页面显示细节。这不难的。

首先,启动Xcode(确保使用4.2版本以上),使用“Single View application”模版创建一个新的项目。

Choose-Xcode-Template

点击“Next”继续。把下图中Xcode项目中需要填写的内容补齐。确认勾选“Use Storyboards”选项。

RecipeCookbook-Xcode-Project

点击“Next”继续。Xcode会询问把“SimpleTable”存到哪里。选择任一文件夹(例如,桌面)保存你的项目。

你可能注意到了Xcode项目中细微的差别。与之前文章中的内容比较,.xib文件(interface builder)被MainStoryboard.storyboard文件代替了。

Empty-Storyboard-in-Xcode

默认情况下,Xcode创建一个标准的视图控制器。因为我们要使用Navigation Controller控制界面导航,首先要将view controller修改为navigation controller。直接选择“Editor”菜单,选择“Embed in”,然后选择“Navigation Controller”。

Storyboard-Embed-in-Navigation-Controller

Xcode会自动给RecipeBook View Controller嵌入Navigation Controller。界面看起来是这个样子的:

Storyboard-Added-with-Navigation-Controller

继续网下进行前,我们运行一下程序,看看是什么样子的。点击“Run”按钮,你应该能看到一个添加了导航条的空白视图。这表明已经成功地把RecipeBook View Controller嵌入到了Navigation Controller中了。

RecipeBookApp-Empty

为数据添加Table View

接下来,我们会添加Table View,显示菜谱。在对象库中选择“Table View”,拖拽到“Recipe Book View Controller”。

请注意,不能把这些对象拖拽到缩小了的编辑器中,如果无法把table view拖拽到view controller,放大后再试。

Storyboard-Add-Table-View

下一件要做的事情是编写代码表格数据(也就是菜谱)。在项目导航栏,选择“RecipeBookViewController.h”,在“UIViewController”后面添加协议参数。

#import <UIKit/UIKit.h>

@interface RecipeBookViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>

@end

如果你读过Simple Table有关的文章,应该非常熟悉上面的代码。我就不在解释细节 了。如果理解起来有困难,查阅一下更早的文章

接下来,选择“RecipeBookViewController.m”,为保存表格数据定义实例变量(也就是菜单数组)。

@implementation RecipeBookViewController {

NSArray *recipes;

}

在“viewDidLoad”方法中,添加如下代码初始化“recipes”数组:

– (void)viewDidLoad

{

[super viewDidLoad];

//Initialize table data

recipes = [NSArray arrayWithObjects:@”Egg Benedict”, @”Mushroom Risotto”, @”Full         Breakfast”, @”Hamburger”, @”Ham and Egg Sandwich”, @”Creme Brelee”, @”White Chocolate Donut”, @”Starbucks Coffee”, @”Vegetable Curry”, @”Instant Noodle with Egg”, @”Noodle with BBQ Pork”, @”Japanese Noodle with Pork”, @”Green Tea”, @”Thai Shrimp Cake”, @”Angry Birds Cake”, @”Ham and Cheese Panini”, nil];

}

最后,我们必须实现两个数据源方法,来生成表格数据:

“tableView:numberOfRowsInSection”和“tableView:cellForRowAtIndexPath”方法。调用这两个方法是UITableViewDataSource协议的一部分。配置UITableVIew时,这两个方法是必须要实现的。第一个方法用来通知Table View区域内的行数,而第二个方法被用来填充表数据,因此我们添加如下代码。

作为参考,下面是整个“RecipeBookViewController.m”的源码。

//

//  RecipeBookViewController.m

//  RecipeBook

//

//  Created by Simon Ng on 14/6/12.

//  Copyright (c) 2012 Appcoda. All rights reserved.

//

#import “RecipeBookViewController.h”

@interface RecipeBookViewController ()

@end

@implementation RecipeBookViewController {

NSArray *recipes;

}

– (void)viewDidLoad

{

[super viewDidLoad];

// Initialize table data

recipes = [NSArray arrayWithObjects:@”Egg Benedict”, @”Mushroom Risotto”, @”Full Breakfast”, @”Hamburger”, @”Ham and Egg Sandwich”, @”Creme Brelee”, @”White Chocolate Donut”, @”Starbucks Coffee”, @”Vegetable Curry”, @”Instant Noodle with Egg”, @”Noodle with BBQ Pork”, @”Japanese Noodle with Pork”, @”Green Tea”, @”Thai Shrimp Cake”, @”Angry Birds Cake”, @”Ham and Cheese Panini”, nil];

}

– (void)viewDidUnload

{

[super viewDidUnload];

// Release any retained subviews of the main view.

}

– (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation

{

return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);

}

– (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section

{

return [recipes count];

}

– (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

static NSString *simpleTableIdentifier = @”RecipeCell”;

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];

if (cell == nil) {

cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault     reuseIdentifier:simpleTableIdentifier];

}

cell.textLabel.text = [recipes objectAtIndex:indexPath.row];

return cell;

}

@end

最后,我们建立Table View和刚刚创建的这两个方法之间的连接。返回Storyboard。按住Control键,选择“Table View”,拖拽到View Controller的图标。看起来是这个样子的:

Storyboard-TableView-Datasource

松开按钮,弹出了“dataSource”和“delegate”,选择“dataSource”,在Table View和它的数据源之间建立连接。重复上述步骤,在Table View和代理之间建立连接。

Storyboard-TableView-Connect-Datasource

测试应用程序之前,最后一件事是给导航栏添加标题。直接选择“Recipe Book View Controller”的导航栏,填写“Attributes Inspector”下的“Title”。记住,填写完成后点击ENTER,修改生效。

Storyboard-Add-Navigation-Bar-Title

可以运行代码了。点击Run按钮,测试应用程序。如果代码正确,你应该得到一个显示菜谱列表的应用程序。这个应用和之前我们构建的SimpleTable应用非常相似。这里最主要的区别就是嵌入了Navigation Controller。

Storyboard-Navigation-Bar-Title

Prototype Cell简介

还记得如何自定义表格吗?几周前,我们向你展示了如何使用Interface Builder设计自定义的表格。简要地说,你需要为表格单独创建一个nib文件,并用编程的方法将它加载到表呢。介绍了Storyboard中的Prototype Cell以后,创建自定义表格就更简单了。ProtoType Cell可以让你在Storyboard编辑器中轻松设计好表格的布局。

我们不会在本文深入谈论自定义的细节,而仅仅是在表格中添加“Disclosure Indicator”。

选择Table View,添加Prototype Cell。在“Attributes Inspector”下,把“Prototype Cells”的值从“0”改到“1”。修改完成后,Xcode立刻自动显示prototype cell。我们也把“Style”选项从“Plain”改成“Group”,显示另一个表格风格。

Storyboard-Prototype-Cell

接下来,选择“Prototype Cell”,你应该能够为表格定制风格了。为了让每个表格显示一个提示箭头,把“Accessory”改成“Disclosure Indicator”。定义重用标识符(Reuse Identifier)比较重要。你可以认为它是表格的ID。我们可以针对某个具体的prototype cell使用它。然而,这里定义为“RecipeCell”是为了与代码中的一致。

Storyboard-Edit-Prototype-Cell

现在再次运行应用程序。它看起来又发生了些变化。我们把表的风格改为“Grouped”,并添加了提示箭头。

Storyboard-Recipe-App-with-Disclosure

添加详细视图控制器

终于到了文章最后的部分。显示菜谱详细内容的详细视图控制器(Detail View Controller)还差点儿什么呢?当用户点击菜谱中的任何一项时,详细视图控制器都应该显现。

好了,我们添加一个新的View Controller作为详细视图控制器。

Storyboard-Create-Detail-View-Controller

本文的主要目的是向你展示如果实现Navigation Controller。我们会让详细视图尽量简单,就用一个标签来显示菜名。从对象库里拖拽一个标签,放到视图的中央。你可以修改标签的字体和字号,使标签更好看一些。

接下来,我们添加一个segue来连接prototype cell和新的View Controller。添加segue对象非常简单。按住control键不放,点击prototype cell,将其拖拽到View Controller。

Storyboard-Add-Segue

放开按钮,会天出三种类型的Segue(push,modal和custom)。

Storyboard-Segues

就像前面说的,segue定义了scene之间点连接。标准的导航控制器选择“Push”类型的连接。一旦完成,Xcode会自动使用segue链接两个scene。看起来这个样子:

Storyboard-Segue-Connection

现在再运行一下应用程序。选择任何一项后,应用程序会显示详细视图控制器。虽然详细视图控制器只显示一个标签,但是导航控制器已经起作用了。

Receipe-App-With-Detail-Controller

接下来讲些什么内容?

这是一篇较长的文章,终于完成了!我希望你对Storyboard有了更好的理解,知道如何设计自己的导航控制器。然而,还有一件事情没有讲:如何将菜单的名称从“Recipe Book View Controller”传递到“Detail View Controller”呢?我会在本周末发布一篇文章对此进行介绍。

这些Storyboard,UITableView和Navigation Controller是UI元素的基础,构建iOS应用程序的时候常被用到。所以要花费一些时间来通读本文,确保对此了如指掌。

[转载此篇译文,请注明译文出处]

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

28 9月

System Architecture: Strategy and Product Development for Complex Systems 1st Edition

System Architecture

 

前言

我们编著本书旨在展现强大的思想。“系统架构”在认可的目光中正在发展。这一概念出现在不同领域,包括电网系统的架构,移动支付系统的架构。这使人联想到系统架构的DNA,及其所具备竞争优势的基础。如今,已经有超过100000种带有系统架构头衔的职业,还有更多拥有不同头衔的人扮演着系统架构的角色。

强大的概念常常没有明确的适用界限。我们发现许多同事,客户和学生都对系统架构问题存在共识,但在使用范围上差别却非常之大。在已有的系统中,这一术语的使用场景大相径庭。就好比说:“这两辆山地车的架构是不同的”。

系统的架构组成内容通常是存在巨大争议的。在某些领域,这一术语被用来做并不常见的区分,把两种系统类型在相对于细节而言较高的层面做区分。就像“分组交换结构”与“电路交换结构”这样的用法。在其他领域,这一术语被用来描述整个实现过程,服务于更小的细节。比如说“作为服务层架构的软件”这样的说法。

我们的目标是展现架构这一思想的强大之处,并刻画出它的适用界限。很多强大的思想都源于潜在的事物,从而形成架构间早期的“以物易物”,看懂事物的走向,辨别出什么样的制约与机会能够成为核心价值。如果架构事无巨细包罗万象,那么就不可能在早期“以物易物”,相互融通和借鉴。如果价值丢失了其重要驱动力,也不可能存在有意义的用处。

我们编著本书是要阐明Eberhardt Rechtin的理念,架构是精深的而不是宽泛的。意在展示系统架构分析与创建的方法学,并构建出系统架构的“科学”。由于所面对的系统更加复杂,所以正文中的某些地方与产品设计原则相比不是那么规范。产品开发人员高度关注设计的地方,我们则更加关注其出现的过程——诸多作用的魅力集中起来诞生出连贯的整体。

本书包含了我们过往的经验。我们有幸参与了很多复杂系统的早期开发。包括通信,运输,移动广告,金融,机器人以及医疗设备领域。复杂性的程度从农用机具到国际空间站。

另外,我们还收录了其他系统架构的研究案例,从双动力环保汽车到商用飞机的所有这些构建原则。遇到当前系统架构所面临的挑战时,仅希望能够对系统架构的推进有所帮助。

本书主要面向两类读者——专业架构人员和工科类学生。系统架构作为思想是从业人员智慧的结晶,试图将开发新框架遇到的问题和挑战整理成典。核心受众之一就是面临架构层面决策的高级架构人员。这一领域囊括了科技业界各种高级技术和管理类的职能和角色,涵盖了软件、电子产品、工业产品、航空航天、汽车和消费品领域。

本书也集中将工科类的学生作为核心受众。书中的内容源于在过去的15年中,我在麻省理工学院所讲授的研究生课程。在那里我有幸给许多政府及私营部门的领导者传道授业,对架构的放大和剖析帮助我们理解当今的系统是如何运行的。并且我们认为在管理和技术组织中,这是一项必备的能力。

复杂系统的架构和功能

系统架构是复杂系统早期决策科学的研究。本书讲述如何在早期系统决策过程中运用经验和分析方法,如何选择符合相关需求的架构,使架构易于整合,能够灵活扩展。通过顶级专家的案例研究,展示系统架构的科学性和艺术性,案例范畴从双动力环保汽车到通讯网络和飞行器。

作者简介

Edward F. Crawley:俄罗斯斯科尔科沃科学技术学院(Skolkovo Institute of Science and Technology, Skoltech)校长,麻省理工学院(MIT)航空航天工程系教授,ACX,BioScale,DataXu和Ekotrope公司的创始人。他还在四个国家的工程院担任院士。

Bruce G. Cameron:技术战略咨询公司Technology Strategy Partners(TSP)创办人,麻省理工学院系统架构实验室主任。他曾经服务过60家世界500强公司,涵盖航空航天,高科技和个人消费品领域,目前在为轨道建立硬件系统。

Daniel Selva:美国康奈尔大学(Cornell University)机械与航空航天工程系教授。他开辟了在系统架构分析中使用机器学习技术的先河,获得了美国国家航空航天局(NASA)颁发的最佳论文奖和最热文章奖。

23 9月

使用JSON和WatchKit构建简单的天气应用程序

文 / Gregory Tareyev:iOS开发者,http://iamchill.com联合创始人。

原文链接:http://www.appcoda.com/weather-watchkit-app/

编辑注:这是论坛游客Gregory Tareyev发的帖,他是iOS开发人员和Chill(iamchill.co)的联合创始人。开发的首个可穿戴通信工具让你和朋友们动动指尖就能互动。在本文中,Gregory将分享自己做Apple Watch开发的经验,向你展示如何使用第三方API和WatchKit构建简单的天气应用程序。我们已经写了两篇与WatchKit有关的文章,都是使用Swift语言完成的。一些读者提到是否能有一篇使用Objective-C的文章,这篇便是。

Gregory的文章从这里开始……

大家好,我是Gregory Tareyev(可以来tareyev.ru联系我),是iOS开发人员和Chill(iamchill.co)的联合创始人。第一款可穿戴的通信工具终于搞定了。最近我们在Product Hunt上展示的产品有着不俗的表现,使我们在社区内获得了广泛的影响力并吸引了主要的技术博客报道这次发布。我们也在讨论使用加速器的问题,并考虑使用最好的那种。为构建应用程序而着实努力着,我相信你们每个人都可以为此而努力。

55d1486e117dc_middle

在这里想要与你分享我的Apple Watch开发经验。很有趣,也不难。

为了调动你的兴趣,我要解释在可穿戴设备上进行开发为何如此重要。

  1. 市场仍然不太大,这等同于作为先行者,你可以获得更大的市场份额。
  2. 市场将会获得极大增长,这等同于相关产品规模会随着市场同步增长。

55d14acdba958_middle

来源:图表由Business Insider提供。

在本文中,构建一个简单的Apple Watch天气应用程序,要完成两件事:

  • 如何在使用WatchKit在应用程序中解析JSON数据
  • 如何使用OpenWeatherMap的API(一旦明白其中的道理,你应该能够选择任何基于JSON的API来使用)

我们开始吧!

应用程序示例

示例程序是一款非常简单的天气应用程序,会使用OpenWeatherMap的API得到特定城市(例如:伦敦)的天气信息。这是应用程序最终样例的截图。

55d14b794590b

创建Xcode工程

首先,创建一个Single View Application,并按照如下截图设置项目信息。没错儿,我们使用Objective-C来做,我觉得Objective-C仍然很重要,还是要继续使用的。

55d14bcd1445f_middle

创建一个Watch应用程序,到Xcode顶部菜单栏选取File > New > Target…,选择Apple Watch > WatchKit App。由于使用WatchKit App模版,所以会生成构建Watch应用程序所需要的一切。

55d14ccb2a8f8_middle

取消选择“Include Notification Scene”,其余项保留,点击Finish。

simple-weather-watch-app

你会得到一条警告信息问是否要激活,点击Activate就是了。然后就会看到两个新文件夹:WatchKit Extension和WatchKit App。

simple-weather-watch-scheme

设计应用程序的用户接口

接下来,我们要着手设计应用程序的用户接口。为此,点击“SimpleWeather WatchKit App”下的Interface.Storyboard文件。

首先,从对象库(Object Library)拖拽一个Lable控件,名称设置为“Weather in London”。控件上字体大小可能需要调整一下。然后拖拽一个Image控件。接着再来一个Button控件。你会发现图像和按钮都会自动布局,垂直堆放。把按钮的Title改为“Update”,颜色调为绿色并调整图像的尺寸。

simple-weather-watch-ui

标签用来显示气象类型,而图像显示气象类型的插图。Update按钮是与用户交互的唯一元素,用来更新天气信息。

Interface Builder可以让你看到Apple Watch不同版本的视图。默认情况下,Interface Controller被设置成Any Screen Size。可以点击Interface Builder下方的“Any Screen Size”按钮,在Apple Watch 38/42mm这两种尺寸间切换。如果切换到Apple Watch 42mm,就会发现图像不完全合适,要调整图像大小,把所有控件布局弄好为止。因为更改图像大小时,Xcode会自动添加对布局做特殊处理。这种情况只在选择Apple Watch 42mm的才出现。

simple-weather-watch-ui-42

理解JSON和OpenWeatherMap的API

已经说过了,我们使用OpenWeatherMap的API来得到天气数据。为了理解它是如何工作的,要打开这个链接:http://api.openweathermap.org/data/2.5/weather?q=London,uk。拷贝结果并粘贴到http://json.parser.online.fr。你会看到结构化的JSON数据。在这里,我们感兴趣的是天气类型,也就是dictionary类型数据“weather”下的“main”键。这就是我们要在屏幕上显示的信息。

openweather-api-json

作为参考,可以在http://openweathermap.org/api查看API文档。

现在,一起来看如何解析JSON数据,并把天气信息在应用程序中展现出来。

打开Assistant Editor,按住Control键拖拽Label控件到InterfaceController.h文件的代码中做关联,把outlet命名为“weatherType”。

add-label-outlet

重复上述过程,将Image控件也做好关联,把Outlet命名为“weatherImage”。在给Update按钮做关联时,不要选择Outlet,选择Action类型,并命名为“updateAction”。

add-update-action

在InterfaceController.m文件中为updateAction方法添加逻辑代码:

- (IBAction)updateAction
{
    NSURLRequest* requestForWeatherData = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://api.openweathermap.org/data/2.5/weather?q=London,uk"]];
    NSURLResponse* response = nil;
    NSError* error = nil; //do it always
 
    NSData* data = [NSURLConnection sendSynchronousRequest:requestForWeatherData returningResponse:&response error:&error]; //for saving all of received data in non-serialized view
    
    NSMutableDictionary *allData = [ NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]; //data in serialized view
    NSString* currentWeather = nil;
    
    NSArray* weather = allData[@"weather"];
    
    for (NSDictionary* weatherDictionary in weather)
    {
        currentWeather = weatherDictionary[@"main"];
    }
}

该方法中,使用了NSURLConnection对OpenWeatherMap同步发出请求。可以使用NSJSONSerialization把JSON数据转换为Foundation基础库类型(例如:NSDictionary)。我们对数据进行解析以后将天气类型存入“currentWeather”变量中。

接下来,要更新标签和图像。

weather-image-if

这样代码看起来不是太美观,对吧?

为了避免对天气类型进行硬编码,可以创建如下方法。这样会使代码更灵活。

  1. -(void)setImageAndTextWithWeather:(NSString* ) weatherName
  2. {
  3.      // Use the weather type as the weather image name. For example, if the weather name is “Rainy”, the image name is set to “rainy.jpg”. 
  4.      // Set the weather type to the given weather name
  5. }

很不错!我们把方法放到真实的代码中:

  1. -(void)setImageAndTextWithWeather:(NSString* ) weatherName
  2. {
  3.         NSString* weatherNameWithoutSpaces = [weatherName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; //delete potential spaces in JSON array
  4.         [_weatherImage setImageNamed:[weatherNameWithoutSpaces stringByAppendingString:@“.jpg”]];
  5.     NSMutableAttributedString *customString = [[NSMutableAttributedString alloc] initWithString:weatherNameWithoutSpaces];
  6.     [customString addAttribute:NSFontAttributeName
  7.                          value:[UIFont systemFontOfSize:18]
  8.                   range:NSMakeRange(0, [weatherNameWithoutSpaces length])]; //Making text more readable by creating a custom string
  9.     [_weatherType setAttributedText:customString];
  10. }

最后,在updateAction方法的结尾加上这行代码:

[self setImageAndTextWithWeather:currentWeather];

在Asset Catalog中添加图像

运行应用程序之前,下载这些图像。解压缩后把这些图像全部添加到SimpleWeather WatchKit App文件夹下的Images.xcassets里。

simple-weather-images

可以随便添加更多带有天气类型名称的图像。应用程序不用更改任何代码就可以运行。

测试应用程序

差不多了!现在可以在Apple Watch模拟器中构建并运行应用程序了。选择“WatchKitDemo WatchKit App”项目,并选择合适的设备,然后点击Run按钮,测试Apple Watch应用程序。另外,还可以修改模拟器显示的尺寸,选择Hardware > External Displays > Apple Watch – 38mm。

55d151607af6f_middle

很棒!你开发完成了一款Apple Watch天气应用程序。

作为参考,可以在这里下载最终的项目

http://www.csdn.net/article/2015-08-17/2825474/2

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

26 8月

探秘ES6: 类语法

来源:Mozilla Web开发者博客         文 / Eric Faust

原文链接:https://hacks.mozilla.org/2015/07/es6-in-depth-classes/

ES In Depth是一个系列,描述按照ECMAScript标准第6版加入到JavaScript语言中的新特性。简称为ES6

在领教了本系列文章前几篇的复杂程度后,我们现在得以有片刻的喘息。再没有闻所未闻的编码方式,使用生成器(generator)编码;再没有无所不能的代理对象(proxy object),为JavaScript语言内部算法实现提供了钩子函数;再没有新的数据结构,避免了用户自主开发的需要。相反,我们要说说与一个旧问题相关的语法和清理技法(idiom),那就是JavaScript中对象构造函数的创建。

问题

我们要说的是,创建面向对象设计原则中最典型的例子:Circle类。想象我们正在为Canvas库编写一个Circle类,除此之外,恐怕还需要知道如何去做以下几点:

  • 为给定的Canvas画一个Circle。
  • 记录所画Circle的个数。
  • 记录给定Circle的半径,如何给不变量(invariant)强行赋值。
  • 计算给定Circle的面积。

目前JS的惯用技法是先拿构造函数当作函数来创建,然后向函数添加任何想要添加的属性,再用一个对象替换构造函数中的prototype属性。该prototype对象包含构造函数最初所创建实例的所有属性。虽然这只是一个简单的例子,但敲出来以后,会是不少样板(boilerplate)代码:

  1. function Circle(radius) {
  2.     this.radius = radius;
  3.     Circle.circlesMade++;
  4. }
  5. Circle.draw = function draw(circle, canvas) { /* Canvas drawing code */ }
  6. Object.defineProperty(Circle, “circlesMade”, {
  7.     get: function() {
  8.         return !this._count ? 0 : this._count;
  9.     },
  10.     set: function(val) {
  11.         this._count = val;
  12.     }
  13. });
  14. Circle.prototype = {
  15.     area: function area() {
  16.         return Math.pow(this.radius, 2) * Math.PI;
  17.     }
  18. };
  19. Object.defineProperty(Circle.prototype, “radius”, {
  20.     get: function() {
  21.         return this._radius;
  22.     },
  23.     set: function(radius) {
  24.         if (!Number.isInteger(radius))
  25.             throw new Error(“Circle radius must be an integer.”);
  26.         this._radius = radius;
  27.     }
  28. });

这样的代码不仅冗长,而且不够直观。不是一下子就能理解函数是如何工作的,也不是很容易理解各个属性用什么方式绑定到所创建的实例对象的。即使这样的实现方式看起来比较复杂也不必着急。本文的主旨就是要展示一种更简单的编码方式,用来解决所有这些问题。

定义方法的语法

首次尝试去规范这个问题时,ES6给出了一种新的语法来为对象添加特殊属性。虽然很容易在上面的Circle.prototype添加area方法,但是对radius添加一对getter和setter让人感觉过于啰嗦。由于JS更加倾向于面向对象化的解决方法,所以人们变得关心如何用简洁的方式给属性添加访问器(accessor)。我们需要一种新的方式给对象添加“方法”,就像obj.prop = method那样被添加进去,而不需要用Object.defineProperty。大家希望能够轻松做到下面几件事:

  1. 给对象添加普通函数(normal function)。
  2. 给对象添加生成器函数(generator function)。
  3. 给对象添加普通访问器函数属性(accessor function property)。
  4. 给已创建的对象添加上述任何函数,好像使用方括号[]语法就能完成的样子。我们称之为计算属性名(computed property name)。

其中的一些事情之前是无法完成的。例如,之前没有办法给obj.prop定义getter或setter对其进行赋值,因此要添加新的语法功能。现在你就可以编写像下面这样的代码了。

  1. var obj = {
  2.     // Methods are now added without a function keyword, using the name of the
  3.     // property as the name of the function.
  4.     method(args) { … },
  5.     // To make a method that’s a generator instead, just add a ‘*’, as normal.
  6.     *genMethod(args) { … },
  7.     // Accessors can now go inline, with the help of |get| and |set|. You can
  8.     // just define the functions inline. No generators, though.
  9.     // Note that a getter installed this way must have no arguments
  10.     get propName() { … },
  11.     // Note that a setter installed this way must have exactly one argument
  12.     set propName(arg) { … },
  13.     // To handle case (4) above, [] syntax is now allowed anywhere a name would
  14.     // have gone! This can use symbols, call functions, concatenate strings, or
  15.     // any other expression that evaluates to a property id. Though I’ve shown
  16.     // it here as a method, this syntax also works for accessors or generators.
  17.     [functionThatReturnsPropertyName()] (args) { … }
  18. };

我们可以使用新的语法重写上面的代码段:

  1. function Circle(radius) {
  2.     this.radius = radius;
  3.     Circle.circlesMade++;
  4. }
  5. Circle.draw = function draw(circle, canvas) { /* Canvas drawing code */ }
  6. Object.defineProperty(Circle, “circlesMade”, {
  7.     get: function() {
  8.         return !this._count ? 0 : this._count;
  9.     },
  10.     set: function(val) {
  11.         this._count = val;
  12.     }
  13. });
  14. Circle.prototype = {
  15.     area() {
  16.         return Math.pow(this.radius, 2) * Math.PI;
  17.     },
  18.     get radius() {
  19.         return this._radius;
  20.     },
  21.     set radius(radius) {
  22.         if (!Number.isInteger(radius))
  23.             throw new Error(“Circle radius must be an integer.”);
  24.         this._radius = radius;
  25.     }
  26. };

严格地说,这段代码与其上面那段并不完全相同。方法定义中所使用的对象字面量(object literal)被设置成了configurable和enumerable。而在上一个代码段中的访问器则会是non-configurable和non-enumerable的。在实践中,这点很少被注意到,为了简洁,我决定忽略掉上面这两种描述。

不过,应该变得更好了对吗?很遗憾,即使有了这样新的定义方法的语法,我们对Circle的定义仍然无法做太多的事情。因为我们还没有定义函数。没有办法将属性绑定到你尚在定义的函数上去。

定义类的语法

虽然这样更好一些,但仍然无法令人满意,人们想要一种更简洁的JavaScript面向对象设计解决方案。他们说其他编程语言为了解决面向对象设计而拥有一种结构,这种结构被称为

很公平,那么就来添加类。

我们需要一个系统,允许将方法添加到已命名的构造函数当中,并还能添加系统的.prototype当中。这样这些方法就会出现在类的结构化实例当中。由于有新奇的语法可以定义方法,我们应该用一下。然后,只需要区分所添加的方法属于类的所有实例,还是专属于某个给定实例。在C++和Java语言中,解决这一问题的关键字是static。用在这里看起来也不错,用一下吧!

现在将众多方法其中的一个指定为函数,它将被称为构造函数。在C++和Java语言中,构造函数的名称要与类名一致,并且没有返回类型。由于JS没有返回类型,所以为了向后兼容,的确需要一个.constructor 属性。我们称这个方法为构造函数。

综上所述,可以重写Circle类了:

  1. class Circle {
  2.     constructor(radius) {
  3.         this.radius = radius;
  4.         Circle.circlesMade++;
  5.     };
  6.     static draw(circle, canvas) {
  7.         // Canvas drawing code
  8.     };
  9.     static get circlesMade() {
  10.         return !this._count ? 0 : this._count;
  11.     };
  12.     static set circlesMade(val) {
  13.         this._count = val;
  14.     };
  15.     area() {
  16.         return Math.pow(this.radius, 2) * Math.PI;
  17.     };
  18.     get radius() {
  19.         return this._radius;
  20.     };
  21.     set radius(radius) {
  22.         if (!Number.isInteger(radius))
  23.             throw new Error(“Circle radius must be an integer.”);
  24.         this._radius = radius;
  25.     };
  26. }

哇!我们不仅可以把与Circle相关的一切组织在一起,而且一切看起来如此地整洁。这绝对比一开始好多了。

即便如此,有些人可能还会有问题,找出极端的例子。我就试着预测一下,并解决其中的一些问题。

  • Q:分号用来做什么?A:为了“让一切看起来更像传统的类”,我们决定使用更为传统的分隔符。不喜欢它吗?这是可选的,分隔符并不是必须的。
  • Q:如果我不想要构造函数,但仍然想给已创建定对象添加方法该怎么办?A:很好!constructor方法完全是可选的。如果不这样做,默认情况就像已经敲了constructor() {}。
  • Q:“constructor”可以是生成器吗?A:不可以!不使用普通方法(normal method)添加constructor会导致TypeError,包括生成器和访问器。
  • Q:可以使用计算属性名定义constructor吗?A:遗憾的是不可以。这将非常难以探测。因此我们就不试了。如果使用计算属性名定义方法的话,最终方法会被命名为“constructor”,仍会得到一个名为 constructor的方法,而不是类的构造函数。
  • Q:如果改变Circle的值会怎样?是否会导致出现新的Circle,并且出错呢?A:不会的!就像函数表达式,类内部绑定了它们的名称。外部力量无法改变这个绑定。因此,在封闭范围内不管怎样设置Circle变量,构造函数中的Circle.circlesMade++都会按预期工作。
  • Q:好吧,但是我可以直接传入对象字面量作为函数的参数。使用新语法定义的类看起来行不通了。A:很幸运,ES6还增加类表达式!可以对其命名或者匿名。除了在声明它们的范围内不会创建变量,其行为与上面描述的函数完全相同。
  • Q:上面的这些把戏如果是可枚举的,或者有什么其他属性会怎么样?A:这样做是为了可以给对象配置方法,但是当你对对象的属性进行枚举的时候,仅得到了已经添加进对象的数据属性。因为这是合理的,所以类里所配置的方法是configurable的,但不是enumerable的。
  • Q:喂,等等……实例变量在哪里?static常量呢?A:好问题!在ES6中,类的定义目前不存在实例变量和静态常量。不过好消息是,连同参与其他规范的过程中,我都强有力地支持在类的语法中设有static和const值。事实上,这已经出现在了规范相关的会议上。我想可以期待以后出现更多与此有关的讨论。
  • Q:好吧,即便如此,这些也都是极好的!我可以使用它们了吗?A:不完全可以。存在那些可选用的polyfill(特别是Babel),你可以试着使用它们。遗憾的是,在被主流浏览器原生地实现以前,还需要一些时间。我们这里讨论的一切都在Nightly版的Firefox浏览器实现过。Edge和Chrome浏览器实现了,但是默认情况下并未启用。遗憾之处是Safari浏览器还没实现这些新特性。
  • Q:JavaC++都使用子类和super关键字,但这里并没提到,JS有相关概念吗?A:有的!但这完全是一个值得另外成文讨论的事情。回头再看我们今后有关使用子类的更新,将讨论更多有关JavaScript类的威力。

没有Jason OrendorffJeff Walden大量认真负责的代码审查和指导,我不可能实现文中的类代码。

下周,Jason Orendorff将结束为期一周的假期,开始撰写let和const主题。

http://www.csdn.net/article/2015-08-14/2825464-es6-in-depth-classes

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出版发行。

13 8月

如何使用iOS SDK获取和解析JSON数据

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

原文链接:http://www.appcoda.com/fetch-parse-json-ios-programming-tutorial/

编辑注:本周,ziad tamim将向你展示如何获取和解析JSON数据。在本文中,我们会关注为iOS应用程序添加对JSON的支持,通过使用Meetup API来展示如何创建一个简单应用程序。这篇文章也许比我们之前介绍的其他文章要复杂一点儿,这正是你所需要的有关JSON和设计模式的基础知识。

JSON编程指南由此开始……

首先,什么是JSON?JSON(short for JavaScript Object Notation)是一个基于文本的,轻量级的,便于数据排序和交换的一种机制。通常用于在客户端/服务器模式的应用程序中表示结构化的数据,并进行数据的交互,作为XML格式数据的替代物。我们日常中所使用的很多服务都适用了基于JSON的API。大多数iOS应用程序都使用JSON格式的网络服务将数据发送到它们的后端web服务。包括Twitter,Facebook和Flick在内都是这样做的。

json-intro-tutorial

{
    "title": "The Amazing Spider-man",
    "release_date": "03/07/2012",
    "director": "Marc Webb",
    "cast": [
        {
            "name": "Andrew Garfield",
            "character": "Peter Parker"
        },
        {
            "name": "Emma Stone",
            "character": "Gwen Stacy"
        },
        {
            "name": "Rhys Ifans",
            "character": "Dr. Curt Connors"
        }
    ]
}

不难发现,JSON数据比XML更具可读性,更容易解析。如果你对JSON一无所知,可以看看这个JSON Guide

iOS5发布以来,iOS SDK更容易获取和分析JSON数据。在这篇文章中,我们将展示如何使用内置API查询来自Meetup基于JSON的API,并恰当地处理返回值。

我们的应用程序

编写代码以前,先来看看要构建什么样的应用程序。

在本文中,我们要创建一个叫做BrowserMeetup的简单应用程序,将会使用Meetup的公共API。从来没听说过Meetup吗,它是全球最大的本地聚会网络平台。你可以免费使用它来组织在当地的聚会,还可以从上千个已经组织好的聚会中找到其中的一个。像其他社交网站一样,Meetup为你的服务端访问其数据提供了开放API。

BrowseMeetup应用程序会使用Meetup的web服务来寻找附近的聚会。应用程序会获得当前的地理位置并自动加载附近的聚会。

Screen-Shot-2013-08-24-at-11.32.09-PM-e1377937354961

注:该应用程序使用Core Location框架,如果你不了解该框架,可以参考How To Get the User Location in iPhone App

动手开发应用程序

可以开始构建和管理BrowseMeetup应用程序了。启动Xcode并创建一个主-从视图类型的应用程序(Master-Detail Application)。项目选项设置中,你要选择Storyboard和ARC(Automatic Reference Counting)选项。该应用仅提供iPhone视图,因此在设备选项中选择iPhone并保存项目。删除掉storyboard中的DetailsViewController,像下面这样设计用户界面:

Screen-Shot-2013-08-25-at-1.24.08-AM-e1377871383429

本文着重介绍JSON的获取和解析。所以为了节省时间就不去建立项目了,你可以直接下载项目模版。模版已经事先构建好了用户接口并实现了Core Location功能。

提示:如果不懂怎么用table view,想知道导航条是如何工作的,可以参考我们免费的iOS资源指南

使用Meetup的API

使用Meetup API前,请在其上先创建一个账号。打开APIs Doc,点击“Request to join this Meetup group”按钮。然后填写必要的信息,点击“Sign Up”。一步一步进行直至完成。

Screen-Shot-2013-08-26-at-10.06.27-PM

我们将使用其中的一个API(https://api.meetup.com/2/groups)来获取在某个地点举行的Meetup聚会信息。调用过程允许开发者使用经纬度来定位。可以使用API console来测试调用。

这是一段请求后简单的JSON响应信息(https://api.meetup.com/2/groups?&sign=true&lat=51.509980&lon=-0.133700&page=1):

{
"results": [
{
"lon": -0.10000000149011612,
"visibility": "public",
"organizer": {
"name": "William Brown",
"member_id": 3817216
},
"link": "http://www.meetup.com/french-32/",
"state": "17",
"join_mode": "approval",
"who": "LFM members",
"country": "GB",
"city": "London",
"id": 63974,
"category": {
"id": 16,
"name": "language/ethnic identity",
"shortname": "language"
},
"topics": [
{
"id": 185,
"urlkey": "french",
"name": "French Language"
},
{
"id": 264,
"urlkey": "friends",
"name": "Friends"
},
{
"id": 3304,
"urlkey": "expatfrench",
"name": "Expat French"
}
],
"timezone": "Europe/London",
"group_photo": {
"photo_link": "http://photos3.meetupstatic.com/photos/event/7/4/a/b/600_929867.jpeg",
"highres_link": "http://photos3.meetupstatic.com/photos/event/7/4/a/b/highres_929867.jpeg",
"thumb_link": "http://photos3.meetupstatic.com/photos/event/7/4/a/b/thumb_929867.jpeg",
"photo_id": 929867
},
"created": 1034097734000,
"description": "<p>The London French Meetup is the biggest group of French speakers of all ages and nationalities in London. We hold regular events to meet up, talk in French and share interests in French culture whilst having a good time.</p>
<p>We have two main events per month where we have the whole of the upstairs of a pub.</p>
<p>In addition, we organise other regular events such as outings to: restaurants, trendy bars, french films, live music, sports related activities, outdoor events and more...</p>
<p>The organising team is made of volunteers from different nationalities and ages. Our members are made up of: 1/3 French nationals, 1/3 British nationals and 1/3 other nationalities and francophone countries. If you have any ideas or suggestions for events or would like to help please let us know.</p>
<p>A bientôt.</p>
<p>LFM Team.</p>",
"name": "London French Meetup",
"rating": 4.37,
"urlname": "french-32",
"lat": 51.52000045776367,
"members": 4889
}
],
"meta": {
"lon": -0.1337,
"count": 1,
"signed_url": "http://api.meetup.com/2/groups?radius=25.0&order=id&desc=false&offset=0&format=json&lat=51.50998&page=1&fields=&lon=-0.1337&sig_id=109020062&sig=4532ed8f987f940748ebfba0f483a26f756dcba3",
"link": "http://www.meetup.com/2/groups",
"next": "http://www.meetup.com/2/groups?radius=25.0&order=id&format=json&lat=51.50998&page=1&desc=false&offset=1&fields=&sign=true&lon=-0.1337",
"total_count": 4501,
"url": "http://www.meetup.com/2/groups?radius=25.0&order=id&format=json&lat=51.50998&page=1&desc=false&offset=0&fields=&sign=true&lon=-0.1337",
"id": "",
"title": "Meetup Groups v2",
"updated": 1377876449000,
"description": """",
"method": "Groups",
"lat": 51.50998
}
}

应用程序设计及工作原理

如前所述,Meetup API提供了请求某方位聚会信息的方法。响应数据将用JSON格式发送。我们需要一个对象,可以从已编码的数据中检索数据,并从中构建我们的域对象(domain object )。下面是关于应用程序设计的介绍,展示如何构建类,以及这些类如何获得meetup聚会信息:

Screen-Shot-2013-08-16-at-10.01.27-PM-e1377877588261

对于有些人来说,也许有点儿复杂。我来带你过一遍。创建MeetupManager是为了请求的Meetup聚会所获得的方位,就像门面一样。如果你没有听说过设计模式中的门面模式(Facade Pattern)的话,可以把它想象成其他类的协调者。门面模式试图为界面试图控制器提供一个简化的接口,并屏蔽掉底层的实现。

MeetupCommunicator用来与Meetup API进行通讯。一旦Meetup返回JSON格式响应,我们就将该响应传递给GroupBuilder,GroupBuilder构建了Group对象。

MasterViewController使用Core Location得到当前的方位,并通知MeetupManager得到该方位的Meetup聚会信息。MeetupManager协调其他的类来检索得到的那些聚会信息。一旦检索成功,MeetupManager会通过代理与MasterViewController通信,并将检索到的聚会信息传递过去。然后MasterViewController将传递过来的聚会信息在table view中展现出来。

创建JSON数据模型

接下来我们将要实现实体类(model层)。Group类表示BrowseMeetup应用中聚会的信息,用来存储从Meetup返回的聚会信息。下面是一个JSON响应中的Group对象示例:

{
     lon: -71.12999725341797,
     visibility: "public",
     organizer: {
          name: "Emma",
          member_id: 2161382
     },
     link: "http://www.meetup.com/bloggers/",
     state: "MA",
     join_mode: "closed",
     who: "Bloggers",
     country: "US",
     city: "Cambridge",
     id: 21458,
    category: {
          id: 34,
          name: "tech",
          shortname: "tech"
     },
     topics: [
          {
               id: 198,
               urlkey: "blog",
               name: "Blog"
          },
          {
               id: 772,
               urlkey: "writers",
               name: "Writers"
          }
     ],
     timezone: "US/Eastern",
     group_photo: {
     photo_link: "http://photos3.meetupstatic.com/photos/event/6/4/9/5/600_25749.jpeg",
     highres_link: "http://photos3.meetupstatic.com/photos/event/6/4/9/5/highres_25749.jpeg",
     thumb_link: "http://photos3.meetupstatic.com/photos/event/6/4/9/5/thumb_25749.jpeg",
     photo_id: 25749
     },
     created: 1034097731000,
     description: "This is a group for people that are interested in blogging and meeting others who are interested in blogging. Topics discussed range from blog content to blog software. All interest levels are welcome.",
     name: "The Greater Boston Area Weblogger Meetup Group",
     rating: 3.33,
     urlname: "bloggers",
     lat: 42.38999938964844,
     members: 119
}

上面的JSON响应代表单个Meetup Group。我们不会使用所有返回来的数据。而是简单地使用“name”,“description”,“who”,“country”和“city”字段。这些字段对我们来说足够了。现在要使用Objective-C类模版来创建一个新文件,命名为Group,将它设置为NSObject的子类,并在头文件中添加如下代码:

@interface Group : NSObject
@property (strong, nonatomic) NSString *name;
@property (strong, nonatomic) NSString *description;
@property (strong, nonatomic) NSString *who;
@property (strong, nonatomic) NSString *country;
@property (strong, nonatomic) NSString *city;
@end

我们将使用这些属性信息,在应用程序中实现我们之前描述的最终结果。

使用Meetup API获取JSON 数据

首先,使用Objective-C的协议模版创建一个文件,并命名为MeetupCommunicatorDelegate,在文件中填充如下代码:

@protocol MeetupCommunicatorDelegate 
- (void)receivedGroupsJSON:(NSData *)objectNotation;
- (void)fetchingGroupsFailedWithError:(NSError *)error;
@end

MeetupCommunicator类负责Meetup API在获取JSON数据过程中的通讯。它依赖于MeetupCommunicatorDelegate来处理解析JSON数据的任务。通讯类本身并不知道JSON数据是如何被处理的。通讯类只关心为Meetup API和获取JSON原始结果数据的过程创建连接。

代理被创建以后,创建另一个类文件,并命名为MeetupCommunicator。打开头文件,键入如下代码:

#import <CoreLocation/CoreLocation.h>
@protocol MeetupCommunicatorDelegate;
@interface MeetupCommunicator : NSObject
@property (weak, nonatomic) id<MeetupCommunicatorDelegate> delegate;
- (void)searchGroupsAtCoordinate:(CLLocationCoordinate2D)coordinate;
@end

我们创建了一个属性,用来跟踪记录通讯代理。然后定义了一个方法,用来查找特定方位的聚会信息。接下来,打开MeetupCommunicator.m文件,替换成如下代码:

#import "MeetupCommunicator.h"
#import "MeetupCommunicatorDelegate.h"
#define API_KEY @"1f5718c16a7fb3a5452f45193232"
#define PAGE_COUNT 20
@implementation MeetupCommunicator
- (void)searchGroupsAtCoordinate:(CLLocationCoordinate2D)coordinate
{
    NSString *urlAsString = [NSString stringWithFormat:@"https://api.meetup.com/2/groups?lat=%f&lon=%f&page=%d&key=%@", coordinate.latitude, coordinate.longitude, PAGE_COUNT, API_KEY];
    NSURL *url = [[NSURL alloc] initWithString:urlAsString];
    NSLog(@"%@", urlAsString);
    [NSURLConnection sendAsynchronousRequest:[[NSURLRequest alloc] initWithURL:url] queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
        if (error) {
            [self.delegate fetchingGroupsFailedWithError:error];
        } else {
            [self.delegate receivedGroupsJSON:data];
        }
    }];
}
@end

Meetup API需要一个key来工作。如果你注册了账号,需要从API Key页面得到key的话,那么简单地点击一下文本框旁边的?图标就可以得到key。把里面的文本拷贝出来,用自己的API key将API_KEY宏的值替换掉。

正如上面提到的,我们使用下面的Meetup API来查找指定方位的聚会信息。API接受经纬度格式的方位信息。下面就是Meetup URL连接的示例:

https://api.meetup.com/2/groups?lat=51.509980&lon=-0.133700&page=20&key=1f5718c16a7fb3a5452f45193232

在方法的实现中,我们首先使用特定的经纬度,以及聚会的数量和API key来构建API URL。为了在UI部分不卡住,我们通过使用NSURLConnection中的“sendAsynchronousRequest:”方法为URL请求异步加载数据。最终检索到JSON数据,将它传递给代理进一步进行处理。

解析JSON数据并绑定Group对象

MeetupManager收到JSON格式的数据时,使用GroupBuilder的类方法将数据转换为Group对象。使用Objective-C类模版创建一个新文件,命名为GroupBuilder。打开头文件,粘贴如下代码:

#import <Foundation/Foundation.h>
@interface GroupBuilder : NSObject
+ (NSArray *)groupsFromJSON:(NSData *)objectNotation error:(NSError **)error;
@end

接下来,打开“GroupBuilder.m”文件,实现对应的方法:

#import "GroupBuilder.h"
#import "Group.h"
@implementation GroupBuilder
+ (NSArray *)groupsFromJSON:(NSData *)objectNotation error:(NSError **)error
{
    NSError *localError = nil;
    NSDictionary *parsedObject = [NSJSONSerialization JSONObjectWithData:objectNotation options:0 error:&localError];
    if (localError != nil) {
        *error = localError;
        return nil;
    }
    NSMutableArray *groups = [[NSMutableArray alloc] init];
    NSArray *results = [parsedObject valueForKey:@"results"];
    NSLog(@"Count %d", results.count);
    for (NSDictionary *groupDic in results) {
        Group *group = [[Group alloc] init];
        for (NSString *key in groupDic) {
            if ([group respondsToSelector:NSSelectorFromString(key)]) {
                [group setValue:[groupDic valueForKey:key] forKey:key];
            }
        }
        [groups addObject:group];
    }
    return groups;
}
@end

方法“groupsFromJSON”被设计为将原始的JSON数据转换成Group对象的数组。iOS 5发布以来,iOS SDK中名为NSJSONSerialization的类用来解析JSON数据。开发人员使用该类可以将JSON数据转换成Foundation框架中的对象,也可以将Foundation框架中的对象转回JSON数据。

当使用NSJSONSerialization读取JSON数据时,所有的键列表(keyed list)都被自动转换成NSDictionary对象。数组则会被转换为NSArray实例。键列表中任何带有已命名项名称的字符串都会被转换为NSString,而纯数字字符串会被转换为NSNumber对象。最后,任何为空的值都会被用NSNull表示。

在本文前面部分向你展示的响应示例中,Meetup API返回的JSON响应包括两部分,结果数据和元(meta)信息。而我们只需要“结果”部分。代码相当直观明了,我们循环遍历了所有的结果,查看了其中的每一个NSDictionary。然后创建了Group对象,对其填充了必要信息,然后将Group对象添加到可变数组当中。

用MeetupManager把各部分集中起来

你现在应该明白JSON是如何工作的了,如何解析数据并将数据转换成对象。接下来,我们要实现MeetupManager,作为底层类的协调者来使用。

首先,使用Objective-C协议模版创建一个文件,命名为MeetupManagerDelegate。在MeetupManagerDelegate.h文件中添加如下代码:

@protocol MeetupManagerDelegate
- (void)didReceiveGroups:(NSArray *)groups;
- (void)fetchingGroupsFailedWithError:(NSError *)error;
@end

这个代理声明了两个方法,当group对象需要的时候会被MeetupManager调用。当从Meetup响应中检索到的group列表被解析时,调用第一个方法。而当发生错误的时候,第二个方法会被调用。MeetupManagerDelegate将会被MasterViewController实现,我们会在随后内容中讲解。

接下来,使用Objective-C类模版创建一个新文件,命名为MeetupManager。打开头文件,添加如下代码:

#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>
#import "MeetupManagerDelegate.h"
#import "MeetupCommunicatorDelegate.h"
@class MeetupCommunicator;
@interface MeetupManager : NSObject<MeetupCommunicatorDelegate>
@property (strong, nonatomic) MeetupCommunicator *communicator;
@property (weak, nonatomic) id<MeetupManagerDelegate> delegate;
- (void)fetchGroupsAtCoordinate:(CLLocationCoordinate2D)coordinate;
@end

前面说过,MeetupManager扮演着门面的角色。应用程序的控制器可与实体类(Group)一起工作,不需要知道任何关于网络连接的细节,也不需要知道JSON如何获取和解析,以及group对象如何创建。控制器需要知道的仅仅是使用“fetchGroupsAtCoordinate:”方法获取Meetup聚会的信息。

我们设置一个属性来记录稍后要用到的communicator实例,还有用来追踪MeetupManagerDelegate的属性。“fetchGroupsAtCoordinate:”方法将被控制器用来获取group。

下一步,打开MeetupManager.m文件,替换成下面的代码:

#import "GroupBuilder.h"
#import "MeetupCommunicator.h"
@implementation MeetupManager
- (void)fetchGroupsAtCoordinate:(CLLocationCoordinate2D)coordinate
{
    [self.communicator searchGroupsAtCoordinate:coordinate];
}
#pragma mark - MeetupCommunicatorDelegate
- (void)receivedGroupsJSON:(NSData *)objectNotation
{
    NSError *error = nil;
    NSArray *groups = [GroupBuilder groupsFromJSON:objectNotation error:&error];
    if (error != nil) {
        [self.delegate fetchingGroupsFailedWithError:error];
    } else {
        [self.delegate didReceiveGroups:groups];
    }
}
- (void)fetchingGroupsFailedWithError:(NSError *)error
{
    [self.delegate fetchingGroupsFailedWithError:error];
}

在这里实现了fetchGroupsAtCoordinate:coordinate方法,通过使用communicator的searchGroupsAtCoordinate:coordinate方法来获取group。还实现了MeetupCommunicatorDelegate下面的那些方法,用来处理communicator检索到的JSON格式的结果。

协议receivedGroupsJSON:objectNotation第一个方法中代码使用GroupBuilder的类方法将JSON数据填入Group对象,然后将Group对象通知给方法的代理。如果在处理请求的过程中出现任何问题,我们就调用代理的另一个方法(fetchingGroupsFailedWithError)来通知控制器,让控制器知道出现了问题。

显示Group列表

首要任务就是要把这些类都拿来放到一起让MeetupManager能够工作。打开MasterViewController.m文件,导入所需的头文件,更新接口,并且声明MeetupManager实例:

#import "Group.h"
#import "MeetupManager.h"
#import "MeetupCommunicator.h"
@interface MasterViewController () <MeetupManagerDelegate> {
    NSArray *_groups;
    MeetupManager *_manager;
}

随后我们就要实现在MeetupManagerDelegate中所定义的方法。但首先我们先在viewDidLoad方法中要实例化MeetupManager:

- (void)viewDidLoad
{
    [super viewDidLoad];
    _manager = [[MeetupManager alloc] init];
    _manager.communicator = [[MeetupCommunicator alloc] init];
    _manager.communicator.delegate = _manager;
    _manager.delegate = self;
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(startFetchingGroups:)
                                                 name:@"kCLAuthorizationStatusAuthorized"
                                               object:nil];
}

正在实例化一个新的manager,然后用一个新的实例给它的communicator属性赋值,最后让当前的视图控制器追踪一切变化。当警告弹出告诉用户使用定位服务开始,就调用startFetchingGroups:方法从服务端获取group,这里的观察者(observer)会捕捉一切用户的响应。

接下来,打开“MasterViewController.m”,键入如下代码开始获取信息:

- (void)startFetchingGroups:(NSNotification *)notification
{
    [_manager fetchGroupsAtCoordinate:self.locationManager.location.coordinate];
}

由于视图控制器应该遵循MeetupManagerDelegate协议,所以要实现其中的方法:

- (void)didReceiveGroups:(NSArray *)groups
{
    _groups = groups;
    [self.tableView reloadData];
}
- (void)fetchingGroupsFailedWithError:(NSError *)error
{
    NSLog(@"Error %@; %@", error, [error localizedDescription]);
}

当从Meetup服务端返回的实例中获得了Meetup聚会信息,将调用“didReceiveGroups:”方法。我们这里要做的是简单地在table view里面显示新数据。

最后,构建table view的相关方法:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return _groups.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    DetailCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    Group *group = _groups[indexPath.row];
    [cell.nameLabel setText:group.name];
    [cell.whoLabel setText:group.who];
    [cell.locationLabel setText:[NSString stringWithFormat:@"%@, %@", group.city, group.country]];
    [cell.descriptionLabel setText:group.description];
    return cell;
}

上段代码非常直观,我们从数组中检索那些group,然后将group的信息填入表格中。

编译和运行

现在,可以测试这个应用程序了。在模拟器中编译并运行(如果注册了iOS开发者计划,可以使用真实的iOS设备)。将模拟器的位置设置为英国伦敦,你就会得到像下面截屏中的Meetup聚会信息了。

1439437273757_757

提示:如果你不了解iPhone模拟器中的方位测试,可以看看Core Location tutorial

总结

在本文中,我们通过采用与JSON数据相配的设计模式,讲解了编程的很多方面,你应该具有了获取和解析JSON数据的实践经验。

如果你对设计模式是生手,这篇文章可能稍许复杂。但这不应该难住你而放弃在应用程序中使用JSON。iOS的SDK让这一过程变得容易了。总之就是,简简单单地创建一个URL连接,通过内置的NSJSONSerialization类获取和解析JSON数据。

在网上可以找到各种各样的免费API(比如KivaTMDb)。当作练习,尝试使用另一个API再开发一款应用程序。也就是说,你可以使用Kiva API开发一款显示近期筹贷款列表简单应用程序。

作为参考,可以在这里下载全部Xcode项目代码

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

15 5月

对近期AFNetworking安全漏洞引发担忧的回应

文 / Alamofire Software Foundation(由Mattt Thompson创立)

Mattt Thompson:毕业于卡内基·梅隆大学(Carnegie Mellon University),获哲学及语言学学士学位。著名的iOS网络通信类库AFNetworking的作者,此外,他还开发了Postgres.app、ASCIIwwdc和Nomad等热门开源项目。

原文链接:https://gist.github.com/AlamofireSoftwareFoundation/f784f18f949b95ab733a

前一段时间,大量关于AFNetworking存在安全漏洞的消息被公之于众,大约1000种应用程序被指称,由于SSL存在Bug,导致这些应用程序容易遭到攻击。这些文章对此存在一些错误的,带有误导性的说法。

我们对此做出回应来澄清和纠正这些说法。

背景信息

就此事,对于那些不熟悉AFNetworking的人,这里有一些与其相关的细节需要了解。

  • AFNetworking是一个第三方的开源库,置于苹果内置框架之上,提供便利的功能。
  • AFSecurityPolicy是AFNetworking的组件之一,根据应用程序设置的规则处理验证挑战(authentication challenge)。包括通过HTTPS连接时,对服务端返回的X.509证书的评估。
  • 证书锁定(certificate pinning)是一项信息安全技术,它在标准TLS评估的基础上做了改进,通过服务器显式地发送证书来匹配包含在客户端的凭证(credentials)。AFNetworking从版本1.2.0开始一直提供证书锁定技术。
  • 中间人攻击(Man-in-the-Middle Attack, MitM)是在客户端和服务器之间插入攻击者本身,使两边都认为自己和对方在直接通信。
  • 这样的一种攻击方式在客户端和服务器之间会涉及某个不可信的Wi-Fi接入点。没有对响应进行恰当地验证,攻击者就可以拦截通讯信息,用户凭证或其他敏感信息因而会遭到泄露。
  • AFNetworking官方文档强烈建议应用程序要通过HTTPS进行通信,并且使用证书或公钥锁定技术来弱化MitM这种攻击行为。工程中所包含的示例代码遵循了这些建议,在应用程序中展示了证书锁定的使用方式。

事件的时间表

收拾心情,整理思绪。下面是与这一事件相关事件的时间表:

  • 2015年2月12日,AFNetworking 2.5.1发布。这一版本包含了一个补丁,修改了证书的安全策略验证方式,将SSLPinningMode修改为AFSSLPinningModeNone。验证挑战过程中,服务器的证书默认是不会被验证的,除非客户端存在与众不同的配置行为,比如使用SSL pinning这样的证书绑定技术。
  • 2015年3月12日,我们从这个GitHub Issue开始意识到上述的修改行为所造成的影响。
  • 2015年3月26日,来自Minded Security Research的Simone Bovi和Mauro Gentile发表了一篇博文,详细说明了AFNetworking 2.5.1潜在的MitM方面的漏洞。
  • 同样在2015年3月26日,AFNetworking 2.5.2发布。这个版本恢复了先前的证书安全策略评估方式。如果安全策略将validatesDomainName设置为YES,那么SSLPinningMode将会被修改为AFSSLPinningModeNone。
  • 2015年4月20日,AFNetworking 2.5.3发布了,该版本做了额外的修改。对所有的安全策略默认设置validatesDomainName为YES。
  • 2015年4月21日,GitHub上新开了一个Issue,要求完善AFNetworking的文档和与安全相关的功能特性。我们正就此积极努力地对参考材料做全面彻底的修改。
  • 还是在2015年4月20日,来自SourceDNA的Nate Lawson发表了一篇博文,宣称某个工具可以识别苹果商店中使用了AFNetworking2.5.1的应用程序。包括来自Ars Technica的Dan Goodin在内的许多记者,在其公布的文章中都引用了该博文并提及了博文的作者。这些公开发布的内容都没有就AFNetworking维护人员的解决方案进行整理而置评。
  • 2015年4月24日,SourceDNA在其后续发布的博文中声称,存在更多带有安全漏洞的应用程序,来自Ars Technica的Dan Goodin随后也发表了一篇带有相同效果的文章。需要强调的是,没有任何一篇公开发表的文章对AFNetworking维护人员的解决方案进行整理而置评。

AFNetworking用户力所能及的事情

下面是AFNetworking用户需要了解的力所能及的事情:

如果应用程序通过HTTPS通信,却没有启用SSL pinning技术的话,应用程序就可能容易受到所报道的MitM攻击。

AFSecurityPolicy的官方文档中的内容:

将固定的SSL证书( pinned SSL)添加到应用程序中,可以帮助应用避免中间人攻击以及存在的其他漏洞。大力鼓励应用程序在处理用户数据或财务信息的时候,所有通信途径都通过HTTPS协议,配置并启用SSL pinning技术。

无论在什么时候,遵循这些建议的应用程序都不应该存在上述安全漏洞。

如果应用使用HTTPS进行通信,并且启用了SSL pinning技术,就不容易遭到所说的MitM攻击

很大一部分应用程序使用AFNetworking是通过推荐的步骤启用了SSL证书或public key pinning机制的,这些应用程序不太不容遭到上面说的MitM攻击。

如果使用的是先前的版本AFNetworking,我们强烈推荐您升级到版本2.5.3

AFNetworking 2.5.1和2.5.2包含的默认配置不适合产品级应用程序——特别是如果不进行额外的配置,就不会提供必要的TLS评估。

AFNetworking 2.5.3默认配置更加安全,即使不使用SSL pinning也会进行域名验证。

如果使用NSURLConnection或NSURLSession代替AFNetworking的话,你仍然需要检查验证挑战的实现方式

苹果内置的NSURLConnection和NSURLSession,还有Security框架所提供的API,都具有对凭证验证的安全实现方式。但是,像任何API一样,某个应用程序的安全性取决于这些API的使用方法。

是否使用AFNetworking本身并不能保证你的应用程序能够灵活应对MitM那样的攻击。是否能够灵活应对攻击完全取决于应用程序使用可用API的方法。在产品环境下,测试应用程序的健壮性和网络安全性最终是开发人员的职责。

如果你要对某个安全漏洞进行吐槽,请发送电子邮件到security@alamofire.org吧!

我们会尽快回应并提出解决方案。

如果你想为AFNetworking更出色而做出贡献,那就在GitHub上提交一个Issue和Pull Request吧!

AFNetworking是开源项目,这意味着每个人都有机会为其更出色而贡献力量,欢迎提交IssuePull Request

对负责任的安全研究和新闻工作的看法

对于终端用户来说,安全研究人员在软件安全方面起着核心作用。研究人员与软件开发人员共同努力,通过遵循既定的负责任的漏洞披露(responsible disclosure),可以快速修复漏洞。同时,将当前用户的风险降到最低。

然而,我们对一些研究人员的做法和一些对AFNetworking的披露感到失望。作为人尽皆知的话题,信息安全从未如此重要。安全研究人员和记者拥有独特的机会来让读者了解这些事实。但不幸的是,这样的披露方式常常通过制造恐惧来增加点击量,而不是客观详实的报道。

尚未有确切的方法可以表明多少应用程序受此问题的影响;这些对安全问题严重程度的揣测摧毁了对问题准确和适度的回应。同样地,根据揣测提出的权利主张对企业和其客户也帮助甚少。

事实上,编写安全的软件一直以来都是一项巨大的挑战。这需要多学科的工程师们一起合作完成。这是一个极其重要的任务,最好由理性且富有责任心的人参与。

作为软件维护人员,我们有很多事情可以做得更好,并积极采取措施来完善自身的组织和流程。从今天起,我们期待与信息安全社区的成员紧密合作,负责任地寻找并解决任何安全漏洞。

对负责任的开源项目维护工作的看法

我们真诚地向使用AFNetworking的开发者和iOS整个开发者社区表示歉意。

作为著名开源项目的维护者,我们有责任提供与高标准相契合的软件,该软件将作为应用程序不可或缺的一部分。我们却没有对应该尽快更新的版本做出回应。我们未能向您有效传达至关重要的安全信息。这所有的一切,我们表示真诚的歉意并负全责。

在未来的几周内,我们将推出重组后的AFNetworking及其相关项目,以确保稳定的通信顺利进行。从用户的角度看,这意味着更加频繁地发布版本,更高的透明度,处理问题与合并请求过程中更多的反馈。我们为此而感到兴奋。

http://www.csdn.net/article/2015-05-12/2824671-AFNetworking

11 5月

应用下载的价值究竟在什么地方?

文 / Scott Stanchak:《纽约时报》移动业务营销总监。此前,他负责Avis Budget Group公司移动营销及产品策略。他还是Winery Passport这款热门应用的创建者,在其站点BakBurner.com撰写了移动营销的相关文章。

原文链接:http://venturebeat.com/2015/02/14/the-value-of-a-download/

下载的价值有多大?

对于我来说,下载没有太大的价值,但是给下载贴上价签以后,它的价值就产生了。

自从我在大公司负责移动营销工作开始,我遇到过许多人,他们把应用的下载量作为应用是否成功的唯一指标。这大概是因为“下载”这个词语与移动应用程序关联最紧密。但除非你的应用程序是付费下载,否则很大的安装量并不代表这些用户完成了你所期望的行动。

我非常清楚下载量和下载速度都会对应用商店排名结果产生影响。这是图表意义上的成功。然而,如果不在市场营销和拓展宣传中投入资金以保持地位,这样的排名常常是难以为继的。如果把钱花在这些地方的话,你要相信,这些钱会生出更多钱的愿景定会实现。

即使是为那些大量廉价的下载花钱做铺天盖地的广告,用来驱动其在应用商店的排名,这样的做法都比单纯的下载更能实现我们所期望的行动。提升排名背后的目的则是使下载量实现连续的增长,这会有更多的益处,非常有可能引出有意图的行动(intended action)。

行动(Action

你的“行动”是什么?这是我在做演示或与他人谈论移动营销问题时常问的一个问题。

在《纽约时报》任职期间,我主要的行动是订阅。在Avis Budget Group公司,我的行动是预定出租车。在Winery Passport公司工作的时候,我的行动是让用户通过一个应用来付费验证他们的护照。多年来,我在营销活动中花费的每一块钱都是在寻找用户的意图,这其中包括订阅者的意图,租客和买方的意图。

无论你的行动是什么,它必须带有一个指定的值。原因很简单:确定投资回报率(Return On Investment, ROI)。我提到的所有这些行动显然都是货币上的价值。但还存在很多并未对我们的产品或服务付费的用户。

工作中观察到所期望的简单行动可能是不同的——登记,电子邮件注册,还有额外的文章阅读等等。总体上了解用户在每个场景中的价值所在,不仅有助于解释递增的营销利润,而且如果这些简单行动没有达到目的,那么这种认识对理解ROI也比较关键。

测量你的行动

几年前,你对应用安装活动唯一可以测量的数据就是点击率。除此之外,你还会从Apple,Google或者其他第三方分析工具得到支持数据,但这些数据并没有与用户间交流的活动。从而无法将特定的点击下载与应用中的事件相联系。

谢天谢地,那些日子已经一去不复返了。第三方公司提出了解决方案,可以测量所有营销活动,不仅是点击,还有用户行动和其间的许多其他事件(event)。

它是这样工作的:当用户点击一个活动后,他们的设备ID将被这些第三方公司的某一家存下来。当同用户再次启动应用的时候,SDK(Software Development Kit)就会读取设备ID,并验证这个设备ID是否与某个活动关联过。如果关联过,就即刻建立起了一对一的关系。你可以追踪该用户对应用的使用状况,包括这些用户是否进行过购买的行动。

未使用这些第三方公司的工具在应用商店对应用进行追踪,应用是不会有市场的。无论是否花钱动用了媒体,或者本身就是媒体。这就像网站上被推荐的资源,有其价值所在,而在这些资源如果在手机上,则会更有价值。

通过移动应用程序成功测得用户有意图的行动,你将能够做出更明智的决定,进而会调整活动的开支,还会对网络的部署进行调整。这会使你获得更高的投资回报率,意味着花同样的钱会获得更大的下载量和更多有行动的用户。

就像漏斗,顶部较小,但底部的回报会更大。这就是作为底部的下载最为有价值的地方。