开场白

在iOS8中,有一个重量级的特性,那就是应用扩展。不论是第三方的应用还是Apple自家的应用,开发者们都可以根据这些应用自身的特点通过应用扩展提升应用与系统之间的交互性、应用的用户体验等等。目前在iOS中提供6种应用场景的扩展:

  • 通知界面widget
  • 分享扩展
  • Action
  • 照片编辑扩展
  • 资源存储扩展
  • 自定义键盘

我们在今后的文章中都会介绍以上这些场景的应用扩展,但今天我们的主角是分享扩展。

关于分享其实我们都不陌生,比如我们打开一个应用,在某个内容页会有系统提供的分享按钮,点击该按钮后会出现一个应用图标列表(比如Twitter,Flickr,新浪微博等),意在让我们选择一个希望分享内容的应用。分享扩展就是能让我们可以增加这个列表,让我们自己的应用图标能出现在分享列表里。

这里需要注意的是,这篇文章的内容有一定的难度,Apple提供的应用扩展特性还是比较复杂的。本文会列举最常用的使用场景作为示例来讲解分享扩展,当然你也可以在我们提供的示例基础上设计属于你自己的应用界面风格。Apple在应用扩展方面提供了非常详尽的文档,如果你在学习这篇文章的过程中有不懂的地方,你可以参阅Apple官方文档。

本文的示例应用名为“ShareAlike”,我们会通过该示例应用向大家展示如何实现图片、文本信息的分享。该示例的代码可以在Github中下载。

创建一个分享扩展

首先我们要清楚,应用扩展不能单独存在,它必须要依附一个应用程序,我们称之为应用扩展的载体应用。应用扩展以二进制文件形式存在于载体应用中。Xcode为每一种应用扩展都提供了一个模板,但是我们必须通过给一个应用程序添加Target的方式创建应用扩展的模板,创建好模板后,会在工程中增加一个应用扩展模块以及必要的属性文件。

我们执行一个分享操作主要是通过一系列界面来完成,所以分享扩展的主要部分就是UI展现。因此我们可以看到分享扩展模板为我们提供了一个继承了SLComposeServiceViewController的类以及一个storyboard文件。SLComposeServiceViewController为我们提供了一些常规的行为(包括字数、图片展示、文本输入、发送和取消按钮),并且这些行为对应的界面都符合iOS UI的标准。在本文的示例中我们基本都会用到这些默认的行为。

除了UIViewController的标准方法外,SLComposeServiceViewController还提供了一些与分享功能生命周期相关的属性和方法:

  • presentationAnimationDidFinish()方法可以让我们执行大数据量的分享任务,我们在分享图片时会用到它。
  • contentText是一个String类型的属性,它存储分享者编辑的一些文字描述。
  • didSelectPost()函数在点击Post按钮时调用。它是上传分享数据的入口。
  • didSelectCancel()函数在点击Cancel按钮时调用。
  • isContentValid()函数在分享者编辑的文字有变化时调用。
  • charactersRemaining是一个Int类型的属性,当描述内容有字数限制时,它表示当前还能编写的字数,如果超出字数,该属性展示的字数会变红,并用负数表示已经超出了多少字数。

只要你的应用工程里添加了分享扩展,那么用户就能很轻松的将选中的内容分享到你的应用中。我们会一步一步展示如何实现这些有用的功能,但首先我们要学习如何编译、运行和调试。

编译,运行和调试

在添加一个扩展Target时,Xcode会让你激活扩展的Scheme。在运行工程之前,如果我们选择app的Scheme,那么会直接运行app,如果选择扩展的Scheme,那么会出现让你选择在哪个应用程序中运行该扩展,也就是选择一个扩展的Host应用,然后就可以针对扩展进行调试了。通常情况下运行分享扩展最好的Host应用是照片应用(Photos),所以我们一般都选择照片应用,然后调试分享扩展。

下面有几个步骤能帮助我们熟悉这个过程:

  • 不管是选择一个扩展的Host应用还是使用扩展的载体应用充当Host应用,它们都应该有比较容易能分享的内容。在示例项目中,ShareAlike扩展使用它的载体应用充当Host应用,载体应用包含一张图片和一个分享按钮,点击分享按钮时会弹出标准的分享内容界面:

@IBAction func handleShareSampleTapped(sender: AnyObject) {
shareContent(sharingText: "Highland Cow",
sharingImage: sharingContentImageView.image)
}

// Utility methods
func shareContent(#sharingText: String?, sharingImage: UIImage?) {
var itemsToShare = [AnyObject]()


if let text = sharingText {
itemsToShare.append(text)
}
if let image = sharingImage {
itemsToShare.append(image)
}


let activityViewController = UIActivityViewController(activityItems: itemsToShare, applicationActivities: nil)
presentViewController(activityViewController, animated: true, completion: nil)
}

图片

  • 在扩展中编写相关代码。
  • 调试或测试时,选择载体应用的Scheme编译并运行,当点击Share Sample时,调试器会自动关联到扩展的进程上,简言之也就是可以捕获到你在分享扩展代码中设置的断点。
  • 你也可以选择模拟器中的其他应用作为Host应用来使用分享扩展。但是调试器不会捕获到你设置的断点,也就是无法进行调试。你可以使用这种方式来测试扩展的运行情况,通常分享扩展最佳的Host应用是Photos。

分享扩展的分享内容属性设置

与在Info.plist文件中设置应用一样,在扩展中也有这个文件。在该文件中有一个属性代表在分享应用列表中,我们扩展的显示名称(分享应用列表中图标下的文字)。

这个属性是Bundle display nameCFBundleDisplayName):

图片

你还可以在Info.plist文件中定义你的分享扩展可以处理什么样的分享任务,比如说是否可以处理分享视频?

在该文件中有一个名为NSExtension字典类型属性,展开该属性后,可以看到NSExtensionAttributes属性,同样是字典类型,再展开该属性我们可以看到NSExtenionActivationRule属性,依然是字典类型的属性。可以看到NSExtension中有Boolean类型的、String类型的、Number类型的或者字典类型的各种属性。在NSExtenionActivationRule属性中就有设置分享图片数量,分享视频、文件、URL数量的属性:

图片

每一个属性表示的意思从字面上就可以理解,比如NSExtensionActivationSupportsImageWithMaxCount,类型为Number,值为1。意思就是我们的分享扩展支持的最大分享图片数为1,我们可以测试一下看看:

图片
图片

从上图中可以看到,当我们选择了一张照片时,在分享应用列表中有我们的ShareAlike,如果选择了两张照片,分享应用列表中就没有我们的扩展了。

验证用户输入的内容

到目前为止,你大概已经知道如何创建一个扩展并设置它的相关属性了,现在让我们看看如何实现一些自定义的扩展行为。首先,我们要知道如何验证用户的输入内容。比如我们通常情况下都需要的一个验证,就是限制用户输入的文字数量,我们可以通过SLComposeServiceViewController来实现它。

在之前我们提到过SLComposeServiceViewController类的一些属性方法,其中有一个方法叫isContentValid() -> Bool。该方法的作用时随时监听着分享界面文本域的变化,也就是说一旦用户输入或删除文字,都会触发该方法。该方法如果返回true,那说明用户输入的内容合法,并可以使用Post按钮完成分享,如果返回false,那说明输入内容不合法,并且不能使用Post按钮。下面的代码片段展示了如何实现文字数量限制功能:


let sc_maxCharactersAllowed = 25

override func isContentValid() -> Bool {
if let currentMessage = contentText {
let currentMessageLength = countElements(currentMessage)
charactersRemaining = sc_maxCharactersAllowed - currentMessageLength

if Int(charactersRemaining) < 0 {
return false
}
}
return true
}

上面代码中的contentTextSLComposeServiceViewController中的属性,类型为String!,它存储着分享界面中TextView中的文本信息。charactersRemaining也是SLComposeServiceViewController中的属性,类型为NSNumber,他表示还有多少合法的字数以及超出的字数,并显示在分享界面的左下角。你可以用countElements()方法获取输入文本的字数,用于计算charactersRemaining的值并判断是否超出了限定的最大字数。至于Post按钮的可用和禁用由charactersRemaining决定,不需要我们额外设置。

图片
图片

上传分享内容

到目前为止,我们已经知道了如何创建一个应用扩展、如何对应用扩展进行配置、如何控制用户的操作行为等。但是作为分享扩展,它的遵旨应该是将文本、图片、视频等资源上传至某个网络客户端(比如FaceBook、Twitter、新浪微博等)。下面就让我们看看应该如何做吧。

应用扩展相比载体应用或者Host应用来说,它是一个轻量级的、处理单一功能任务的组件,所以用户一般不会因为使用扩展而停止当前使用的应用或关闭正在看的内容。你可以试想一下,如果一个Host应用使用某个应用扩展让它处理一个简单的任务时,该扩展因为同时占用了Host应用的运行内存从而使Host应用不可用、退出以至于崩溃,这是多么令人发指的事情。因此,所有的上传操作都应该在后台执行(iOS7中NSURLSession类对实现该功能很有帮助)。你可能会认为跟着去年iOS7 Day-by-Day系列文章就能很容易的实现该功能,但事实并非如此。

事情往往不是你想象的那么简单。首先提取分享的内容就不是一件容易的事(比如图片),而且你还要将它们分享出去,其次应用扩展没有任何写硬盘的权限。关于第二点,有人可能会有疑问:为什么应用扩展需要写硬盘的权限呢?因为所有后台上传的网络程序,在上传时都会先将上传内容缓存在硬盘中,然后从硬盘获取缓存的内容开始上传。为了能模拟硬盘缓存,我们需要在Host应用中创建一个存放分享内容的容器,并且要允许应用扩展使用该容器缓存分享内容。我们会在下面的内容中讲解如何实现该功能,但首先,我们先来看看如何提出分享内容中的图片。

提取要分享的图片

SLComposeServiceViewController类中有一个属性叫extensionContext,它存储着与当前应用扩展有关联的所有数据,其中包含一个NSInputItem类型的数组,名叫inputItems。每个NSInputItem都含有一个attachments集合,它们的类型都为NSItemProvider。这些attachments存储的就是分享内容中的媒体资源,比如图片、视频、文件或链接。


func imageFromExtensionItem(extensionItem: NSExtensionItem, callback: (image: UIImage?)->Void) {
for attachment in extensionItem.attachments as [NSItemProvider] {
...
}
}

上面这个方法的作用是从分享内容中提取图片(UIImage),注意这个方法没有返回值,而是用一个闭包回调函数来替代。

为了确定attachments中是否包含了图片类型的资源,我们需要用到hasItemConformingToTypeIdentifier()方法。


if(attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as NSString)) {
...
}

该方法的参数类型是NSString,并且类型标示符kUTTypeImage属于MobileCoreServices框架中的属性,所以我们要引入MobileCoreServices框架:


import MobileCoreServices

该框架中还包含几种类型标示符:

  • kUTTypeImage
  • kUTTypeMovie
  • kUTTypeAudio
  • kUTTypeSpreadsheet

现在你可以确定attachments中至少包含了一个图片资源,并且需要将该图片资源提取出来。因为执行该任务会消耗很高的内存资源,所以为了确保在执行时UI界面不会出现无响应的情况,它应该在后台队列中执行。我们可以使用loadItemForTypeIdentifier()方法,与刚才的方法类似,该方法也有类型标示符参数,并且用闭包实现对图片的操作:


// Marshal on to a background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, UInt(0))) {
attachment.loadItemForTypeIdentifier(kUTTypeImage as NSString, options: nil) {
(imageProvider, error) -> Void in
...
}
}

如果你使用Objective-C,那么你可以用block将返回结果强制转换为你期望的类型(比如UIImage),然后实现回调。但是在Swift中就行不通了,所以上面代码中的imageProvider变量的类型应该是NSSecureCoding,然后你可以将它强转为UIImage类型:


var image: UIImage? = nil
if let e = error {
println("Item loading error: \(e.localizedDescription)")
}
image = imageProvider as? UIImage
dispatch_async(dispatch_get_main_queue()) {
callback(image: image)
}

大家注意,在dispatch_async中我使用了callback()函数,也就是imageFromExtensionItem方法的第二个参数,并将刚才强转为UIImage类型的image对象作为其参数。

这段代码可以让用户在输入分享文本信息的同时将attachments中的图片提取出来。看起来非常完美:


var attachedImage: UIImage?

override func presentationAnimationDidFinish() {
// Only interested in the first item
let extensionItem = extensionContext.inputItems[0] as NSExtensionItem
// Extract an image (if one exists)
imageFromExtensionItem(extensionItem) {
image in
if image {
dispatch_async(dispatch_get_main_queue()) {
self.attachedImage = image
}
}
}
}

该方法的作用就是提取出图片,并将提取出的图片赋值给我们定义的attachedImage变量。

执行后台上传的任务

一旦用户输入完要分享的文本信息,按下Post按钮后,分享扩展就应该将所有内容通过Web服务上传到某个地方。本文的示例为了达到这个目的,我们在视图控制器中定义了一个常量属性sc_uploadURL,值为一个URL,也就是一个服务地址:


let sc_uploadURL = "http://requestb.in/oha28noh"

这个URL是Request Bin的服务,Request Bin可以给你提供一个临时的URL,用于测试一些网络操作。上面代码中的这个URL对你们来说没有什么用,因为这是我申请的,你们可以去requestb.in申请一个自己的URL用于测试。

在前面我们提到过,应用扩展不应该和Host应用抢占内存资源,它应该在后台执行相关任务。因此,当Post按钮被按下时,在当前的Host应用中我们不会看到任何有关应用扩展的执行痕迹,像同步、网络操作等。此时,我们就需要使用NSURLSession给我们提供的API来实现后台的网络操作。

当用户点击Post按钮后会调用didSelectPost()方法,它的代码片段应该是这样的:


override func didSelectPost() {
// Perform upload
...

// Inform the host that we're done, so it un-blocks its UI.
extensionContext.completeRequestReturningItems(nil, completionHandler: nil)
}

设置NSURLSession很简单,有规范的设置流程:


let configName = "com.shinobicontrols.ShareAlike.BackgroundSessionConfig"
let sessionConfig = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(configName)
// Extensions aren't allowed their own cache disk space. Need to share with application
sessionConfig.sharedContainerIdentifier = "group.ShareAlike"
let session = NSURLSession(configuration: sessionConfig)

我们要特别注意上面代码片段中sharedContainerIdentifier的设置,它给NSURLSession使用的共享容器(用于缓存分享内容)指定了一个名称,这个容器也是扩展载体应用的一部分(在这个例子中,载体应用就是ShareAlike),所以我们要通过Xcode对载体应用进行设置:

  1. 在工程设置面板中选择Capabilities页签,然后在左侧Targets栏中选中载体应用。
  2. 开启载体应用的App Groups
  3. 创建一个App Group,起一个合适的名称,但是必须要以group.开头。在我们的示例中,我们刚才设置的名称为group.ShareAlike
  4. Xcode会进行一系列检查,然后创建App Group。

图片

然后选中应用扩展,开启App Groups,然后选择刚才创建好的App Group。

图片

这些App Group都是登记在你的开发者账号下的,这样才能确保只有你的应用可以使用这些共享容器。

Xcode会为每个工程创建一个.entitlements授权文件,里面就包含有共享容器的访问名称。

现在NSURLSession就已经设置成功了,你还需要创建一个URL request对象来发送请求:


// Prepare the URL Request
let request = urlRequestWithImage(attachedImage, text: contentText)

urlRequestWithImage函数的作用是构造一个URL请求结构,通过HTTP Post方式发送一些JSON格式的数据。这其中就包含了文本内容和图片的元数据信息:


func urlRequestWithImage(image: UIImage?, text: String) -> NSURLRequest? {
let url = NSURL.URLWithString(sc_uploadURL)
let request = NSMutableURLRequest(URL: url)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.HTTPMethod = "POST"

var jsonObject = NSMutableDictionary()
jsonObject["text"] = text
if let image = image {
jsonObject["image_details"] = extractDetailsFromImage(image)
}

// Create the JSON payload
var jsonError: NSError?
let jsonData = NSJSONSerialization.dataWithJSONObject(jsonObject, options: nil, error: &jsonError)
if jsonData {
request.HTTPBody = jsonData
} else {
if let error = jsonError {
println("JSON Error: \(error.localizedDescription)")
}
}

return request
}

这个方法实际上并不是创建一个上传图片的请求,虽然也可以这样做,但这里我们将图片的详细信息(元数据)提取出来进行上传,下面代码是提取图片详细信息的方法:


func extractDetailsFromImage(image: UIImage) -> NSDictionary {
var resultDict = NSMutableDictionary()
resultDict["height"] = image.size.height
resultDict["width"] = image.size.width
resultDict["orientation"] = image.imageOrientation.toRaw()
resultDict["scale"] = image.scale
resultDict["description"] = image.description
return resultDict.copy() as NSDictionary
}

最后你就可以通过resume()在后台发起上传请求了:


// Create the task, and kick it off
let task = session.dataTaskWithRequest(request)
task.resume()

如果你按下Post按钮完成分享后,在requestb.in中,你应该可以看到如下的结果:

图片

原文地址:iOS8 Day-by-Day :: Day 2 :: Sharing Extension

大家应该还记得Swift官方博客中的《Patterns Playground》和《Balloons》这两篇文章,虽然博文很短,但是它们都包含了一个Playground文件,通过这个文件向我们阐述当前文章的主题。当我们打开这两个Playground文件时,发现里面别有洞天,里面有代码,有说明文档,有引用,而且格式规整。它们看起来非常像《The Swift Programming Language》、《Using Swift with Cocoa and Objective-C》这两本iBook,但是却比这两本书有更高的交互性。让我们来看看这两个Playground文件的片段:

片段
片段

大家可以看到,在Playground中还有小练习,加上它所见所得的特性,运用在教学中再好不过。可以想象,以后Objective-C,Swift的教学视频、材料都少不了Playground的身影。

说了这么多,大家一定很捉急想知道如何在Playground中添加说明文档吧。别着急,让我们一步一步进行:

  • 创建一个Playground文件,并保存。
  • 找到刚刚保存的Playground文件,在其上点击鼠标右键,选择显示包内容
  • 在该Playground文件包内新建一个文件夹,命名为“Documentation”。
  • 使用Mou或MacDown编辑你的说明文档:

图片

  • 这里以MacDown为例(其实Mou也一样),点击File -> Export -> HTML,将编辑好的.md说明文档导出HTML文件,然后将该HTML文件拷贝到刚才创建的“Documentation”文件夹中。

你也可以自己添加CSS文件,修饰你的HTML内容。

  • 打开Playground文件包内的contents.xcplayground文件,在<sections>标签中加入下面这行:

<documentation relative-path='playgroundDocumentation.html'></documentation>

也就是将我们刚才拷贝到“Documentation”文件夹中的HTML文件添加到Playground文件的xml文件中,使用<documentation>标签,将relative-path的值设置为HTML文件的名称:

图片

现在再打开刚才创建的Playground文件看看,是不是很酷呢!

图片

普通子类

通常情况下,一说到required修饰符,我们最先想到的应该就是普通类(class)的init()方法了。比如下面这个类:


class MyClass {
var str:String
init(str:String) {
self.str = str
}
}

当我们定义一个MyClass的子类(subclass)并实例化这个子类时,我们一般会如何做呢?没错,通常情况下都会是这样:


class MyClass {
var str:String
init(str:String) {
self.str = str
}
}

class MySubClass:MyClass {

}

var MySubClass(str:"Hello Swift")

大伙应该已经注意到了,在实例化MySubClass时,其实是继承了它父类MyClassinit()方法。那我们再来看看子类的初始化方法。

子类的初始化方法

如果我们在子类中添加一个init()方法,像这样:


class MyClass {
var str:String
init(str:String) {
self.str = str
}
}

class MySubClass:MyClass {
override init(str:String) {
super.init(str:str)
}
}

var MySubClass(str:"Hello Swift")

那么我们首先要在init()方法前加上override修饰符,表示MySubClass重写了其父类的init()方法,然后还要调用父类的init()方法,并将参数一并传给父类的方法。

在实际运用中,也有另外一种情况,当子类的初始化方法参数类型与父类的初始化方法参数类型不同时,我们就不必在子类的初始化方法前加override修饰符了,但是要把子类初始化方法的参数类型转换为符合父类初始化方法的参数类型,然后传给父类的初始化方法:


class MyClass {
var str:String
init(str:String) {
self.str = str
}
}

class MySubClass:MyClass
{

init(i:Int) {
super.init(str:String(i))
}
}

MySubClass(i: 10)

required修饰符

我们给父类的init()方法加上required修饰符后会发生什么呢,我们来看看:


class MyClass {
var str:String
required init(str:String) {
self.str = str
}
}

class MySubClass:MyClass
{

init(i:Int) {
super.init(str:String(i))
}
// 编译错误
}

MySubClass(i: 10)

我们可以看到上面的代码在编译会发生错误,因为我们没有实现父类中要去必须要实现的方法。我们应该这样写:


class MyClass {
var str:String
required init(str:String) {
self.str = str
}
}

class MySubClass:MyClass
{

required init(str:String) {
super.init(str: str)
}

init(i:Int) {
super.init(str:String(i))
}

}

MySubClass(i: 10)

从上面的代码示例中不难看出,如果子类需要添加异于父类的初始化方法时,必须先要实现父类中使用required修饰符修饰过的初始化方法,并且也要使用required修饰符而不是override

如果子类中不需要添加任何初始化方法,我们则可以忽略父类的required初始化方法:


class MyClass {
var str:String
required init(str:String) {
self.str = str
}
}

class MySubClass:MyClass
{



}

MySubClass(str: "hello swift")

在这种情况下,编译器不会报错,因为如果子类没有任何初始化方法时,Swift会默认使用父类的初始化方法。在Apple的文档中也有相关描述:

You do not have to provide an explicit implementation of a required initializer if you can satisfy the requirement with an inherited initialiser.

required修饰符的使用规则

  1. required修饰符只能用于修饰类初始化方法。
  2. 当子类含有异于父类的初始化方法时(初始化方法参数类型和数量异于父类),子类必须要实现父类的required初始化方法,并且也要使用required修饰符而不是override
  3. 当子类没有初始化方法时,可以不用实现父类的required初始化方法。

Swift中的final修饰符可以防止类(class)被继承,还可以防止子类重写父类的属性、方法以及下标。需要注意的是,final修饰符只能用于类,不能修饰结构体(struct)和枚举(enum),因为结构体和枚举只能遵循协议(protocol)。虽然协议也可以遵循其他协议,但是它并不能重写遵循的协议的任何成员,这就是结构体和枚举不需要final修饰的原因。

final修饰符的几点使用原则

  • final修饰符只能修饰类,表明该类不能被其他类继承,也就是它没资格当父类。
  • final修饰符也可以修饰类中的属性、方法和下标,但前提是该类并没有被final修饰过。
  • final不能修饰结构体和枚举。

代码示例


final class Train {
//Todo...
}

class MaglevTrain: Train { // 编译失败
//Todo...
}

上面代码中,因为Train类被final修饰过,所以当MaglevTrain类继承了Train后,编译器会提示错误。


class Train {
final func method() {
//Todo...
}
}

class MaglevTrain: Train {
override func method() { // 编译失败
//Todo...
}
}

上面的代码中,因为Train类中的method方法被final修饰过,所以当子类MaglevTrain重写父类的method方法时,编译器会提示错误。

在Swift中,根据已有的、明确的规则或约定,描述和匹配一组值的方式,我们可将其称之为一种编码模式,比如:

  • 所有的元组在取数据时是从0开始的。
  • 表示数字的范围我们可以使用1...5这种形式。
  • 匹配或判断某些类实例的类型。

该Playground文件需要使用OS X Mavericks或OS X Yosemite beta系统中的Xcode6打开。

Patterns.playground

这个playground文件介绍了一些匹配模式的概念。在Swift中,你可以使用条件语句(比如switch语句)通过简明、易读的方式匹配多个值,这种方式就是一种模式。

注意:
如果你看不到控制台输出界面,你可以通过View > Assistant Editor > Show Assistant Editor选项或使用Option-Command-Return快捷键打开Timeline区域。

匹配元组中的值

下面这个例子向你展示了如何使用匹配模式写出简明、优雅的switch语句。在这个例子中使用了FizzBuzz游戏作为场景进行说明。我们先来简单介绍一下这个游戏,在FizzBuzz游戏中,你从1开始数数,如果你数到的数字能被3整除,那么你就不能说出该数字而要说“Fizz”。如果你数到的数字能被5整除,那么你就要说“Buzz”。如果你数到的数字既能被3整除又能被5整除,那么你就要说“FizzBuzz”。所以一般情况下数数的情形像这样:“1,2,Fizz,4,Buzz…”。那么在这个例子中我们用一个名为fizzBuzz的函数代表该游戏,这个函数有一个参数,代表我们要数的数字,因为我们需要说出“Fizz”、“Buzz”以及“FizzBuzz”,所以返回值为String类型。


func fizzBuzz(number: Int) -> String {
switch (number % 3, number % 5) {
case (0, 0):
// number既能被3整除又能被5整除
return "FizzBuzz!"
case (0, _):
// number能被3整除
return "Fizz!"
case (_, 0):
// number能被5整除
return "Buzz!"
case (_, _):
// number既不能被3整除也不能被5整除
return "\(number)"
}
}

我们通过for循环语句,让fizzBuzz函数参数从1到100执行100次,模拟我们在游戏中从1数到100。然后看看控制台输出的结果。


for i in 1...100 {
println(fizzBuzz(i))
}

fizzBuzz函数中的switch语句中,判断表达式是一个元组,它包含两个成员,这两个成员也是表达式。第一个表达式number % 3,意思是number取3的余数,第二个表达式number % 5,意思是number取5的余数。每一个case语句都对该元组中这两个表达式计算出的值进行匹配判断。

比如,如果number等于15,那么(number % 3, number % 5)的结果就是(0, 0),这代表15既能被3整除又能5整除。这符合switch语句中的第一个case判断,所以返回“FizzBuzz!”。


fizzBuzz(15)

如果number等于6,那么元组的结果为(0, 1),这将符合switch语句中的第二个case判断(0, _),因为下划线在Swift中约定是通配符,它代表任何值。所以将返回“Fizz!”。


fizzBuzz(6)

如果number等于11,那么元组的结果为(2, 1),这将符合switch语句中的第四个case判断(_, _),因为第四个case判断的是既不能被3整除又不能被5整除的情况,所以用两个下划线表示元组中的两个成员。返回结果为“11”。

练习:
fizzBuzz函数根据其他数字返回不同的消息。
再加一种数字的特殊情况,让该函数返回“Bang!”。使用返回“Fizz!”和“Buzz!”相同的模式,比如如果number能被7整除,就返回“Bang!”。别忘了还有“FizzBuzzBang!”这种情况,尽可能将case情况列举全。
如果最后一个case你用default代替case (_ ,_)会发生什么呢?这两种方式都能正确的返回不满足其他case的值么?

枚举和关联值

使用枚举和它的关联值匹配枚举中特定的case场景也是一种匹配模式。下面的例子使用枚举展示了火车的到站时间状态。


enum Status {
case OnTime
case Delayed(minutes: Int)
}

如果火车正点到站,那么它的状态为Status.OnTime,并且没有关联值。当火车晚点,那么它的状态为Status.Delayed(Int),并需要传入一个关联值用于表示火车到底晚了多久。


let goodNews = Status.OnTime
let badNews = Status.Delayed(minutes: 90)

这里有一个名为Train的类,包含一个status属性,默认值为Status.OnTime


class Train {
var status = Status.OnTime
}

你可以使用匹配模式,将Status.Delayed(Int)这种情况的关联值提出来进行判断。下面的代码将Train类进行了扩展,使之遵循Printable协议,添加了一个只读属性description。这个扩展可以很容易的检索出包含火车晚点分钟数的String字符串,并返回。


extension Train: Printable {

var description: String {

switch status {

case .OnTime:
// 满足正点到达的情况,返回“On time”
return "On time"

case .Delayed(let minutes) where 0...5 ~= minutes:
// 将传入的关联值通过“~=”操作符在一定范围内匹配
return "Slight delay of \(minutes) min"

case .Delayed(_):
// 用下划线通配符匹配不在晚点分钟数范围内的情况
return "Delayed"

}

}

}

switch语句中的第一个case用于匹配当火车状态为OnTime时的情况,并返回简单的字符串。

第二个case要稍复杂一些,它创建了一个临时常量minutes来表示传入的关联值,并用where关键字申明一个0到5的范围,判断minutes是否在该范围内,如果在该范围内,那么将这个关联值嵌入字符串返回。

第三个case用于匹配不在晚点范围内的情况。

你现在可以创建一些Train类的实例验证一下。


let trainOne = Train()
let trainTwo = Train()
let trainThree = Train()

trainTwo.status = .Delayed(minutes: 2)
trainThree.status = .Delayed(minutes: 8)

然后使用description属性查看每个Train实例的状态值。


trainOne.description
trainTwo.description
trainThree.description

练习:
改变trainTwotrainThreestatus属性,看看它们的description属性有何变化。
改变Train扩展中switch的最后一个case语句,让它返回包含关联值的字符串,比如“Delayed by 17 min”。
加分项,再增加一种case情况,当关联值大于60时,返回列车晚点几小时几分的字符串。
提示:可以使用>=操作符。

检查和转换子类型

还有一种模式可以让你动态的匹配类的实例。考虑一下下面代码中类的所属关系:


extension Train {
func cleanPassengerCars() -> String {
return "Clean the passenger cars"
}
}

class MaglevTrain: Train {
func referToSpecialist() -> String {
return "Refer the maglev to a specialist"
}
}

let maglev = MaglevTrain()
let train = Train()

有一种简单的类型匹配模式,使用is关键字就可以进行父类与子类之间的匹配和判断。


func trainDescription(train: Train) -> String {
switch train {
case is MaglevTrain:
return "The fastest train on earth."
default:
return "Some other kind of train."
}
}

你可以将刚才创建的Train类和MaglevTrain类实例传入trainDescription函数,看看会有什么结果。


trainDescription(maglev)
trainDescription(train)

练习:
trainDescription函数中的switch语句中再添加一个case,用于匹配判断train是不是Train类型的,然后看看会提示什么错误?为什么呢?
再定义一个Train类的子类SteamTrain,然后在trainDescription函数的switch语句中添加一个case,用于匹配判断train的类型是不是SteamTrain,然后返回适当的字符串描述。将SteamTrain实例传入trainDescription函数,看看是否返回正确的描述。

不过使用is关键字匹配类型只适用于检查子类。如果你想检查对象类型是不是某个类的子类,并且想使用父类的属性或方法时,可以使用as关键字(作用类似类型强制转换中的as)将判断的对象类型转换为父类型,这样在switch语句中就可以同时进行类型检查和类型转换了。使用as关键字时,需要先创建一个临时常量,用于表示需要判断或转换的对象。

下面的代码中有一个名为determineMaintenanceRequirements的函数,在switch语句中判断对象的类型是不是MaglevTrain的子类,如果是MaglevTrain的子类,那么将该对象的类型转换为MaglevTrain。如果转换成功,就可以使用转换后类型的方法。如果失败则返回default的返回值。


func determineMaintenanceRequirements(train: Train) -> String {
switch train {
case let maglev as MaglevTrain:
return maglev.referToSpecialist()
default:
return train.cleanPassengerCars()
}
}

determineMaintenanceRequirements(train)
determineMaintenanceRequirements(maglev)

练习:
SteamTrain类中添加一个名为cleanFirebox的函数,在determineMaintenanceRequirements函数中的switch语句里添加一个case语句,用于判断对象的类型是不是SteamTrain的子类,如果是,将对象的类型转换为SteamTrain类型,并调用SteamTrain类的cleanFirebox函数。然后将SteamTrain类的实例传入determineMaintenanceRequirements函数看看是否能返回正确的描述信息。

原文地址:Patterns Playground