随着 iOS 8 的发布,苹果为开发者们开放了很多新的 API,而在这些开放的接口中 通知中心插件 无疑是最显眼的一个。通知中心就不用过多介绍了,相信大家对这个都很清楚了。在以往的 iOS 版本中,我们只能使用 iOS 系统自带的有限的几个 通知中心组件。 这次新开放的这个功能,就相当于为大家提供了一个全新的市场。相信通过大家的智慧创造,一定会出现很多非常流行的应用。

其他就不多说了,现在我们来以一个新闻插件作为例子,来为大家介绍如何来创建一个 通知中心 插件。我们做好之后,大概就是这个样子:

<!-- more -->

准备工作

我们要开发的是一个简单的新闻插件,那么这就需要一个数据源。 我们这里使用 BBC News 的 RSS 订阅接口来作为新闻数据的来源。

http://feeds.bbci.co.uk/news/rss.xml

这是一个标准的 RSS 2.0 接口,它用 XML 格式返回新闻的数据。如果对 RSS 不了解的话可以参看这篇关于它的介绍 http://en.wikipedia.org/wiki/RSS

打开这个 RSS 接口后,我们会看到类似这样的 XML 数据:

<rss xmlns:media="http://search.yahoo.com/mrss/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"\>  
  <channel> 
    <title>BBC News - Home</title>  
    <link>http://www.bbc.co.uk/news/#sa-ns_mchannel=rss&ns_source=PublicRSS20-sa</link>  
    <description>The latest stories from the Home section of the BBC News web site.</description>  
    <language>en-gb</language>  
    <lastBuildDate>Tue, 30 Dec 2014 23:52:29 GMT</lastBuildDate>  
    <copyright>Copyright: (C) British Broadcasting Corporation, see http://news.bbc.co.uk/2/hi/help/rss/4498287.stm for terms and conditions of reuse.</copyright>  
    <image> 
      <url>http://news.bbcimg.co.uk/nol/shared/img/bbc_news_120x60.gif</url>  
      <title>BBC News - Home</title>  
      <link>http://www.bbc.co.uk/news/#sa-ns_mchannel=rss&ns_source=PublicRSS20-sa</link>  
      <width>120</width>  
      <height>60</height> 
    </image>  
    <ttl>15</ttl>  
    <atom:link href="http://feeds.bbci.co.uk/news/rss.xml" rel="self" type="application/rss+xml"/>  
    <item> 
      <title>Poppy duo and acting stars honoured</title>  
      <description>The creators of the World War One ceramic poppy display at the Tower of London join acting grandees Joan Collins and John Hurt on the New Year Honours list.</description>  
      <link>http://www.bbc.co.uk/news/uk-30633687#sa-ns_mchannel=rss&ns_source=PublicRSS20-sa</link>  
      <guid isPermaLink="false">http://www.bbc.co.uk/news/uk-30633687</guid>  
      <pubDate>Tue, 30 Dec 2014 23:31:59 GMT</pubDate>  
      <media:thumbnail width="66" height="49" url="http://news.bbcimg.co.uk/media/images/79989000/jpg/_79989926_honours2_composite.jpg"/>  
      <media:thumbnail width="144" height="81" url="http://news.bbcimg.co.uk/media/images/79989000/jpg/_79989927_honours2_composite.jpg"/> 
    </item>  
  </channel>
</rss>

注意一下 <item>节点,这个节点里面包含了我们每条新闻的具体数据,比如新闻标题,描述,发布日期等等。

有了这个数据源,我们就可以开始专注于代码的编写啦。首先,我们要创建一个新项目。打开 Xcode,然后进入 File > New Project... > Single View Application.

然后,再接下来的界面中,填写项目的信息:

ProductName: rsswidget Orgnaization Name: theswiftworld Orgnaization Identifier: com.theswiftworld Language: Swift Devices: iPhone

都填写好后,点击 Next 按钮。 在接下来的界面中,选择一个合适的位置来存放项目文件。然后点击 Create 按钮来创建项目。这样,我们的基础项目就创建好了。

到现在位置,大家可能会发现,我们创建的还是一个普通的 App 项目。因为任何 App Extension 都是需要在一个宿主应用上运行的。比如我们现在在制作的新闻扩展插件,也是需要通过一个原生的 App 来安装到设备上。

那么接下来就是开始创建 App Extension 的过程了,

打开项目的设置页面,点击左下角的加号按钮。

在弹出的窗口中,选择 Application Extension 分类,然后选择该分类下的 Today Extension 选项,然后点击 Next 按钮。

在弹出的 Extension 详细信息窗口中,填写好创建信息:

Product Name: extension Organization Name: theswiftworld

到这里,我们的 Extension 就创建完成了,现在可以在模拟器中运行它,来看到最终的效果了。注意将运行的 Target 选择到 extension。

然后再宿主App中选择 Today 应用:

选择好后,我们就可以在模拟器中,看到我们自己的 App Extension 运行在通知中心里了。 一个 Hello World 显示在通知中心里面。到这里我们关于 Extension 的基础结构搭建就完成了。接下来我们就要考虑如何在我们自定义的 Extension 中显示新闻列表了。

新闻数据源

我们先暂时抛开 Extension 一会儿,现在我们将注意力转移到新闻的数据源中。我们在上面已经介绍了,我们的新闻数据源是以 XML 格式返回给我们的,我们需要用到以下这些第三方库:

Alamofire PKHUD AEXML

下面一一对它们进行介绍:

  • Alamofire

Alamofire 是专门为 Swift 打造的网络操作库,对很多系统方法进行了封装,并提供了方便地异步处理方法和JSON等数据格式的支持。如果你以前用过 AFNetworking,就会对这个库更加了解啦,它其实就是 AFNetworking 的作者 Matt Thompson 的另外一个作品。并且 AFNetworking 中前两个字母 AF 其实就是 Alamofire 的字头缩写。

  • PKHUD

PKHUD

  • AEXML

AEXML

介绍完需要用到的这些库我们就可以继续我们的教程了。

首先,我们需要引入 Alamofire 库,进入 Alamofire 的首页 https://github.com/Alamofire/Alamofire, 然后在右边的菜单中得最下面点击 Download Zip 按钮把它下载下来。将这个 Zip 包解压后的文件夹放到项目的目录中:

然后将 Alamofire 的项目文件拖动到 Xcode 中的项目结构中。

并且将 Alamofire 设置为 宿主应用Extension的依赖库。进入 rsswidgetextension 的 Build Phrases > Target Dependencies,并将 Alamofire 添加为依赖库:

选择 Alamafire 并点击 Add,将库添加进来。

为主应用添加完成后,还要记得给 Extension 也添加一遍哦。

最后再将 Alamofire 添加一遍就完成了。

现在我们将 Alamofire 集成到项目中了,接下来我们来集成 AEXML。

我们进入 AEXML 的主页 https://github.com/tadija/AEXML ,然后点击 Download Zip 将这个库的包下载下来。这个库的集成就非常容易了,只需将解压后包中得 AEXML.swift 文件拖放到 Xcode 项目结构中即可:

怎么样,很简单吧。最后,我们再把 PKHUD 集成进来。

首先,进到 PKHUD 的主页,https://github.com/pkluz/PKHUD, 然后点击 Download Zip 下载 PKHUD 的文件包,然后解压出来,并将整个文件夹放到项目目录中:

然后将项 PKHUD 的项目文件拖动到 XCode 工程结构里面:

这样,我们的集成工作就完成了。

读取数据

现在,我们这个项目所必须得资源库都已经配置好了,接下来我们就来写代码吧!

首先,我们需要一个用于和新闻订阅接口交互的类,我们新建一个实体类 NewsItem

在 XCode 中打开 File > New > File.. 然后选择 Swift File。点击 Next 按钮:

然后将文件名命名为 NewsItem.swift,并且要注意同时选中主应用Extension两个Target:

文件创建好后,我们来看看这个类的代码:

import Foundation

class NewsItem {

    var title:String?
    var link:String?
    var pubDate:String?
    var description:String?
    var thumbnail:String?

    init(title:String, link:String,pubDate:String,description:String, thumbnail:String){

        self.title = title
        self.link = link
        self.pubDate = pubDate
        self.description = description
        self.thumbnail = thumbnail

    }
}

这个类很简单,就是一个对新闻数据的实体封装,里面定义了5个属性,分辨对应:

  • title 新闻标题
  • link 新闻链接
  • pubDate 新闻发布时间
  • description 新闻描述
  • thumbnail 新闻标题图片

还定义了一个构造方法,使用这5个参数来分别对这几个属性进行初始化,这个代码应该很容易理解吧。这里只有一天需要提醒下大家,就是我们看到每个类属性定义后面都有一个问号 ?,这个是 Swift 中的一个特性,它叫做 Optionals,关于这个特性的介绍,可以参看 浅谈 Swift 中的 Optionals 这篇文章,里面有详细的介绍。

有了实体类后,我们还需要一个方法来读取新闻数据,这里我们还是在这个类中来定义这个读取方法:

class func getNews(completionHandler: (Array<NewsItem>) -> Void) {

        //数据结构的URL 地址
        var url:String = "http://feeds.bbci.co.uk/news/rss.xml"

        //使用 Alamofire 库来请求网络
        Alamofire.request(.GET, url).responseString { (_, _, string, err) in

            //验证请求结果
            if(err != nil){
                print(err?.debugDescription)
            }

            var error: NSError?

            //将新闻结构返回的数据转换为 NSData 类型,并准备进行 XML 解析。
            if let xmlData:NSData = string?.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true){

                if let xmlDoc = AEXMLDocument(xmlData: xmlData, error: &error) {

                    var resultNewsList:Array<NewsItem> = Array()

                    for item in xmlDoc.rootElement["channel"]["item"].all {

                        var newsTitle:String = item["title"].value
                        var newsLink:String = item["link"].value
                        var newsPubDate:String = item["pubDate"].value
                        var newsDescription:String = item["description"].value
                        var thumbnail:String = item["media:thumbnail"].all[1].attributes["url"] as String

                        var newsItem:NewsItem = NewsItem(title: newsTitle, link: newsLink, pubDate: newsPubDate, description: newsDescription, thumbnail:thumbnail)
                        resultNewsList.append(newsItem)

                    }

                    completionHandler(resultNewsList)

                }

            }

        }

    }

我们来解释一下上面这段代码, 首先我们定义了一个类方法:

class func getNews(completionHandler: (Array<NewsItem>) -> Void) {

这个类方法接受一个 completionHandler 回调函数,用于在任务完成的进行回调通知。

在这个方法里面,我们定义了变量 url 作为数据接口的地址。随后我们用 Alamofire 库来发送网络请求:

Alamofire.request(.GET, url) 第一个参数是请求方式,在这里我们用 .GET 来请求。第二个参数是发送请求的 url 地址。随后我们用一个回调 responseString { (_, _, string, err) 来接受请求的返回。这个回调方法里面的 string 变量代表这个接口返回的数据。

在回调方法中,我们先将这个 string 变量转换为 NSData,随后交由 AEXML 来处理。

//将新闻结构返回的数据转换为 NSData 类型,并准备进行 XML 解析。
 if let xmlData:NSData = string?.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true){

   if let xmlDoc = AEXMLDocument(xmlData: xmlData, error: &error) {

我们看到 AEXMLDocument 的构造方法接收的是 NSData数据,随后它会返回一个 XML 文档对象,我们这里存放在 xmlDoc 变量中。

接下来我们遍历这个 xmlDoc 对象,并将它里面的数据转换成实体类,保存起来:

var resultNewsList:Array<NewsItem> = Array()

   for item in xmlDoc.rootElement["channel"]["item"].all {

   var newsTitle:String = item["title"].value
   var newsLink:String = item["link"].value
   var newsPubDate:String = item["pubDate"].value
   var newsDescription:String = item["description"].value
   var thumbnail:String = item["media:thumbnail"].all[1].attributes["url"] as String

   var newsItem:NewsItem = NewsItem(title: newsTitle, link: newsLink, pubDate: newsPubDate, description: newsDescription, thumbnail:thumbnail)
   resultNewsList.append(newsItem)

}

这个转换过程应该很容易理解吧,就不多做解释了。 最后,我们调用作为参数传递进来的回调函数 completionHandler 并将我们封装好的实体类数组传递进去,通知上级代码来处理这个新闻列表(我们后面会用这个回调函数来通知 UITableView 刷新数据)。

completionHandler(resultNewsList)

构造 Extension 的 UI 界面

有了数据源的支持,我们接下来就可以创建我们的前端显示了。

首先,我们将 Extension 自带的 Hello World 标签删除掉,打开 MainInterface.storyboard 文件,然后将 UI 中的 Hello World 删除掉:

然后打开 Extension 中的 TodayViewController.swift 文件。 我们加入两个成员变量的定义:

var newsListTableView:UITableView?
 var newsList:Array<NewsItem>?

这两个变量分别代表用于显示新闻数据的 UITableView 和用来存放数据的数组。 然后我们重写这个类的 viewDidLoad() 方法:

override func viewDidLoad() {
        super.viewDidLoad()

        self.preferredContentSize = CGSizeMake(0, 263)
        self.newsListTableView = UITableView(frame: CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height))
        self.newsListTableView?.delegate = self
        self.newsListTableView?.dataSource = self
        self.view.addSubview(self.newsListTableView!)

        NewsItem.getNews { (newsList) in

            self.newsList = newsList
            let table:UITableView? = self.newsListTableView            

            dispatch_async(dispatch_get_main_queue()){

                table!.reloadData()

            }

        }

    }

第一行代码 self.preferredContentSize = CGSizeMake(0, 263) 设置了 Extension 组件在通知中心的高度。

接下来,我们对 UITableView 进行了初始化:

self.newsListTableView = UITableView(frame: CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height))
   self.newsListTableView?.delegate = self
   self.newsListTableView?.dataSource = self
   self.view.addSubview(self.newsListTableView!)

然后用我们前面写的 NewsItem 类的数据读取方法 getNews 来取得新闻数据,并用得到的数据刷新 UITableView 显示。

NewsItem.getNews { (newsList) in

            self.newsList = newsList
            let table:UITableView? = self.newsListTableView            

            dispatch_async(dispatch_get_main_queue()){

                table!.reloadData()

            }

        }

我们在 getNews 的回调方法中将得到的新闻列表存储到属性中,并刷新了 UITableView 的数据显示。这样 viewDidload 方法就完成了。

接下来我们添加 UITableView 的代理方法和数据源方法:

  1. 用于确定 UITableView 的 Section 数量,我们的新闻列表只需要一个 Section。
func numberOfSectionsInTableView(tableView: UITableView) -> Int {

        return 1

    }
  1. 用于确定 UITableView 的显示行数,这里有一个判断,由于我们指定了 extension 的高度为 263,这个高度只可以显示 6 条新闻,所以如果我们的数据源多于 6 条新闻,我们也按 6 条来显示:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

        if(self.newsList != nil){

            if(self.newsList!.count > 6){

                return 6

            }else{

                return self.newsList!.count

            }


        }else{
            return 0;
        }

    }
  1. 处理 UITableView 的选择状态,这个主要是让用户点击完某条新闻后,清空单元格的选中状态。
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

        tableView.deselectRowAtIndexPath(indexPath, animated: false)

    }
  1. 构造 UITableView 的每个单元格,用我们保存的 newsList 里面的实体类的来构造每个单元格。
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

        var cellIdentifier:String = "cellIdentifier"

        var cell:UITableViewCell? = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as UITableViewCell?

        if(cell == nil){

            cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: cellIdentifier)

        }

        cell?.textLabel.textColor = UIColor.lightGrayColor()
        cell?.textLabel.text = self.newsList![indexPath.row].title!

        return cell!

    }

经过一番折腾,我们的 extension 基本完成了,现在我们可以运行它看看效果了。还是将 Target 设置为 extension,并选择一个合适的模拟器来运行,我们会看到这样的效果:

是不是发现有些问题呢,新闻列表整体向右偏移了一块。这是因为 extension 默认的内容有一个左边距,我们需要设置一下才可以正常显示,所以我们还需要在 TodayViewController 中重写一个方法:

func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets {

        return UIEdgeInsetsZero

    }

这个方法会将 Extension 的边距设置为 0,这样我们的新闻列表就显示正常啦。 再次运行 Extension,我们会在模拟器中看到:

这样我们的新闻列表就算是完成了,到这里我们对 Extension 差不多有了一个初步的认识啦,并且我们自己完成了一个 Extension 的制作。但这个 Extension 还需要进一步的完善,比如这个列表的排版还很初级,而且点击这个列表里面的新闻后,没有任何的反应,我们还需要一个新闻内容的显示界面来响应从通知中心的点击,等等。

相信读完这篇后,大家对通知中心扩展插件已经有了一个比较具体的认识啦,我们会在下一部分继续介绍这个通知中心扩展的继续完善过程,让大家对它的运用有更深入的了解,同时,希望大家继续支持.