Facade设计模式

pic

Facade设计模式为多个子模块或子系统提供统一的、单独的API接口。也就是说,不用给用户暴露一堆乱七八糟的接口,只需要暴露一个简单的、标准的接口即可。

下面这张图能更好的描述这个概念:

pic

用户在使用我们暴露的标准的API时,根本不知道在这个API底下其实疏导着大量复杂的接口。该设计模式是暴露大量接口的最佳模式,尤其当接口使用很复杂、很难理解时,尤为体现该模式的价值。

Facade模式可以有效保证调用各个模块功能的接口和你隐藏起来的实现模块功能的逻辑代码是一个松耦合的关系。同时也可以减少你的子系统或子模块对外部代码的依赖。例如一个遵循Facade模式的类,如果逻辑有变化,需要修改内部实现代码,但是并不需要修改该逻辑的接口。使用接口的用户甚至根本都不知道你已经修改了该接口背后的某些逻辑功能。

如何使用Facade设计模式

目前,你们已经有PersistencyManager类用于在本地保存专辑的数据,以及HTTPClient类用于和远程服务器进行交互。项目中的其他类压根不会知道这两个类中具体的处理逻辑,因为他们将要遵循Facade模式,隐藏与LibraryAPI身后。

要遵循Facade模式,我们要有一个暴露给用户接口的类,那就是LibraryAPI,它持有PersistencyManagerHTTPClient的实例,并暴露一些简单的接口来访问这两个类中的方法:

pic

LibraryAPI就是作为暴露给其他类的标准接口,它能有效的向接口使用者规避掉HTTPClientPersistencyManager复杂的逻辑代码。

打开LibraryAPI.swift文件添加如下常量属性:


private let persistencyManager: PersistencyManager
private let httpClient: HTTPClient
private let isOnline: Bool

isOnline属性决定了是否向远程服务器上更新专辑数据的变化,比如添加或删除专辑。

然后需要添加init初始化方法:


override init() {
persistencyManager = PersistencyManager()
httpClient = HTTPClient()
isOnline = false

super.init()
}

因为本篇教程中的示例应用只是为了向大家介绍如何运用各种设计模式,所以没有远程服务器的需求,HTTPClient自然不会用到。所以这里isOnline属性总是设置为false

下一步,向LibraryAPI.swift文件中添加如下三个方法:


func getAlbums() -> [Album] {
return persistencyManager.getAlbums()
}

func addAlbum(album: Album, index: Int) {
persistencyManager.addAlbum(album, index: index)
if isOnline {
httpClient.postRequest("/api/addAlbum", body: album.description())
}
}

func deleteAlbum(index: Int) {
persistencyManager.deleteAlbumAtIndex(index)
if isOnline {
httpClient.postRequest("/api/deleteAlbum", body: "\(index)")
}
}

大家看一下addAlbum(_:index:)方法,该方法顾名思义是添加专辑的方法,在实现时会先更新本地的数据,如果网络连通且需要使用远程服务的时候,再调用远程服务接口更新数据状态。当该模块以外的类调用了LibraryAPI接口的addAlbum方法时,它并不知道添加专辑的具体实现逻辑,并且也不需要知道,这就是Facade设计模式的魅力所在。

现在编译运行你们的应用。你们会看到两个空的视图,和一个Toolbar。顶部的视图用于显示专辑的封面,下面的视图会以列表的形式显示该专辑的相关信息。

pic

接下来的工作就是将专辑的数据或图片等显示在屏幕上,这就给我们带来了学习另一个设计模式的机会 – Decorator设计模式

Decorator设计模式

Decorator模式可以自动的为对象添加某些行为或响应能力,并且不需要对该对象做任何修改。

该模式可以通过将希望添加的行为或响应能力打包到另一个对象中,然后通过该对象获得添加的行为或响应能力。

在Swift中,有两种最为常用的实现该模式的方案: ExtensionsDelegation

Extensions

你们可以对class、struct或者enum添加Extension,用于添加新的行为或响应能力,最关键的是不需要让它们继承乱七八糟的父类。更厉害的是通过Extension你们甚至不需要去访问目标class、struct或enum,就可以给它们添加新的能力。这意味着,你们可以让Cocoa框架中的对象元素更加符合你们自己的口味,比如给UIView或UIImage添加你们想要的能力。这就是Extension机制的强大之处。

这里要注意,通过Extension新添加的方法在编译阶段被加载,然后你们可以像使用某类的原生方法一样使用扩展的方法。但是与传统的Decorator模式有些许不同的是,扩展体不会持有被扩展对象的实例,说白了就是你不能实例化一个扩展体,只能通过实例化被扩展的对象,才能使用其扩展的方法。

如何使用Extensions

下图中展示了一种情况,你现在有了Albun对象,然后你想把该对象中的数据展示在一个UITableView中:

pic

在该情况下,专辑的这些数据从哪来呢?很明显是从Album对象中获取,因为它是一个Model对象,而且它根本不关心它持有的数据要如何展现,展现在哪里。那么这时,你就要使用额外的代码让Album具备将数据按需归位的能力,并且不能直接修改该类。

这就需要使用Extension机制了,你们将通过Extension机制扩展Album类,目的是给Album添加一个方法,并返回一个UITableView便于使用的数据结构。

该数据结构图如下:

pic

接下来我们新建一个Swift文件,名为 AlbumExtensions ,然后打开 AlbumExtensions.swift 文件,添加如下代码:


extension Album {
func ae_tableRepresentation() -> (titles:[String], values:[String]) {
return (["Artist", "Album", "Genre", "Year"], [artist, title, genre, year])
}
}

这里注意该方法名的开头ae_,它是AlbumExtensions的缩写。这是一个约定俗成的扩展方法名的写法,目的在于防止扩展方法与原生方法名产生冲突。

注意:通常类可以重写父类的方法或属性,但是在Extension中不可以。Extension中的方法名、属性名也不能和原生方法名、原生属性名相同。

我们再来总结一下Extension机制:

  • Album被扩展后,直接使用Album访问扩展方法。
  • 你们通过扩展的方式已经给Album添加了新的能力,如果你们还想通过子类的方式,那么依然可以这么做。
  • 上面这个简单的扩展可以看出,你们不用对Album做任何改动,它就具备了可以返回适用于UITableView数据格式的能力。

Delegation

另一个Decorator设计模式是Delegation,它是一种代表其他对象或协调其他对象的机制。

比如当你使用UITableView时,你必须要实现tableView(_:numberOfRowsInSection:)方法,你不要期望UITableView会知道你希望每个Section里有多少行。因此,计算每个Section有多少行的任务就交给了UITableView的代理。这样就可以使UITableView显示与数据松耦合,独立开来。

下图是UITableView和它的代理之间的运行关系示意图:

pic

UITableView的工作是将一些信息展示在一个列表视图中。但是它只关注于展示,并不会持有需要展示的数据。那么这时就需要向它的代理询问获取相关的信息了。在Objective-C的代理模式中,代理或协议可以申明两种类型的方法,一种是必须类型,另一种是可选类型。前者是遵循该协议的类必须要实现的方法,后者是可以实现,也可以不实现的方法。你们在该教程中会实践到这些内容。

这时大伙可能会有疑问了,为何我们要使用协议而不直接继承一个对象,然后重写需要的方法呢?这样不是更省事么?但是你们考虑一下,如果这样做,你们只能基于一个单独的父类去实现其子类,也就是说,该父类只能作为某一个对象的代理。如果想让一个对象成为多个对象的代理,那么用继承父类这种形式就行不通了。

注意:代理机制是一个很重要的模式。Apple在UIKit框架中大量应用了该模式,比如UITableViewUITextViewUITextFieldUIWebViewUIAlertUIActionSheetUICollectionViewUIPickerViewUIGestrueRecognizerUIScrollView等等。

如何使用Delegate模式

打开ViewController.swift文件,然后添加以下私有属性:


private var allAlbums = [Album]()
private var currentAlbumData : (titles:[String], values:[String])?
private var currentAlbumIndex = 0

然后重写viewDidLoad方法:


override func viewDidLoad() {
super.viewDidLoad()
//1
self.navigationController?.navigationBar.translucent = false
currentAlbumIndex = 0

//2
allAlbums = LibraryAPI.sharedInstance.getAlbums()

// 3
// the uitableview that presents the album data
dataTable.delegate = self
dataTable.dataSource = self
dataTable.backgroundView = nil
view.addSubview(dataTable!)
}

我们对上述的代码来进行一一讲解:

  1. 关闭导航栏的透明效果。
  2. 通过API获取所有专辑的列表。这里要记住,要使用符合Facade模式的LibraryAPI而不是直接使用PersisencyManager
  3. UITableView进行相关设置,将它的delegate和datasource设置为当前的ViewController。这样一来UITableView所有对数据的请求都会由当前的ViewController响应并提供了。这里需要注意的是如果你们在Storeboard中创建UIViewControllerUITableView,那么也可以在Storeboard中通过拖拽来设置delegate和datasource。

接下来,在ViewController.swift中添加如下方法:


func showDataForAlbum(albumIndex: Int) {
// 保证代码健壮性,确保专辑数至少大于0,避免数组下标越界的错误
if (albumIndex < allAlbums.count && albumIndex > -1) {
//获取专辑对象
let album = allAlbums[albumIndex]
// 保存专辑的数据,用于一会在tableview中展现
currentAlbumData = album.ae_tableRepresentation()
} else {
currentAlbumData = nil
}
// 我们已经获取到了需要的数据,可以刷新tableview来显示数据了
dataTable!.reloadData()
}

showDataForAlbum()方法的作用是从专辑数组中获取到专辑的对象,然后请求并保存专辑的数据。当你想显示新的数据或有改变的数据时,你只需要调用reloadData方法即可。这样UItableView会重新请求它的代理获取相关数据,比如一共要显示多少个Section、每个Section里显示多少行、每行看起来是什么样的等等。

我们在viewDidLoad方法中再加入一行代码:


self.showDataForAlbum(currentAlbumIndex)

该行代码让应用启动时就开始加载专辑数据。因为之前已经将currentAlbumIndex的值设为了0,所以从专辑数组中的第一个专辑开始显示。

现在,是时候实现DataSource协议的方法了,你们可以将DataSource的方法直接写在ViewController里。也可以通过使用扩展的方式使代码保持整洁。

如果使用扩展的方式,那么一定要确保它们是写在文件的最下面,并且要在ViewController类定义的大括号之外!


extension ViewController: UITableViewDataSource {
}

extension ViewController: UITableViewDelegate {
}

上面这两行代码的含义就是ViewController通过扩展的方式遵循了Delegate和DataSource协议 – 你可以把协议想象成是与委托之间的约定,只要你实现了约定的方法,就算是实现了委托。在我们的代码中,ViewController需要遵守UITableViewDataSourceUITableViewDelegate这两个协议。这样 UITableView 才能明确的知道,需要用到的代理中的方法是由这个ViewController实现的。

在遵循UITableViewDataSource协议的扩展中添加如下代码:


func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let albumData = currentAlbumData {
return albumData.titles.count
} else {
return 0
}
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
if let albumData = currentAlbumData {
cell.textLabel?.text = albumData.titles[indexPath.row]
if let detailTextLabel = cell.detailTextLabel {
detailTextLabel.text = albumData.values[indexPath.row]
}
}
return cell
}

tableView(_:numberOfRowsInSection:)方法返回每个Section中显示多少行内容,这里对应着专辑数据结构中的标题。

tableView(_:cellForRowAtIndexPath:)方法会逐个创建每行的内容,包括专辑标题和它的值。

注意:你们可以把这些协议的方法直接加在类声明里面,也可以放在扩展里,编译器不会去管DataSource的方法是放在扩展里还是类申明里,只要实现了必须的方法即可。而我们之所以这样写,是为了保证代码的整洁性和可读性。

编译并运行你们的项目,你们的应用应该已经可以显示出专辑的基本信息了:

pic

未完待续……

原文地址:Introducing iOS Design Patterns in Swift

iOS设计模式 - 大伙应该都听说过,但是有多少人真正的了解他们呢?虽然大多数开发者都认同设计模式的重要性,但是在实际开发中却并不怎么注意使用设计模式,而且关于设计模式的文章也是凤毛麟角,这更使得开发者无从下手去学习设计模式。

设计模式是一个处理软件设计中常见问题的解决方法,并可以重复使用。它向开发者提供了设计模板,使开发者更容易写出逻辑清晰、具有可复用性的代码。它还可以使代码具有松耦合性,能让开发者轻松的更新或替换项目中使用的组件。

在本教程中,大伙要开发一个音乐仓库应用,能显示你们收藏的专辑以及相关信息。

在开发过程中,大伙会逐渐掌握大多数通用的Cocoa设计模式:

  • 构建设计模式:Singleton。
  • 架构设计模式:MVC,Decorator,Adapter,Facade。
  • 行为设计模式:Observer,Memento。

千万不要认为该文章只是对设计模式理论上的讲解,大伙需要在你们的音乐仓库应用中运用到这些设计模式。你们的应用最终看起来大概是这个模样:

pic

准备开始

从这里下载初始项目,解压Zip文件,然后在Xcode中打开BlueLibrarySwift.xcodeproj工程。

在开始之前,有三件事需要大伙注意:

  • ViewController中有两个IBOutlet连接着Storyboard中的TableView和Toolbar。
  • 在Storyboard中的ViewController里含有三个组件,并且设置了AutoLayout布局约束。最上面的组件用来显示音乐专辑的封面。中间是一个TableView,用来显示与该专辑相关的信息。最下面是两个Toolbar按钮,一个是撤销操作按钮,另一个是删除选中专辑的按钮。你们的Storyboard看起来应该是下面这个样子:

pic

  • 在工程里还有一个HTTP Client类(HTTPClient),这个类目前是空的,但是你们在之后会充实它。

注意:你们知道吗,当你们创建了一个新的Xcode项目后,你们所编写的代码其实就已经在遵循一定的设计模式了,Model-View-Controller, Delegate, Protocol, Singleton这些设计模式统统可以免费使用哦。

在带你们深入了解第一个设计模式之前,你们需要创建两个类,用于存储和展示音乐专辑的数据。

点击菜单 File\New\File… (或者按下 Command+N 快捷键)。选择 iOS > Cocoa Touch Class ,点击 Next。设置该类的名称为 Album ,并让它继承 NSObject。最后选择语言为 Swift 然后点击 Next,最后点击 Create

打开Album.swift文件,在Album类中定义如下属性:


var title : String!
var artist : String!
var genre : String!
var coverUrl : String!
var year : String!

然后添加一个初始化方法:


init(title: String, artist: String, genre: String, coverUrl: String, year: String) {
super.init()

self.title = title
self.artist = artist
self.genre = genre
self.coverUrl = coverUrl
self.year = year
}

这段代码为Album类创建了一个初始化方法,当你要创建一个新的专辑时你要通过这个初始化方法,传入专辑名称、演唱者、风格、专辑封面图片的URL以及年份这些属性。

接下来需要再添加一个方法:


func description() -> String {
return "title: \(title)" +
"artist: \(artist)" +
"genre: \(genre)" +
"coverUrl: \(coverUrl)" +
"year: \(year)"
}

description()方法将专辑的这些属性拼成一个字符串,并返回。

我们再次通过刚才创建类的步骤创建一个名为AlibumView的类,注意该类要继承UIView

打开AlbumView.swift文件,在该类中添加两个属性:


private let coverImage: UIImageView!
private let indicator: UIActivityIndicatorView!

coverImage属性用来展示专辑封面,indicator是当正在下载封面图片时转动的菊花。

大伙注意,这里的两个属性分配了private访问级别,也就是说这两个属性只能在AlbumView.swift文件中使用,因为其他的类压根没有必要知道这两个属性的存在。(译者:其实这里用不用private都无所谓,因为咱们写的又不是library或者framework。)

接下来为AlbumView类添加初始化方法:


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

init(frame: CGRect, albumCover: String) {
super.init(frame: frame)
backgroundColor = UIColor.blackColor()
coverImage = UIImageView(frame: CGRectMake(5, 5, frame.size.width - 10, frame.size.height - 10))
addSubview(coverImage)
indicator = UIActivityIndicatorView()
indicator.center = center
indicator.activityIndicatorViewStyle = .WhiteLarge
indicator.startAnimating()
addSubview(indicator)
}

由于UIView遵循了NSCoding,而AlbumView又继承了UIView,所以这里需要写一个NSCoder的初始化方法,但我们不会在这个方法中处理什么逻辑,所以调用super.init即可。

AlbumView真正的初始化方法是另外一个init方法,在这个方法中设置了一些默认的属性,比如将背景色设置为黑色、实例化了coverImage属性,并让它与父容器有5px的四周间距、实例化indicator并设置位置及风格。

最后再添加一个方法:


func highlightAlbum(#didHighlightView: Bool) {
if didHighlightView == true {
backgroundColor = UIColor.whiteColor()
} else {
backgroundColor = UIColor.blackColor()
}
}

这个方法是专辑封面高亮开关,高亮时专辑封面背景色为白色,否则为黑色。

现在编译你们的工程,确保一切都没有问题,然后准备开始第一个设计模式的学习。

设计模式的王者 - MVC模式

pic

Model-View-Controller (MVC)设计模式是Cocoa框架的基石,毋庸置疑它是开发者们最常用的设计模式没有之一。它把应用中的对象按它们的角色进行分类,并鼓励开发者按这种角色分类创建项目目录,将代码放置在合适的位置,保证项目结构清晰明确。

顾名思义,MVC中有三种角色:

  • Model:这种对象保存应用程序的数据,并定义如何操作处理这些数据。比如之前你们创建的Album类就是一个Model对象。
  • View:这种对象主要负责Model对象的呈现,以及用户交互。基本上,由UIView衍生出的类都是View对象。在你们的音乐仓库应用中AlbumView就是一个View对象。
  • Controller:这种对象充当着应用程序的协调者,由它来协调所有的事情。它会访问Model对象的数据,然后展示在相应的View对象中;它也会监听用户在View对象上的交互,从而通知Model对象进行相应的数据操作等等。你们应用中的ViewController就是一个Controller对象。

下图可以很好的说明Model对象和View对象是如何通过Controller对象进行通信的:

pic

当Model对象的数据发生改变时,它会通知Controller对象,然后Controller对象更新对应的View对象上展示的数据。当用户在View对象进行了交互操作时,View对象会通知Controller对象,然后Controller对象会更新对应的Model对象中的数据。

你们可能会有这样的疑惑,为什么不把这些操作处理都写在Controller对象中呢,这样就不用通知来通知去的,不是更简单吗?

我告诉你们两个概念,你们就明白这样做的目的了,那就是低耦合性和高复用性。我举个例子,在一个应用中每个界面中的数据大多数是来自于多个Model对象,如果把View对象和Model对象绑定死了,那么就没法处理这种情况了。

拿咱们这个音乐仓库的应用来说,如果你们以后想做一个电影仓库或者图书仓库,你仍然可以使用AlbumView这个View对象来展示你的电影或者图书Model对象。假如你的电影仓库应用需要展示电影主题曲的一些信息,那么或许你就可以直接复用Album对象,因为Album对象不依赖于任何View对象。这就是MVC设计模式的强大之处。

如何使用MVC设计模式

首先,你要确保项目中每个类的功能,要么是Controller,要么是View,要么是Model。千万不要将两种角色混合于一个类,每个类只有单一的职责。不过,到目前为止,你们已经创建了标准的Model类Album和View类AlbumView

其次,为了使项目目录结构清晰明了,以及能够更感官的实行MVC模式,你们需要在工程中创建三个文件组,来区分开这三种角色的文件。

通过File\New\Group(或同时按下Command+Option+N)在工程中创建一个名为 Model 的文件组,然后以同样的方式创建 View 文件组和 Model 文件组。

最后将 Album.swift 文件拖进Model文件组,将 AlbumView.swift文件拖进View文件组,将 ViewController.swift 文件拖进Model文件组。

此时,你们的项目目录结构应该是这样的:

pic

现在的项目结构看起来已然井然有序,当然你们还可以创建其他的文件组和类文件,但是要记住的是,Model、View、Controller这三个文件组是整个程序的核心所在。

现在项目结构已经理清楚了,接下来的工作就需要从某个地方获取到专辑的相关数据。你们可以创建一个名为API的类,用于负责整个应用的数据管理工作。并且将拉开你们要了解的下一个设计模式 – Singleton。

Singleton设计模式

单例模式使一个类在整个应用生命周期内只存在一个实例,并且有一个全局的方法来访问这个实例。在单例模式下,当第一次访问某个类的实例时,该类通常使用延迟加载的方式创建该类的单例。

注意: Apple在iOS和OSX中大量使用了单例模式,比如: NSUserDefaults.standardUserDefaults()UIApplication.sharedApplication()UIScreen.mainScreen()NSFileManager.defaultManager()

你们可能会有疑问,为什么我们要这么在意一个类有一个或多个实例?代码和内存现在是如此的廉价,不是么?

其实不然,有些情况下,确实只需要类实例化一次,且仅有一次。比如有这么一种情况,在一个应用的生命周期里,应用(Application)设备的主屏幕是只存在一份的,那么你当然希望应用和设备屏幕的实例有且只有一个。或者你需要一个全局的处理配置的类,这样能线程安全的访问配置文件,避免多个配置类同时访问一个配置文件。这些就是单例模式的好处所在。

如何使用Singleton设计模式

先看看下面这张图:

pic

上图是Logger的类图,从图中可以看出,Logger有一个instance属性以及sharedInstanceinit两个方法。

当第一次调用sharedInstance方法时,instance属性还没有初始化,所以你会创建一个新的Logger类实例,并返回一个该实例的引用。

当再次调用sharedInstance方法时,instance属性会立即返回,并且不需要再进行任何实例化操作。这个逻辑就保证了Logger类的实例有且仅有一份。

你们将要通过Singleton设计模式,创建一个单例的类用于管理所有专辑的数据。

你们应该注意到了,在项目的目录结构中,有一个文件组叫做API,这个文件组里存放的类基本都是为应用提供服务的类。我们在这个文件组中创建一个名为 LibraryAPI 的类。

打开 LibraryAPI.swift 文件,添加如下代码:


//1
class var sharedInstance: LibraryAPI {
//2
struct Singleton {
//3
static let instance = LibraryAPI()
}
//4
return Singleton.instance
}
  1. 创建一个类变量的计算属性。类变量类似Objective-C中的类方法,也就是说在任何时候你访问sharedInstance属性时,都不需要对LibraryAPI进行实例化,关于属性类型更多的知识请参阅Swift文档 – properties
  2. 在类变量中内嵌一个结构体,名为Singleton
  3. Singleton中包含一个名为instance的静态常量属性。用static申明属性意味着该属性只能存在一份。这里要注意的是Swift中的静态属性都会延迟加载,也就是说只有instance被使用时,才会初始化它。还要注意的一点是,一旦instance被初始化了,那么它就是一个常量属性,不会有第二次初始化的机会了。这就是Singleton模式的精髓所在。
  4. 返回该计算属性的值。

注意:如果想了解Swift中创建单例的其他方法请参阅这里:Github page

你们现在已经有一个单例模式的对象,作为管理专辑数据的入口。接下来要更进一步,创建一个类,用于处理你们数据的持久性。

继续在 API 这个文件组中创建一个类,名为PersistencyManager,并让它继承NSObject类。

打开 PersistencyManager.swift 文件,申明一个属性:


private var albums = [Album]()

这里申明了一个private访问权限的变量属性,用于储存专辑数据。这个数组是可变数组,所以你们可以很轻松的增删专辑。

接下来我们在该类中添加如下初始化方法:


override init() {
//Dummy list of albums
let album1 = Album(title: "Best of Bowie",
artist: "David Bowie",
genre: "Pop",
coverUrl: "http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png",
year: "1992")

let album2 = Album(title: "It's My Life",
artist: "No Doubt",
genre: "Pop",
coverUrl: "http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png",
year: "2003")

let album3 = Album(title: "Nothing Like The Sun",
artist: "Sting",
genre: "Pop",
coverUrl: "http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png",
year: "1999")

let album4 = Album(title: "Staring at the Sun",
artist: "U2",
genre: "Pop",
coverUrl: "http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png",
year: "2000")

let album5 = Album(title: "American Pie",
artist: "Madonna",
genre: "Pop",
coverUrl: "http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png",
year: "2000")

albums = [album1, album2, album3, album4, album5]
}

在初始化方法中,你们可以构建一些专辑添加到albums数组中,这里我构建了5个专辑。

然后再添加几个方法:


func getAlbums() -> [Album] {
return albums
}

func addAlbum(album: Album, index: Int) {
if (albums.count >= index) {
albums.insert(album, atIndex: index)
} else {
albums.append(album)
}
}

func deleteAlbumAtIndex(index: Int) {
albums.removeAtIndex(index)
}

这些方法可以让你们方便的存、取、删除album数组中的专辑。

然后编译你们的项目,确保编译通过。此时大伙也许又有了疑问,我们应该如何使用PersistencyManager类呢?别着急,在下一节里,会向大家介绍 Facade 设计模式,届时你们就会明白LibraryAPIPersistencyManager之间的关系,以及如何使用PersistencyManager了。

未完待续……

原文地址:Introducing iOS Design Patterns in Swift

动态的文本格式和存储

在上篇文章中,同学们已经看到了Text Kit可以根据用户在设置选项里对文本属性设置而动态的改变App中的文本属性。但你们不知道它还有更酷的功能,那就是可以根据文本本身的某种字符而更改文本属性。

比如说,你们也许希望我们这个应用中还有以下这些自动的功能:

  • 被波浪号(~)括起来的文本会显示艺术字体。
  • 被下划线(_)括起来的文本会显示斜体。
  • 被破折号(-)括起来的文本会在其上划一道横线。
  • 所有大写字体会显示红色。

pic

如何用TextKit实现上述这些功能就是这篇文章将要向同学们介绍的内容。

在学习这些之前,大家需要先理解在TextKit中,文本的存储系统是如何工作的。下面的图展示了TextKit中对文本进行存储、渲染、显现的基本流程:

pic

在开发过程中,如果你创建了UITextViewUILabelUITextField,那么同时Apple会在幕后自动创建上图中的这些类。你可以使用这些类默认实现的行为,也可以自己实现这些类,从而达到你想要的一些对文本控制的行为。让我们来看看这些类的作用:

  • NSTextStorage的功能是存储文本,并通过字符串属性渲染文本。当文本内容发生任何变化的时候,它会告知布局管理器(Layout Manager)。你们也许脑海中已经有了一点想法,那就是实现一个NSTextStorage的子类,以便实现当文本更新时动态改变文本属性的功能,Bingo!想法基本正确(在后文中你将会看到它的运用)。
  • NSLayoutManager的功能是从NSTextStorage中获取文本并在屏幕上渲染文本。它将作为你应用中的布局引擎。
  • NSTextContainer的功能是在屏幕上描绘一个几何图形区域,要显示的文本就是在这个区域内进行渲染的。每个Text Container一般都和一和UITextView相关联。你可以实现一个NSTextContainer的子类,描绘一个复杂的几何图形,让文本在其中进行渲染。

如果想要在我们的应用中实现动态文本格式的功能,那么就需要实现一个NSTextStorage的子类,然后根据用户键入的字符动态的设置文本格式属性。

一旦你实现了NSTextStorage的子类,那么你就要替换UITextView默认的存储类示例,同学们接着往下看。

UITextStorage的子类

我们选中左侧项目结构目录中的SwiftTextKitNotepad,然后选择菜单栏中的New File… ,接着选择iOS/Source/Cocoa Touch Class ,点击Next

我们给该类起名为SyntaxHighlightTextStorage,让它继承NSTextStorage,确认Language选项为 Swift,然后点击 Next ,最后点击Create

打开SyntaxHighlightTextStorage.swift文件,申明一个新的属性:


let backingStore = NSMutableAttributedString()

文本存储的子类必须要保证自己的持久性,因此使用NSMutableAttributedString作为backingStore的类型。后面我们会经常用到。

然后在该类中添加如下代码:


override var string: String {
return backingStore.string
}

override func attributesAtIndex(index: Int, effectiveRange range: NSRangePointer) -> [NSObject : AnyObject] {
return backingStore.attributesAtIndex(index, effectiveRange: range)
}

首先我们重写了计算属性stringget方法,让它返回咱们定义的backingStore中字符串的值,同样重写了attributesAtIndex方法,并返回backingStoreattributesAtIndex方法的返回值。

最后重写这些必须要重写的方法:


override func replaceCharactersInRange(range: NSRange, withString str: String) {
println("replaceCharactersInRange:\(range) withString:\(str)")

beginEditing()
backingStore.replaceCharactersInRange(range, withString:str)
edited(.EditedCharacters | .EditedAttributes, range: range, changeInLength: (str as NSString).length - range.length)
endEditing()
}

override func setAttributes(attrs: [NSObject : AnyObject]!, range: NSRange) {
println("setAttributes:\(attrs) range:\(range)")

beginEditing()
backingStore.setAttributes(attrs, range: range)
edited(.EditedAttributes, range: range, changeInLength: 0)
endEditing()
}

和上面一样,这些方法的实现都使用我们自己定义的backingStore属性的相同方法代替。然而,从上面的示例代码中可以看到使用backingStore的相关方法时,它是被包围在beginEditingeditedendEditing三个方法中的。这三个方法是必须要写的,它们的作用是当文本准备被编辑时通知与之相关联的布局管理器(Layout Manager)。

通过上面的示例代码你们可能已经注意到了要实现一个NSTextStorage的子类要重写这么多的方法(写这么多的代码)。因为NSTextStorage是一个类集群的公共接口,所以在实现它的子类时只重写一两个方法是没法真正扩展其功能的,上面这几个主要的方法是我们必须要重写的。

注意:类集群是Apple框架中经常使用的一种设计模式。类集群是Objective-C对抽象工厂设计模式的一种简单的实现,它的作用就是将相似的类聚集到一起,通过一个工厂类暴露给开发者,该工厂类提供若干工厂方法,每个或每几个工厂方法对应其包含的某个类。我们比较熟悉的像NSArrayNSNumber都是类集群。

Apple使用类集群将私有的具体的子类压缩到一个公共的抽象超类中,该超类会申明一些方法,用于创建其包含子类的实例。开发者不必关心公共抽象类内部的事情,也无法得知,因为开发者永远只是和这个抽象类打交道。

使用类集群确实可以简化接口,使类的使用和学习更加简单,但是有一点很重要,那就是在使用类集群时要权衡可扩展性和易用性。比如说如果使用类集群,那么你就很难创建一个个性化的子类。

现在,我们已经有了一个自定义的NSTextStorage,接下来就需要用UITextView来使用它了。

让UITextView使用我们自定义的Text Kit Stack

如果我们在Storyboard中创建一个UITextView,那么就同时会自动创建NSTextStorageNSLayoutManagerNSTextContainer这三个类的实例(也就是Text Kit Stack),并且这三个实例是只读状态的。也就意味着在Storyboard中,我们没法让UITextView使用我们自己定义的Text Kit Stack。真的没招了吗?非也,我们可以在代码中创建UITextView,然后让它使用自定义的Text Kit Stack。

打开Main.storyboard,找到NoteEditorViewController,展开它,在Detail Scene/Detail/View中找到 Text View,然后删掉UITextView实例。

然后打开NoteEditorViewController.swift文件,移除UITextViewoutlet。添加如下代码重新申明UITextViewSyntaxHighlightTextStorage


var textView: UITextView!
var textStorage: SyntaxHighlightTextStorage!

接下来移除viewDidLoad方法中的这两行代码:


textView.text = note.contents
textView.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)

因为我们已经移除了UITextViewoutlet,并且手动申明了新的UITextView,所以这两行就不需要了,因为我们先要对新的UITextView进行初始化和相关设置。

继续在NoteEditorViewController.swift中添加如下代码:


func createTextView() {
// 1. 初始化用于备份编辑器中文本的存储器
let attrs = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]
let attrString = NSAttributedString(string: note.contents, attributes: attrs)
textStorage = SyntaxHighlightTextStorage()
textStorage.appendAttributedString(attrString)

let newTextViewRect = view.bounds

// 2. 创建layoutManager
let layoutManager = NSLayoutManager()

// 3. 创建text container
let containerSize = CGSize(width: newTextViewRect.width, height: CGFloat.max)
let container = NSTextContainer(size: containerSize)
container.widthTracksTextView = true
layoutManager.addTextContainer(container)
textStorage.addLayoutManager(layoutManager)

// 4. 初始化并设置UITextView
textView = UITextView(frame: newTextViewRect, textContainer: container)
textView.delegate = self
view.addSubview(textView)
}

这次加的代码有点多,不过没关系,我们一一来分析:

  1. 初始化我们之前自定义的文本存储类,并定义了文本的属性。
  2. 创建了一个布局管理器。
  3. 创建了一个文本容器,并添加到上步创建的布局管理器中,然后又将布局管理器添加到我们自定义的文本存储器中。
  4. 初始化文本视图,其frame为父视图的大小,textContainer为上步创建的容器。然后设置了文本视图的代理,并将其添加到父视图中。

到目前为止,之前向大家展示过的这张图中的四个关键点(存储,布局器,容器,文本视图)之间的关系应该会更容易了理解了:

pic

这里要注意的是,在上面添加的代码中文本容器的宽度为屏幕的宽度,但是高度是无限大的,这样设置的目的是为了当UITextView中的文本很长的时候可以上下滚动。

现在我们就可以在viewDidLoad方法中调用createTextView()方法了,不过要记得加在super.viewDidLoad()方法的后面:


super.viewDidLoad()
createTextView()

最后还有一点需要注意的是:在代码中创建的自定义的视图不能沿用storyboard中添加的布局约束,这就意味着当改变设备的横竖屏方向时,代码中创建的视图是不能根据布局约束改变大小和位置的。你需要在代码中添加布局约束。

此时我们还需要在viewDidLayoutSubviews方法中加一行代码:


textView.frame = view.bounds

编译并运行应用,在应用中打开一条笔记,然后去编辑它,此时注意观察Xcode的控制台,你可以看到在控制台中像瀑布一般的刷出好多日志信息:

pic

产生这些日志信息的来源是SyntaxHighlightTextStorage类,该类中输出的日志信息可以让你们知道你们自定义的类是否在正常的工作。

现在文本解析器的基本框架看似已经可以稳定的运行了,接下来实现动态文本格式吧!

动态文本格式

在接下来的步骤中,你们要对自定义的文本存储类进行修改,让它实现被星号包围起来的文本加粗的功能。

打开SyntaxHighlightTextStorage.swift文件,添加如下代码:


func applyStylesToRange(searchRange: NSRange) {
// 1. 创建一些字体
let fontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody)
let boldFontDescriptor = fontDescriptor.fontDescriptorWithSymbolicTraits(.TraitBold)
let boldFont = UIFont(descriptor: boldFontDescriptor, size: 0)
let normalFont = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)

// 2. 匹配由星号包围的文本
let regexStr = "(\\*\\w+(\\s\\w+)*\\*)"
let regex = NSRegularExpression(pattern: regexStr, options: nil, error: nil)
let boldAttributes = [NSFontAttributeName : boldFont]
let normalAttributes = [NSFontAttributeName : normalFont]

// 3. 遍历所有匹配上的文本,设置加粗字体属性
regex.enumerateMatchesInString(backingStore.string, options: nil, range: searchRange) {
match, flags, stop in
let matchRange = match.rangeAtIndex(1)
self.addAttributes(boldAttributes, range: matchRange)

// 4. 还原字体格式
let maxRange = matchRange.location + matchRange.length
if maxRange + 1 < self.length {
self.addAttributes(normalAttributes, range: NSMakeRange(maxRange, 1))
}
}
}

上面的代码主要功能是:

  1. 通过UIFontDescriptor创建一个加粗的字体格式和一个正常的字体格式。UIFontDescriptor可帮助你们避免使用硬编码来设置字体的类型和格式。
  2. 创建一个正则表达式,用于查找被星号包围的文本。比如说,这有一个字符串”iOS8是*非常完美*的一个系统”,那么正则表达式就可以将”*非常完美*“过滤出来。如果你不熟悉正则表达式也没关系,在后面会详细给大家介绍的。
  3. 遍历通过正则表达式过滤出的文本,将我们定义的加粗字体属性设置给它们。
  4. 重置跟在最后一个星号后的文本的属性为正常,确保只在闭合星号内的文本才显示为粗体。

在上面的方法后面添加如下方法:


func performReplacementsForRange(changedRange: NSRange) {
var extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRangeForRange(NSMakeRange(changedRange.location, 0)))
extendedRange = NSUnionRange(changedRange, NSString(string: backingStore.string).lineRangeForRange(NSMakeRange(NSMaxRange(changedRange), 0)))
applyStylesToRange(extendedRange)
}

上面这个方法的作用是设置检查被星号包围文本的范围。这个方法是很重要的,因为changedRange通常代表一个字符,而lineRangeForRange将其扩展到一行的范围。

最后在performReplacementsForRange()方法后再添加一个方法:


override func processEditing() {
performReplacementsForRange(self.editedRange)
super.processEditing()
}

当文本发生改变时,processEditing方法会向布局管理器发送通知,告知布局管理器文本发生了变化。

编译运行应用,打开一条笔记,输入一些文本信息,然后用星号包围几段文本,你会看到被星号包围的文本变成了粗体:

pic

是不是很方便呢,你还可以依法炮制,举一反三,比如两个下划线包围的文本显示为斜体等等。

深入完善

实现该功能的基本方法其实很简单:在applyStylesToRange方法中通过正则表达式过滤出你想要的文本信息,然后给过滤出的文本设置新的文本属性。

打开SyntaxHighlightTextStorage.swift文件,添加如下方法:


func createAttributesForFontStyle(style: String, withTrait trait: UIFontDescriptorSymbolicTraits) -> [NSObject : AnyObject] {
let fontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody)
let descriptorWithTrait = fontDescriptor.fontDescriptorWithSymbolicTraits(trait)
let font = UIFont(descriptor: descriptorWithTrait, size: 0)
return [NSFontAttributeName : font]
}

该方法可以创建正文文本字体的样式。在该方法中,构建UIFont时,其构造函数中的size属性设置为0,这是为了让字体的大小使用用户当前设置的字体大小。

接下来,在该类中添加一个属性和一个方法:


var replacements: [String : [NSObject : AnyObject]]!

func createHighlightPatterns() {
let scriptFontDescriptor = UIFontDescriptor(fontAttributes: [UIFontDescriptorFamilyAttribute : "Zapfino"])

// 1. 让我们的手写字体的大小基于首选设置中正文字体的大小
let bodyFontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody)
let bodyFontSize = bodyFontDescriptor.fontAttributes()[UIFontDescriptorSizeAttribute] as NSNumber
let scriptFont = UIFont(descriptor: scriptFontDescriptor, size: CGFloat(bodyFontSize.floatValue))

// 2. 创建一些字体属性
let boldAttributes = createAttributesForFontStyle(UIFontTextStyleBody, withTrait:.TraitBold)
let italicAttributes = createAttributesForFontStyle(UIFontTextStyleBody, withTrait:.TraitItalic)
let strikeThroughAttributes = [NSStrikethroughStyleAttributeName : 1]
let scriptAttributes = [NSFontAttributeName : scriptFont]
let redTextAttributes = [NSForegroundColorAttributeName : UIColor.redColor()]

// 创建一个正则表达式的字典
replacements = [
"(\\*\\w+(\\s\\w+)*\\*)" : boldAttributes,
"(_\\w+(\\s\\w+)*_)" : italicAttributes,
"([0-9]+\\.)\\s" : boldAttributes,
"(-\\w+(\\s\\w+)*-)" : strikeThroughAttributes,
"(~\\w+(\\s\\w+)*~)" : scriptAttributes,
"\\s([A-Z]{2,})\\s" : redTextAttributes
]
}

这个方法主要有以下几个功能:

  • 首先,使用Zapfino字体创建一个手写风格的字体。然后让它的字体大小基于正文文本设置的字体大小。
  • 其次,创建若干用于匹配的字体属性。
  • 最后,创建了一个字典,key为各种正则表达式,值为刚才创建的字体属性。

如果你们不熟悉正则表达式,可能这个字典看起来会比较奇怪。但是如果你们一点一点去解析它,就会发现其实很简单。

我们拿出字典中的第一个正则表达式,也就是过滤被星号围绕的文本的表达式来看看:

(\\*\\w+(\\s\\w+)*\\*)

在正则表达式中一个反斜杠“\”代表将下一个字符标记为特殊字符或一个原义字符,双反斜杠的作用是不会使程序解析时忽略“\”,如果你把额外的一个反斜杠去掉,那么该正则表达式应该是这样:

(\*\w+(\s\w+)*\*)

现在我们来一步一步解析这个正则表达式:

  1. (* - 匹配一个星号。
  2. \w+ - 匹配包括下划线在内任何单词字符以及该字符之前的字符。
  3. (\s\w+)* - 这是一个字表达式,\s的含义是匹配任何空白字符,包括空格、制表符、换页符等,*的含义是匹配前面的子表达式0次或多次。
  4. *) - 匹配后面的星号。

注意:如果你想了解更多关于正则表达式的知识,请查阅这篇文章NSRegularExpression tutorial and cheat sheet

接下来我们在SyntaxHighlightTextStorage类的初始化方法中调用createHighlightPatterns()方法:


override init() {
super.init()
createHighlightPatterns()
}

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

最后,我们来修改applyStylesToRange()方法:


func applyStylesToRange(searchRange: NSRange) {
let normalAttrs = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]

// 遍历每个需要替换字体属性的文本
for (pattern, attributes) in replacements {
let regex = NSRegularExpression(pattern: pattern, options: nil, error: nil)
regex.enumerateMatchesInString(backingStore.string, options: nil, range: searchRange) {
match, flags, stop in
// 设置字体属性
let matchRange = match.rangeAtIndex(1)
self.addAttributes(attributes, range: matchRange)

// 还原字体样式
let maxRange = matchRange.location + matchRange.length
if maxRange + 1 < self.length {
self.addAttributes(normalAttrs, range: NSMakeRange(maxRange, 1))
}
}
}
}

在之前,该方法只使用一个正则表达式过滤出被星号围绕的文本,并将其设置为粗体,现在,虽然该方法所做的事情没有变,但是却不只使用一个正则表达式,而是遍历正则表达式字典中的所有表达式,过滤出文本中所有被符号围绕的文本,然后设置相应的字体样式。

现在再次编译运行应用,你看到了什么?

pic

到目前为止,似乎这个记事本应用已经大功告成了,但是还有几个小问题需要解决。

第一个问题是如果在使用该应用时,改变手机的方向,也就是从竖屏变为横屏,你会注意到屏幕上的内容并没有变化,这是因为自定义的子类不支持这种转变,之前我们也提到过。

第二个问题是当一条笔记中有很多文本信息的时候,文本的底部会被键盘遮住,这时你打字写东西就很困难。

也是时候解决这两个问题了。

回顾动态类型

要想通过动态类型正确的解决该问题,你们得修改代码,当文本显示区域大小发生改变的通知发生时,要通过字体属性来改变字体。

SyntaxHighlightTextStorage类中添加如下方法:


func update() {
// update the highlight patterns
createHighlightPatterns()

// change the 'global' font
let bodyFont = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]
addAttributes(bodyFont, range: NSMakeRange(0, length))

// re-apply the regex matches
applyStylesToRange(NSMakeRange(0, length))
}

上面的方法更新了所有与各种正则表达式相关联的字体,让所有字符串都基于正文文本的属性,然后重新过滤文本。

最后,打开NoteEditorViewController.swift文件,更新preferredContentSizeChanged()方法:


func preferredContentSizeChanged(notification: NSNotification) {
textStorage.update()
updateTimeIndicatorFrame()
}

编译运行应用,在设置中修改文本大小,看看会发生什么:

pic

改变文本视图的大小

剩下的另一个问题就是编辑长文本时,键盘会遮住一半文本的问题。

解决这个问题的核心就是当键盘弹出时减小文本视图的高度。

打开NoteEditorViewController.swift文件,在viewDidLoad()方法中的createTextView()后面加一行代码:


textView.scrollEnabled = true

这行代码允许文本视图开启滚动条。

然后在viewDidLoad()的最后面添加如下代码:


NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardDidShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidHide:", name: UIKeyboardDidHideNotification, object: nil)

这两个通知可以让你捕捉到键盘弹出和收起的事件,好让你修改文本视图的相应大小。

再在该类中添加如下方法:


func updateTextViewSizeForKeyboardHeight(keyboardHeight: CGFloat) {
textView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height - keyboardHeight)
}

该方法让文本视图的高度减去键盘的高度,保证合适的文本视图高度。

最后就是实现keyboardDidShowkeyboardDidHide方法了:


func keyboardDidShow(notification: NSNotification) {
if let rectValue = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue {
let keyboardSize = rectValue.CGRectValue().size
updateTextViewSizeForKeyboardHeight(keyboardSize.height)
}
}

func keyboardDidHide(notification: NSNotification) {
updateTextViewSizeForKeyboardHeight(0)
}

当键盘弹出时,你需要从通知中获取到键盘的高度,然后让文本视图减去这个高度,当键盘收回时,要复原文本视图的高度。

注意:在iOS8之前的版本中,当设备的方向发生变化时,也就是从竖屏变为横屏时,我们要计算新的文本视图的大小(因为UIView的高和宽会交换,但是键盘的高和宽不会交换),但是在iOS8中已经没这个必要了。

现在编译运行应用,选择一条长文本的日记,编辑文本,此时弹出的键盘就不会再遮挡文本信息了:

pic

原文链接:Text Kit Tutorial in Swift

这篇文章向大家展示如何使用WatchKit创建Page-Based Navigation,我们还是在Helloworld项目的基础上进行改善。

创建Controller

打开Interface.storyboard,再添加两个Interface Controller,然后给它们添加不同的背景色。

pic

现在给每个Controller之间添加Segue,选中第一个Controller,按着control+鼠标左键拖拽至第二个Controller,然后选择next page。

pic

编译运行,看看它是如何工作的:

pic

Interface Controller生命周期

接下来我们自定义两个WKInterfaceController,分别设置给新添加的两个Interface Controller。然后我们来看看InterfaceController的生命周期。首先在GreenInterfaceControllerRedInterfaceController中添加如下三个方法:

pic

编译运行,当Watch模拟器显示出内容时,在控制台中我们可以看到GreenInterfaceControllerRedInterfaceController执行了init方法:

pic

当切换到绿色InterfaceController时,在控制台中可以看到GreenInterfaceController执行了willActivate方法:

pic
当切换到粉红色InterfaceController时,在控制台中可以看到RedInterfaceController执行了willActivate方法,而GreenInterfaceController执行了didDeactivate方法:

pic

所以由此可见InterfaceController的生命周期主要由三个方法组成:

  • initWithContext:当Watch App第一次启动,把三个InterfaceController压入栈的时候调用该方法。
  • willActivate:当进入InterfaceController时,调用该方法。类似UIViewController中的viewDidAppear方法。
  • didDeactivate:当InterfaceController不在当前屏幕显示时调用该方法。类似UIViewController中的viewDidDisappear方法。

上篇文章向大家演示了如何创建一个Helloworld Apple Watch App。这篇文章向大家演示如何使用WatchKit给你的Watch App创建Table。我们在Helloworld项目的基础上进行完善。

创建一个Table

打开Interface.storyboard文件,删除我们之前添加的Label,然后将一个Table组件拖拽到你的Watch屏幕中。

pic

这里要注意一下,WatchKit中每一个WKInterfaceTableRow都是一个Table Row Controller,如果你展开它,你会发现它里面有一个Group,我们分别将一个Label和Image组件拖拽至Group中。调整Label和Image的大小,调整到你觉得合适就可以。

pic

创建自定义的Table Row Controller

我们选中TableRowController,然后选择右侧工具栏的Identify inspector,我们可以在Custom Class一栏中看到,其实TableRowController是一个NSObject

pic

我们可以创建一个自己的NSObject,然后将TableRowController的class设置为我们创建的NSObject,这样就达到了自定义的目的。我们在HelloworldWatchKit WatchKit Extension目录中创建一个Swfit文件,让它继承NSObject,然后导入WatchKit框架。

pic

将TableRowController的class设置为MyTableRowController,并给它设置一个Identifier。

pic

pic

现在你就可以在MyTableRowController中添加Label和Image的IBOutlet了。

pic

创建数据源

我们知道,在UIKit的UITableView或者UITableViewController中都有DataSource,负责Table Row中的数据,但是在WatchKit中的TableRowController并没有DataSource,但是这个概念是有的,也就是说我们要为TableRowController创建一个数据源,不论是数组还是字典。在这里,我们创建一个数组,作为TableRowController的数据源,数组中的元素都是String。因为我们的TableRowController中有一个Image组件、一个Label组件,所以我们将图片的名字作为数组的元素。

我们在InterfaceController中创建一个数据源weather数组,数组元素是图片名称,然后创建Table的IBOutlet。

pic

加载数据源

我们在InterfaceController中添加loadTableData()方法,然后在init()方法中调用。

pic

现在编译运行一下,你就会看到Watch中显示出了Table,是不是很酷呢!

pic

pic