使用 Swift 制作一个新闻通知中心插件(1)

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

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

Swift 制作一个新闻通知中心插件1-LMLPHP

<!-- more -->

准备工作

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

这是一个标准的 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&amp;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&amp;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&amp;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.

Swift 制作一个新闻通知中心插件1-LMLPHP

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

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

Product Name: extension Organization Name: theswiftworld

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

新闻数据源

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

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

  • Alamofire

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

  • PKHUD

PKHUD 是用 Swift 写的一个非常简洁健壮的浮出框组件,提供了多种显示方式,我们这里用到它来作为我们的加载提示框。

  • AEXML

AEXML 是用来解析 XML 数据的 Swift 库,我们这里的新闻数据源是 XML 格式的,所以我们用到它来解析我们收到的 XML 数据。

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

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

读取数据

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

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

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

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 删除掉:

Swift 制作一个新闻通知中心插件1-LMLPHP

然后打开 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,并选择一个合适的模拟器来运行,我们会看到这样的效果:

Swift 制作一个新闻通知中心插件1-LMLPHP

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

    func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets {

        return UIEdgeInsetsZero

    }

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

Swift 制作一个新闻通知中心插件1-LMLPHP

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

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

更多文章请访问: www.theswiftworld.com

04-14 18:39