RAC文档翻译-框架概览

框架概览

本文档从高层次描述ReactiveCocoa框架中不同的组成部分,并尝试解释他们是如何配合工作和分工的。本篇文档作为学习新模块和更具体文档的起点。

如果是为了寻找例子或者理解RAC的用法的话,请阅读README或者Design Guidelines.

流(Streams)

RACStream抽象类代表了一个,是由一些对象值所组成的任意序列。

值可能马上或者在将来某个时刻变得可用,但是每个值必须按顺序检索。在检索流中的第二个值之前,你必须对流中的第一个值进行处理。

流是单子(monads)。除了其他方面,这将允许将许多复杂的操作建立在一些原始的操作上(特别是-bind:操作)。RACStream的实现相当于Haskell中的MonoidMonadZip的typeclasses.

RACStream本身作用并不大。大部分流其实都被当做了信号或者序列

信号(Signals)

一个RACSignal类对象代表了一个信号,是一个推动式( push-driven )的

信号通常都代表了将来会被送达的数据。比如一个方法被调用或者收到了数据,值在信号中被 发送( sent ),信号将会把他们‘推给’任何订阅者。用户必须订阅一个信号来获取它里面的值。

信号会发送三种不同的事件给它的订阅者:

  • next将会从流中获得一个新的值。RACStream只会操作此种类型的事件。和Cocoa中的集合类 型不同,信号中包含一个nil也是合法的。

  • error事件表明一个错误在信号发出之前产生。这个时间可能会包含一个NSError对象,表明发生 了什么错误。错误必须被特殊处理-他们不被包含在流的值当中。

  • completed事件表明这个信号成功完成了发送,这个流将不会再有更多地值。完成事件必须被特殊处 理-它不被包含在流的值当中。
    一个信号的完整生命周期中可能包含多个next事件,接着可能是一个error或者completed事件。

订阅(Subscription)

一个订阅者可以是任何正在等待或者能够获取[信号][#信号]中的事件的对象。在RAC当中,订阅者是任何实现了RACSubscriber协议的对象。

通过-subscribeNext:error:completed:或者对应的方便方法可以产生一个订阅。从技术上来说,大部分RACStreamRACSignal的操作符也会产生订阅,但是它们都是中间层的订阅,属于框架实现的细节。

订阅会将信号retain,并会在信号完成发送或者产生错误的时候被析构(disposed)。订阅也可被手动析构

Subjects

一个subject,代表一个RACSubject类对象,是一种可以手动控制的信号

Subjects可以被认为是一种”可变(mutable)”的信号,就像NSMutableArrayNSArray。Subjects对于将非RAC的变得RAC非常有用。

比如,我们可以不用在block当中处理程序的回调,这些block可以将事件通过一个subject单例发送出去。这个subject可以以一个RACSignal的形式返回,并隐藏掉这些回调的实现细节。

一些subjects也提供了特殊的行为。特别是RACReplaySubject可以被用来将事件缓存给将来的订阅者,比如将一个网络请求的结果缓存并等待其他的对象准备好处理它。

Commands

一个command,代表一个RACCommand对象,创建并订阅一个信号以响应一些动作。动作使得一些会产生副作用的行为变得十分简单,比如用户操作App。

通常情况下一个command被UI事件触发,比如一个按钮被点击。Commands也可以通过一个信号来自动的被禁用或者可用,而且这个禁用状态也可以通过将和这个command相关的UI控件禁用来体现。

在OS X中,RAC通过NSButton来添加一个rac_command属性来自动支持这些行为。

连接(Connections)

一个连接,代表一个RACMulticastConnection对象,是一个可以被多个订阅者分享的[订阅][#订阅]。

信号在默认情况下都是 的,这表明它们 每次 在一个新的订阅产生时都会工作一次。但是这种特性常常都不是我们想要的,因为这意味着每当一个新的订阅产生时都会将信号中的值重新计算一次,如果这个信号有副作用或者工作量很大的话(比如,发送一个网络请求)就会产生较大的问题。

我们通过RACSignal中的 -publish 或者 -multicast: 方法来创建一个连接,并确保无论有多少个订阅者订阅了这个连接,都只有一个内部的订阅被创建。当连接建立了之后,这个连接的信号就是 的,并且内部的订阅会一直保持活跃直到 所有 对这个连接的订阅被[析构][#析构]。

序列 (Sequences)

一个序列代表一个RACSequence对象,是一个 拉动式

序列一种集合,类似于 NSArray。和数组不同的是,序列中的值默认是 演算的。(比如,当他们被需要时),这样就能在序列中只有一部分的值被使用的时候提升性能。和Cocoa的集合类型一样,序列中也不能包含 nil

序列和 Clojure’s sequences(特别是lazy-seq)或者Haskell中的 List很像。

RAC给Cocoa中的大部分集合类型都添加了 -rac_sequence 方法,来让他们像 RACSequences 一样被使用。

析构(Disposables)

RACDisposable类对象被用来取消任务和清除资源。

析构被用来取消对一个信号的订阅。当一个订阅被取消时,相应的订阅者将不会再收到 任何 来自这个信号的事件。并且,任何和这个订阅相关的工作(后台处理,网络请求,等等。)都会被取消,因为这些结果都已经不需要了。

查看更多关于取消订阅的内容,请阅读RACDesign Guidelines

Schedulers

一个scheduler代表一个RACScheduler类对象,是一个顺序执行信号的队列,来开展他们的工作或者传递它们的结果。

Schedulers和GCD类似,但是schedulers支持取消(通过析构),而且一定是顺序执行的。除了 +immediateScheduler之外,schedulers不提供任何同步执行的方法。这有助于避免死锁,我们鼓励使用signal operators而不是阻塞工作。

RACScheduler也和NSOperationQueue类似,但是schedulers并不支持任务的重新排序或者任务间的相互依赖。

值类型 (Value types)

RAC提供一些通用的类,以让值在中被传递:

  • RACTuple 是一个小巧的,constant-sized的集合类型,并可以包含nil(以RACTupleNil表示)。它通常被用来合并多个流中的值。
  • RACUnit是一个’空’值单例,当一个流中没有有意义的值存在时,并且流没有被关闭时,流使用这个值来表示无意义。
  • RACEvent信号事件转化为一个值。它主要被RACSignal中的-materialize 方法使用。

WebKit框架概览-Part2

WebKit框架概览-Part2

第一部分中,我们了解了WebKit框架的基础部分。在本篇文章中,我们会深入了解WebKit框架并学习如何在原生App中定制网页。我们也会学习如何从网页中获得数据,并在App中使用数据。

接下来我们将建立一个专门浏览appcoda.com的App。首先,请下载初始项目。初始项目就是一个名为Coda的简单浏览器,跟我们在第一个部分编写的App差不多。唯一的区别就是没有textfield控件给用户输入url,而且我也将前进、后退和刷新按钮更换成了图片。

处理外部链接

如果你运行这个App并点击了一个外部链接,webview会加载这个链接。但是这个App使用来浏览Appcoda的,所以我们需要防止加载外部链接。如果用户点击了外部链接,这个链接的内容就会在Safari中打开。

我们需要的是定制网页加载的方式。达到这个目标,我们需要干涉加载网页的正常过程。在完成这个目标之前,让我们先来了解一下网页加载的过程。

网页加载由一个动作(Action)触发。这可能是任何导致网页加载的动作,比如:触碰一个链接、点击后退、前进和刷新按钮,JavaScript设置了window.location属性,子窗口的加载或者对WKWebView的loadRequest()方法的调用。然后一个请求被发送到了服务器,我们会得到一个响应(可能是有意义的也可能是错误状态码,比如:404)。最后服务器会发送更多地数据,并结束加载过程。

WebKit允许你的App在动作(Action)和响应(Response)阶段之间注入代码,并决定是否继续加载,取消或是做你想做的事情。

在ViewController中加入如下方法。

func webView(webView: WKWebView!, decidePolicyForNavigationAction navigationAction: WKNavigationAction!, decisionHandler: ((WKNavigationActionPolicy) -> Void)!) {
if (navigationAction.navigationType == WKNavigationType.LinkActivated && !navigationAction.request.URL.host!.lowercaseString.hasPrefix("www.appcoda.com")) {
    UIApplication.sharedApplication().openURL(navigationAction.request.URL)
    decisionHandler(WKNavigationActionPolicy.Cancel)
} else {
    decisionHandler(WKNavigationActionPolicy.Allow)
}
}

上述是一个WKNavigationDelegate代理方法,在网页加载时会被多次调用。其中一个参数WKNavigationAction对象包含了帮助你决定是否让一个网页被加载的信息。在上面的代码中,我们使用其中两个属性,navigationType和request。我们只想中断被用户点击的外部链接的加载过程,所以我们检查了navigationType。然后我们检查了request的url来确认它是否是一个外部链接。如果两个条件都满足,这个url就会在用户的浏览器中打开(通常都是Safari)并且WKNavigationActionPolicy.Cancel终止了App加载网页的过程。否则这个网页就会被加载并显示。

运行这个程序,点击任何外部链接,这个链接都会在Safari中被加载。

设置网页标题

如果网页能有标题来提示用户在哪里的话,这将会非常有用。在前面的文章中,我们学习了一些WKWebView的KVO属性比如loading和estimatedProgress。title也是一个KVO属性,我们将用它来获得当前网页的标题。

在viewDidLoad()其他addObserver()方法下面加入如下代码:

webView.addObserver(self, forKeyPath: "title", options: .New, context: nil)

然后在observeValueForKeyPath(_:, ofObject:)方法其他if语句下方加入如下代码。

if (keyPath == "title") {
title = webView.title
}

运行程序,随便逛逛,你将发现navigationbar的title会被正确地更新。

修改网页内容

现在这个Coda App是一个Appcoda的专用浏览器,但是我们还可以做几样事情来提升一下用户体验。

因为设备的特性,移动App以简明的方式展示数据和信息。用户希望能看到他们想看的东西,而且不用做大量的滑动来获得信息。

目前为止,这个App展示了Appcoda网页的所有内容。我们想忽略某些和网页内容相关程度不大的东西。我们将会移除侧边栏和底部展示Appcoda Swift book的栏目。

为了达到这个目标,我们使用JavaScript向网页注入CSS规则以隐藏这些栏目。首先,我们需要检查网页然后决定规则。

为了检查网页,我们使用大多数浏览器都支持的开发者工具。你也可以自己以插件(plugins)或者add-ons的形式安装到你的浏览器,比如火狐的Firebug。我将使用Chrome的开发者工具,但你可以使用任何你喜欢的。其过程大致一样。

打开Chrome开发者工具,View->Developer->Developer Tools。

这将在浏览器底部打开一个开发者窗口。开发者窗口将和上半部分左边的网页源码和邮编的CSS样式查看分离开来。在底部,是JavaScript命令行,这里你可以输入你的代码,它将会在网页执行。

我们需要检查id属性然后标记处我们想要隐藏的栏目。

侧边栏会在所有的网页中显示,而底部的书籍栏目只会在文章页面显示。点击任意一篇文章,打开开发者工具。首先,右击侧边栏并选择检查元素。在开发者窗口中,会高亮显示对应的元素代码。如果你将你的鼠标移动到对应的代码,网页部分相对应的区域也会高亮显示。我们希望得到包含了整个侧边栏的根元素id(或者class)。

根据你选择审查元素时所处的位置,向上折叠标签直到只有侧边栏在页面中被高亮显示。上一个被折叠的标签就是我们要的根元素。在这里,他是一个div标签,id为’sidebar‘。

在将代码写入你的App之前,你最好在浏览器中测试一下它。因为如果发生了什么错误的话,在App中调试会非常困难。我们首先在浏览器中测试CSS和JavaScript。

点击我们在上面找到的div标签。在窗口的右边你将会看见它的CSS布局。点击+按钮添加一条CSS规则,如下所示。

div#sidebar{
}

在上面的代码中添加如下代码:

display:none;

在你添加完上述代码后,侧边栏应该会从页面消失。

现在删除这条布局规则以显示侧边栏。现在我们要使用JavaScript往DOM中添加代码。在html页面之下,是运行JavaScript的命令行。将如下代码粘贴到命令行。

var styleTag = document.createElement("style");

上述代码创建了一个元素并赋值给了一个变量。接下来我们如下代码,他将会给这个元素添加css规则。我也把底部书籍栏目也添加了进去。

styleTag.textContent = 'div#sidebar, .after-post.widget-area {display:none;}';

最后,使用下面的代码给DOM添加样式标签。这段代码会马上执行,侧边栏和底部的书籍栏目会消失。

document.documentElement.appendChild(styleTag);

上面的几个过程是隐藏页面元素的必须过程。

回到Xcode,创建一个新文件File->New->File->iOS->Other->Empty并命名为hideSection.js。添加如下代码。

var styleTag = document.createElement("style");
styleTag.textContent = 'div#sidebar, .after-post.widget-area {display:none;}';
document.documentElement.appendChild(styleTag);

在ViewController中,替换init()中方法为如下:

required init(coder aDecoder: NSCoder) {
let config = WKWebViewConfiguration()
let scriptURL = NSBundle.mainBundle().pathForResource("hideSections", ofType: "js")
let scriptContent = String(contentsOfFile:scriptURL!, encoding:NSUTF8StringEncoding, error: nil)
let script = WKUserScript(source: scriptContent!, injectionTime: .AtDocumentStart, forMainFrameOnly: true)
config.userContentController.addUserScript(script)
self.webView = WKWebView(frame: CGRectZero, configuration: config)
super.init(coder: aDecoder)
self.webView.navigationDelegate = self    
}

上述代码创建了一个WKWebViewConfiguration对象,它拥有一些属性来作为原生代码和网页之间沟通的桥梁。JavaScript代码被一个WKUserScript对象加载和包装。然后这个脚本被赋值给WKWebViewConfiguration对象的userContentController属性,接着webView使用这个配置来初始化。

当创建WKUserScript对象时,我们决定这个脚本什么时候应该被注入,和被作用于整个页面或者某个特定的frame。

运行程序,你不在会看到侧边栏(在iPhone中,它将会在页面底部以下的区域显示)和底部书籍栏目了。

提取网页数据

Appcoda的主页显示最近的10篇文章。当我们在设备上浏览主页时,你必须滑动许多次以看到底部的内容。我们希望有一个更简单地方式来获取最近的文章。我们将创建一个tableview来保存最近的文章。

我们通过提取网页数据来创建这个tableview。这里我不再会注入html了。我将会给出一段我所使用的用来获得文章的JavaScript代码,并解释它如何工作。

如果你在主页运行如下JavaScript代码,一列包含这些文章的标题和url的数据将会被打印到命令行。

var postsWrapper = document.querySelector('#content')
var posts = postsWrapper.querySelectorAll('.post.type-post.status-publish')

for (var i = 0; i < posts.length; i++) {
var post = posts[i];
var postTitle = post.querySelector('h2.entry-title a').textContent;
var postURL = post.querySelector('h2.entry-title a').getAttribute('href');
console.log("Title: ", postTitle, " URL: ", postURL);
}

如果你观察网页文章部分的html结构,你回发现类似下面的东西。

<div id='content'>
<div class='post type-post status-publish'>
<h2 class='entry-title'>
    <a></a>
    </h2>
<div class='post-info'></div>
<div class='entry-content'></div>
<div class='post-meta'></div>
</div>
<div class='post type-post status-publish'></div>
<div class='post type-post status-publish'></div>
<div class='post type-post status-publish'></div>
...
...
...
</div>

在上面的JavaScript代码中,我们通过‘content’id获得元素。这个是一个div元素,文章列表的中间父元素。我们将会获得这个div下的所有子元素,然后赋值给posts变量。它将会持有一个class为post的div数组。我们遍历这个数组,获得每个h2标签中得得文本。我们也通过另外一个链接标记的href属性来获得每个文章的URL。然后我们打印这些内容。

打开Xcode,创建一个新文件File->New->File->iOS->Other->Empth。命名为getPost.js。粘贴如下代码。

var postsWrapper = document.querySelector('#content')
var posts = postsWrapper.querySelectorAll('.post.type-post.status-publish')

function parsePosts() {
pos = []

for (var i = 0; i < posts.length; i++) {
    var post = posts[i];
    var postTitle = post.querySelector('h2.entry-title a').textContent;
    var postURL = post.querySelector('h2.entry-title a').getAttribute('href');
    pos.push({'postTitle' : postTitle, 'postURL' : postURL});
}

return pos
}

var postsList = parsePosts();
webkit.messageHandlers.didGetPosts.postMessage(postsList);

上面的代码获得了所有文章的标题和url并把他们保存到了一个数组。最后一行代码是的JavaScript和原生代码之间能够交流。webkit.messageHandlers是一个全局对象,用来帮助触发原生代码回调。didGetPosts代表了和一个原生代码方法一样名字的消息。postMessage向回调中传递了postsList数组。

在故事板中,拖放一个导航栏按钮到导航栏的左边。并改变它的名称为‘Recent’。然后创建一个它的outlet并命名为recentPostsButton。你应该会看到如下代码。

@IBOutlet weak var recentPostsButton: UIBarButtonItem!

在viewDidLoad()方法底部,添加如下代码。我们希望这个按钮一直不可点,直到posts数组有了数据。

recentPostsButton.enabled = false

在ViewController,import语句下面添加如下代码。

let MessageHandler = "didGetPosts"

在类文件中添加如下属性。

var postsWebView: WKWebView?

在viewDidLoad()底部添加如下代码。

let config = WKWebViewConfiguration()
let scriptURL = NSBundle.mainBundle().pathForResource("getPosts", ofType: "js")
let scriptContent = String(contentsOfFile:scriptURL!, encoding:NSUTF8StringEncoding, error: nil)
let script = WKUserScript(source: scriptContent!, injectionTime: .AtDocumentEnd, forMainFrameOnly: true)
config.userContentController.addUserScript(script)
config.userContentController.addScriptMessageHandler(self, name: MessageHandler)
postsWebView = WKWebView(frame: CGRectZero, configuration: config)
postsWebView!.loadRequest(NSURLRequest(URL:NSURL(string:"http://www.appcoda.com")!))

这里我们像之前导入JavaScript文件一样导入一个JavaScript文件,我们只希望DOM被构建好时及.AtDocumentEnd时被注入一次。我们也将MessageHandler加入了WKWebViewConfiguration作为WKWebView初始化的配置。

更新类声明,遵循WKScriptMessageHandler协议。

class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler

我们建立一个模型(model)文件来保存文章数据。创建一个文件File->New->File->iOS->Source->Cocoa Touch Class。命名为Post并作为NSObject的子类。在类中粘贴如下代码。

import UIKit

class Post: NSObject {

var postTitle: String = ""
var postURL: String = ""

init(dictionary: Dictionary) {
    self.postTitle = dictionary["postTitle"]!
    self.postURL = dictionary["postURL"]!
    super.init()
}
}

在ViewController类中添加如下变量。

var posts: [Post] = []

添加WKScripMessageHandler协议必须遵守的方法。

func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
if (message.name == MessageHandler) {
    if let postsList = message.body as? [Dictionary] {
        for ps in postsList {
            let post = Post(dictionary: ps)
            posts.append(post)
        }
        recentPostsButton.enabled = true
    }
}
}

上面的代码首先将检查接收到得消息是否是我们想要的,如果是,就会将消息中的数据提取成一个字典数组,然后使用其中的字典创建Post对象,并将这些Post对象依次添加到posts数组中,最后recentPostsButton就可被点击了。

打开故事版,在画板中添加一个Table View Controller。选择它,使用Editor->Embed In->Navigation Controller将它嵌入一个navigation controller。

按下Control,点击View Controller中得Recent按钮,拖到这个新的navigation controller中,选择popover presentation from the popup。选择这个被新创建了segue,设置它的Identifier为‘recentPosts’。

创建一个新文件File->New->File->iOS->Source->Cocoa Touch class。命名为PostsTableViewController并选择为UITableViewController的子类。

在故事板中,选择创建的Table View Controller,选择 Identity Inspector,设置class为PostsTableViewController。选择table view的prototype cell,在Attributes Inspector中设置Identifier为postCell。

向PostTableViewController添加如下代码。

import UIKit

class PostsTableViewController: UITableViewController {

var posts: [Post] = []

override init(style: UITableViewStyle) {
    super.init(style: style)
}

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

override func viewDidLoad() {
    super.viewDidLoad()
    self.title = "Recent Articles"
    tableView.reloadData()
}

override func numberOfSectionsInTableView(tableView:
    UITableView?) -> Int {
    return 1
}

override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
    return posts.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("postCell", forIndexPath: indexPath) as UITableViewCell
    let post = posts[indexPath.row]
    cell.textLabel?.text = post.postTitle
    return cell
}
}

这里我们实现了tableview的数据源,他将会显示文章的标题。

添加下面代码到ViewController。

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "recentPosts") {
    let navigationController = segue.destinationViewController as UINavigationController
    let postsViewController = navigationController.topViewController as PostsTableViewController
    postsViewController.posts = posts
}
}

当点击Recent按钮时,此方法会被调用。在显示tableview View controller之前,它将posts数组传递给了tableview view controller。

运行程序。点击Recent按钮,你会看到一个充满了文章列表的tableview。在iPhone上,它已满屏显示,在iPad在一个popover中显示。

当你点击一个cell的时候,没有任何事情发生。我们希望被点击的文章被加载到web view上面。

在ViewController中,添加如下代码到import语句下面。

let PostSelected = "postSelected"

当点击一个cell的时候,我们将发送一个通知。上面的常量就是这个通知的名字。

在PostsTableViewController中添加如下方法。

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let post = posts[indexPath.row]
NSNotificationCenter.defaultCenter().postNotificationName(PostSelected, object: post)
dismissViewControllerAnimated(true, completion: nil)
}

在上述方法中,每当一个cell被点击的时候会发送一个通知并隐藏(dismiss)这个tableview controller。

在ViewController类中,viewDidLoad()方法底部添加如下代码。

NSNotificationCenter.defaultCenter().addObserver(self, selector: "postSelected:", name: PostSelected, object: nil)

上述代码将这个ViewController设置为了cell点击发送的通知的观察者(observer)。

在ViewController中添加如下方法。

func postSelected(notification:NSNotification) {
webView.loadRequest(NSURLRequest())
let post = notification.object as Post
webView.loadRequest(NSURLRequest(URL:NSURL(string:post.postURL)!))
}

通过上面的方法我们得到了通知中附加的post,然后加载了post中的url。

运行程序,你应该可以在tableview中的任意文章之间切换了。

到目前为止,当我们点击Recent按钮时,我们无法隐藏(dismissing)tableview,除非我们选择并点击一篇文章。我们需要添加一个取消按钮。

在故事版中,在Table view controller的导航栏(navigationbar)的右边添加一个按钮。设置它的Identifier为Cancel。

打开Assistan Editor,按下Control点击Cancel按钮拖动到PostTableViewController类中创建一个方法。命名为cancel,确保其参数类型为UIBarButtonItem。按照下面的代码编辑这个方法。

@IBAction func cancel(sender: UIBarButtonItem) {
dismissViewControllerAnimated(true, completion: nil)
}

现在你应该有一个取消按钮了,它可以用来隐藏这个Table view。

结论

新的WebKit框架使得开发者能够让App和网页内容之间实现无缝交互。我们学习了如何自定义网页样式。从网页中提取数据,并在App中使用这些数据。

如果你的App只是一个网页版App的容器,使用WebKit框架吧!它将带来如原生App般的性能和操作体验。WebKit框架将会为这些体验不好的App力挽狂澜。

如果你想了解更多关于此框架的内容,这个WWDC视频将是个非常好的开始。

你可以在这里下载完整项目。

RAC文档翻译-基本操作符

基本操作符

本篇文档阐述了一些在RAC中经常使用的操作符,也包含了一些例子以说明他们的用法.

序列信号所共同使用的操作符被称之为操作符.

订阅信号执行自定义操作

  1. 订阅信号
  2. 注入自定义操作

转换流

  1. 映射
  2. 过滤

合并流

  1. 串联
  2. 降维
  3. 映射和降维

合并信号

  1. sequence
  2. 合并
  3. 合并最新信号
  4. 开关

订阅信号执行自定义操作

大多数信号都是’冷’的,这意味着:直到有订阅产生,他们才会产生信号.

一旦有订阅产生,信号和订阅者就能执行自定义操作了,比如在命令行打印,网络请求,更新UI,等等.

自定义操作也能被注入到一个信号.这个操作也许不会被立即执行,但每当有订阅产生时,就会执行一次.

订阅(Subscription)

通过-subscribe…方法,你可以订阅当前和将来的信号.

1
2
3
4
5
6
RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
// 输出: A B C D E F G H I
[letters subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];

对于’冷’信号来说,每当有订阅产生,它的自定义操作就会执行一次.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__block unsigned subscriptions = 0;
RACSignal *loggingSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
subscriptions++;
[subscriber sendCompleted];
return nil;
}];
// 输出:
// 订阅 1
[loggingSignal subscribeCompleted:^{
NSLog(@"订阅 %u", subscriptions);
}];
// 输出:
// 订阅 2
[loggingSignal subscribeCompleted:^{
NSLog(@"订阅 %u", subscriptions);
}];

这种特性可以通过连接来改变.

注入自定义操作(Injecting effects)

通过-do…方法,可以向信号注入自定义操作,而且不会产生一个订阅.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__block unsigned subscriptions = 0;
RACSignal *loggingSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
subscriptions++;
[subscriber sendCompleted];
return nil;
}];
// 目前不会打印任何东西
loggingSignal = [loggingSignal doCompleted:^{
NSLog(@"即将完成订阅 %u", subscriptions);
}];
// 输出:
// 即将完成订阅 1
// 订阅 1
[loggingSignal subscribeCompleted:^{
NSLog(@"订阅 %u", subscriptions);
}];

转换流(Transforming streams)

以下操作符将一个流转换成另外一个流.

映射(Mapping)

通过-map:方法,可以将一个流中的值映射成一个新的值,并返回一个带有新值的流:

1
2
3
4
5
6
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
// 包括: AA BB CC DD EE FF GG HH II
RACSequence *mapped = [letters map:^(NSString *value) {
return [value stringByAppendingString:value];
}];

过滤(Filtering)

-filter:将通过一个block来检查流中的每一个值,并返回一个包含每个符合要求的值的新的流:

1
2
3
4
5
6
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
// 包含: 2 4 6 8
RACSequence *filtered = [numbers filter:^ BOOL (NSString *value) {
return (value.intValue % 2) == 0;
}];

合并流(Combining streams)

以下操作符将多个流合并成了一个新的流.

串联(Concatenating)

通过-concat:方法,可以将一个流和另外一个流串联起来:

1
2
3
4
5
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
// 包含: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *concatenated = [letters concat:numbers];

降维(Flattening)

通过向一个包含流的流发送-flatten消息,可以将这个流中的流的值取出并返回一个新的流.

序列将被串联:

1
2
3
4
5
6
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *sequenceOfSequences = @[ letters, numbers ].rac_sequence;
// 包含: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *flattened = [sequenceOfSequences flatten];

信号将被合并:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *signalOfSignals = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
[subscriber sendNext:letters];
[subscriber sendNext:numbers];
[subscriber sendCompleted];
return nil;
}];
RACSignal *flattened = [signalOfSignals flatten];
// 输出: A 1 B C 2
[flattened subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];
[letters sendNext:@"A"];
[numbers sendNext:@"1"];
[letters sendNext:@"B"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];

映射和降维(Mapping and flattening)

单独使用降维并没有多大作用,但为了使用-flattenMap:方法,理解它很重要.

-flattenMap:被用来转换每个流的值,然后再返回包含这个值的信号到一个新的流中.然后将对这个流降维,也就是说先-map:-flatten.

可以用来扩展或者编辑序列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
// 包含: 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
RACSequence *extended = [numbers flattenMap:^(NSString *num) {
return @[ num, num ].rac_sequence;
}];
// 包含: 1_ 3_ 5_ 7_ 9_
RACSequence *edited = [numbers flattenMap:^(NSString *num) {
if (num.intValue % 2 == 0) {
return [RACSequence empty];
} else {
NSString *newNum = [num stringByAppendingString:@"_"];
return [RACSequence return:newNum];
}
}];

或者是创建几个互相关联的信号:

1
2
3
4
5
6
7
8
9
RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
[[letters
flattenMap:^(NSString *letter) {
return [database saveEntriesForLetter:letter];
}]
subscribeCompleted:^{
NSLog(@"All database entries saved successfully.");
}];

合并信号(Combining signals)

These operators combine multiple signals into a single new RACSignal.

以下操作符将多个信号合并成一个新的信号.

Sequencing

-then: 将会触发对原本的信号触发一次订阅,当原本的信号完成时,产生一个新的信号.

1
2
3
4
5
6
7
8
9
10
11
12
RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
// 新的信号包含: 1 2 3 4 5 6 7 8 9
//
// 但是当订阅产生时,他会打印: A B C D E F G H I
RACSignal *sequenced = [[letters
doNext:^(NSString *letter) {
NSLog(@"%@", letter);
}]
then:^{
return [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence.signal;
}];

使用[-doNext:]给一个信号注入自定义操作,然后当自定义操作完成时返回一个信号,十分方便.

合并(Merging)

+merge:将许多信号合并为一个新的信号,当每个信号有新的值时,它也将发送这个值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *merged = [RACSignal merge:@[ letters, numbers ]];
// 输出: A 1 B C 2
[merged subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];
[letters sendNext:@"A"];
[numbers sendNext:@"1"];
[letters sendNext:@"B"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];

合并最新值 (Combining latest values)

+combineLatest:+combineLatest:reduce:会观察所有多个信号(signal),当所有的信号(signal)都发生变化时,发送所有最新的所有的他们的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *combined = [RACSignal
combineLatest:@[ letters, numbers ]
reduce:^(NSString *letter, NSString *number) {
return [letter stringByAppendingString:number];
}];
// 打印: B1 B2 C2 C3
[combined subscribeNext:^(id x) {
NSLog(@"%@", x);
}];
[letters sendNext:@"A"];
[letters sendNext:@"B"];
[numbers sendNext:@"1"];
[numbers sendNext:@"2"];
[letters sendNext:@"C"];
[numbers sendNext:@"3"];

我们注意到这个合并后的信号只会在他所有的输入产生变化时发送信号。上面的例子说明@"A"永远不会被转发,应为numbers并没有发送值。

开关(Switching)

-switchToLatest被流的流使用,而且总是转发最新的信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSubject *signalOfSignals = [RACSubject subject];
RACSignal *switched = [signalOfSignals switchToLatest];
// 打印: A B 1 D
[switched subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];
[signalOfSignals sendNext:letters];
[letters sendNext:@"A"];
[letters sendNext:@"B"];
[signalOfSignals sendNext:numbers];
[letters sendNext:@"C"];
[numbers sendNext:@"1"];
[signalOfSignals sendNext:letters];
[numbers sendNext:@"2"];
[letters sendNext:@"D"];

WebKit框架概览-Part-1

WebKit框架概览-Part 1

译者:iBenjamin

如果你曾今在你的App当中使用UIWebView加载网页内容的话,你应该体会到了其诸多不尽人意之处。UIWebView是基于移动版的Safari的,所以它的性能表现十分有限。特别是在对几乎每个Web应用都会使用的JavaScript,表现的尤为糟糕。

但是,所有的这一切都在iOS8引入了一个新的框架,WebKit,之后变得好起来了。在WebKit框架中,有WKWebView可以替换UIKit的UIWebView和AppKit的WebView,而且提供了在两个平台可以一致使用的接口。

WebKit框架使得开发者可以在原生App中使用Nitro来提高网页的性能和表现,Nitro就是Safari的JavaScript引擎。

WKWebView保证在滑动时保持60的帧率,同时具有KVO,内建手势,以及在App和网页之间的原生交流方式。

横跨2篇文章,我们即将建立2个App来探索WebKit的功能(特别是WKWebView)。在第一个App当中,我们将建立一个和Safari功能相似的浏览器。在第二篇文章中,我们会深入到Webkit中去,探索更强大的功能:注入JavaScript到网页以改变内容和获取数据。

开始

打开Xcode,创建一个新的工程。选择Single View Application,取名叫Browser,选择Swift为开发语言,Devices选择Universal。

在ViewController.swift中导入WebKit框架。

import WebKit

将下面的变量加入到类中

var webView: WKWebView

将下面的方法加入到类中。它将会初始化webview并设置其frame为0.稍后我们会使用自动布局(auto layout)来给webview添加约束,这样这个webview就能在任何苹果设备和任何方向上正常工作了。

required init(coder aDecoder: NSCoder) {
self.webView = WKWebView(frame: CGRectZero)
super.init(coder: aDecoder)
}

在viewDidLoad()底部,添加如下语句。这样这个webView就被添加到主视图了。

view.addSubView(webView)

接下来,在viewDidLoad()方法底部添加如下约束

webView.setTranslatesAutoresizingMaskIntoConstraints(false)
let height = NSLayoutConstraint(item: webView, attribute: .Height, relatedBy: .Equal, toItem: view, attribute: .Height, multiplier: 1, constant: 0)
let width = NSLayoutConstraint(item: webView, attribute: .Width, relatedBy: .Equal, toItem: view, attribute: .Width, multiplier: 1, constant: 0)
view.addConstraints([height, width])

在此处,我们首先禁止了自动约束。然后我们对webview的宽和高添加了约束。这样webview就会和它的superview拥有一样的宽和高了。

在程序启动时,我们将会打开一个默认页。然后我们添加一个textfield控件,这样用户就能输入自己想浏览的地址了。在viewDidLoad()底部添加如下代码

let url = NSURL(string:"http://www.appcoda.com")
let request = NSURLRequest(URL:url!)
webView.loadRequest(request)

运行程序。它将会加载Appcoda的主页。注意当你滑动页面的时候在导航栏下面也能看到模糊的网页,我们将禁止这样。首先打开Main.storyboard选择网页显示的ViewController。在属性检查(也就是右边的第四栏中)将Extend Edges中得Under Top Bars去掉勾选。再次运行程序,我们就会发现导航栏已经没有了毛玻璃效果而且我们也不能看见它下面的网页内容了。

接下来让我们添加给用户输入URL的控件。

在storyboard文件中,给向导航栏中拖放一个view。在属性检查(右边第四栏)中,设置其背景色为透明(clear color)。因为我们无法给导航栏中的view添加约束,我们将会以代码的形式调整其大小。

@IBoutlet weak var barView: UIView!

在viewDidLoad()的super.viewDidLoad()之后添加如下代码

barView.frame = CGRect(x:0, y: 0, width: view.frame.width, height: 30)

这段代码设置了barView的大小。

将下面的方法添加到类

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
barView.frame = CGRect(x:0, y: 0, width: size.width, height: 30)
}

这段代码将在设备方向改变时重新设置barView的大小。

接下来运行程序,你可以看见在导航栏中展开的view,如果更改设备方向,这个view也会跟着改变大小。

接下来,拖放一个textfield控件到这个view里面。然后点击画布右下方的Pin按钮(第二个)。将其top,bottom,right,left的距离设置为0,如下图所示。

然后依次点击,Editor-》Resolve Auto Layout Issues-》Selected View-》Update Frame,解决警告。

然后创建一个outlet。取名urlField。你应会看到如下代码。

@IBOutlet weak var urlField: UITextField!

我们希望viewcontroller能成为UITextFieldDelegate的代理,在storyboard的Document Outline中,按下control然后将textfield拖放到viewcontroller,然后在弹出视图选择delegate。

选择textfield控件,在属性检查(右边第四栏)设置如下属性。

  1. Clear Button:Appears while editing
  2. Correction: NO
  3. Keyboard Type:URL
  4. Return Key:Go

在类声明部分添加实现UITextFieldDelegate代码。

class ViewController: UIViewController, UITextFieldDelegate

接下来添加如下UITextFieldDelegate代码

func textFieldShouldReturn(textField: UITextField) -> Bool {
urlField.resignFirstResponder()
webView.loadRequest(NSURLRequest(URL: NSURL(string: urlField.text)!))
return false
}

以上代码会隐藏键盘,然后加载用户输入的url。尝试输入一个url,我们发现。我们必须输入一个完整的url,比如:http://google.com。对我们的用户这有一点麻烦,我们可以检查用户的输入,然后给用户输入的url在必要时添加‘http://’前缀。这里我们不会详述了。

浏览历史

现在我们的浏览器已经能工作了,但是它还缺少了一些浏览器应有的功能。载入进度提示,前进和后退,刷新按钮,等等。

通过KVO(Key Value Observing),我们可以监听WKWebView的载入进度、网页标题和url属性。你可以使用这些来更新你的UI。

首先,让我们添加后退、前进及刷新按钮。

在storyboard中,选择View Controller,点击属性检查(Attributes Inspector),在Simulated Metrics一栏,将BottomBar选择为None。

拖放一个Toolbar到view得底部。添加其left,right和bottom距离为0,并确保Constrans to margins一栏没有被勾选。

在viewDidLoad()方法中调整webView的高度,以显示toolbar。

let height = NSLayoutConstraint(item: webView, attribute: .Height, relatedBy: .Equal, toItem: view, attribute: .Height, multiplier: 1, constant: -44)

移除toolbar中得button,然后按顺序添加以下控件:Bar Button Item、Fixed Space Bar Button Item、Bar Button Item,Flexible Space Bar Button Item和Bar Button Item。toolbar应该是这个样子。

编辑bar button成下面的样子。这些按钮将成为我们的前进后退及刷新按钮。在一个真实的App当中,为这些按钮放上图标将是更好的选择,但是为了简便,我们使用文本。toolbar接下来应该是这个样子。

为每一个bar button创建一个outlet。取名为backButton,forwardButton和reloadButton。你应该会得到如下代码

@IBOutlet weak var backButton: UIBarButtonItem!
@IBOutlet weak var forwardButton: UIBarButtonItem!
@IBOutlet weak var reloadButton: UIBarButtonItem!

然后为每个按钮依次分别创建back,forward和reload方法。将每个action的Type更改为UIBarButtonItem。你应该会得到如下代码。

@IBAction func back(sender: UIBarButtonItem) {
}

@IBAction func forward(sender: UIBarButtonItem) {
}

@IBAction func reload(sender: UIBarButtonItem) {
}

在viewDidLoad()的底部添加如下代码。我们不希望后退和前进按钮在App被启动时就可点击。

backButton.enabled = false
forwardButton.enabled = false

在viewDidLoad()的约束条件添加代码之后,创建并载入一个任务代码之前,添加如下代码。这句代码使得这个类成为了loading属性的监听者。

webView.addObserver(self, forKeyPath: "loading", options: .New, context: nil)

添加下面的方法到类。这个方法将会在可监听的属性变化时执行。后退和前进按钮将根据当前webview的状态来决定是否可被点击。

override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer) {
if (keyPath == "loading") {
    backButton.enabled = webView.canGoBack
    forwardButton.enabled = webView.canGoForward
}
}

修改back(),forward()和reload()方法。

@IBAction func back(sender: UIBarButtonItem) {
webView.goBack()
}

@IBAction func forward(sender: UIBarButtonItem) {
webView.goForward()
}

@IBAction func reload(sender: UIBarButtonItem) {
let request = NSURLRequest(URL:webView.URL!)
webView.loadRequest(request)
}

运行并测试这些按钮。后退和前进按钮开始时应该不可点击。当你浏览一个页面时,后退按钮应该可被点击。当你后退时,前进按钮应该可被点击。点击R按钮,会重新加载页面

处理错误

我们不能保证用户总是输入正确地url,我们需要写代码来获取这一错误并提示用户。

首先修改类声明,如下所示:

class ViewController: UIViewController, UITextFieldDelegate, WKNavigationDelegate

WKWebView有一个属性navigationDelegate,接受一个实现WKNavigationDelegate协议的对象。这个协议提供了多种方法来处理导航事件,包括载入错误。

在init()底部添加如下代码。通过下面的代码,我们将这个类设置成了webview的navigation代理。

self.webView.navigationDelegate = self

接下来添加如下方法。这个代理方法将会在有错误发生时被调用。

运行程序,输入一个错误的url测试一下。

展示进度

最后,我们将会添加一个进度展示。

在storyboard文件中,在导航栏下方添加一个progress view。设置其top,right和left如下所示。

为progress view创建一个outlet。你应该会得到下面的代码。

@IBOutlet weak var progressView: UIProgressView!

在ViewController中,替换西面的代码

view.addSubview(webView)

view.insertSubview(webView, belowSubview: progressView)

在viewDidLoad()中创建和载入一个url任务之前添加下面的代码,调用webview的addObserver方法。

webView.addObserver(self, forKeyPath: "estimatedProgress", options: .New, context: nil)

在observeValueForKeyPath()方法中的其他if语句之后添加如下代码。

if (keyPath == "estimatedProgress") {
progressView.hidden = webView.estimatedProgress == 1
progressView.setProgress(Float(webView.estimatedProgress), animated: true)
}

这段代码将会更新progressview的进度,如果加载完毕会隐藏progressview。

在类中添加如下代码。这是一个WKNavigationDelegate的代理方法,将会在页面载入完毕后执行。当一个任务完成后,我们使用它来重置progress view的进度。

func webView(webView: WKWebView!, didFinishNavigation navigation: WKNavigation!) {
progressView.setProgress(0.0, animated: false)
}

运行程序,当网页加载时,你将会看到一个蓝色的进度条。

总结

我们已经了解了WebKit的基础部分。我们看到如何添加一些和Safari相似的功能,如载入url,浏览历史,检测错误,展示进度。在这个教程的第二部分,我们会深入了解如何将一个JavaScript注入网页,来构建一个更强大的程序。

你可以通过这个链接来下载这个教程的实例代码。

原文链接:http://www.appcoda.com/webkit-framework-intro/

在ios8创建一个交互性强的本地通知

在iOS8创建一个交互性强的本地通知

通知,是App用来和用户交流的一种方式,特别是当App并没有在运行的时候。通知,正如它的名称所强调的,被用作向用户‘通知’一个事件,或者仅仅向用户提示一条重要信息。总而言之,通知在提示类型的App当中非常有用,甚至在一些别的类型的App当中也是如此。比如,当用户进入一个指定区域(这是iOS8的新特性),一个下载任务完成,或者当朋友给你发送一条信息的时候,一条通知就可以被显示出来。无论如何,通知的目的就是获得用户的关注,然后他们就能处理通知了。

从编程的角度来说,通知有着一套相当标准的API,可以非常简单地被实现。不需要太多的脑力,开发者可以根据文档轻松的在App中加入通知功能。也就是说,详细规定由系统转发的通知的内容,在App启动时处理通知,最后,从iOS8开始,处理任何由通知指定的动作(actions)。每一个App唯一改变的只有业务逻辑而已。

图片1

从iOS8开始,本质上来说有两种通知:

  1. 本地通知(Local Notifications):由开发者定义,App触发。触发的时间是被事先安排好的。
  2. 远程通知(remote notifications):这种情况下,通知可以被分成两个类别:(a)推送通知(The push notifications),被服务器初始化,然后通过APNS,最终到达用户设备。(b)静默通知(The silent notifications),其实也是推送通知,但是他们并没有被展示给用户,而是立即被App处理以发起某项任务,最后当一切都完成时,一个本地通知被显示以提示用户。

除了以上的2种以外,iOS8引入了地点通知(location notifications)。它其实也是本地通知(local notifications),但是他们只会在用户一个特定的地理或者iBeacon区域时,才会被触发。虽然我们看不到什么细节,地点通知(location notifications)实现起来也很容易。

这个好消息昭示着一个重要的信息:从iOS8开始,通知被加入了新的特性。简单地说,从现在开始,当一个通知被展示时,开发者可以指定用户可触发的具体的动作(actions),而且甚至不用启动App也可以处理这个通知。

随着新特性的引入,通知变得越来越引人注目。用户被给予一个选择清单,然后可以命令App立即执行特定的命令。用户再也不用浪费时间在启动App、处理通知上了。动作(Actions)使得通知和App越来越强大,当然也极大的提升了用户体验。

动作(Actions)可以被归类到一个类目(categories)下。特别是当App安排了不少通知显示的时候相当方便。用类目(categories),一个通知所有相关的动作(actions)都可以一次性的被捆绑和指定。反之,处理动作(actions)也非常简单,只需要实现其代理方法即可。每一个动作(actions)都有一个特殊的属性标示符(identifier),被App用来辨别收到的动作(actions)然后适当地处理它。

如你所见,本篇文章的目的就是让你了解以上所有细节。尽管动作(actions)和类目(categories)是新的技术,你最后会发现,他们其实实现起来并不困难。然而,在我们进入到下一个部分之前,我需要说明我们只会本地通知(local notifications)进行详尽说明。我认为,如果想要在本篇文章中将通知的每个种类都详尽说明,那么这篇文章就会变得很泛泛,除此之外,我们也会将文章的重点放在对iOS8新引入的功能进行说明。

跟往常一样,我强烈地推荐你去看看官方文档。不仅仅是苹果开发者中心,还去看看 WWDC 2014 #713 session video。将本篇文章和以上结合起来学习,你将会获得所有通知新特性的知识。而且,我们在这里并不会讨论远程和地点通知,官方文档是你学习他们的一个好地方。

关于本地通知

本地通知被安排在指定的日期和时间被App触发。时刻记在心中,尽管在和用户通信时通知十分有用,你也应该小心,过分的使用可能会导致较差的用户体验。

有几种方式来提示用户一个通知,接下来会展示所有支持的通知类型。正如你已经了解的,你可以指定通知类型为他们中的几个或者所有。

  1. Alert or Banner:通知可以用alert或者banner来显示,这取决于用户在设置中得选择。他们都应当包含通知的消息(当然是可以本地化的)。
  2. 声音(Sound):当一个通知被送达时,你可以‘告诉’iOS播放一段自定义或者系统默认的声音。因为用户不会一直看着设备,被展示的通知有可能会被忽略,所以声音显得很有用。但是,对于不重要的通知,声音不应该被使用。
  3. Badge:当通知到达时,一个badge数字会在App的图标上显示。当一个通知到达时,badge数字必增加1,当通知被处理后badge数字减1。当badge数字不为0或者为0,iOS会显示或者隐藏badge。

在iOS7时,用户只能点击通知(或者在锁屏时滑动)然后关联的App会启动,最后处理相关动作。现在开发者可以用户提供具体的预先定义的动作了。相关联的App可以在不被启动的情况下,处理不关键或者重要的任务了,而且也可以根据用户的选择来执行不同的代码。马上我们就能通过这篇文章来学习如何实现它。

除了上面所说的,一个本地通知还可以包含附加的数据,此数据可以被App处理。此数据可以被包含在一个用户信息字典中,App可以通过通知来访问此数据,在启动时或者未启动时。

可以被安排的本地通知的数量并不是无限的,最多有64个本地通知可以被安排和展示。如果多余这个数字,所有超过这个数字的通知都会被废弃。尽管如此,无论通知是以什么样的形式被安排的,最早的那个会被最先展示。

接下来,让我们看看我们今天学习会使用的到得示例App。

示例App概览

通过开发一个示例App,我们会学习到所有通知的功能。实际上我们将会开发一款购物清单应用,用户通过此应用发送的通知会得到他需要购买的物品清单。完成这个,我们需要下面两个功能:

  1. 添加和删除一个物品
  2. 选择一个日期和时间通知用户。

如你所想,我们只会实现本地通知。通过它,演示iOS8通知的新特性已经足够了。

为了添加一个新物品,我们需要用到textfield控件。添加的物品会在一个tableview中展示出来,在tableview已经存在的物品中,可以通过在物品cell左滑动来删除物品。然后,date picker控件可以被用来设置通知展示的日期和时间。此date picker会在一个按钮被点击了之后展示出来,当选择好了日期之后,这个按钮会被用来安排通知和重新显示tableivew。我们会以动画的形式来显示和隐藏date picker,所以我们的App就会显得更加吸引人啦,值得注意的是这里也演示的如何在Swift中使用UIView来实现简单地动画效果。

对于我们将要安排的本地通知,我们会定义3种不同的动作(除了默认通知以外,所有本地通知都支持让App启动)。这些动作会给用户如下的选择(我写下了动作标题和它将要做的事情):

  1. “好,买到了”(OK, got it):这个动作其实并不会做什么,除了让通知消失以外。在App中,没有任何任务将会执行。
  2. “编辑清单”(Edit list):这个动作将会启动App,然后textfield会获得焦点,然后用户可以直接写下一个新的物品。
  3. “删除清单”(delete list):这个动作不会启动App。现存的物品清单将会被删除,但是App并不会被启动。下次用户启动App时,物品清单就会不见啦。

下面几张图演示了这个App的功能。正如我所说的,它很简单,但是用来达到本篇文章的目的已经足够啦:

图片2
图片3
图片4
图片5

最后再说几句,我会在实现动作的时候具体讲到他们。所以,目前将通知动作当做是根据通知用户可触发的方法就行了,所有与他们相关的内容你马上就会看到了。

基本项目

我们的目标是了解所有和通知相关的新东西而不是从头创建一个项目,你可以从这里获得基本项目。下载它,解压它,打开它,这个项目将会作为我们实现通知的模板。

这个项目已经设计好了基本界面,在你阅读下一章之前,请概览一下此项目。打开Main.storyboard,看看那些subviews和已经连接好的IBoutle和IBAction。

除此之外,你会看到所有ViewController应该实现的协议也已经声明好了。tableview的datasouce和delegate也已经写好了,但是里面并没有逻辑相关的代码。这仅仅是为了避免Xcode报错而已。最后,在viewDidLoad方法中,你可以看到哪些对象被设置了代理。

这个基本项目很容易理解,所以你没有必要花太多时间来看它。但是它还是值得你快速的阅览一下的。

设计一个购物清单

首先,让我们来实现这个购物清单。本部分我们的目标是通过textfield添加一个物品和在tableivew中展示所有的物品。当然,我们也会实现删除已经存在物品的功能。显然,我们需要一个数据结构来保存我们的数据,并且作为tableview的数据源。接下来,让我们给ViewController类添加一个NSmutableArray属性。确保你选择的是ViewController.swift文件。在此文件中,找到IBOutlet属性声明然后添加下面的实例变量:

var shoppintList: NSMutableArray!

好了,我们已经完成了第一步。注意到我们并不会在viewDidLoad方法中对它进行初始化,而是在我们会添加新的物品到它之中时初始化它。

现在保存物品的数据结构已经声明好了,接下来让我们允许用户通过textfied添加物品。实际上,我们希望新添加的物品在添加到了数组之后,tableivew马上更新显示它。要实现此功能,我们必须实现textFieldShouldReturn(textField:) 代理方法。正如你在基本项目中所见到的,UITextFieldDelegate协议已经被遵从了,ViewController也设置成为了txtAddItem textfield的代理类。

在这个代理方法中,我们希望做以下事情:

  • 如果shoppintList为nil,初始化它
  • 将textfield的txt添加为一个新的物品到数组
  • 让tableview展示新的物品(我们稍后就会实现此功能)
  • 添加新物品后清空textfield的内容
  • 移除textfield的焦点,所以键盘就会隐藏了

上面的事情是不是看起来很多?其实不多,以可以通过下面的代码片段来做这些事情:

func textFieldShouldReturn(textField: UITextField) -> Bool {
if shoppingList == nil{
    shoppingList = NSMutableArray()
}
shoppingList.addObject(textField.text)

tblShoppingList.reloadData()

txtAddItem.text = ""
txtAddItem.resignFirstResponder()

return true

}

上面的代码所实现的功能已经非常清楚了,所以我认为不需要更多地讨论了。

接下来让我们在tableview中展示shoppingList的内容。在ViewController类中,tableview的datasource和delegate方法我们已经加上去了,但是现在我们必须加上合适的代码以让它工作。让我们从简单的开始,tableview中得section和row的数量,每个cell的高度。你可以用下面的代码整体替换项目中的活着只是简单的替换方法的内容。

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}


func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var rows = 0

if let list = shoppingList{
    rows = list.count
}

return rows
}


func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 50.0
}

注意到我们对shoppingList数组进行了空指针判断。

接下来,让我们将数组中的每个元素赋值给cell的label,然后他就会在tableview中展示出来了。在此之前,我需要强调一下,在Interface Builder中有一个cell原型,标示符(identifier)为idCellItem。首先,让我们重用(dequeue)这个cell,然后将数组中每个元素赋值给这个cell的label的text属性。代码如下:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("idCellItem") as UITableViewCell

cell.textLabel?.text = shoppingList.objectAtIndex(indexPath.row) as NSString

return cell
}

下面代码的类型转换部分:

cell.textLabel?.text = shoppingList.objectAtIndex(indexPath.row) as NSString

非常重要(as NSString部分),因为我们想让编译器清楚我们是想将一个string赋值给label。

现在,我们终于可以添加一个新物品和展示所有的物品在tableview中啦。但其实还差一点点,这里还差最后一个功能:删除已存在的物品。

这实现起来很简单,让我们定义一个超级简单的新方法:

func removeItemAtIndex(index: Int) {
shoppingList.removeObjectAtIndex(index)

tblShoppingList.reloadData()
}

此方法只接收一个参数,待删除物品的数组下标。我们使用这个参数,删除shoppingList数组中对应的物品,然后刷新tableview的数据。

现在让我们在左滑cell后显示的删除按钮上调用上面的方法。要实现此功能,我们需要实现tableView(tableView:commitEditingStyle:forRowAtIndexPath:) 代理。在此代理方法中我们调用上面的删除方法。下面是代码:

func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == UITableViewCellEditingStyle.Delete {
    removeItemAtIndex(indexPath.row)
}
}

这里有两件事情需要注意一下:1.这个if判断语句是非常必要的,有了这个if判断,删除方法就只会在用户点击删除按钮后被触发。2.代理方法中的indexPath.row的row就是我们想要删除的物品的数组下标。

你可能会想,为什么我们要定义一个新的removeItemAtIndex(index:) 方法呢?毕竟我们只需要2行代码就能在代理方法中实现删除了。嗯,现在我不会回答此问题;去搜索思考吧。

最后,我需要强调的是我们并没有必要向这个简单的App中加入编辑物品功能。毕竟这也没什么难度。我们现在做的已经够了。

保存和加载清单

尽管前面我们已经实现了这个App的基本功能,但是我们还需要加入另外两个重要的功能来让这个App正常的工作。我们需要将清单保存到磁盘,然后在程序启动时从磁盘读取清单。

NSMutableArray提供了将数据写入磁盘和从磁盘读取数据的方法,我们可以方便使用他们。接下来我们将会定义两个方法,来保存和读取数据。首先让我们实现保存方法,保存到的文件名字为shopping_list:

func saveShoppingList() {
let pathsArray = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
let documentsDirectory = pathsArray[0] as String
let savePath = documentsDirectory.stringByAppendingPathComponent("shopping_list")
shoppingList.writeToFile(savePath, atomically: true)
}

方法的第一二行代码,返回了App的Ducument类目。然后我们构建保存的文件的路径。最后,调用NSMutableArray方法writeToFile(_:atomically:)。这个方法将数据保存到了磁盘。

实现了保存方法后我们就能调用他了。如果你有对前面的App基本功能进行思考,应该能想到我们会在两个地方调用到保存方法:1.当一个新的物品被添加时,2.删除一个已经存在的物品时。

首先,在 textFieldShouldReturn(textField:) 代理方法中的return之前添加保存方法:

func textFieldShouldReturn(textField: UITextField) -> Bool {
...

saveShoppingList()

return true
}

非常好,现在我们可以将新添加的物品保存到磁盘啦。接下来,让我们到removeItemAtIndex(index:)方法中也加入保存方法:

func removeItemAtIndex(index: Int) {
...

saveShoppingList()
}

接下来任何可能对我们的数据产生影响的操作,都会被保存到磁盘啦。

现在我们可以实现读取数据的方法啦。首先让我们来看看方法定义:

func loadShoppingList() {
let pathsArray = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
let documentsDirectory = pathsArray[0] as String
let shoppingListPath = documentsDirectory.stringByAppendingPathComponent("shopping_list")

if NSFileManager.defaultManager().fileExistsAtPath(shoppingListPath){
    shoppingList = NSMutableArray(contentsOfFile: shoppingListPath)
    tblShoppingList.reloadData()
}
}

这里需要提醒一下,在读取磁盘文件之前我们总是会检查一下这个文件存不存在。在iOS当中,我们通过NSFileManager来检查文件存不存在。如果文件不存在那么当然什么事情都不会发生啦。如果文件存在的话,我们就会用文件的内容来初始化shoppingList数组,然后让tableview重新加载数据。

最后,我们当然得调用此方法。我们希望App在启动完毕后马上加载数据,在viewDidLoad方法中调用它就是一个很好的选择。

override func viewDidLoad() {
...

loadShoppingList()
}

当我们不启动App处理通知时,这两个方法将会非常有用。

选择一个提醒时间

现在textfield和tableview在我们的修改之后工作的非常好了。对物品清单的管理也几乎要完成了。我们现在可以将重心移到date picker控件上来了。在这个部分,我们将会做一些非常简单有趣的事情。我们会以动画的形式展现date picker,然后我们就能为一个通知选择一个日期和时间啦。

如果你去看看ViewController的viewDidLoad方法,你会发现一行隐藏date picker控件的代码:

datePicker.hidden = true

接下来我们将会使Schedule Reminder按钮像一个开关一样工作:当点击它时,date picker将变得可见,tableview将会隐藏,再次点击它时它又会做完全相反的事情。

如我所说,tableview和date picker之间将会以动画的形式切换,为了实现这个动画切换,我竟会定义一个新的方法。在方法中,我会使用UIView的animateWithDuration(duration:animations:completionHandler:) 方法。这个方法帮助我们快速而方便的创建动画,如果你曾经使用过它,你应该就会知道它的方便快捷之处。

我们先定义一个animateMyViews(viewToHide:viewToShow:)方法。从方法名就能看出,这个方法接收两个参数,第一个是需要隐藏的view,第二个是将要显示的view。请记住,在此方法中我们需要同时隐藏或者显示tableview和date picker,所以我们需要在调用此方法是依次传入合适的参数。

让我们挪到代码部分,首先我们来看看方法的实现,然后我们会详尽的对方法进行讨论:

func animateMyViews(viewToHide: UIView, viewToShow: UIView) {
let animationDuration = 0.35

UIView.animateWithDuration(animationDuration, animations: { () -> Void in
    viewToHide.transform = CGAffineTransformScale(viewToHide.transform, 0.001, 0.001)
    }) { (completion) -> Void in

        viewToHide.hidden = true
        viewToShow.hidden = false

        viewToShow.transform = CGAffineTransformScale(viewToShow.transform, 0.001, 0.001)

        UIView.animateWithDuration(animationDuration, animations: { () -> Void in
            viewToShow.transform = CGAffineTransformIdentity
        })
}
}

让我们来看看这个方法都做了什么:首先,我们以秒为单位定义了每个动画的持续时间。注意到,这里有两个动画将会先后顺序执行。第一个动画隐藏需要被隐藏的view,第二个动画展示需要被展示的view。

首先,我们启动第一个动画,然后转换viewToHide的transform属性,这样它的宽和高就会按比例缩小。这个view的实际frame并不会变化,但是这个动画会产生一种拉远消失的漂亮效果。当第一个动画完成时,在它的动画完成回调闭包(closure)里我们首先设置2个view是否可见,第一个view被隐藏,第二个view变得可见了。然后我们对第二个view的transform属性立即缩放。最后我们对第二个view的transform逐步放大到正常值。第二个动画的最终效果就是拉近。

注意到,如果你想改变动画的持续时间,简单的改变animationDuration值就行啦。上面的动画持续时间是0.7秒钟(0.35+0.35)。

你应该对这个动画是如何实现的和最终的效果非常好奇吧?没关系,马上你就会知道了,现在我们还差一点点工作。我们现在需要实现唯一的一个IBAction方法。

这个方法将要做的事情非常简单:首先检查date picker最近的状态,如果它是隐藏着的,我们会调用上面的动画方法并以tableview为第一个参数,date picker为第二个参数。如果它没有隐藏的话,我们就会给动画方法传入相反顺序的参数。

@IBAction func scheduleReminder(sender: AnyObject) {
if datePicker.hidden {
    animateMyViews(tblShoppingList, viewToShow: datePicker)
}
else{
    animateMyViews(datePicker, viewToShow: tblShoppingList)
}

txtAddItem.enabled = !txtAddItem.enabled
}

上面的if-else语句已经非常清楚了。在这个方法的最后,我们使用一个布尔值标志,来允许或者不允许添加物品,当tableview显示时允许,反之不允许。

现在,Schedule Reminder按钮可以被用来切换显示tableview和date picker啦。稍后,我们会在上面的IBAction方法中加入更多代码,然后我们就能安排显示本地通知啦。

现在你可以稍微把玩一下这个App了,在模拟器或者iPhone上面跑跑它吧。试试添加删除物品和动画切换。如果你懒得跑它下面的图片也展示了这个App目前的效果:

规定通知类型

到目前为止我们已经完成了一些有趣和cool的事情,我们的App也如我们所想的运行起来了。从此部分开始,我们将学习本地通知,并实践每个新的功能。

在开始之前,先说明一下,在本部分和接下来的部分中我们都只会在同一个一个方法中编写代码。尽管如此,我们将会一步步的实现它,这样我们就能讨论每一个新功能了。

在本文章的开头,我简要的说明了一下本地通知。其中,我说明了通知的集中类型:在alert或者banner中显示消息(稍后我会道速你如何在他们之中切换),声音和badge。本部分,我们会规定通知的类型。总之,我们只会实现在alert和banner中显示消息,和播放一段声音。我们不会实现badge数字,如果你想你可以自己实现它。实现它的方法在本文章的末尾有。

在开始下一个部分之前,有一个非常重要的地方我需要强调一下。所有有关一个App的通知的设置都会在用户设置中体现出来。如果一个App使用了通知,那么在第一次启动程序时,App会询问用户是否允许App发送通知。不管用户选择了允许还是不允许,他都可以以后在用户设置中改变他。所以,在程序启动时看到了下图中的alert时别惊讶,选允许,否则我们就什么通知都看不懂了。

接下来我们将创建一个叫setupNotificationSettings()的方法。除去定义方法部分,我们只会在这里编写一行代码。也许你会想,这很简单,但是其实它做了一个非常重要的工作。通过这行代码,我们告诉了App我们想要支持的通知类型。将此设置保存进一个变量,以后我们会用到它。

下面是目前的代码:

func setupNotificationSettings() {
// Specify the notification types.
var notificationTypes: UIUserNotificationType = UIUserNotificationType.Alert | UIUserNotificationType.Sound

}

UIUserNotificationType是一个枚举(enum)类型,它包含了通知所有可能的类型。如你所见,OR操作符(“|”)用来包括多种类型。在这里你可以看到通知所有可能的类型。如果你想重温一下Swift位操作符的话,这里将会有你想要的。

在我们使用通知类型之前让我们先看一点别的东西。

创建通知动作(Notification Actions)

前面我笼统的介绍了几次通知动作,现在让我们来详细的了解一下他们。

一个动作就是一个UIMutableUserNotificationAction类的对象。UIMutableUserNotificationAction是iOS8新引入的类,有着许多有用的配置属性:

  • 标示符(identifier):字符串,标示了一个对于整个App唯一的字符串。很明显,你永远不应该在同一个App中定义两个同样地标示符。通过此标示符,我们可以决定在用户点击不同的通知时,调用哪个动作。
  • 标题(title):用来在展示给用户的动作按钮上。可以是简单地或者本地化的字符串。为了让用户能马上理解动作的含义,一定要仔细考虑这个标题的值,最好是1到2个字符。
  • (destructive):布尔值。当设置为true时,通知中相应地按钮的背景色会变成红色。这只会在banner通知中出现。通常,当动作代表着删除、移除或者其他关键的动作是都会被标记为destructive以获得用户的注意。
  • authenticationRequired:布尔值。当设置为true时,用户在点击动作之前必须确认自己的身份。当一个动作十分关键时这非常有用,因为为认证的操作有可能会破坏App的数据。
  • ActivationMode:枚举。决定App在通知动作点击后是应该被启动还是不被启动。此枚举有两个值:(a)UIUserNotificationActivationModeForeground,(b)UIUserNotificationActivationModeBackground。在background中,App被给予了几秒中来运行代码。

当我描述此App时,我说过我们将会创建3中不同的动作:

  1. 一个简单的通知,点击后消失,不会做任何事情。
  2. 点击通知动作后添加一个物品。
  3. 点击通知动作后删除整个清单。

让我们用代码来实现每个动作。对于每个动作,我都会使用到上面描述的每个属性。

var justInformAction = UIMutableUserNotificationAction()
justInformAction.identifier = "justInform"
justInformAction.title = "OK, got it"
justInformAction.activationMode = UIUserNotificationActivationMode.Background
justInformAction.destructive = false
justInformAction.authenticationRequired = false

动作的标示符是“提示而已(justInform)”。动作只会在backgroun运行,不会产生任何安全问题,所以我们设置了destructive和authenticationRequired为false。

下一个动作:

var modifyListAction = UIMutableUserNotificationAction()
modifyListAction.identifier = "editList"
modifyListAction.title = "Edit list"
modifyListAction.activationMode = UIUserNotificationActivationMode.Foreground    
modifyListAction.destructive = false
modifyListAction.authenticationRequired = true

很明显,为了让用户能够标记物品清单,我们需要App启动。而且我们不希望用户的物品清单被未验明身份的人乱动,我们设置了authenticationRequired为true。

最后一个动作:

var trashAction = UIMutableUserNotificationAction()
trashAction.identifier = "trashAction"
trashAction.title = "Delete list"
trashAction.activationMode = UIUserNotificationActivationMode.Background
trashAction.destructive = true
trashAction.authenticationRequired = true

通过这个动作,我们允许用户在App没有启动的情况下删除整个物品清单。这个动作可能导致用户丢失所有数据,所以我们设置了destructive和authenticationRequired为true。

通过上面的代码,你应该了解到了配置动作其实很简单。

现在让我们把以上三个动作配置代码片段加入到setupNotificationSettings方法中吧!

func setupNotificationSettings() {
...    

// Specify the notification actions.
var justInformAction = UIMutableUserNotificationAction()
justInformAction.identifier = "justInform"
justInformAction.title = "OK, got it"
justInformAction.activationMode = UIUserNotificationActivationMode.Background
justInformAction.destructive = false
justInformAction.authenticationRequired = false

var modifyListAction = UIMutableUserNotificationAction()
modifyListAction.identifier = "editList"
modifyListAction.title = "Edit list"
modifyListAction.activationMode = UIUserNotificationActivationMode.Foreground
modifyListAction.destructive = false
modifyListAction.authenticationRequired = true

var trashAction = UIMutableUserNotificationAction()
trashAction.identifier = "trashAction"
trashAction.title = "Delete list"
trashAction.activationMode = UIUserNotificationActivationMode.Background
trashAction.destructive = true
trashAction.authenticationRequired = true

}

当一个通知的所有动作被配置好了之后,他们可以被包进一个类目(categories)里。如果你的通知支持动作,那么你就必须创建一个类目(categories)。通常情况下一个类目(category)配对一个通知,假设一个App中得所有通知都支持动作,那么这个App也会有和通知一样多的类目(categories)。

我们只会在这个示例App中创建一个通知,所以这里也只会有一个类目(category)。从编程的角度来说,类目(category)就是一个UIMutableUserNotificationCategory类的对象,这也是iOS8新引入的类。这个类只有一个属性和一个方法。标示符属性用来表示一个唯一的类目(category),方法用来将多个动作包含进来。

让我们来了解一下这个方法,先看看这个方法的声明(来自苹果官方文档):

func setActions(_ actions: [AnyObject]!, forContext context: UIUserNotificationActionContext)

第一个参数指明了需要包含进来的动作。是一个包含所有动作的数组,他们在数组中的顺序也代表着他们将会在一个通知中调用的先后顺序。

第二个参数非常重要。context形参是一个枚举类型,描述了通知alert显示时的上下文,有两个值:

  1. UIUserNotificationActionContextDefault:在屏幕的中央展示一个完整的alert。(未锁屏时)
  2. UIUserNotificationActionContextMinimal:展示一个banner alert。

在默认上下文(default context)中,类目最多接受4个动作,会以预先定义好的顺序依次在屏幕中央显示。在minimal上下文中,最多可以在banner alert中设置2个动作。注意在第二个情况中,你必须选择一个较为重要的动作以显示到banner通知里。接下来我们会将这两种情况都用代码实现。

如我所说,上述方法的第一个参数必须为一个数组。所以在我们的配置通知方法中我们首先为两个上下文创建两个数组:

func setupNotificationSettings() {
...

let actionsArray = NSArray(objects: justInformAction, modifyListAction, trashAction)
let actionsArrayMinimal = NSArray(objects: trashAction, modifyListAction)

}

然后让我们来创建一个新的类目(category)吧,首先我们设置它的标示符(identifier),然后将上面的2个数组分别设置:

func setupNotificationSettings() {
...

// Specify the category related to the above actions.
var shoppingListReminderCategory = UIMutableUserNotificationCategory()
shoppingListReminderCategory.identifier = "shoppingListReminderCategory"
shoppingListReminderCategory.setActions(actionsArray, forContext: UIUserNotificationActionContext.Default)
shoppingListReminderCategory.setActions(actionsArrayMinimal, forContext: UIUserNotificationActionContext.Minimal)

}

然后…这样就行啦,为一个通知相关的动作创建一个类目就这样完成了。

注册通知设置

通过上面的3个部分,我们已经将本地通知的所有新功能已经实现了。现在我们需要将这些设定注册到用户设置中。为了完成这个目标,我们将会用到UIUserNotificationSettings类(iOS8新引入),然后在下面的init方法中,我们会指定通知类型和类目(category)。

convenience init(forTypes allowedUserNotificationTypes: UIUserNotificationType, categories actionSettings: NSSet?)

第一个参数是我们为通知设置的类型,第二个方法是一个集合(NSSet),在这个集合中必须包含一个App所有通知支持的类目。在本例中,我们只有一个类目,但是我们还是需要使用集合来传递它。

下面是代码实现:

func setupNotificationSettings() {
...

let categoriesForSettings = NSSet(objects: shoppingListReminderCategory)

}

现在,我们就可以创建一个UIUserNotificationSettings对象了,然后传入相应的参数。

func setupNotificationSettings() {
...

let newNotificationSettings = UIUserNotificationSettings(forTypes: notificationTypes, categories: categoriesForSettings)

}

最后,让我们将它注册一下吧!

func setupNotificationSettings() {
...
UIApplication.sharedApplication().registerUserNotificationSettings(newNotificationSettings)
}

第一次启动App时上述代码就会执行,它会在用户设置中创建一条我们的App记录。

最后,在我展现一个完整的setupNotificationSettings(),还有一点需要注意。这个方法会在viewDidLoad方法中被调用,这意味着每当App被启动的时候它都会执行一次。很显然一遍又一遍的设置同样地值是在做无用功,这样如果我们将上面的方法用一个if判断执行一下的话就好了。在这个判断中,我们检查通知的类型是否已经被设定了,如果没有if块中的代码就会被执行。

func setupNotificationSettings() {
let notificationSettings: UIUserNotificationSettings! = UIApplication.sharedApplication().currentUserNotificationSettings()

if (notificationSettings.types == UIUserNotificationType.None){
    ...
}
}

首先,我们通过UIApplication的类方法currentUserNotificationSettings()来获取通知的类型。通过这个方法返回的UIUserNotificationSettings类的对象,我们可以检查它的types枚举属性。请记住这个属性为枚举类型。如果它的值为None,那么通知类型就还没有被注册,然后我们就运行上面的方法来注册通知类型,否则什么也不做。

通过上面的代码我们避免了重复注册通知类型。但是如果你想修改通知类型、添加动作或者类目的话,你可以将if开始和结束行注释掉,然后运行一次App。新的设定会被添加,你就能测试一下他们了。然后移除注释,避免重复注册。

好了,设置通知的工作已经完成了。下面你可以看到setupNotificationSettings()方法的完整版本:

func setupNotificationSettings() {
let notificationSettings: UIUserNotificationSettings! = UIApplication.sharedApplication().currentUserNotificationSettings()

if (notificationSettings.types == UIUserNotificationType.None){
    // Specify the notification types.
    var notificationTypes: UIUserNotificationType = UIUserNotificationType.Alert | UIUserNotificationType.Sound


    // Specify the notification actions.
    var justInformAction = UIMutableUserNotificationAction()
    justInformAction.identifier = "justInform"
    justInformAction.title = "OK, got it"
    justInformAction.activationMode = UIUserNotificationActivationMode.Background
    justInformAction.destructive = false
    justInformAction.authenticationRequired = false

    var modifyListAction = UIMutableUserNotificationAction()
    modifyListAction.identifier = "editList"
    modifyListAction.title = "Edit list"
    modifyListAction.activationMode = UIUserNotificationActivationMode.Foreground
    modifyListAction.destructive = false
    modifyListAction.authenticationRequired = true

    var trashAction = UIMutableUserNotificationAction()
    trashAction.identifier = "trashAction"
    trashAction.title = "Delete list"
    trashAction.activationMode = UIUserNotificationActivationMode.Background
    trashAction.destructive = true
    trashAction.authenticationRequired = true

    let actionsArray = NSArray(objects: justInformAction, modifyListAction, trashAction)
    let actionsArrayMinimal = NSArray(objects: trashAction, modifyListAction)

    // Specify the category related to the above actions.
    var shoppingListReminderCategory = UIMutableUserNotificationCategory()
    shoppingListReminderCategory.identifier = "shoppingListReminderCategory"
    shoppingListReminderCategory.setActions(actionsArray, forContext: UIUserNotificationActionContext.Default)
    shoppingListReminderCategory.setActions(actionsArrayMinimal, forContext: UIUserNotificationActionContext.Minimal)


    let categoriesForSettings = NSSet(objects: shoppingListReminderCategory)


    // Register the notification settings.
    let newNotificationSettings = UIUserNotificationSettings(forTypes: notificationTypes, categories: categoriesForSettings)
    UIApplication.sharedApplication().registerUserNotificationSettings(newNotificationSettings)
}
}

别忘了在viewDidLoad方法中调用它:

override func viewDidLoad() {
    ...

    setupNotificationSettings()
}

安排本地通知

如果你在iOS之前的版本中使用过本地通知的话,你一定知道安排一个通知是很简单地事情。在iOS8,安排一个通知并没有什么变化。事实上,所有的基本设置都是一模一样的。唯一的新东西就是必须给一个通知设置一个类目,这样通知就能知道当用户点击的时候该启动哪些动作了。

你可能已经猜到了,我们会定义一个新的方法来配置和安排一个本地通知。在我们实现这个方法之前,我们先看看一个本地通知中得重要属性:

  • fireDate:一个通知应当被显示的日期和时间。NSDate对象。
  • alertBody:通知的内容。应当尽量的简洁明了,这样用户才能马上理解它。
  • alertAction:在默认情况下,点击一个banner通知会导致App启动。在以alert形式显示的通知中,会创建一个和这个动作对应的按钮。在此属性中,你必须指定这个按钮的标题。比如,在这个示例App中,我们将View List设置为它的标题或者alert的动作。

牢记以上几点,现在放我们定义这个方法配置这个通知。不用说先让我们创建一个UILocalNotification对象:

func scheduleLocalNotification() {
var localNotification = UILocalNotification()
localNotification.fireDate = datePicker.date
localNotification.alertBody = "Hey, you must go shopping, remember?"
localNotification.alertAction = "View List"

}

还有一点,我们必须指定用户点击通知后对应的类目动作。回忆一下,我们前面已经定义了一个类目和类目标示符,我们在这里就能使用到这个标示符了:

func scheduleLocalNotification() {
...

localNotification.category = "shoppingListReminderCategory"
}

这很简单。最后,我们需要使用UIApplication的scheduleLocalNotification(_:) 方法来真正的安排一个通知,不然这个通知永远都不会“通知”到你啦。

func scheduleLocalNotification() {
...
UIApplication.sharedApplication().scheduleLocalNotification(localNotification)
}

接下来,让我们调用这个方法吧。让我们在scheduleReminder(sender:) 按钮方法中加入新的代码,在显示tableview之前我们调用上面的代码。注意,这里有一点需要避免:如果我们在已经安排了一个本地通知以后在重新安排一个,之前的通知仍然会有效。如果我们忽略了这一点,我们可能会创建许多个不需要的通知。为了避免这样,我们简单的在date picker显示的时候移除所有已经安排的通知,这样就没事啦。下面是IBAction方法代码:

@IBAction func scheduleReminder(sender: AnyObject) {
if datePicker.hidden {
    animateMyViews(tblShoppingList, viewToShow: datePicker)

    UIApplication.sharedApplication().cancelAllLocalNotifications()
}
else{
    animateMyViews(datePicker, viewToShow: tblShoppingList)

    scheduleLocalNotification()
}

txtAddItem.enabled = !txtAddItem.enabled
}

这就是啦。在配置了类型、动作和其他的细节之后,安排他是一件很简单的事情。有了上面的代码,现在通知已经可以如我们期望的工作了。

修复安排通知的时间问题

目前为止我们都做的很好,每个功能都工作的很完美。但是,当你运行这个程序的时候你可能会发现一个关于通知送达时间的问题尽管它看起来是正常的,我发现它存在一点问题。所以,在我们测试通知之前,让我先深入的讲一下这个问题。也许在你运行这个App的时候并不会察觉这个问题,但是在实时性强的App中,通知推送的时间非常重要,而且不准确的时间可能会造成非常严重的问题。

那么这个问题是什么呢?嗯,接下来让我以一个例子来说明它:如果你在10:23:14的时候安排了一个通知在14:00被推送(不用管日期,假设是同一天),这个通知其实不会在14:00:00的时候被推送。而是在14:00:14的时候。我认为对于通知来说这是一个非常严重的问题。为什么会这样呢?那是因为在date picker中,我们可以设置时间,但是却不能设置到秒。然后系统就会将我们设置时的时间的秒赋值给通知的推送时间,而不是使用0。

那么,我们该怎样修复这个BUG呢?很简单,通过代码来修复。如果你以前没有接触过日期和时间的话也不用担心。,这将是一个简单而有趣的任务。

一个日期对象(NSDate对象)可以分为几个部分,叫做date components。这些component是NSDateComponents类的属性,可以可以同时读和写,所以当我们从date picker选择的日期得到了这些components之后我们就可以修改其中的seconds属性了。很明显,通过这些components我们可以重新得到一个NSDate对象,有了这个对象之后,我们就可以方便的将其设置为通知的推送日期了。

正如你在下面的实现中所看到的,将一个日期对象转换成date components依赖于NSCalendar类。这个类提供让我们完成这些工作的方法。

func fixNotificationDate(dateToFix: NSDate) -> NSDate {
var dateComponets: NSDateComponents = NSCalendar.currentCalendar().components(NSCalendarUnit.DayCalendarUnit | NSCalendarUnit.MonthCalendarUnit | NSCalendarUnit.YearCalendarUnit | NSCalendarUnit.HourCalendarUnit | NSCalendarUnit.MinuteCalendarUnit, fromDate: dateToFix)

dateComponets.second = 0

var fixedDate: NSDate! = NSCalendar.currentCalendar().dateFromComponents(dateComponets)

return fixedDate
}

详细讨论NSCalendar并不是我们的目的。但是这也将是一个非常有趣的话题,所以你可以通过这里来了解更多有关NSCalendar的东西。

现在是时候选择一个合适的地点来调用这个方法了,在设置通知推送时间的时候调用这个方法将是一个很好地选择。返回scheduleLocalNotification()方法,将date picker选择的日期作为参数传入此方法,然后将此方法返回的日期设置为通知的fireDate。下面是更新后的scheduleLocalNotification()方法:

func scheduleLocalNotification() {
var localNotification = UILocalNotification()
localNotification.fireDate = fixNotificationDate(datePicker.date)
localNotification.alertBody = "Hey, you must go shopping, remember?"
localNotification.alertAction = "View List"
localNotification.category = "shoppingListReminderCategory"

UIApplication.sharedApplication().scheduleLocalNotification(localNotification)
}

现在,我们的通知终于可以如愿以偿的按时被推送了!

处理通知动作

现在关于通知,我们只差最后一个部分了,那就是处理用户点击通知相关按钮时候的各种动作。和往常一样,这里有几个主要的委托方法我们需要实现。

在我们实现和处理动作之前,让我给你介绍几个代理方法,通过他们你可以方便的开发你的App。注意,在这个示例程序中,我们并不会真正的使用到他们,我们只会通过他们打印一些消息。现在,请打开AppDelegate.swift文件。

第一个代理方法是关于通知设置的。这个代理方法在程序启动时被调用(不管是正常启动还是通过一个本地通知),包含了所有App通知的设置选项。接下来你可以看到它的定义。我们所做的,仅仅是打印通知类型:

func application(application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) {

println(notificationSettings.types.rawValue)
}

通过上述的方法,你可以得到所有UIUserNotificationSettings支持的类型。当你需要检查你的App所支持的通知和动作的类型时,这个方法非常有用。别忘了,用户可以通过用户设置来改变通知类型,所以我们不能保证,初始的通知类型一直都有效。

当你安排了一个通知之后,无论你的App是否在运行,这个通知都将被推送。通常情况下,开发者设置通知如何在App没有运行或者被挂起的时候被推送,所有的代码实现也聚焦在这两个方面。但是,我们也应该处理当App在运行时通知如何被处理。感谢苹果,iOS SDK让这变得非常简单,有一个代理方法正可以处理这种情况:

func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
// Do something serious in a real app.
println("Received Local Notification:")
println(notification.alertBody)
}

当然在某些情况下在App运行时你并不需要处理通知。但是在另外一个情况下,上面的代理方法是处理通知动作的地方。

现在让我们来看看当用户点击了一个通知动作按钮后将会调用的代理方法。更具我们给动作设置的标示符(identifier),我们决定那个动作被调用,然后App就会执行对应的代码了。我们会有三种动作:

  1. 简单地让通知消失(标示符:justInform)。
  2. 添加一个新的物品(标示符:editList)。
  3. 删除整个物品清单(标示符:trashAction)。

从上面我们看出,我们不需要对第一个动作做任何事情。但是我们需要处理另外两种动作。我们将根据identifier的值给每一种情况发送一个NSNotification,在ViewController类中,我们监视这些NSNotification,然后我们处理他们。

让我们从新的代理方法开始:

func application(application: UIApplication, handleActionWithIdentifier identifier: String?, forLocalNotification notification: UILocalNotification, completionHandler: () -> Void) {

if identifier == "editList" {
    NSNotificationCenter.defaultCenter().postNotificationName("modifyListNotification", object: nil)
}
else if identifier == "trashAction" {
    NSNotificationCenter.defaultCenter().postNotificationName("deleteListNotification", object: nil)
}

completionHandler()
}

在上述几种情况中我们根据动作的的标示符,发送不同名称的NSNotification对象。注意到,我们在方法的结束调用了completionHandler()方法,根据规定我们必须调用它,这样系统才能知道我们已经处理完了通知动作。在处理本地通知时,这个代理方法非常重要,在这里你通过用户的点击执行相应地代码。

接下来,让我们打开ViewController.swift文件。首先,让我们监视我们之前发送的NSNotification。在viewDidLoad中加入下面的代码:

override func viewDidLoad() {
...

NSNotificationCenter.defaultCenter().addObserver(self, selector: "handleModifyListNotification", name: "modifyListNotification", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "handleDeleteListNotification", name: "deleteListNotification", object: nil)
}

modifyListNotification()和deleteListNotification()方法都是我们自定义的方法,我们接下来会实现他们。

首先我们实现第一个方法。因为App是通过用户点击了编辑物品动作启动的,所以我们需要将textfield控件设置为第一响应。完成此任务,只需加入一行代码:

func handleModifyListNotification() {
txtAddItem.becomeFirstResponder()
}

通过这行代码,键盘会自动展现然后用户就能立即添加一个新的物品了。

通过删除清单按钮,我们希望从物品对象数组移除所有的物品。接下来,我们首先移除shoppingList数组中得所有对象,然后将它(空数组)保存到磁盘。最后我们重新加载tableview的数据,这样的话,当用户启动App时就什么物品都看不到了。

func handleDeleteListNotification() {
shoppingList.removeAllObjects()
saveShoppingList()
tblShoppingList.reloadData()
}

有了上面的代码实现,我们这个示例程序现在终于完成啦!

启动示例App

是时候测试一下我们的App了,首先在模拟器或者真机中启动它。添加一些物品,然后安排一个本地通知。为了避免等待太久时间,安排他在1到2分钟之后被推送,然后退出App。下面的图片模拟了上述的几个操作,和通知在不同的情况被推送的样子。

添加一个新的物品到清单:

安排一个本地通知:

banner形式展现通知,查看动作(minimal context):

alert形式展现通知,所有动作(default context):

在通知中心展示通知(minimal context):

为了在banner和alert之间切换,在你的设备中打开设置App。找到Shopping Alert选项,点击进入。

在下图标记的地方,根据你希望通知展示的样子选择alert或者banner。

退出设置App,安排一个新的本地通知,这样你就能你选择的结果了。

总结

在iOS8当中,通知看起来更好用了,用户现在甚至可以直接处理通知而不用启动App。在本篇文章中我们提到了几个新的概念,有新也有旧。重要的是,现在设置通知的类型、动作和类目都十分简单方便,如果你的App需要通知,那就使用他们吧!正如我在介绍中所说,还有另外几种通知,如远程和地点。尽管我们没有去实现他们,在知道了本地通知是如何工作了之后,你也会知道如何实现其他通知的大致路径了。以后你只需要去搜索他们的实现具体细节。好了,上面所有就是我敬献给你的,静下来好好思考一下。玩得开心!

你可以从这里下载完整的项目以供参考。

原文:Creating Interactive Local Notifications in iOS 8