diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index 0ad5b48..0000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/.gitignore b/.gitignore
index e13260a..91bb6e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,7 @@
*.DS_Store
+.DS_Store
.media/
.tmp/
Guide.localized/
+
+issue-15/Strings 在 Swift 2中字符串设计的背后思想
diff --git a/README.md b/README.md
index 8248489..8438990 100644
--- a/README.md
+++ b/README.md
@@ -32,10 +32,12 @@
## 文章分类
| 来源 | 介绍 |
|----------|-------------|
-| [Ray Wenderlich] | iOS社区,经常更新优秀文章[Ray Wenderlich](http://www.raywenderlich.com/) |
-| [App Coda] | iOS社区,经常更新优秀文章[App Coda](http://www.appcoda.com/) |
-| [Medium] | 类似于Reddit的社区,这是其中的iOS板块[Medium](https://medium.com/ios-os-x-development/) |
-| [Others] | 其他来源的优秀文章 |
+| [Ray Wenderlich](http://www.raywenderlich.com/) | iOS社区,经常更新优秀文章 |
+| [App Coda](http://www.appcoda.com/) | iOS社区,经常更新优秀文章 |
+| [iOS Dev Weekly](http://iosdevweekly.com/issues/) | 每周发布热点资讯,第三方工具库,教程等 |
+| [iOS Development Tips](http://iosdevtips.co) | 干货多多,就是发布的间期有点点长 |
+| [Medium](https://medium.com/ios-os-x-development/) | 类似于Reddit的社区,这是其中的iOS板块 |
+| Others | 其他来源的优秀文章 |
## 推荐文章
@@ -65,6 +67,65 @@
## 已完成列表
+# 2015.10.18 ( 第十九期 )
+| 文章标题 | 译者 |
+|----------------------|------------------------|
+| [创建自注册的Swift UI 控件](issue-19/创建自注册的Swift UI 控件.md) | [kmyhy](https://github.com/kmyhy) |
+| [如何实现iOS图书动画-第1部分](issue-19/如何实现iOS图书动画-第1部分.md) | [kmyhy](https://github.com/kmyhy) |
+| [如何实现iOS图书动画-第2部分](issue-19/如何实现iOS图书动画-第2部分.md) | [kmyhy](https://github.com/kmyhy) |
+| [iOS9 Core Data教学](issue-19/iOS9 Core Data教学.md) | [LastDay](http://lastday.github.io) |
+
+# 2015.9.28 ( 第十八期 )
+| 文章名称 | 译者 |
+|---------|--------|
+| [什么是Dependency Injection(依赖注入)?](issue-18/1.md) | [@祈祈祈祈祈祈](http://weibo.com/u/2801408731)
+
+# 2015.9.14 ( 第十七期 )
+| 文章名称 | 译者 |
+|---------|--------|
+| [objective - 在LLDB中的调用](issue-17/1.md) | [LastDays](https://github.com/MrLoong)
+| [如何实现iOS图书动画:第1部分](issue-17/2.md) | [](https://github.com/alier1226) |
+
+
+# 2015.9.14 ( 第十六期 )
+| 文章名称 | 译者 |
+|---------|--------|
+| [ReactNavtive框架教程](issue-16/ReactNavtive框架教程.md) | [kmyhy](https://github.com/kmyhy)
+| [介绍iOS设计模式1:2(Swift)](issue-16/介绍iOS设计模式1:2(Swift).md) | [alier1226](https://github.com/alier1226) |
+| [介绍iOS设计模式2:2(Swift)](issue-16/介绍iOS设计模式2:2(Swift).md) | [LastDays](https://github.com/MrLoong) |
+| [使用一个MVC替代Brigade’s Experience](issue-16/使用一个MVC.md) | [Quzhiyu](https://github.com/Quzhiyu)
+
+# 2015.7.31 ( 第十四期 )
+| 文章名称 | 译者 |
+|---------|--------|
+| [单例在Swift中的正确实现方式](issue-14/单例在Swift中的正确实现方式.md) | [Gottabe](https://github.com/Gottabe)
+| [在Swift怎样创建CocoaPod](issue-14/在Swift怎样创建CocoaPod.md) | [MrLoong](https://github.com/MrLoong)
+| [如何做一个iOS分形App](issue-14/如何做一个iOS分形App.md) | [alier1226](https://github.com/alier1226) |
+
+# 2015.7.6 ( 第十一期 )
+| 文章名称 | 译者 |
+|---------|--------|
+| [Swift-2.0-Beta-1标准库的改变](issue-11/Swift-2.0-Beta-1标准库的改变.md) | [samw00](https://github.com/samw00)
+
+
+
+# 2015.6.29 ( 第十期 )
+| 文章名称 | 译者 |
+|---------|--------|
+| [使用Quick框架和Nimble来测试ViewControler](issue-10/使用Quick框架和Nimble来测试ViewControler.md) | [Mr.Simple](https://github.com/bboyfeiyu)
+| [为watchOS-2而生的WatchKit-初印象](issue-10/为watchOS-2而生的WatchKit-初印象.md) | [StormXX](https://github.com/StormXX)
+| [iOS依赖注入](issue-10/iOS依赖注入.md) | [HarriesChen](https://github.com/mrchenhao) |
+
+
+# 2015.6.23 ( 第九期 )
+| 文章名称 | 译者 |
+|---------|--------|
+| [Swift 2 有哪些新特性](issue-9/Swift2-有哪些新特性.md) | [MollyMmm](https://github.com/MollyMmm)
+| [Swift-EventKit的初学者指南--请求权限](issue-9/Swift-EventKit的初学者指南--请求权限.md) | [MollyMmm](https://github.com/MollyMmm)
+| [Swift的异步机制-Future](issue-9/Swift的异步机制-Future.md) | [Javier Soto](https://twitter.com/Javi) |
+| [在Swift开发中通过UINavigationController来访问Sub-Controllers](issue-9/在Swift开发中通过UINavigationController来访问Sub-Controllers.md) | [samw00](http://www.andrewcbancroft.com/2015/06/02/access-sub-controllers-from-a-uinavigationcontroller-in-swift/) |
+
+
### 第八期 (2015.6.12)
| 文章标题 | 译者 |
|----------------------|------------------------|
diff --git a/iOS9 APP Search.md b/iOS9 APP Search.md
new file mode 100644
index 0000000..43684cc
--- /dev/null
+++ b/iOS9 APP Search.md
@@ -0,0 +1,385 @@
+> * 原文链接 : [iOS 9 App Search Tutorial: Introduction to App Search](http://www.raywenderlich.com/116608/ios-9-app-search-tutorial-introduction-to-app-search)
+* 原文作者 : [Chris Wagner](http://www.raywenderlich.com/u/cwagdev)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [kmyhy](https://github.com/kmyhy)
+
+
+> Ray 注:本文作为《iOS 9 Feast》中的一部分,节略自 《iOS 9 Tutorials》其中一章——通过本文,您可对全书内容窥见一斑。祝您阅读愉快!
+
+在相当长的一段时间内,iOS 的 Spotlight 都是一个大坑。尽管用户可以用它来搜索你的 App,但他们却无法看到其中的他们真正关心内容。现在,当用户想读取一个 App 中的内容时,他们只能回到 Home 屏一屏一屏翻,找到 App,打开 App,搜索他们想要的内容——假设你的App实现了搜索功能的话。
+
+对于比较老练的用户,则可能会通过Siri 或者 Spotlight 来打开他的 App,但无论哪个工具都不能让用户查找“非苹果官方App”内的内容。也就是说,苹果在 Spotlight 中可以查找通讯录、备忘录、信息、邮件以及其它支持查找功能的App中的内容。用户只需要点击搜索结果就可以直接访问相应的内容。这真是太不公平了!
+
+有时苹果会将一些有趣的功能保留给自己专用,比如 Spotlight。好消息是,每当苹果的开发者调教好一个功能,觉得已经可以把它放出去的时候,他们就会让大伙也尝尝鲜,比如 iOS 8 中的 App 扩展。
+
+在iOS 9 中,苹果又放出来一个很酷的功能给我们,第三方开发者现在可以在Spotlight 中搜索他们的App 内容了!
+
+在本教程中,你将领略 App Search 的威力,并学会如何将它集成到你自己的App 中。
+
+在iOS 9 中,App Search由三个部分组成。每一部分根据不同的目的分成独立的 API,但它们也能和其它部分一起使用:
+
+ - NSUserActivity
+ - Core Spotlight
+ - Web markup
+
+
+###NSUserActivity
+
+在App Search 中,使用了NSUserActivity,这是一个灵活小巧功能,在iOS 8 Handoff 中就使用到了NSUserActivity 。
+
+在iOS 9 中,NSUserActiviy增加一些新的属性以支持 App Search。从理论上讲,如果一个任务能够转变成一个 NSUserActivity 并转交给其它设备,它也能转换为一个搜索项并在同一个设备上继续处理。这就有可能对App 上的活动、状态和导航点进行索引,这样用户才能在 Spotlight 中对其进行搜索。
+
+例如,一个旅游类App 可能会将用户查看过的酒店进行索引,而一个新闻类App 会将用户浏览过的文章进行索引。
+
+> 注意:本教程不涉及 Handoff,我们只会讨论当一个内容被浏览后如何创建可搜索的内容。如果你想熟悉了解 Handoff 的内容,请阅读[ Getting Started with Handoff 教程](http://www.raywenderlich.com/84174/ios-8-handoff-tutorial)。
+
+### Core Spotlight
+
+第二个同时也是App Search 中最“常用到的”概念就是 Core Spotlight,它是存储类 App 诸如邮件、备忘录用于索引内容的东西。它既可以允许用户搜索之前访问过的内容,也可以用它来一次性构建一个巨大的可搜索内容的集合。
+
+你可以将Core Spotlight 看成是一个专门用于搜索的数据库。它提供了对添加搜索索引中的内容的细粒度的控制,例如这些内容是什么、什么时候添加的以及如何添加到搜索索引中的。你可以检索任何类型的内容,包括文件、视频、消息等等,还可以更新和移除搜索索引中的条目。
+
+Core Spotlight 为全面搜索 App 内部内容提供了一种最好的方式。
+
+本教程关注于使用前面提及的 NSUserActivity 对象获取 Spotlight 搜索结果。本教程的完整版位于《iOS 9 Tutorials》中,其中介绍了如何通过Core Spotlight 全面检索你的内容。
+
+### Web markup
+
+App Search的第三个方面是 Web Markup,这个功能允许App 将它们的内容镜像到一个Web站点上。比较好的例子如 Amazon,你可以搜索它上面成千上万的在售产品,甚至是raywenderlich.com上的产品。在web 内容上使用了标准的标签进行标记,你可以将App 内容显示在 Spotlight 和 Safari的搜索结果中,甚至可以直接链接到你的App。
+
+本教程不涉及 Web Markup,你可以在《iOS 9 by Tutorials》第三章“Your App On The Web"中学习这部分内容。
+
+##开始
+
+你将学习的示例程序叫做 Colleagues,它模拟一个公司通讯录。它可以将你的同时添加到你的联系人中,而不是直接给你一个同事的目录。为了简单起见,它使用的是本地数据库,由一个文件夹(存放头像图片)和一个 JSON文件(包含了所有公司职员信息)组成。在生产环境中,你应该使用一个网络组件从 Web 上抓取这些数据。作为教程,JSON 文件就足够了。[下载](http://cdn5.raywenderlich.com/wp-content/uploads/2015/09/intro-app-search-starter.zip)并打开初始项目,不需要做任何事情,直接编译运行。
+
+
+
+你会看到一张职员列表。这是一个小型创业公司,只有25个职员的规模。选择 Brent Reid,可以查看这个职员的信息。你同时还可以看到 Brent Reid 所在的部门的其他人的列表。那是 App 的一个扩展功能——其实非常简单!
+
+搜索功能将让 App 增色不少。但是现在,你甚至无法在 App 搜索。你不用在 App 中增加搜索功能,相反,你可以用 Spotlight 从 App 外部增加一个搜索功能。
+
+### 示例项目
+
+花点时间来熟悉一下示例项目的代码。项目中存在两个 Target,一个是 Colleagues,即 App 自身;一个是 EmployeeKit,负责和职员数据库进行交互。
+
+在 Xcode 中,展开 EmployeeKit 文件夹,打开 Employee.swift。这是职员信息的模型类,定义了一系列相关属性。Employee 对象使用一个 JSON 对象进行实例化,后者来自于 Database 文件夹下的 employees.json 文件。
+
+然后打开 EmployeeService.swift。在文件头部声明了一个扩展,扩展中有一个 destroyEmployeeIndexing()方法,这个方法用TODO标记进行注明。你将在稍后实现这个方法。这个方法负责销毁所有显示过的索引。
+
+在 EmployeeKit 这个 Target 中有许多内容,但都和 App Search 毫无关联,因此我们就不多说了。当然,你可以花时间看一看。
+
+打开Colleagues 文件夹下的 AppDelegate.swift。注意只有一个方法在里边:
+`application(_:didFinishLaunchingWithOptions:)`。这个方法判断Setting.searchIndexingPreference 是否设置为 .Disabled,如果是,则将所有存在的搜索索引删除。
+
+除了知道有这么一个设置项存在外,你并不需要做任何事情。你可以通过 iOS 的设置程序中的 Colleagues 来修改这个设置。
+
+参观到此结束。接下来你需要修改 View Controller 中的代码。
+
+###搜索曾经浏览过的记录
+
+实现App Search时,NSUserActivity 总是第一个要实现的,因为:
+
+1.它最简单。创建一个 NSUserActivity 实例就如同设置几个属性那么简单。
+2.当你用 NSUserActivity 表示用户活动时,iOS 会对内容进行排序,以便搜索结果对经常被访问的内容进行优先处理。
+3.它和实现 Handoff 很像。
+现在,让我们来看看实现 NSUserActivity 到底有多简单!
+
+###实现 NSUserActivity
+
+选中 EmployeeKit 文件夹,依次选择 File \ New \ File...,然后选择 iOS\ Source \ Swift File 模板,再点击 Next。将文件命名为 EmployeeSearch.swift,并确保其 Target 为 EmployeeKit。
+在这个文件中,首先导入 CoreSpotlight:
+```
+import CoreSpotlight
+```
+
+然后定义一个 Employee 的扩展:
+
+```
+extension Employee {
+ public static let domainIdentifier = "com.raywenderlich.colleagues.employee"
+}
+```
+
+反域名字符串将用于唯一标识 NSUserActivity 所属的一类活动类型。接着,在domainIdentifier 之后增加一个计算属性:
+
+```
+public var userActivityUserInfo: [NSObject: AnyObject] {
+ return ["id": objectId]
+}
+```
+
+这个字典用于 NSUserAcitivity 唯一标识某个活动(Activity)。然后再添加一个计算属性,名为 userActivity:
+
+```
+public var userActivity: NSUserActivity {
+ let activity = NSUserActivity(activityType: Employee.domainIdentifier)
+ activity.title = name
+ activity.userInfo = userActivityUserInfo
+ activity.keywords = [email, department]
+ return activity
+}
+```
+
+这个属性用于很方便地根据一个 Employee 创建一个 NSUserActivity 实例。它创建了一个 NSUserActivity 对象,并用对以下属性进行了赋值:
+
+activityType:活动所属的类型。你会在后面用它来识别 iOS 传递给你的NSUserActivity实例。苹果建议该值采用反域名命名规则。
+
+title:活动的名字——这将用于在搜索结果中作为主要名显示。
+userInfo:一个字典,用于存放你想传递的任意数据。当你的App 收到一个活动时——比如用户从 Spotlight 点击了一个搜索结果,你就可以获取这个字典。你将在这个字典中存放同事的唯一 ID,这样 App 打开后就能显示正确的同事资料。
+keywords:一个本地化的关键字列表,用于作为搜索关键字。
+
+然后,我们将使用刚才定义的 userActivity 属性去搜索同事记录。因为这些代码位于 EmployeeKit 框架中,我们需要编译框架才能在 Colleagues App 中使用它们。
+
+按 CommandB,编译项目。
+
+打开 EmployeeViewController.swift,在viewDidLoad()方法最后加入代码:
+
+```
+let activity = employee.userActivity
+
+switch Setting.searchIndexingPreference {
+case .Disabled:
+ activity.eligibleForSearch = false
+case .ViewedRecords:
+ activity.eligibleForSearch = true
+}
+
+userActivity = activity
+```
+
+上述代码读取 userActivity 属性——这个属性是我们刚才通过定义 Employee 扩展时添加的。然后检查 App 的搜索设置。
+
+如果搜索被禁用,将 activty 标记为不可用于搜索。如果该设置为 ViewedRecords,则将 activity 标记为能够用于搜索。
+
+最后,将 View Controller 的 userActivity 属性设置为 employee 的 userActivity。
+
+> 注意:View Controller 的 userActivity 属性继承自 UIResponder 。这个属性是苹果为了支持 Handoff 而增加到 iOS 8 中的。
+
+最后还应该覆盖 updateUserActivityState() 方法。这样,当某个搜索结果被选择时,你才可以获得所需要的数据。
+
+在 viewDidLoad() 方法后增加这个方法:
+
+```
+override func updateUserActivityState(activity: NSUserActivity) {
+ activity.addUserInfoEntriesFromDictionary(
+ employee.userActivityUserInfo)
+}
+```
+
+在 UIResponder 的生命周期中,系统会多次调用这个方法,你应该在这个方法中保持更新 activity。在我们的例子里,你只需要将包含有 employee的 objectId 的 userActivityUserInfo 字典传递给 activity。
+
+好了!现在,在搜索设置被开启的情况下,每当你浏览了一个同事,浏览历史将被记下并可用于搜索。
+
+在模拟器或设备上,打开设置程序,找到 Colleagues。将 Indexing 设置改成 Viewed Records。
+
+
+
+现在,编译运行程序,然后选择 Brent Reid。
+
+OK,看起来没有什么新奇的事情发生,但在你不知不觉中,Brent 的活动已经被加到搜索索引中了。回到 Home 屏幕(shiftcommandH),通过下拉屏幕或者向右划动屏幕,打开 Spotlight。在搜索栏输入 brent reid 。
+
+
+
+"Brent Reid"显示出来了!如果你没看见,可能需要向下滚动列表。如果你点击这个Brend Reid,它将移动到列表上部,以便下次你可以搜索同一个关键字。
+
+
+
+虽然到现在为止结果还蛮不错,但这个搜索结果却是有点索然无味了。
+
+除了显示一个名字外,我们还能干什么?现在就让我们彻底进入 Core Spotlight 的殿堂探索一番。
+
+###在搜索结果中显示更多信息
+
+NSUserActivity 有一个 contentAttributeSet 属性。这个属性的类型是 CSSearchableItemAttributeSet,它允许你用一系列属性来描述你的内容。查看 CSSearchableItemAttributeSet 类参考,你可以发现很多利用这些属性来描述内容的方法。
+
+下图是我们需要的搜索结果,每个部分都分别标出了所用的属性名:
+
+
+
+前面已经设置过 NSUserActivity 的 title 属性,这个属性正如你所看到的。其它3个属性,thumbnailData、supportsPhoneCall 和 contentDescription 全部都是通过 CSSearchableItemAttributeSet 来设置的。
+
+打开 EmployeeSearch.swift,在文件头部,导入 MobileCoreServices:
+
+```
+import MobileCoreServices
+```
+
+MobileCoreServices 是必须的,因为在我们创建 CSSearchableItemAttributeSet 对象时需要用到其中定义的一个常量。你已经导入过 CoreSpotlight了,这个框架也是必须的,它的所有 API 都使用了 CS 作为前缀。
+
+仍然在 EmployeeSearch.swift中,在 Employee 扩展中添加新的计算属性:
+
+```
+public var attributeSet: CSSearchableItemAttributeSet {
+ let attributeSet = CSSearchableItemAttributeSet(
+ itemContentType: kUTTypeContact as String)
+ attributeSet.title = name
+ attributeSet.contentDescription = "\(department), \(title)\n\(phone)"
+ attributeSet.thumbnailData = UIImageJPEGRepresentation(
+ loadPicture(), 0.9)
+ attributeSet.supportsPhoneCall = true
+
+ attributeSet.phoneNumbers = [phone]
+ attributeSet.emailAddresses = [email]
+ attributeSet.keywords = skills
+
+ return attributeSet
+}
+```
+
+初始化 CSSearchableItemAttributeSet 时,需要提供一个 itemContentType 参数,我们传递了一个 kUTTypeContact 进去(该常量在 MobileCoreServices 框架中定义,关于该常量,请阅读苹果的 [UTType 参考](http://apple.co/1NilqiZ))。
+
+attributeSet 中包含了一些与当前 employee 搜索时用到的相关数据:title 来自于 NSUserActivity 的 title,contentDescription 包括了这个同事的部门、称谓和电话号码等信息,而 thumbnailData 则调用 loadPicture() 方法结果并转换为 NSData。
+
+要显示”打电话“按钮,我们必须将 supportsPhoneCall 设置为true,并给 phoneNumbers 属性赋一个数组。最后,我们设置了 email 地址,并将同事的 skills (技能)作为 keyword 关键字。
+
+现在所有的数据都准备好了,Core Spotlight 在搜索时会检索这些数据并添加到搜索结果中。这样,用户就可以搜索同事的姓名、部门、称谓、电话号码、email甚至是技能。
+
+仍然是 EmployeeSearch.swift,在返回 userActivity 前面添加以下语句:
+```
+activity.contentAttributeSet = attributeSet
+```
+
+这句代码告诉 NSUserActivity 使用这些信息作为 contentAttributeSet属性的值。
+
+编译运行。查看 Brent Reid 的个人信息以便索引生效。回到 Home 屏幕,拉出 Spotlight,搜索 brent reid。如果你先前的搜索结果仍然存在,你只需要清除并重新搜索。
+
+
+
+噢,你是不是很奇怪实现的代码太少了?
+
+好了!现在 Spotlight 能够如我们所想的一样搜索同事了。不过,似乎我们还是遗漏了点什么...当你尝试通过搜索结果打开 App 时,什么也不会发生。
+
+###打开搜索结果
+
+理想的用户体验是直接打开 App 并显示相关的内容。事实上——这个是一个要求——苹果会将能够启动并显示有用的信息的App的排在搜索结果的前列。
+
+通过将一个 activityType 和一个 userInfo 对象赋给 NSUserActivity 对象,你已经在上一节中为后续的工作做了铺垫。
+
+打开 AppDelegate.swift,在`application(_:didFinishLaunchingWithOptions:)` 方法下面,添加
+一个`application(_:continueUserActivity:restorationHandler:)` 方法:
+
+```
+func application(application: UIApplication,
+ continueUserActivity userActivity: NSUserActivity,
+ restorationHandler: ([AnyObject]?) -> Void) -> Bool {
+
+ return true
+}
+```
+
+当用户选择了一个搜索结果时,这个方法会被调用——这个方法也会被Handoff 用来接收其他设备传来的活动。
+
+在这个方法返回 true 之前,加入以下语句:
+
+```
+guard userActivity.activityType == Employee.domainIdentifier,
+ let objectId = userActivity.userInfo?["id"] as? String else {
+ return false
+}
+```
+
+guard 语句检查 activityType 是否是我们希望的类型(用于处理 Employee 的活动),然后从 userInfo 中获取 objectId。如果这两个条件中有一个不满足则返回 false,通知系统该活动不会被处理。
+
+接着,在 guard 语句后,将 return true 语句替换为:
+
+```
+if let nav = window?.rootViewController as? UINavigationController,
+ listVC = nav.viewControllers.first as? EmployeeListViewController,
+ employee = EmployeeService().employeeWithObjectId(objectId) {
+ nav.popToRootViewControllerAnimated(false)
+
+ let employeeViewController = listVC
+ .storyboard?
+ .instantiateViewControllerWithIdentifier("EmployeeView") as!
+ EmployeeViewController
+
+ employeeViewController.employee = employee
+ nav.pushViewController(employeeViewController, animated: false)
+ return true
+}
+
+return false
+```
+
+获得 id 之后,你的目标就是用EmployeeViewController 显示匹配的同事信息。
+
+上述代码稍微有点乱,但你可以想象一下 App 的设计。App 中有两个 View Controller,一个是同事的列表,另一个是则显示同事的详细信息。上述代码先将导航控制器的视图控制器堆栈弹回到列表界面,然后push 一个该同事细节窗口。
+
+如果因为某种原因视图无法呈现,方法会返回一个false。
+
+OK,编译和运行!从同时列表中选择 Cary Iowa,然后回到 Home 屏。调出 Spotlight 搜索 Brent Reid。找到结果后,点击它。App 会打开,并且可以看到 Cary 的详情界面迅速地过渡到了 Bent 的详情界面。干得不错!
+
+###从搜索索引中删除条目
+
+回到 App 的话题上来。想象一下,在某个狂风暴雨的一天,一个同事因为将老板用胶带绑在墙上而被解雇。显然,你是无论如何都不想和这个人有任何关系了,因此你必须将他和其他离开公司的人一起从 Colleagues 的搜索索引中删除。
+
+由于只是一个示例App,你可以在 App 的索引设置关闭的前提下将整个索引删除。
+
+打开EmployeeService.swift 在文件头部添加导入语句:
+
+```
+import CoreSpotlight
+```
+
+找到 destoryEmployeeIndexing(),将 TODO 注释替换为:
+
+```
+CSSearchableIndex
+ .defaultSearchableIndex()
+ .deleteAllSearchableItemsWithCompletionHandler { error in
+ if let error = error {
+ print("Error deleting searching employee items: \(error)")
+ } else {
+ print("Employees indexing deleted.")
+ }
+}
+```
+
+这个无参的方法将删除整个App的索引数据库。Good!
+
+现在可以来测试一下。通过下列步骤来测试是否索引一如我们希望的那样已被删除:
+
+1.编译运行程序。
+
+2.用 Xcode 终止程序。
+
+3.在模拟器或者设备中,打开 设置 \ Colleagues,将 Indexing 设置为 Viewed Records。
+
+4.再次打开 App,选择一个新的同事,让索引生效。
+
+5.回到 Home 屏,调出 Spotlight。
+
+6.搜索浏览过的同事,等待索引项出现。
+
+7 回到 设置 \ Colleagues,将 Indexing 设置为关。
+
+8.退出 App。
+
+9.重新打开 App。这将清除搜索索引。
+
+10.回到 Home 屏,调出 Spotlight。
+
+11.搜索浏览过的同事,你会发现没有和 Colleagues App 有关的搜索结果。
+
+呵呵,删除整个搜索索引实在太容易了。但如果你想只删除某个单独的记录呢?幸运的是——有两个 API 能够让你更精确地删除想删的记录:
+
+`deleteSearchableItemsWithDomainIdentifiers(_:completionHandler:)` 方法允许你删除整个 domain ID 相同的一组索引。
+
+`deleteSearchableItemsWithIdentifiers(_:completionHandler:)` 方法允许你通过唯一ID 指定要删除哪条记录。
+
+也就是说,如果你所索引的记录具有多种类型的话,全局 ID (在同一个 App 组中)必须唯一。
+
+> 注意:如果你不能保证跨类型ID 是唯一的,比如你的 ID 是通过数据库中的自增长类型获得的,则你可以采取一种简单办法,即在记录 ID 前面加上一个类型前缀。例如,如果你有一个联系人记录的 ID 为 123,一个订单记录的 ID 也是 123,则可以将它们的唯一 ID 设置为 contact.123 和 order.123。
+
+如果你在运行过程中遇到任何问题,你可以从[这里](http://cdn5.raywenderlich.com/wp-content/uploads/2015/09/intro-app-search-final.zip)下载到最终完成的项目。
+
+##接下来做什么?
+
+这篇 iOS 9 App Search 教程介绍了在 iOS 9 中使用 User Activity 搜索 App 内部内容的简单但强大的方法。搜索的内容从来不会受到限制——你可以用这种方法搜索 App 中的导航点。
+
+想象一下,一个 CRM App,它拥有许多窗口,比如联系人、订单和任务。通过 User Activity,用户随时可以到达这些窗口,用户可以搜索订单,然后直接跳到 App 的某个订单界面。这个功能太有用了,尤其是你的 App 有很多层级的导航时。
+
+有许多独特的方法将内容推给你的用户。想突破沙盒的限制,就要教会用户使用这个强大的功能。
+
+这个教程是《iOS 9 by Tutorials》第2章的精简版。如果你想学习用 Core Spotlight 检索大数据集,或者学习 iOS 9 的 Web Content,请阅读这本书!
\ No newline at end of file
diff --git "a/issue-10/iOS\344\276\235\350\265\226\346\263\250\345\205\245.md" "b/issue-10/iOS\344\276\235\350\265\226\346\263\250\345\205\245.md"
new file mode 100644
index 0000000..b6af9a8
--- /dev/null
+++ "b/issue-10/iOS\344\276\235\350\265\226\346\263\250\345\205\245.md"
@@ -0,0 +1,362 @@
+iOS依赖注入
+---
+
+> * 原文链接 : [Dependency Injection: Give Your iOS Code a Shot in the Arm
+June 05, 2015](https://corner.squareup.com/2015/06/dependency-injection-in-objc.html)
+* 原文作者 : [Square Engineering Blog](https://corner.squareup.com)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [HarriesChen](https://github.com/mrchenhao)
+
+##什么是依赖注入呢?
+依赖注入(DI)是一种非常流行的设计模式在许多的语言之中,比如说Java和C#,但是它似乎并没有在Objective-C广泛的被采用,这篇文章的目的就是简要的介绍一下在Objective-C中使用依赖注入的例子。虽然代码是用Objective-C写的,但是也可以应用到Swift。
+
+依赖注入的概念很简单:一个对象应该被依赖而不是被创建。强烈建议阅读Martin Fowler的[excellent discussion on the subject](http://martinfowler.com/articles/injection.html)作为背景阅读。
+
+注入可以通过对象的初始化(或者可以说构造器)或者通过特性(setter),这些就指的是构造器注入和setter方法注入。
+
+##构造器注入
+
+```
+- (instancetype)initWithDependency1:(Dependency1 *)d1
+ dependency2:(Dependency2 *)d2;
+
+```
+
+##Setter 注入
+
+```
+@property (nonatomic, retain) Dependency1 *dependency1;
+@property (nonatomic, retain) Dependency2 *dependency2;
+```
+
+根据福勒的描述,[首选构造器注入](http://martinfowler.com/articles/injection.html#ConstructorVersusSetterInjection),一般来说只有在构造器注入不可行的情况下才会选择settter注入。在构造器注入中,你可以还是会使用`@property`来定义这些依赖,但是在接口中你应该让他们为只读,
+
+##为什么使用依赖注入
+依赖注入提供了一系列的好处,其中重要的是一下几点:
+
+* **依赖定义清晰** 可以很明显的看出对象需要什么来进行操作,从而可以避免依赖类似(dependencies—like),全部变量消失(globals—disappear)。
+* **组合** 依赖注入提倡[组合而不是继承](https://en.wikipedia.org/wiki/Composition_over_inheritance), 提高代码的可重用性。
+* **定制简单** 当创建一个对象的时候,可以轻松的定制对象的每一个部分来面对最糟糕的情况。
+* **所有权清晰** 特别是当使用构造函数注入,严格执行对象的所有权规则,帮助建立一个[有向无循环对象图](https://en.wikipedia.org/wiki/Directed_acyclic_graph)。
+* **可测试性** 更重要的是,依赖注入提高了你的对象的可测性。因为他们可以被非常简单的创建通过初始化方法,没有隐藏的依赖关系需要管理。此外,可以简单的模拟出依赖让你关注于被测试的对象。
+
+##使用依赖注入
+
+你的代码可能还没有使用依赖注入的设计模式来设计,但是很容易上手。依赖注入的一个很好的方面是,你不需要采用“全或无”。相反,你可以将它应用到代码的特定区域并从那里展开。
+
+###类型注入
+
+首先,让我们把类分成两种:基本类和复合类。基本类是没有依赖的,或只依赖于其他基础类。基本类被继承的可能性极低,因为它们的功能是明确的,不变的,不引用外部资源。Cocoa本身有很多的基本类,如`NSString` `NSArray`,`NSDictionary` `NSNumber`……。
+
+复杂类则相反,他们有复杂的依赖关系,包括应用级别的逻辑(可能被改变),或者访问外部资源,比如磁盘,网络或者一些全局的服务。在你应用内的大多数类都是复杂的,包括大多数的控制器对象和模型对象。需要Cocoa类也食非常复杂的,比如说`NSURLConnection `或者`UIViewController `
+
+判断这个的最简单方法就是拿起一个复杂类然后看它初始化其他复杂类的地方(搜索"alloc] init" or "new]"),介绍了类的依赖注入,改变这个实例化的对象是通过作为一个类的初始化参数而不是类的实例化对象本身。
+
+###初始化中的依赖分配
+
+让我们来看一个例子,一个子对象(依赖)被初始化作为父对象的一部分。一般代码如下:
+
+```
+@interface RCRaceCar ()
+
+@property (nonatomic, readonly) RCEngine *engine;
+
+@end
+
+
+@implementation RCRaceCar
+
+- (instancetype)init
+{
+ ...
+
+ // Create the engine. Note that it cannot be customized or
+ // mocked out without modifying the internals of RCRaceCar.
+ _engine = [[RCEngine alloc] init];
+
+ return self;
+}
+
+@end
+
+```
+
+依赖注入版本的有一些小小的不同
+
+```
+@interface RCRaceCar ()
+
+@property (nonatomic, readonly) RCEngine *engine;
+
+@end
+
+
+@implementation RCRaceCar
+
+// The engine is created before the race car and passed in
+// as a parameter, and the caller can customize it if desired.
+- (instancetype)initWithEngine:(RCEngine *)engine
+{
+ ...
+
+ _engine = engine;
+
+ return self;
+}
+
+@end
+
+```
+
+###依赖惰性初始化
+
+一些对象有时会在很后面才会被用到,有时甚至在初始化之后不会被用到,举个例子(在依赖注入之前)也许看起来是这样子的:
+
+```
+@interface RCRaceCar ()
+
+@property (nonatomic) RCEngine *engine;
+
+@end
+
+
+@implementation RCRaceCar
+
+- (instancetype)initWithEngine:(RCEngine *)engine
+{
+ ...
+
+ _engine = engine;
+
+ return self;
+}
+
+- (void)recoverFromCrash
+{
+ if (self.fire != nil) {
+ RCFireExtinguisher *fireExtinguisher = [[RCFireExtinguisher alloc] init];
+ [fireExtinguisher extinguishFire:self.fire];
+ }
+}
+
+@end
+
+```
+
+在这种情况下,赛车希望不会出事,而我们也不需要使用灭火器。由于需要该对象的机会很低,我们不想每一个赛车的创建的时候慢下来。另外,如果我们的赛车需要从多个事故恢复,就需要创建多个灭火器。针对这些情况,我们可以使用工厂。
+
+工厂标准的Objective-C块不需要参数和返回一个对象的实例。一个对象可以控制当其依赖的是使用这些块而不需要详细了解如何创建。
+
+这里是一个例子使用了依赖注入和工厂模式来创建灭火器。
+
+```
+typedef RCFireExtinguisher *(^RCFireExtinguisherFactory)();
+
+
+@interface RCRaceCar ()
+
+@property (nonatomic, readonly) RCEngine *engine;
+@property (nonatomic, copy, readonly) RCFireExtinguisherFactory fireExtinguisherFactory;
+
+@end
+
+
+@implementation RCRaceCar
+
+- (instancetype)initWithEngine:(RCEngine *)engine
+ fireExtinguisherFactory:(RCFireExtinguisherFactory)extFactory
+{
+ ...
+
+ _engine = engine;
+ _fireExtinguisherFactory = [extFactory copy];
+
+ return self;
+}
+
+- (void)recoverFromCrash
+{
+ if (self.fire != nil) {
+ RCFireExtinguisher *fireExtinguisher = self.fireExtinguisherFactory();
+ [fireExtinguisher extinguishFire:self.fire];
+ }
+}
+
+@end
+
+```
+
+工厂模式在我们创建未知数量的依赖的时候也是非常有用的,即使是在初始化期间,例子如下:
+
+```
+@implementation RCRaceCar
+
+- (instancetype)initWithEngine:(RCEngine *)engine
+ transmission:(RCTransmission *)transmission
+ wheelFactory:(RCWheel *(^)())wheelFactory;
+{
+ self = [super init];
+ if (self == nil) {
+ return nil;
+ }
+
+ _engine = engine;
+ _transmission = transmission;
+
+ _leftFrontWheel = wheelFactory();
+ _leftRearWheel = wheelFactory();
+ _rightFrontWheel = wheelFactory();
+ _rightRearWheel = wheelFactory();
+
+ // Keep the wheel factory for later in case we need a spare.
+ _wheelFactory = [wheelFactory copy];
+
+ return self;
+}
+
+@end
+
+```
+
+##避免麻烦的配置
+
+如果对象不应该在其他对象分配,他们在哪里分配?是不是所有的这些依赖关系很难配置?大多不都是一样的吗?这些问题的解决在于类方便的初始化(想一想+[NSDictionary dictionary])。我们会把我们的对象图的配置移出我们正常的对象,把他们抽象出来,测试,业务逻辑。
+
+在我们添加简单初始化方法之前,先确认它是有必要的。如果一个对象只有少量的参数在初始化方法之中,这些参数没有规律,那么简单初始化方法是没有必要的。调用者应该使用标准初始化方法。
+
+我们需要从四个地方搜集依赖来配置我们的对象:
+
+* **没有明确默认值的变量** 这些包括布尔类型和数字类型的有可能在每个实例中是不同的。这些变量应该在简单初始化方法的参数中。
+* **共享对象** 这些应该在简单初始化方法中作为参数(例如电台频率),这些对象先前可能被访问作为一个单例或者通过父指针。
+* **新创建的对象** 如果我们的对象不与另一个对象分享这种依赖关系,合作者对象应该是新实例化的类中简单初始化。这些都是以前分配的对象直接在该对象的实现。
+* **系统单例** Cocoa提供了很多单例可以直接被访问,例如`[NSFileManager defaultManager]`,那里有一个明确的目的只需要一个实例被使用在应用中,还有很多单例在系统中。
+
+一个对于赛车的简单初始化看起来如下:
+
+```
++ (instancetype)raceCarWithPitRadioFrequency:(RCRadioFrequency *)frequency;
+{
+ RCEngine *engine = [[RCEngine alloc] init];
+ RCTransmission *transmission = [[RCTransmission alloc] init];
+
+ RCWheel *(^wheelFactory)() = ^{
+ return [[RCWheel alloc] init];
+ };
+
+ return [[self alloc] initWithEngine:engine
+ transmission:transmission
+ pitRadioFrequency:frequency
+ wheelFactory:wheelFactory];
+}
+```
+
+你的类的简单初始化方法应该是最合适的。一个经常被用到的(可重用)的配置在一个.m文件中,而一个特定的配置文件应该在在`@interface RaceCar (FooConfiguration)`的类别文件中比如`fooRaceCar `。
+
+##系统单例
+
+在Cocoa的许多对象中,只有一种实例将一直存在。
+例如`[UIApplication sharedApplication]`, `[NSFileManager defaultManager]`, `[NSUserDefaults standardUserDefaults]`, 和 `[UIDevice currentDevice]`,如果一个对象对他们存在依赖,那么应该在初始化的参数中包含他们,即使也许在你的代码中只有一个实例,你的测试可能要模拟实例或创建一个实例每个测试避免测试相互依存。
+
+建议避免创建全局引用单例,而是创建一个对象的单个实例当它第一次被需要的时候并把它注入到所有依赖它的对象中去。
+
+##不可变构造器
+
+偶尔在某个类的构造器或者初始化方法不能被改变或者调用的时候,我们可以使用setter注入。例如:
+
+```
+// An example where we can't directly call the the initializer.
+RCRaceTrack *raceTrack = [objectYouCantModify createRaceTrack];
+
+// We can still use properties to configure our race track.
+raceTrack.width = 10;
+raceTrack.numberOfHairpinTurns = 2;
+
+```
+
+setter注入允许你配置对象,但是它也引入了易变性,你必须进行测试和处理,幸运的是,有两个场景导致初始化方法不能改变和调用,他们都是可以避免的。
+
+###类注册
+
+使用类注册的工厂模式意味着不能修改初始化方法。
+
+```
+NSArray *raceCarClasses = @[
+ [RCFastRaceCar class],
+ [RCSlowRaceCar class],
+];
+
+NSMutableArray *raceCars = [[NSMutableArray alloc] init];
+for (Class raceCarClass in raceCarClasses) {
+ // All race cars must have the same initializer ("init" in this case).
+ // This means we can't customize different subclasses in different ways.
+ [raceCars addObject:[[raceCarClass alloc] init]];
+}
+```
+
+一个简单的替代方法就是使用工厂块而不是类的声明列表。
+
+
+```
+typedef RCRaceCar *(^RCRaceCarFactory)();
+
+NSArray *raceCarFactories = @[
+ ^{ return [[RCFastRaceCar alloc] initWithTopSpeed:200]; },
+ ^{ return [[RCSlowRaceCar alloc] initWithLeatherPlushiness:11]; }
+];
+
+NSMutableArray *raceCars = [[NSMutableArray alloc] init];
+for (RCRaceCarFactory raceCarFactory in raceCarFactories) {
+ // We now no longer care which initializer is being called.
+ [raceCars addObject:raceCarFactory()];
+}
+```
+
+###故事版
+
+故事提供了一个方便的方式来设计你的用户界面,但也带来一些问题的时候。特别是依赖注入,实例化初始视图控制器在故事板中不允许你选择初始化方法。同样,当下面的事情就是在故事板中定义,目标视图控制器被实例化也不允许你指定的初始化方法。
+
+解决这个问题的办法是避免使用故事板。这似乎是一个极端的解决方案,但是我们发现故事板有其他问题在大型团队的开发中。此外,没有必要失去大部分故事版的好处。xibs提供所有相同的好处和故事板,除了`segues`,但仍然让你自定义初始化。
+
+##公共和私有
+
+依赖注入鼓励你暴露更多的对象在你的公共接口。如前所述,这有许多好处。但当你构建框架,它会明显的膨胀公共API。使用依赖注入之前,公共对象A可能使用私有对象B(这反过来使用私有对象C),但对象B和C分别从未曝光的外部框架。使用依赖注入的对象A会使对象B暴露在公共初始化方法,对象B反过来将对象C暴露在公共初始化方法。。
+
+```
+// In public ObjectA.h.
+@interface ObjectA
+// Because the initializer uses a reference to ObjectB we need to
+// make the Object B header public where we wouldn't have before.
+- (instancetype)initWithObjectB:(ObjectB *)objectB;
+@end
+
+@interface ObjectB
+// Same here: we need to expose ObjectC.h.
+- (instancetype)initWithObjectC:(ObjectC *)objectC;
+@end
+
+@interface ObjectC
+- (instancetype)init;
+@end
+```
+
+对象B和对象C都是具体的实现,但你并不需要框架的使用者来担心他们,我们可以通过协议的方式来解决。
+
+```
+@interface ObjectA
+- (instancetype)initWithObjectB:(id )objectB;
+@end
+
+// This protocol exposes only the parts of the original ObjectB that
+// are needed by ObjectA. We're not creating a hard dependency on
+// our concrete ObjectB (or ObjectC) implementation.
+@protocol ObjectB
+- (void)methodNeededByObjectA;
+@end
+```
+
+##结束语
+
+依赖注入在 Objective-C(同样在Swift),合理的使用会使你的代码更加易读、易测试和易维护。
+
+
+
diff --git a/issue-10/readme.md b/issue-10/readme.md
new file mode 100644
index 0000000..e69de29
diff --git "a/issue-10/\344\270\272watchOS-2\350\200\214\347\224\237\347\232\204WatchKit-\345\210\235\345\215\260\350\261\241.md" "b/issue-10/\344\270\272watchOS-2\350\200\214\347\224\237\347\232\204WatchKit-\345\210\235\345\215\260\350\261\241.md"
new file mode 100644
index 0000000..e68a0f9
--- /dev/null
+++ "b/issue-10/\344\270\272watchOS-2\350\200\214\347\224\237\347\232\204WatchKit-\345\210\235\345\215\260\350\261\241.md"
@@ -0,0 +1,133 @@
+为watchOS 2而生的WatchKit: 初印象
+---
+
+> * 原文链接 : [WatchKit for watchOS 2: Initial Impressions](http://www.raywenderlich.com/108415/watchkit-for-watchos-2)
+* 原文作者 : [Mic Pringle](http://www.raywenderlich.com/u/micpringle)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [StormXX](https://github.com/StormXX)
+* 校对者: [Mr.Simple](https://github.com/bboyfeiyu) [Harries Chen](https://github.com/mrchenhao)
+* 状态 : 已完成
+
+和Ray一样,我也没有去今年的WWDC现场,但是我仍然花了一些时间来看WWDC的视频,读文档,并且和Jake,Nate还有Tutorial团队的人们讨论这些新闻。
+
+你可能从播客里知道,我是一个狂热的Apple Watch粉丝。所以我对watchOS 2里的新东西感到异常的兴奋!
+
+我在过去的几天里看完了新的Apple Watch文档,所以你可以在这篇文章里能非常快的浏览在新的watchOS 2中有什么新玩意~
+
+
+
+***WatchKit for WatchOS 2***
+
+请在下面的评论区说出你自己的想法,当然,你也可以提一些我没有提到的东西啦~
+
+## 架构
+
+或许在这次的发行版里,对于WatchKit最大的改变就是可以构建本地的专属手表的独立app了。
+
+
+
+***Wait, did he just say native?***
+
+不像watch的上一个版本--界面运行在手表上但是代码却在你的iPhone上被调用。在watchOS 2里,两者都在watch上被存储和调用。
+
+这样就会产生一些积极的影响,比如说让你的app启动的更快,并且操作能够很快的得到响应。因为当交互发生的时候,手表再也不需要通过蓝牙来和你的手机通信。
+
+对于手机上的app来说,下面的这些改变你也必须要了解一下:
+
+- `openParentApplication(_:reply:)` 已经从SDK中删除了。但是不要担心,它被更丰富的[Watch Connectivity](https://developer.apple.com/library/prerelease/watchos/documentation/WatchConnectivity/Reference/WatchConnectivity_framework/index.html#//apple_ref/doc/uid/TP40015269)框架代替了。点击链接了解更多:)
+- 通过app groups来共享文件不再是一个选项。又一次,你可以使用新的 Watch Connectivity框架来使它更容易。但是在watchOS 2中,苹果提供了对原始手表app的网络请求的支持。所以,如果你以前是使用app groups来共享从API抓取的数据,现在你可以直接在手表上使用NSURLSession
+- iCloud在Watch上还是不能用!
+
+## 手表连接
+
+通过Watch Connectivity框架(framework),你可以在手表和它配对的iPhone之间传送文件或者数据。如果两个app都运行在前台,那么这个连接就是活动的,否则,它将会运行在后台线程。所以,当接收的app启动的时候,传送来的数据或者文件才能被使用。
+
+在新的框架中,比较重要的是`WCSession`类,它控制着两个设备间(手表和手机)的通信。你不需要创建这个类的实例,因为你可以使用`defaultSession`来获取它的单例。
+
+你最好分配一个继承了`WCSessionDlegate`协议的对象,并且把它赋值给session的delegate属性,然后调用`activateSession()`。如果你计划频繁的使用Watch Connectivity,我强烈推荐你今早在你的app lifecycle代码中做上面的步骤。
+
+一旦你把你的session配置并激活了,你可以用下列方法来转换你的数据和文件:
+
+- `updateApplicationContext(_:error)`这个方法被用来传递一个Dictionary的数据给配对的app。但是,它不会马上生效,只有当配对的app启动或者是到前台的时候,这个传递的数据才能被利用。配对的app接收到这个Dictionary通过它的session代理(delegate)`session(_:didReceiveUpdate:)`方法。
+- `sendMessageData(_:replyHandler:errorHandler:)`被用来立即传送数据给配对的app。对这个方法的调用都会存在一个队列里,然后被调用的顺序就是你调用它们的顺序。如果你从watch传送数据到iOS app,那么如果这个app不在运行中,它就会在后台被唤醒。如果你从iOS app发送数据到watch,当watch上的这个app不在运行的时候,就会调用error的这个闭包。配对的app接受数据通过session的代理(delegate)`session(_:didReceiveMessageData:replyHandler:)`方法。
+- `transferFile(_:metadata:)`用来在后台传递文件,比如说图片。这个方法和前面两个方法一样,唤醒iOS app如果它不在运行的话,或者是调用error闭包如果watch app不在运行的话。你们需要实现的代理方法是`session(_:didReceiveFile:)`,它用来接受文件。
+
+这个框架提供了很多的其他的方法,所以我强烈推荐你们看这份[指南](https://developer.apple.com/library/prerelease/watchos/documentation/WatchConnectivity/Reference/WatchConnectivity_framework/index.html#//apple_ref/doc/uid/TP40015269)
+
+看吧,我已经告诉过你们不需要去担心失去了`openParentApplication(_:reply:)`的。:)
+
+## 一些其他的WatchKit新东西
+
+我已经讲了新的WatchKit中的两个最大的改变的细节,但是仍然有一些其他的东西我认为值得一提。
+
+### 动画
+
+WatchKit仍然保留了以前的动画方法--用预先设置好的一系列图像来做循环。**WKInterfaceController**现在提供了一些基础的动画支持,类似于iOS,通过这个新的 `animateWithDuration(_:animations:)`方法。
+这个需要一个duration和一个block作为参数,然后在这个block中并且在duration时间内,对界面对象做出一些动画。例如:
+
+```
+animateWithDuration(1) { () -> Void in
+ self.label.setHorizontalAlignment(.Left)
+ self.label.setAlpha(0)
+}
+```
+
+你也可以对下列的**WKInterfaceObject**的属性进行动画操作。
+
+- alpha(透明度)
+- width and height(宽和高)
+- vertical and horizontal alignment(垂直和水平对齐)
+- background color(背景色)
+- layout group insets(group的间距)
+
+>注意:glance和自定义通知不支持动画,还有,许多你在动画方法的block中做出的改变将会立即生效而不会有动画效果。
+
+我觉得这看起来有点初级,因为没有办法去提供一个表示时间的曲线,或者是不知道什么时候动画已经完成了。但是我确信你也会觉得Apple在对的方向走了一大步,而且我也希望Apple在将来提供这些特性。
+
+### 更多的操作硬件的API
+
+watchOS 2的WatchKit提供了更多的硬件和传感器接口。包括Digital Crown,Taptic Engine,心率传感器,重力加速器还有麦克风。
+
+这些接口为你们的app打开了新世界的大门!比如说,能够通过心率传感器精确的检测心跳然后用在你的健康app里,或者是使用Taptic Engine来提供一个更加真实的游戏体验。我是真的等不及看到你们能用这些东西创造些什么了!!
+
+### 新的界面元素
+
+在最新的WatchKit中,Apple新加了一个控件:**WKInterfacePicker**。它提供了一个可以滚动的列表,你通过Digital Crown来和它进行交互。
+
+picker中的item可以是文字,图片,或者是两者的混合。当用户选择了一个item,Watchkit调用picker的事件方法来传递这个item的索引。
+
+一个picker控件有下列三种不同的方式来展示:
+
+- List:这个方式是用来展示竖直的像栈一样的列表。
+- Stacked:这个方式展示item就像一堆卡片,item从Pikcer的底部用动画的方式进入。通过Digital Crown来进行控制。
+- Image Sequence:这个方式是用来从一系列图片中展示一张图片。
+
+当然,在一个interface controller中,包含多个picker也是可行的。
+
+讲到interface controller就不得不说,**WKInterfaceController**现在提供一个展示alerts和action sheets的方法了!
+
+调用`presentAlertControllerWithTitle(_:message:preferredStyle:actions:)`方法来在当前interface controller展示一个alert或者是一个action sheet,展示哪种当然依赖于你传的**preferredStyle**参数。这个**action**参数或许是这四个参数中最有趣的,因为它是一个**WKAlertAction**实例的数组。这个WKAlertAction提供了一个block,当用户点击对应这个action的button的适合,这个block就会被调用。让这个alert或者是action sheet消失的按钮的block你完全不需要写任何的东西。。
+
+### One More Thing…
+
+在播客的前面,当Jake和我正在讨论我们在WWDC上想看到什么,有一件事是我们都想看到的。那就是自定义Complications(这个我不知道该如何翻译成中文,请校对者注意一下这里)。你可以把它想象成放在钟表界面上的小插件。我猜Tim厨子应该也是一个很好的倾听者,我们提意见,他就把它实现。
+
+OK,现在你可以给你自己的手表app构建自定义complications,然后让你的数据显示在用户选择的表的界面上。
+
+自定义complications是建立在一种家族的概念上,每种家族提供一个或多个模板类,然后你创建一个继承于它的子类来实现你自己的complication。在complications中,现在有五种家族:
+
+- Modular:有一些小的和大的变体在这个家族中,可以在MODULAR表的界面上看到它们。
+- Circular:在这个家族中只有一个小的变体,可以在SIMPLE表的界面的上看到它们。
+- Utilitarian:像Modular一样,有一些小的和大的变体在这个家族中,可以在UTILITY表的界面上看到它们。
+
+如果你没有一个Apple Watch或者是不熟悉这些表的界面,查阅这份[指南](https://developer.apple.com/library/prerelease/watchos/documentation/General/Conceptual/AppleWatch2TransitionGuide/DesigningaComplication.html#//apple_ref/doc/uid/TP40015234-CH11-SW1)来获取更多。阅读[watchOS 2 Transition Guide](https://developer.apple.com/library/prerelease/watchos/documentation/General/Conceptual/AppleWatch2TransitionGuide/index.html)里的[Providing Custom Complications](https://developer.apple.com/library/prerelease/watchos/documentation/General/Conceptual/AppleWatch2TransitionGuide/DesigningaComplication.html#//apple_ref/doc/uid/TP40015234-CH11-SW1)这一节来获取更多的关于构建自定义complications的细节。
+
+自定义complications对我来说是非常棒的特性,我等不及深入了解它的api然后开始构建我自己的complication!
+
+### 接下来何去何从?
+
+总而言之,我对watchOS 2里的所有新特性感到非常的激动!我期待和我的Turorial团队在接下来的几个月出[WatchKit By Tutorials](http://www.raywenderlich.com/store/watchkit-by-tutorials) 的第二版书。
+
+记住,这仅仅是watchOS 2里的WatchKit的第一版,按Apple的尿性,在接下来的会增加更多的特性和API。在这篇文章我讲到的在未来都可能会改变或者是被更好的替代。以前发生过得,以后也会发生!:)
+
+我很好奇你的对WatchKit最新版本的初印象,请在评论区说出你的想法哦! :)
\ No newline at end of file
diff --git "a/issue-10/\344\275\277\347\224\250Quick\346\241\206\346\236\266\345\222\214Nimble\346\235\245\346\265\213\350\257\225ViewControler.md" "b/issue-10/\344\275\277\347\224\250Quick\346\241\206\346\236\266\345\222\214Nimble\346\235\245\346\265\213\350\257\225ViewControler.md"
new file mode 100644
index 0000000..f1049ca
--- /dev/null
+++ "b/issue-10/\344\275\277\347\224\250Quick\346\241\206\346\236\266\345\222\214Nimble\346\235\245\346\265\213\350\257\225ViewControler.md"
@@ -0,0 +1,354 @@
+
+# Testing view controllers with Quick and Nimble
+# 使用Quick框架和Nimble来测试ViewControler
+----------------------------------------------
+
+> * 原文链接 : [Testing view controllers with Quick and Nimble ](https://medium.com/@MarcioK/how-you-can-test-view-controllers-with-quick-nimble-328f895b55ee)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [Mr.Simple](https://github.com/bboyfeiyu)
+* 校对 : [Lollypo](https://github.com/Lollypo)
+
+
+ViewController是iOS应用开发的支柱之一,[它们是用户界面和业务逻辑、模型的纽带](http://www.objc.io/issue-15/behavior-driven-development.html),即便只是修改了它其中的一点代码也可能会引发严重的问题,因此对ViewController进行测试就变得极为重要。然而,为ViewController写测试代码一直以来都是一个晦涩难懂的话题。这篇文章中,我会尝试使用[Quick和Nimble](https://github.com/Quick/Quick)框架来解开这些疑惑。
+
+#### Quick 和 Nimble
+
+**Quick** 是一个针对于Swift和Objective-C的[行为驱动开发](http://en.wikipedia.org/wiki/Behavior-driven_development)框架. 它的灵感来自于
+[RSpec](https://github.com/rspec/rspec),
+[Specta](https://github.com/specta/specta), 和
+[Ginkgo](https://github.com/onsi/ginkgo).
+
+**Nimble** 是一个同时适用于Swift和Objective-C语言的匹配框架.
+
+换句话说,Quick是一个用于创建[BDD](http://en.wikipedia.org/wiki/Behavior-driven_development)测试的框架。配合Nimbl,可以为你创建更符合预期目标的测试。
+
+### 示例
+
+这个示例是使用Swift 1.2创建的名为Pony的iOS应用。这个应用含有一个底部栏和一个用于在应用启动时弹出应用简介信息的ViewController。Main.storybard看起来如下图所示 :
+
+
+
+
+运行后 :
+
+
+
+( 如果浏览器不支持视频播放,可以手动拷贝地址到浏览器中观看,地址在 https://d262ilb51hltx0.cloudfront.net/max/1600/1*9HuloDCQQ3Ul2t4KGQGkYg.ogv ).
+
+1. 加载应用.
+2. 弹出简介信息.
+
+### 为Pony配置Quick和Nimble
+
+如果你安装了最新版的[CocoaPods](http://cocoapods.org),你可以将下面的配置代码添加到Podfile中 :
+
+```
+platform :ios, '8.0'
+
+source 'https://github.com/CocoaPods/Specs.git'
+use_frameworks!
+
+target 'PonyTests', :exclusive => true do
+ pod 'Nimble', :git => 'https://github.com/Quick/Nimble.git' #, :branch => 'swift-1.1' # if you want to use swift 1.1
+ pod 'Quick', :git => 'https://github.com/Quick/Quick.git', # :branch => 'swift-1.1'
+end
+```
+
+#### Quick文件模板(可选)
+
+*你可以使用Alcatraz 或者* [*通过
+Rakefile手动创建*](https://github.com/Quick/Quick/blob/master/Documentation/InstallingFileTemplates.md)
+
+#### 注意
+
+**在开始之前,确保你的类是public的, 例如:**
+
+```
+public class MyPublicClass {
+
+ public var myPublicProperty: String?
+ public func myPublicFunc () {
+ //...
+ }
+}
+```
+
+#### 创建一个测试类
+
+如果Quick模板已经安装了,那么你可以选择quick模板、创建一个新的文件,然后import应用模块到文件中,例如 :
+
+```swift
+import Quick
+import Nimble
+import MyAppModule // Importing the app module
+
+class HelloTest: QuickSpec {
+ override func spec() {
+ //...
+ }
+}
+```
+
+如果你导入应用模块有问题,可以参考这篇文章:
+
+
+
+### 测试
+
+#### PonyTabController: UITabBarController
+
+PonyTabController是UITabController的子类,它的职责是在第一次进入应用时展示应用简介(appIntroViewController)。
+
+
+```swfit
+public class PonyTabController: UITabBarController {
+
+ override public func viewDidAppear(animated: Bool) {
+ super.viewDidAppear(animated)
+
+ let userDefaults = NSUserDefaults.standardUserDefaults()
+
+ if !userDefaults.boolForKey("appIntroHasBeenPresented") {
+ let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
+ let appIntroViewController = storyboard.instantiateViewControllerWithIdentifier("appIntroViewControllerID") as! AppIntroViewController
+
+ appIntroViewController.delegate = self
+
+ self.presentViewController(appIntroViewController, animated: true) {
+ userDefaults.setBool(true, forKey: "appIntroHasBeenPresented")
+ }
+ }
+ }
+}
+
+extension PonyTabController: AppIntroDelegate {
+
+// MARK: - AppIntroDelegate
+
+ public func appIntroDidFinish(appIntro: UIViewController!) {
+ // Dismissing app intro
+ dismissViewControllerAnimated(true, completion:nil)
+ }
+}
+```
+
+让我们从viewDidAppear中开始 :
+
+***“当应用简介从来没有被dismissed时,它会被当做appIntroDelegate设置给PonyTabController”***
+
+
+```swift
+import Quick
+import Nimble
+import Pony
+
+class PonyTabBarControllerSpec: QuickSpec {
+ override func spec() {
+ var tabBarController: PonyTabController!
+
+ describe("viewDidAppear"){
+ describe("When app intro had never been dismissed"){
+ it("should be set as the appIntroDelegate"){
+ }
+ }
+ }
+ }
+}
+```
+
+现在我们有了期望的描述信息,但是我们还缺少一些东西。我们还没有断言或者用于测试的对象,也没有任何的测试方法会被调用。为了更简介的进行初始化,我们可以将测试代码分为三个部分,例如 :
+
+**准备工作:**
+
+- 创建PonyTabController实例;
+
+**执行:**
+
+- 触发PonyTabController的生命周期方法;
+
+**断言:**
+
+- tabController作为appIntroViewController的delegate;
+
+完成的测试代码 :
+
+```swift
+import Quick
+import Nimble
+import Pony
+
+class PonyTabBarControllerSpec: QuickSpec {
+ override func spec() {
+ var tabBarController: PonyTabController!
+
+ describe(".viewDidAppear"){
+
+ describe("When app intro had never been dismissed"){
+
+ var appIntroViewController: AppIntroViewController?
+
+ beforeEach{
+ // Arrange:
+ NSUserDefaults.standardUserDefaults().setBool(false, forKey: "appIntroHasBeenPresented")
+ let storyboard = UIStoryboard(name:"Main", bundle: NSBundle.mainBundle())
+ tabBarController = storyboard.instantiateInitialViewController() as! PonyTabController
+ let window = UIWindow(frame: UIScreen.mainScreen().bounds)
+ window.makeKeyAndVisible()
+ window.rootViewController = tabBarController
+
+ // Act:
+ tabBarController.beginAppearanceTransition(true, animated: false) // Triggers viewWillAppear
+ tabBarController.endAppearanceTransition() // Triggers viewDidAppear
+ appIntroViewController = tabBarController.presentedViewController as! AppIntroViewController!
+ }
+
+ it("should be set as the appIntroDelegate"){
+ // Assert:
+ expect(appIntroViewController!.delegate as? PonyTabController).to(equal(tabBarController))
+ }
+ }
+ }
+ }
+}
+```
+
+通过[Arrange-Act-Assert](http://c2.com/cgi/wiki?ArrangeActAssert%5C)模式组织测试代码是一种非常好的实践,它的优点如下 :
+
+- 明确区分出初始化与验证步骤中有哪些需要被测试;
+- 关注点集中在测试历史记录和必须的测试步骤上。
+
+现在,做同样的操作 :
+
+“***当应用简介从来没有被dismissed时,它应该被展示****”*
+
+```swift
+import Quick
+import Nimble
+import Pony
+
+class PonyTabBarControllerSpec: QuickSpec {
+ override func spec() {
+ var tabBarController: PonyTabController!
+
+ describe(".viewDidAppear"){
+
+ context("When app intro had never been dismissed"){
+
+ var appIntroViewController: AppIntroViewController?
+
+ beforeEach{
+ // Arrange:
+ NSUserDefaults.standardUserDefaults().setBool(false, forKey: "appIntroHasBeenPresented")
+ let storyboard = UIStoryboard(name:"Main", bundle: NSBundle.mainBundle())
+ tabBarController = storyboard.instantiateInitialViewController() as! PonyTabController
+ let window = UIWindow(frame: UIScreen.mainScreen().bounds)
+ window.makeKeyAndVisible()
+ window.rootViewController = tabBarController
+
+ // Act:
+ tabBarController.beginAppearanceTransition(true, animated: false) // Triggers viewWillAppear
+ tabBarController.endAppearanceTransition() // Triggers viewDidAppear
+
+ appIntroViewController = tabBarController.presentedViewController as! AppIntroViewController!
+ }
+
+ it("should be set as the appIntroDelegate"){
+ // Assert:
+ expect(appIntroViewController!.delegate as? PonyTabController).to(equal(tabBarController))
+ }
+
+ it("should be presented"){
+ // Assert:
+ expect(tabBarController.presentedViewController).toEventually(beAnInstanceOf(AppIntroViewController))
+ }
+ }
+ }
+ }
+}
+```
+
+“***当应用简介从来没有被dismissed且appIntroDidFinish被调用时应该dismiss应用简介"***
+
+
+
+```swift
+import Quick
+import Nimble
+import Pony
+
+class PonyTabBarControllerSpec: QuickSpec {
+ override func spec() {
+ var tabBarController: PonyTabController!
+
+ describe(".viewDidAppear"){
+ describe("When app intro had not been presented"){
+ // ... previous test
+ context("and appIntroDidFinish is called") { // Introducing a context
+
+ let userDefaults = NSUserDefaults.standardUserDefaults()
+
+ beforeEach {
+ // Arrange:
+ userDefaults.setBool(false, forKey: "appIntroHasBeenPresented")
+
+ // Act:
+ // Triggers viewWillAppear and viewDidAppear:animated
+ tabBarController.beginAppearanceTransition(true, animated: false)
+ tabBarController.endAppearanceTransition()
+
+ // - Dismissing app intro.
+ tabBarController.appIntroDidFinish(appIntroViewController)
+ }
+
+ it("should dismiss app intro"){
+ // Assert:
+ expect(appIntroViewController!.isBeingDismissed()).toEventually(beTrue())
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+“describe” 和 “context”的简单介绍:
+
+**Describe**: 包装一个功能测试集合;
+**Context:** 在同一状态下包装一个功能测试集合;
+
+
+**你可以在[这里](https://github.com/getyourguide/Pony)查看该项目完整的代码** 。
+
+
+### 其他
+
+Nimble含有一个[*waitUntil*](https://github.com/Quick/Nimble#asynchronous-expectations)方法,你能够在它里面执行闭包代码,当done函数被调用时表明它已经ready,如果done函数没有被调用,那么第二个测试将会失败。如果你需要扩展定时函数来指定一个超时参数来确定你在执行某个函数时多长时间视为失败。例如,当你想等待一个ViewController被展示时你可以这么处理 :
+
+```swift
+waitUntil{
+ done in
+ tabBarController.presentViewController(viewController,
+ animated: false){
+ done()
+ }
+}
+```
+
+### 结论
+
+- 明确测试目标,你可以使用Quick框架和Nimble来实现更有意义的测试;
+- 你必须要遵守它的生命周期函数 ,例如如果另一个ViewController正在展示或者视图不是你的视图层级的一部分时你不能展示另一个ViewController ;
+- UIKit提供了一些public的函数来帮助你触发view controller 的状态;
+- 测试ViewController有时候是相当蛋疼的,尽量保持这些测试代码的简洁性。
+
+### 参考资料
+
+- [www.objc.io/issue-15/behavior-driven-development.html](http://www.objc.io/issue-15/behavior-driven-development.html)
+-
+-
+-
+
+### 特别鸣谢
+
+[@eldudi](https://twitter.com/eldudi) 和
+[@banaslee](https://twitter.com/banaslee) 。
diff --git "a/issue-11/Swift-2.0-Beta-1\346\240\207\345\207\206\345\272\223\347\232\204\346\224\271\345\217\230.md" "b/issue-11/Swift-2.0-Beta-1\346\240\207\345\207\206\345\272\223\347\232\204\346\224\271\345\217\230.md"
new file mode 100644
index 0000000..60aafb1
--- /dev/null
+++ "b/issue-11/Swift-2.0-Beta-1\346\240\207\345\207\206\345\272\223\347\232\204\346\224\271\345\217\230.md"
@@ -0,0 +1,196 @@
+#Swift 2.0 Beta 1 标准库的改变
+---
+
+> * 原文链接 : [Changes to the Swift Standard Library in 2.0 Beta 1](http://airspeedvelocity.net/2015/06/09/changes-to-the-swift-standard-library-in-2-0-beta-1/?utm_campaign=iOS%2BDev%2BWeekly&utm_medium=web&utm_source=iOS_Dev_Weekly_Issue_202)
+* 原文作者 : [airespeedvelocity](http://airspeedvelocity.net)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [samw00](https://github.com/samw00)
+* 校对者: [Mr.Simple](https://github.com/bboyfeiyu)
+* 状态 : 完成
+
+OK 先不要慌 - 看起来好像有很多东西都改变了,但事实上不是。说是改造了,朝着更好的方向...可能更加贴切。总之,没有什么是Migration Assitant处理不了的。
+
+目前为止,给标准库带来最大变化的是新的扩展协议。`map`函数已死,集合类型和序列类型的`map`扩展万万岁。这意味着现在你永远只能把`map`当作一个方来来调用 - 再也不用在函数和方法之间来回穿梭,就像你在看一场网球比赛一样。
+
+为了展示我所说的,先定义一个我常用的例子,`mapSome`:
+
+```swift
+extension SequenceType {
+ /// Return an `Array` containing the results of mapping `transform`
+ /// over `self`, discarding any elements where the result is `nil`.
+ ///
+ /// - Complexity: O(N).
+ func mapSome(@noescape transform: Generator.Element -> U?) -> [U] {
+ var result: [U] = []
+ for case let x? in self.map(transform) {
+ result.append(x)
+ }
+ return result
+ }
+}
+```
+
+你可以在method-calling风格中使用它,比如,在全新的字符串类型转浮点类型可失败构造器中:
+
+```swift
+let a = ["3.14", "foo", "6.02e23"]
+let double = a.mapSome{ Double($0) }
+print(doubles) // no more println!
+// prints [3.14, 6.02e+23]
+```
+
+顺带地, `mapSome`使用了`for`的新的格式匹配能力来将所有的`nils`都过滤掉了。函数中的`case let`语句是要匹配一个枚举类型 - 刚好可选值类型本质就是枚举类型,所以对它们就正起作用。同时`for`语句也意味着跟着一个`where`语句:1
+
+```swift
+let isEven = { $0%2 == 0 }
+for even in 0..<10 where isEven(even) {
+ print(even)
+}
+```
+
+不单单就这些。你可以给扩展添加约束,就好比你可以给一个泛型函数的占位符添加约束一样。这就意味着你可以使扩展中的方法在拥有制定的属性前提下才能起作用。举个例子,现在有一个用在数组(或者任何集合类型)上的新`sort`方法是不用传一个`isOrderedBefore`参数的,只要数组的内容是具有可比性就行了。如果你想要更加自定义的东西,总可以写个重载然后传一个闭包,但是如果你只要一些很默认的行为(升序),你就不需要搞那么复杂了。
+
+下面就是一个简单的例子,当所有的值都等于某个值的时候就返回真:
+
+```swift
+// a version that only applies when the contents of the
+// sequence are equatable
+extension SequenceType where Generator.Element: Equatable {
+ /// Return `true` if every element of `self` is `x`.
+ func all(equalTo: Generator.Element) -> Bool {
+ // of course, contains is now a method too
+ return [self.contains { $0 != equalTo }
+ }
+}
+
+// and an unconstrained version where the caller supplies a predicate
+extension SequenceType {
+ /// Return `true` if every element of `self` satisfies `predicate`.
+ func all(criteria: Generator.Element -> Bool) -> Bool {
+ return [self.contains { !criteria($0) }
+ }
+}
+
+[1, 1, 1].all(1) // true
+
+let isEven = { $0%2 == 0 }
+[2, 4, 6].all(isEven) // true
+```
+
+其结果就是,标准库中的自由函数数量从101个减少到77个。我在想还会减少多少个呢?- 最终是否只会一些`unsafeBitCast`杂牌军之流吗?`abs()`会成为`SignedNumberType?`的扩展吗?现在它就行了,那么它应该成为一个属性吗?统统这些还有更多将再下一期Swift发布中揭晓...
+
+#Grab Bag
+
+下面是关于标准库的其他一些改变:
+
+* `Array.withUnsafeMutableBufferPointer`会产生一个警告:调用这个方法时,不要使用数组本身,而只要将指针指向数组内容就行。想必数组的更新可能会推迟到方法调用完成之后。同理适用于`ContiguousArray`和`Slice`。
+* 新标准库中一些`internal-type`的构造器被私有化了。
+* 一些已`_`开头的协议也开始消失了。比如`_BidirectionalIndexType`就被去掉了,然后`BidirectionalIndexType`继承它前任的方法。还有`_Comparable`也没了,`Comparable`先在有`<`操作符。
+* `CollectionOfOne`和`EmptyCollection`现在有一个`count`属性 - 可以用来检查你是否被它们所误导了。
+* 现在开启了伟大的扩展时代...`CollectionType`现在有`isEmpty, count, map, filter, first, last, indexOf, indices 和 reverse`。这些方法所有的集合类型都能使用了。而集合类型对应的方法则被去掉了。
+* `find`曾经经常调用`indexOf`。现在有个新版本可以传`predicate`。
+* 新加入了一个`ErrorType`协议,用来支持新的错误处理机制。
+* 原来的`CustomStringConvertible`和`CustomDebugStringConvertible`现在变为`Printable`和`DebugPrintable`。
+* 还有`CustomReflectable, CustomLeafReflectable` 和 `CustomPlaygroundQuickLookable`协议让供你在playground对你类型的行为进行微调。
+* 还有一个`Mirror`类型供你配合和`CustomReflectable`协议使用。
+* 还有一个新的`DictionaryLiteral`类型,现在你可以放心大胆的给除字典以外用`[:]`,而不必担心会有相同的`key`碰撞在一起。
+* `Double, Float`和`Float80`从`strings`那里获得了可失败构造器。
+* 现在整数型有一个一样,替换了字符串类型的`toInt`方法。并且接受一个`radix`参数!你仍然不能从头文件看出默认参数就是什么默认值,但是我猜这一次是10.
+* `FloatingPointType`中的`_toBitsPattern`和`_fromBitPattern`没了。如果你喜欢和`bits`打交道你可以使用`unsafeBitCast`。
+* 所有延迟加载集合类型都有一个`underestimateCount`属性。
+* `MutableCollectionType`是一个带有`where`语句很好的扩展例子 - 比如为了让某个集合类型有`partition`和`sortInPlace`支持需要该集合类型能够被随机访问。
+* `SequenceType`在调用`contains, sort, underestimateCount, enumerate, minElement, maxElement, elementsEqual, lexicographicalCompare`和`flatMap`时表现会非常糟糕。
+* 比较两个`sequence`是否`equal`的新方法名是`elementsEqual`。
+* `minElement`和`maxElement`现在返回的是可选值类型,而且还接受`isOrderedBefore`闭包。
+* 有一个新的协议叫`SetAlgebraType`,适用于'set里面每一个独特元素不一定不相交‘的类型。`Set`目前并没有遵守该协议(但已有所有必要的属性了)。
+* `Set`倒是遵守`OptionSetType`协议,这都是为了`bitfield`枚举。
+* `print`也没了!现在`println`就是`print`而老的`print`是`print(1, newLine: false)`。
+* `toString`不再被使用了。简单粗暴就用String的构造器。
+* `readLine`从标准输出流读取,返回一个可选字符串类型,所以你可以用`while let`来循环。
+
+#协议扩展为默认
+
+先把愚蠢的玩笑放一边,`CollectionOfOne`和`EmptyCollection`之所以会有`count`是有一个非常站得住脚的理由的。如果协议有一个默认的实现方式,但是你知道你的类型自己自带一个更好的实现方法能够更加快速的解决问题,那么这个类型自带的方法则会覆盖协议当中的那个版本。
+
+假设,我们实现了下面这个集合类型中的`next`逻辑(没啥特别意义):`CollectionOfTwo:`
+
+```swfit
+struct CollectionOfTwo: CollectionType {
+ let first: T, second: T
+ subscript(idx: Int) -> T {
+ precondition(idx < 2, "Index out of bounds")
+ return idx == 0 ? first : second
+ }
+ var startIndex: Int { return 0 }
+ var endIndex: Int { return 2 }
+}
+
+let two = CollectionOfTwo(first: "42", second: "420")
+",".join(two) // "42,420"
+```
+
+注意我并没有定义一个generator - 全托这个就手的新协议扩展:
+
+```swift
+// CollectionType 遵守 _CollectionGeneratorDefaultsType
+// 而后者被加了如下扩展
+extension _CollectionGeneratorDefaultType {
+ func generate() -> IndexingGenerator
+}
+```
+
+不管怎么讲,`CollectionOfTwo`遵守了集合类型所有它自然而然的有`count`属性。但是它得到结果的方式是从一开始就减去最后的索引,这看起来是一个挺迂回的方式。所以你可以直接把值写死然后明确的说明要实现它:
+
+```swift
+extension CollectionOfTwo {
+ var count: Int { return 2 }
+}
+```
+
+现在,当调用`count`的时候,则会返回代码中的数而不是默认值了。
+
+协议当中的方法也可以覆盖其他协议中定义的方法 - 所以集合类型可以定义`map`虽然序列类型也定了`map`。
+
+虽然我们在实现一些简单的小类型 - 自定义字符串来渲染自定义类型现在要变的好多了。虽然它们现在并不遵守`CustomeStringConvertible`,如果你打印`CollectionOfTwo`,你会得到的东西看起来像`CollectionOfTwo(first:42, second 420)`,这要比你之前会得到的`__lldb_expr_11.CollectionOfTwo`要好的多了。
+
+#字符串再也不是集合类型了
+
+如果有些东西让你觉得惊讶 - 那就是字符串类型再也不是集合类型了。尽管它有所有必要的属性(`extension String: CollectionType { }`这样是不会抱错的),它再也不会被标注为集合类型了。这肯定是因为,就算字符串用一个个字符来表示,将集合的算法作用于字符串上也有可能导致一些`non-Unicode-correct`结果。
+
+当然,你仍然需要改变你的字符串,与其死盯着它们看,你不如使用字符的新属性`CharacterView`:
+
+```swift
+let s = "comma,separated,strings"
+let fields = split(s.characters) { $0 == "," }.map { String($0) }
+Since String's Index is just a typealias for String.CharacterView.Index, you can use them interchangeably:
+
+let s = "Hello, world!"
+if let comma = s.characters.indexOf(",") {
+ print(s[s.startIndex.. * 原文链接 : [File Management Tutorial in iOS8 with Swift](http://www.ioscreator.com/tutorials/file-management-tutorial-ios8-swift)
+* 原文作者 : [http://www.ioscreator.com](http://www.ioscreator.com/)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [HarriesChen](https://github.com/mrchenhao)
+* 校对者: [这里校对者的github用户名](github链接)
+* 状态 : 未完成 / 校对中 / 完成
+
+
+当一款iOS的App安装到设备上以后,将会创建一个文件夹,其中包含了用于存储临时文件的临时文件夹。这篇文章我们在管理该临时文件夹,包括创建读取和删除文件。教程基于iOS8.1和Xcode6.1
+
+打开Xcode创建一个新的`Single View Application`项目,以`IOS8SwiftFileManagementTutorial `作为项目的名称,填写组织名称和组织标识符。选择Swift语言,确保Devices为iPhone。
+
+
+
+在故事版中拖四个按钮到主视图中,名称分别为以下:
+
+* Create File
+* List Directory
+* View File Content
+* Delete File
+
+故事版看起来像这样
+
+
+
+通过按住Ctrl键选择全部按钮,然后点击在底部的第三个按钮选择`Resolve Auto Layout Issues`来添加缺少的约束。
+
+
+按钮依次放置,即使在设备旋转的时候,选择`Assistant Editor`并让ViewController.swift可见。按住Ctrl将`Create File`拖到`ViewController `类,创建以下动作。
+
+
+
+
+按住Ctrl将`List Directory`拖到`ViewController `类,创建以下动作。
+
+
+
+
+按住Ctrl将`View File Content`拖到`ViewController `类,创建以下动作。
+
+
+
+
+按住Ctrl将`Delete File`拖到`ViewController `类,创建以下动作。
+
+
+
+
+在`ViewController.swift`文件中添加下列属性。
+
+```
+var fileManager = NSFileManager()
+var tmpDir = NSTemporaryDirectory()
+let fileName = "sample.txt"
+```
+
+* `NSFileManager `类让你查看文件系统的内容并改变它。
+* `NSTemporaryDirectory `是当前用户的一个临时文件夹
+* "sample.txt"文件将在本教程中用到
+
+
+在动作方法实现之前先创建一个`enumerateDirectory`方法用来检查文件是否存在。
+
+```
+func enumerateDirectory() -> String? {
+ var error: NSError?
+ let filesInDirectory = fileManager.contentsOfDirectoryAtPath(tmpDir, error: &error) as? [String]
+
+ if let files = filesInDirectory {
+ if files.count > 0 {
+ if files[0] == fileName {
+ println("sample.txt found")
+ return files[0]
+ } else {
+ println("File not found")
+ return nil
+ }
+ }
+ }
+ return nil
+}
+
+```
+
+临时文件夹中的所有文件将被放在一个字符串的数组中,这个数组将被用来检查sample.txt是否存在。如果存在,文件名会被返回。否则返回nil。接着实现文件创建方法。
+
+```
+@IBAction func createFile(sender: AnyObject) {
+ let path = tmpDir.stringByAppendingPathComponent(fileName)
+ let contentsOfFile = "Sample Text"
+ var error: NSError?
+
+ // Write File
+ if contentsOfFile.writeToFile(path, atomically: true, encoding: NSUTF8StringEncoding, error: &error) == false {
+ if let errorMessage = error {
+ println("Failed to create file")
+ println("\(errorMessage)")
+ }
+ } else {
+ println("File sample.txt created at tmp directory")
+ }
+}
+```
+
+使用`writeToFile:atomically:encoding:error`将sample.txt被创建并写到临时文件夹中。如果失败,一个错误信息会被打印到控制台。接下来实现`listDirectory `方法。
+
+
+```
+@IBAction func listDirectory(sender: AnyObject) {
+ // List Content of Path
+ let isFileInDir = enumerateDirectory() ?? "Empty"
+ println("Contents of Directory = \(isFileInDir)")
+}
+```
+
+
+`enumerateDirectory `被调用,当没有文件存在的时候将`isFileInDir `赋值为`Empty`。??操作符用来判断表达式`(enumerateDirectory() )`是否为nil,当为真的时候可选变量会被拆包。当为假的时候会有默认值"Empty",文件夹的内容会被打印到控制台。接下来实现`viewFileContent `方法。
+
+```
+@IBAction func viewFileContent(sender: AnyObject) {
+ let isFileInDir = enumerateDirectory() ?? ""
+
+ let path = tmpDir.stringByAppendingPathComponent(isFileInDir)
+ let contentsOfFile = NSString(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil)
+
+ if let content = contentsOfFile {
+ println("Content of file = \(content)")
+ } else {
+ println("No file found")
+ }
+}
+```
+
+`contentsOfFile:encoding:error`方法将文件内容放在一个字符串中。如果文件存在,将会被打印到控制台,如果不存在,将会打印'No file found'消息。最后来实现`deleteFile `方法。
+
+```
+@IBAction func deleteFile(sender: AnyObject) {
+ var error: NSError?
+
+ if let isFileInDir = enumerateDirectory() {
+ let path = tmpDir.stringByAppendingPathComponent(isFileInDir)
+ fileManager.removeItemAtPath(path, error: &error)
+ } else {
+ println("No file found")
+ }
+}
+```
+
+`removeItemAtPath:error`方法用来删除文件,编译并运行工程,选择`console`按钮来查看控制台输出。
+
+
+
+
+你可以在GitHub下载源码[IOS8SwiftFileManagementTutorial](https://github.com/ioscreator/ioscreator)
\ No newline at end of file
diff --git a/issue-12/readme.md b/issue-12/readme.md
new file mode 100644
index 0000000..e69de29
diff --git "a/issue-13/Swift\350\247\206\351\242\221\345\275\225\345\210\266\345\274\200\345\217\221\346\214\207\345\215\227.md" "b/issue-13/Swift\350\247\206\351\242\221\345\275\225\345\210\266\345\274\200\345\217\221\346\214\207\345\215\227.md"
new file mode 100644
index 0000000..f4508ea
--- /dev/null
+++ "b/issue-13/Swift\350\247\206\351\242\221\345\275\225\345\210\266\345\274\200\345\217\221\346\214\207\345\215\227.md"
@@ -0,0 +1,132 @@
+# Swift视频录制开发指南
+===
+
+> * 原文链接 : [Take Video Tutorial in iOS8 with Swift](http://www.ioscreator.com/tutorials/take-video-tutorial-ios8-swift)
+* 原文作者 : [ioscreator](www.ioscreator.com)
+* [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [Mr.Simple](https://github.com/bboyfeiyu)
+* 校对者: [StormXX](https://github.com/StormXX)
+
+Apple提供了一个UIImagePickerController用户界面类来让用户使用内置的摄像头来拍摄视频。我的开发环境是iOS 8.4和xcode 6.4,在这篇文章中我会教大家获取已经存储在Photo Library的视频。
+
+首先打开Xcode,然后创建一个名为IOS8SwiftTakeVideoPlayerTutorial的应用,Organization名和Organization标识符使用你自己的即可。选择Swift作为开发语言,并且该应用只支持Iphone。
+
+
+
+到Storyboard页面,从Object Library拖两个按钮到主视图上,给这两个按钮设置title,分别为"Take Video" 和 "View Library",Storyboard此时应该如下图所示:
+
+
+
+按住Ctrl键,然后选中这两个按钮,点击在Storyboard右下方的“Resolve Auto Layout Issues”按钮,然后选择“Add Missing Constraints”。如下图所示 :
+
+
+
+切换到Assistant Editor界面,并且确保ViewController.swift是可见的。按住Ctrl并从Take Video按钮区域拖出以创建一个Action.
+
+
+
+同样的,按住按住Ctrl并从View Library按钮区域拖出以创建一个Action。
+
+
+
+切换到ViewController.swfit文件,在文件的头部添加如下代码。
+
+```
+import MobileCoreServices
+import AssetsLibrary
+```
+
+修改ViewController类的定义如下 :
+
+`class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
+`
+
+这些delegates使得ViewController能够处理UIImagePickerController的代理事件,takeVideo的实现如下 :
+
+```
+@IBAction func takeVideo(sender: AnyObject) {
+ // 1 Check if project runs on a device with camera available
+ if UIImagePickerController.isSourceTypeAvailable(.Camera) {
+
+ // 2 Present UIImagePickerController to take video
+ controller.sourceType = .Camera
+ controller.mediaTypes = [kUTTypeMovie as! String]
+ controller.delegate = self
+ controller.videoMaximumDuration = 10.0
+
+ presentViewController(controller, animated: true, completion: nil)
+ }
+ else {
+ println("Camera is not available")
+ }
+}
+```
+
+1. isSourceTypeAvailable函数是检测设备的Camera是否可用;
+2. ImagePickerController用于显示Sourcetype为Camera、mediaType为Movie的视频,视频的最大长度为10秒。
+
+viewLibrary的实现 :
+
+```
+@IBAction func viewLibrary(sender: AnyObject) {
+ // Display Photo Library
+ controller.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
+ controller.mediaTypes = [kUTTypeMovie as! String]
+ controller.delegate = self
+
+ presentViewController(controller, animated: true, completion: nil)
+ }
+```
+
+在调用上述代码之后,Photo library就会被显示。如果mediaType没有设置为Movie类型,视频文件就不会被显示出来。下一步就是实现UIImagePickerControllerDelegate协议的方法 :
+
+```
+func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject: AnyObject]) {
+ // 1
+ let mediaType:AnyObject? = info[UIImagePickerControllerMediaType]
+
+ if let type:AnyObject = mediaType {
+ if type is String {
+ let stringType = type as! String
+ if stringType == kUTTypeMovie as! String {
+ let urlOfVideo = info[UIImagePickerControllerMediaURL] as? NSURL
+ if let url = urlOfVideo {
+ // 2
+ assetsLibrary.writeVideoAtPathToSavedPhotosAlbum(url,
+ completionBlock: {(url: NSURL!, error: NSError!) in
+ if let theError = error{
+ println("Error saving video = \(theError)")
+ }
+ else {
+ println("no errors happened")
+ }
+ })
+ }
+ }
+ }
+ }
+ // 3
+ picker.dismissViewControllerAnimated(true, completion: nil)
+}
+```
+
+当用户选择了一个视频时`imagePickerController(\_:didFinishPickingMediaWithInfo:)`函数会被调用,info参数中会包含被选择的视频URL。
+
+1. 首先会检测info dictionary中的mediatype是否是movie类型,如果是那么则会提取这个视频的URL;
+2. 调用writeVideoAtPathToSavedPhotosAlbum函数将视频存储到图片相册中;
+3. 隐藏该ViewController。
+
+`imagePickerControllerDidCancel`实现如下 :
+
+```
+func imagePickerControllerDidCancel(picker: UIImagePickerController) {
+ picker.dismissViewControllerAnimated(true, completion: nil)
+}
+```
+
+当用户按下取消按钮,该界面的View Controller就会被隐藏。因为模拟器没有摄像头,因此编译并且将该项目运行到真实的设备,点击"Take Video"按钮来拍一段视频,然后点击“Use Video”。再下一步选择“View Library”,此时该Video就会显示在Library中了。
+
+
+
+你可以在[这里](https://github.com/ioscreator/ioscreator)下载IOS8SwiftTakeVideoPlayerTutorial项目的完整代码。
+
diff --git "a/issue-13/iOS8\347\224\250AVAudioPlayer\346\222\255\346\224\276\351\237\263\344\271\220-Swift.md" "b/issue-13/iOS8\347\224\250AVAudioPlayer\346\222\255\346\224\276\351\237\263\344\271\220-Swift.md"
new file mode 100644
index 0000000..c3af8ea
--- /dev/null
+++ "b/issue-13/iOS8\347\224\250AVAudioPlayer\346\222\255\346\224\276\351\237\263\344\271\220-Swift.md"
@@ -0,0 +1,134 @@
+iOS8 用AVAudioPlayer播放音乐(Swift)
+---
+
+> * 原文链接 : [Play Music with AVAudioPlayer in iOS8 with Swift](http://www.ioscreator.com/tutorials/play-music-avaudioplayer-ios8-swift)
+* 原文作者 : [http://www.ioscreator.com](http://www.ioscreator.com/)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [HarriesChen](https://github.com/mrchenhao)
+
+
+`AVAudioPlayer `类提供了播放音频文件的功能,在本次教程中,我们将对一个音乐文件进行播放暂停和停止操作,此外还会显示标题和播放时间。本次教程使用iOS8和Xcod6.3.1
+
+打开Xcode创建一个新的`Single View Application`,使用`IOS8SwiftPlayMusicAVAudioPlayerTutorial `作为`product name`,并填写组织名称和标识符。选择`Swift`语言并确保设备选择iPhone。
+
+
+
+在这个工程中我们需要开始暂停和停止按钮的图片和一个mp3文件。你可以在这里[下载](http://www.ioscreator.com/tutorials/play-music-avaudioplayer-ios8-swift#)。解压文件并把它们添加到工程,确保是拷贝他们到目标文件夹。
+
+
+
+转到故事板拖两个标签和两个按钮到主视图。依次选择他们并在`Size Inspector`填入下列值。
+
+* 上面的标签 -> X: 16, Y: 60, Width: 568, Height:17
+* 中间的标签 -> X: 250, Y: 120, Width: 100, Height: 36
+* 左边的按钮 -> X: 16, Y: 220, Width: 102, Height: 102
+* 右边的按钮 -> X:484, Y: 220, Width: 102, Height: 102
+
+再依次选择他们在`Attributes Inspector`添加以下值。
+
+* 上面的标签: Center Alignment, Font - System 14.0
+* 中间的标签: Center Alignment. Font - System Bold 30.0
+* 左边的按钮: Type -Custom, Image - playpause.png
+* 右边的按钮: Type - Custom, Image - stop.p
+
+按下`Resolve Auto Layout`按钮在故事板的右下角,选择`Reset to Suggested Constraints`
+
+现在故事板看起来是这样的
+
+
+
+选择助理窗口并确保`ViewController.swift`可见。
+
+按住`Ctrl`拖动顶部的标签创建下列的插口。
+
+
+
+按住`Ctrl`拖动中间的标签创建下列的插口。
+
+
+
+按住`Ctrl`拖动播放暂停图片创建下列动作。
+
+
+
+按住`Ctrl`拖动播放停止图片创建下列动作。
+
+
+
+在`ViewController.swift`文件中导入`AVFoundation `框架。
+
+```
+import AVFoundation
+```
+
+添加下列属性在`ViewController `类中
+
+```
+var audioPlayer = AVAudioPlayer()
+var isPlaying = false
+var timer:NSTimer!
+```
+
+`AVAudioPlayer `类可以让你播放任何iOS支持的音频格式。使用`isPlaying`布尔值来表示当前是否正在播放音乐。`NSTimer `属性用来显示当前播放音乐的时间。
+
+
+修改`viewDidLoad `方法
+
+```
+override func viewDidLoad() {
+ super.viewDidLoad()
+
+ trackTitle.text = "Future Islands - Tin Man"
+ var path = NSBundle.mainBundle().URLForResource("Future Islands - Tin Man", withExtension: "mp3")
+ var error:NSError?
+
+ audioPlayer = AVAudioPlayer(contentsOfURL: path!, error: &error)
+}
+
+```
+
+将标题赋值给标题的标签,然后用音乐文件初始化`AVAudioplayer `,接着实现`playOrPauseMusic `方法。
+
+
+```
+@IBAction func playOrPauseMusic(sender: AnyObject) {
+ if isPlaying {
+ audioPlayer.pause()
+ isPlaying = false
+ } else {
+ audioPlayer.play()
+ isPlaying = true
+
+ timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "updateTime", userInfo: nil, repeats: true)
+ }
+}
+```
+
+首先我们判断是否正在播放,如果是,那么暂停音乐并改变`isPlaying `的值,如果没有播放,我们播放它并改变`isPlaying `的值。然后创建一个`NSTimer`对象用来每秒调用`updateTime`方法,我们来实现它。
+
+```
+func updateTime() {
+ var currentTime = Int(audioPlayer.currentTime)
+ var minutes = currentTime/60
+ var seconds = currentTime - minutes * 60
+
+ playedTime.text = NSString(format: "%02d:%02d", minutes,seconds) as String
+}
+```
+
+播放器的`currentTime `属性将被分割成分和秒两部分用来显示播放的时间。最后实现`stopSound `方法。
+
+```
+@IBAction func stopMusic(sender: AnyObject) {
+ audioPlayer.stop()
+ audioPlayer.currentTime = 0
+ isPlaying = false
+}
+```
+
+播放器调用停止方法并重置当前播放时间。当下次按下播放时,将会从头开始播放。`isPlaying `属性将被赋值为false,构建并运行程序,按下播放/暂停按钮和停止按钮来控制音乐。
+
+
+
+
+你可以下载`IOS8SwiftPlayMusicAVAudioPlayerTutorial`的源代码在[GitHub](https://github.com/ioscreator/ioscreator)的仓库上。
\ No newline at end of file
diff --git "a/issue-13/\345\234\250iOS8\344\270\255\347\273\231TableView\345\212\240\344\270\200\344\270\252\346\220\234\347\264\242\346\240\217(Swift).md" "b/issue-13/\345\234\250iOS8\344\270\255\347\273\231TableView\345\212\240\344\270\200\344\270\252\346\220\234\347\264\242\346\240\217(Swift).md"
new file mode 100644
index 0000000..43b4fc9
--- /dev/null
+++ "b/issue-13/\345\234\250iOS8\344\270\255\347\273\231TableView\345\212\240\344\270\200\344\270\252\346\220\234\347\264\242\346\240\217(Swift).md"
@@ -0,0 +1,128 @@
+在iOS8中给TableView加一个搜索栏(Swift)
+---
+> * 原文链接 : [Add Search to Table View Tutorial in iOS8 with Swift](http://www.ioscreator.com/tutorials/add-search-table-view-tutorial-ios8-swift)
+* 原文作者 : [Arthur Knopper](https://twitter.com/ioscreator)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [StormXX](https://github.com/StormXX)
+* 校对者: [Harries Chen](https://github.com/mrchenhao)
+* 状态 : 已校对
+
+现在大多数的iOS应用都会在TableView上放一个搜索框来提供一个搜索内容的入口。本篇教程将会讲述如何在TableView中添加一个搜索功能,能让用户来搜索整个TableView中的内容。在以前的iOS版本中,都是使用一个UISearchDisplayController对象来实现搜索。然而在iOS8中,这个类被弃用了,UISearchController被用来代替它。本篇教程也将会教大家使用UISearchController.
+注:此教程的开发环境是iOS 8.3,Xcode 6.3,Swift 1.2.
+
+打开Xcode然后创建一个新的**Single View Application**。在**product name**这一栏填写IOS8SwiftAddSearchTableViewTutorial 然后在Organization Name和Organization Identifier填写你自己的值。编程语言选择Swift然后在Devices选择iPhone。
+
+
+
+上面的做好之后,打开Storyboard,移除自动生成的ViewController,然后拖拽一个NavigationController到Storyboard里。当刚刚的"初始"ViewController被删除了之后,这个APP就没有“起点”了。选择刚刚拖进来的NavigationController然后进到右边的Attribute Inspector。在 View Controller这一个节点下勾选”Is Initial View Controller“
+
+
+
+双击左边 TableViewController 的导航栏,把它的title设为“Numbers”。选择TableViewCell然后到右边的 Attributes Inspector 面板,把 Cell 的 Indentifier 设为"Cell"。
+
+
+
+Storyboard现在看起来应该像下面这样
+
+
+
+因为我们删掉了自动生成的ViewController,所以我们也可以删掉ViewController.swift。添加一个新的swift文件到项目中,选择**iOS->Source->Cocoa Touch Class**。把它命名为TableViewController 然后设置它为UITableViewController的子类。
+
+
+
+点开TableViewController.swift文件,把类的声明这一行改成下面这样:
+
+`class TableViewController: UITableViewController, UISearchResultsUpdating {`
+
+UISearchResultsUpdating协议用来更新搜索结果,稍后我们将会添加一个代理方法来让TableViewController继承这个协议。现在在这个类中添加下列属性:
+
+```
+let tableData = ["One","Two","Three","Twenty-One"]
+var filteredTableData = [String]()
+var resultSearchController = UISearchController()
+```
+
+这个`tableData`属性用来存储TableView的数据,`filteredTableData`属性用来存储搜索后的结果数据。Search Controller控制搜索的结果,在viewDidLoad中添加下列代码:
+
+```
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ self.resultSearchController = ({
+ let controller = UISearchController(searchResultsController: nil)
+ controller.searchResultsUpdater = self
+ controller.dimsBackgroundDuringPresentation = false
+ controller.searchBar.sizeToFit()
+
+ self.tableView.tableHeaderView = controller.searchBar
+
+ return controller
+ })()
+
+ // Reload the table
+ self.tableView.reloadData()
+}
+```
+
+resultSearchController的初始化使用了一个闭包。搜索的结果将会在当前的TableView上展示,所以searchResultsController这个参数在UISearchController的初始化方法中被设置成nil。还有,searchResultsUpdater属性被设置成了当前的Table View Controller,background dimming也被设置成false。这个搜索被加到Table View上,数据也被重新加载。接下来,实现TableView的Data Source代理方法:
+
+```
+ override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ // 1
+ // Return the number of sections.
+ return 1
+}
+
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ // 2
+ if (self.resultSearchController.active) {
+ return self.filteredTableData.count
+ }
+ else {
+ return self.tableData.count
+ }
+}
+
+
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
+
+ // 3
+ if (self.resultSearchController.active) {
+ cell.textLabel?.text = filteredTableData[indexPath.row]
+
+ return cell
+ }
+ else {
+ cell.textLabel?.text = tableData[indexPath.row]
+
+ return cell
+ }
+}
+```
+
+1. 这个TableView中只有一个Section
+2. 当Search Controller被激活,搜索结果的数目被赋值到numberOfRows方法上,否则就使用tableData这个数组。
+3. 当Search Controller被激活,所搜结果被展示出来,否则就使用tableData的数据来展示。
+
+最后,实现`UISearchResultsUpdating`协议中的`updateSearchResultsForSearchController`代理方法。
+
+```
+ func updateSearchResultsForSearchController(searchController: UISearchController)
+ {
+ filteredTableData.removeAll(keepCapacity: false)
+
+ let searchPredicate = NSPredicate(format: "SELF CONTAINS[c] %@", searchController.searchBar.text)
+ let array = (tableData as NSArray).filteredArrayUsingPredicate(searchPredicate)
+ filteredTableData = array as! [String]
+
+ self.tableView.reloadData()
+ }
+```
+
+当用户在搜索框输入的时候,`updateSearchResultsForSearchController`被调用。在这个方法里。我们处理我们的搜索条件。`NSPredicate`是一个用来放置筛选表达式的类,我们用它来存储搜索条件。“c”意思是区分大小写。筛选的结果被赋值给filteredTableData然后TableView调用了reloadData方法。编译运行这个项目,然后在搜索框输入搜索关键字,结果就会被展示出来。
+
+
+
+你可以下载`IOS8SwiftAddSearchTableViewTutorial`的源代码在ioscreator的[Github](https://github.com/ioscreator/ioscreator)上。
+
diff --git a/issue-14/readme.md b/issue-14/readme.md
new file mode 100644
index 0000000..23f40d2
--- /dev/null
+++ b/issue-14/readme.md
@@ -0,0 +1,2 @@
+add issue-14
+
diff --git "a/issue-14/\345\215\225\344\276\213\345\234\250Swift\344\270\255\347\232\204\346\255\243\347\241\256\345\256\236\347\216\260\346\226\271\345\274\217.md" "b/issue-14/\345\215\225\344\276\213\345\234\250Swift\344\270\255\347\232\204\346\255\243\347\241\256\345\256\236\347\216\260\346\226\271\345\274\217.md"
new file mode 100644
index 0000000..1f52278
--- /dev/null
+++ "b/issue-14/\345\215\225\344\276\213\345\234\250Swift\344\270\255\347\232\204\346\255\243\347\241\256\345\256\236\347\216\260\346\226\271\345\274\217.md"
@@ -0,0 +1,155 @@
+# 单例在Swift中的正确实现方式
+---
+> * 原文链接 : [The Right Way to Write a Singleton](http://krakendev.io/blog/the-right-way-to-write-a-singleton?utm_campaign=This%2BWeek%2Bin%2BSwift&utm_medium=web&utm_source=This_Week_in_Swift_45)
+* 原文作者 : [Hector Matos](http://krakendev.io/?author=5592eaffe4b08369d0205792)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [Gottabe](https://github.com/Gottabe)
+
+尽管在我之前的博文里我就写过关于管理状态的那些坑,但是有时候我们就是无法避免它们。其中一类管理状态的方式我们耳熟能详 - 单例。但是在Swift中有好几种不同的方式来实现一个单例。到底哪一个才是正确的方式呢?在这边博客里,我将和你好好聊聊单例的历史和在Swift中单例正确的实现方式。
+
+如果你想直接就看在Swift中如何正确地写出单例同时看到证明其“正确性”,你可以直接滚动到这篇博文的底部。
+
+## 让我们先回忆一下
+
+Swfit源于Objective-C,高于Objective-C。在Objective-C中,我们是这样写单例的:
+
+```objc
+@interface Kraken : NSObject
+@end
+
+@implementation Kraken
+
++ (instancetype)sharedInstance {
+ static Kraken *sharedInstance = nil;
+ static dispatch_once_t onceToken;
+
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[Kraken alloc] init];
+ });
+ return sharedInstance;
+}
+
+@end
+```
+
+当我们把它写出来之后,就能清楚的看到一个单例的架构,接下来我们把单例的规则理一理,以便更好的理解它:
+
+## 单例的道义
+
+关于单例,有三件事是你必须要记住的:
+
+* 单例必须是唯一的,所以它才被称为单例。在一个应用程序的生命周期里,有且只有一个实例存在。单例的存在给我们提供了一个唯一的全局状态。比如我们熟悉的NSNotification,UIApplication和NSUserDefaults都是单例。
+* 为了保持一个单例的唯一性,单例的构造器必须是私有的。这防止其他对象也能创建出单例类的实例。感谢所有帮我指出这点的人。
+* 为了确保单例在应用程序的整个生命周期是唯一的,它就必须是线程安全的。当你一想到并发肯定一阵恶心,简单来说,如果你写单例的方式是错误的,就有可能会有两个线程尝试在同一时间初始化同一个单例,这样你就有潜在的风险得到两个不同的单例。这就意味着我们需要用GCD的dispatch_once来确保初始化单例的代码在运行时只执行一次。
+
+成为独一无二并只在一个地方做初始化并不难。这篇博客接下来要记住的内容就是要单例遵守了更难察觉的dispatch_once的规则。
+
+## Swift单例
+
+打从Swift 1.0开始,就有好几种创建单例的方法。在[这里](https://github.com/hpique/SwiftSingleton),[这里](http://stackoverflow.com/questions/24024549/dispatch-once-singleton-model-in-swift)和[这里](https://developer.apple.com/swift/blog/?id=7)都有详细的说明。但谁有空点进去看呢?先来个剧透;一共有四种写法,请让我一一道来:
+
+### 最丑陋的写法
+
+```swift
+class TheOneAndOnlyKraken {
+ class var sharedInstance: TheOneAndOnlyKraken {
+ struct Static {
+ static var onceToken: dispatch_once_t = 0
+ static var instance: TheOneAndOnlyKraken? = nil
+ }
+ dispatch_once(&Static.onceToken) {
+ static.instance = TheOneAndOnlyKraken()
+ }
+ return Static.instance!
+ }
+}
+```
+
+这种写法其实就是把Objective-C中的写法照搬过来。我觉得奇丑无比因为Swift是一门更加简洁且表达力更强的语言。我们要做的比那些搬运工要好,要比他们好。
+
+### 结构体写法
+
+```swift
+class TheOneAndOnlyKraken {
+ class var sharedInstance: TheOneAndOnlyKraken {
+ struct Static {
+ static let instance = TheOneAndOnlyKraken()
+ }
+ return Static.instance
+ }
+}
+```
+
+这个在Swift 1.0的时候必须得这么写,因为那个时候,类还不支持全局类变量。而结构体却支持。正因为在全局变量上的局限性,我们不得不这么写。这比直接把Objective-C那一套搬过来要好,但是还不够。好玩的是,在Swift 1.2发布后的几个月后,我还是经常能看到这些的写法,但这个以后再表。
+
+### 全局变量写法(又名“一句话写法”)
+
+```swift
+private let sharedKraken = TheOneAndOnlyKraken()
+class TheOneAndOnlyKraken {
+ class var sharedInstance: TheOneAndOnlyKraken {
+ return sharedInstance
+ }
+}
+```
+
+在Swift 1.2之后,我们拥有了访问控制修饰词和可以使用全局类变量。这意味着我们不用整一个全局变量集群,也可以防止命名空间冲突。这才是我心目中Swift应该有的样子。
+
+这会你可能会问我为什么没有在我们的结构体和全局变量实现中看不到dispatch_once。但其实Apple指出,这两个方法同时满足了我上面提到的dispatch_once条文。下面就截取他们写的[Swift Blog](https://developer.apple.com/swift/blog/?id=7)中的一段话来证明他们以将dispatch_once整合进去了:
+
+> "全局变量(静态成员变量和结构体以及枚举)的延迟构造器在其被第一次访问时会加载,并以`dispatch_once`的方式启动来确保初始化的原子性。这让你写代码时可以用一种很酷的方式来使用`dispatch_once`:直接用一个全局变量的构造器去做初始化并用private来修饰。“ -- Apple's Swift Blog
+
+就官方文献来看,Apple就给出这点说明。但这仅意味着对全局变量和结构体/枚举的静态成员我们是有证据的!目前来看,100%不会错的是用一个全局变量将一个单例的初始化包在一个隐含了dispatch_once的延迟构造器中。但是我们的全局类变量怎么办呢?!?!?!?
+
+### 正确的写法
+
+```swift
+class TheOneAndOnlyKraken {
+ static let sharedInstance = TheOneAndOnlyKraken()
+}
+```
+
+为了写这篇文章,我做了一番研究。事实上,之所以写这篇文章,是因为今日在Capital One的一次讨论,一位PR希望在我们所有的应用中用统一的方式来写单例。我们其实知道什么才是单例“正确”的书写方式,但是我们无法自证。如果不旁征博引来证明我们是正确的简直就是徒劳。这是我和缺乏信息的互联网/博文圈之间的较量。每个人都知道如果网上没写那就不是真的。这让我非常难过。
+
+
+
+我的搜索达到了互联网的尽头(Google到了第10页)却啥也没得到。难道真就发表一行单例的证明吗?也许有人做了,就是没找到而已。
+
+所以我觉得我自己把所有的初始化构造器的方法都实现一遍,然后通过断点来检查他们。当分析完每个栈帧我所发现的相似点的时候,我终于发现期待已久的东西 - 证据!
+
+直接上图,对了(还有emoji类哦!):
+
+
+
+使用全局单例
+
+
+
+使用一行单例
+
+第一张图显示了一个全局let实例化。红框表示的地方就是证据。在实际去实例化Karken单例之前,是先由一个*swfit_once*调用了一个*swift_once_block_invoke*。加上Apple说他们通过一个dispatch_once的block去延迟初始化一个全局变量,我们现在可以说这就证明了他们所说的。
+
+带着这个信息,我又跟踪了我们既亮眼又简洁的一行单例。正如第二张图所以,两者简直一样!这就足以证明我们的一行单例实现是正确的。现在全世界都清净了。还有,既然这篇文章已经上了互联网,那么它*肯定*就是真理!
+
+##不要忘了INIT的私有化!
+
+[@davedelong](https://twitter.com/davedelong),Apple的架构师,非常含蓄的给我指出,你必须确保你的inits是私有的。只有这样才能确保你的单例是真正的独一无二,也能防止其他对象通过访问控制机制来创建他们自己的但是是你这个类的单例。因为在Swift中,所有对象的构造器默认都是public,你需要重写你的init让其成为私有的。这并不难实现而且也能确保我们的一行单例的优雅和简洁:
+
+```swift
+class TheOneAndOnlyKraken {
+ static let sharedInstance = TheOneAndOnlyKraken()
+ private init() {} // 这就阻止其他对象使用这个类的默认的'()'初始化方法
+}
+```
+
+这么做能确保任何类如果尝试通过()初始化方法来初始化TheOneAndOnlyKraken时,编译器都会报错:
+
+
+
+你看!这就是完美的,一行实现单例。
+
+## 结论
+
+呼应一下[jtbandes](http://stackoverflow.com/users/23649/jtbandes)在[top rated answer to swift singletons on Stack Overflow](http://stackoverflow.com/a/24147830)上精彩的评论,我就无法找到关于使用'let'就能确保线程安全的任何文献。我其实依稀记得在去年的WWDC上有类似的这么一个说法,但你可不能指望读者或者googlers在尝试证明这就是在Swift中写单例的正确方法的时候就恰巧碰到那说法。不管怎么讲,我希望这篇博文能帮助一些人理解一行单例在Swift中是打开单例的正确方式。
+
+基友们 Happy Coding!
\ No newline at end of file
diff --git "a/issue-14/\345\234\250Swift\346\200\216\346\240\267\345\210\233\345\273\272CocoaPod.md" "b/issue-14/\345\234\250Swift\346\200\216\346\240\267\345\210\233\345\273\272CocoaPod.md"
new file mode 100644
index 0000000..80b27f8
--- /dev/null
+++ "b/issue-14/\345\234\250Swift\346\200\216\346\240\267\345\210\233\345\273\272CocoaPod.md"
@@ -0,0 +1,809 @@
+
+* 原文链接:[How to Create a CocoaPod in Swift](http://www.raywenderlich.com/99386/create-cocoapod-swift)
+* 原文作者:[ Joshua Greene ](http://www.raywenderlich.com/u/JRG.Developer)
+* [译文出自:开发者前线 www.devtf.cn](www.devtf.cn)
+* 译者:[MrLoong](https://github.com/MrLoong)
+* 校对者:[MrLoong](https://github.com/MrLoong)
+* 状态:完成
+
+[](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/ice_cream_shop_logo.png)
+
+
+在制作这个美味的冰淇凌中的教程中学会如何使用自己的CocoaPod
+
+
+你可能熟悉一些众所周知的东西,开源的[open-source
+CocoaPods](https://github.com/CocoaPods/Specs/tree/master/Specs),例如[Alamofire](https://github.com/Alamofire/Alamofire)或[MBProgressHUD](https://github.com/jdg/MBProgressHUD),但有的时候你并不能找到一个符合你要求的pod,或者你可能想要把一个大的项目分成小的项目,或是可复用组建。
+
+
+幸运的是创建自己的CocoaPods很容易。
+
+
+如果你已经针对你的元件创建一个Cocoa Touch框架,那么你已经完成了大部分困难的工作。如果你没有创建过,不要惊慌,因为他确实很简单。
+
+
+如果你仅仅是创建了IOS应用的一部分,那也是ok的。你可以很轻松的创建一些新的pod用于特殊环境下使用的推送类和功能。
+
+
+本教程以[How to Use CocoaPods with
+Swift](http://www.raywenderlich.com/97014)结束。如果你之前从来没有使用过CocoaPods,然而这是本教程的一先决条件。
+
+开始
+-------------------
+
+
+你的顶级客户是一个冰淇凌店,他们的冰淇凌是如此的受欢迎,他们的柜台跟不上客户的订单。他们需要创建一个简洁的IOS应用。让用户使用他们的Phone订购冰淇凌。你将开始开发这个应用程序。
+
+
+从[这](http://cdn2.raywenderlich.com/wp-content/uploads/2015/06/IceCreamShop_Final1.zip)下载这个开始的项目
+
+
+
+这是[How to Use CocoaPods with
+Swift](http://www.raywenderlich.com/97014)最终版本
+
+
+
+这个应用有一些pod开发包已经包含在下载中,所以你不需要运行pod install去安装他们。
+
+
+
+
+打开*IceCreamShop.xcworkspace*然后点击*Main.storyboard*,在*Views\\Storyboards & Nibs*看看这个应用是如何布局的。
+
+
+
+下面是快速*Choose Your Flavor*的应用场景,核心功能如下
+
+- *PickFlavorViewController*: handles user interaction, such as when
+ the user selects an ice cream flavor.
+- *PickFlavorViewController*:处理用户交互,例如当用户选择一个口味的冰淇凌的时候
+- *PickFlavorDataSource*: is the data source for the collection view
+ that displays ice cream flavors.
+- *PickFlavorDataSource*:是选择冰淇凌口味试图的数据源。
+- *IceCreamView*: is a custom view that displays an ice cream cone and
+ is backed by a `Flavor` model.
+- *IceCreamView*: 是一个冰淇凌自定义模型。
+- *ScoopCell*: is a custom collection view cell that contains a
+ *ScoopView*, which is also backed by an instance of the `Flavor`
+ model class.
+- *ScoopCell*:自定义集合试图集合单元包含一个*ScoopView*,也是风味模型的一个支持类。
+
+[](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/storyboard_annotated.png)
+
+这个冰淇凌店的老板非常喜欢这个应用程序,但他们又添加了一个新的要求:在他们的应用程序中需要有相同的口味风格选择。
+
+
+等等,那不是最初的设计,但是,应该没有问题的对于一个NB的开发者来说。
+
+[](http://cdn1.raywenderlich.com/wp-content/uploads/2014/11/SuperDev.jpg)
+
+
+
+你能猜猜这个功能是怎么做的吗?是的,你讲把这个功能集成到你自己的CocoaPod!。
+
+ 建立你自己的Pod
+---------------------------------
+
+
+
+创建一个新的Xcode项目并且选择*iOS\\Framework & Library\\Cocoa
+Touch Framework*然后点击下一步。
+
+[](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/cocoa_touch_framework.png)
+
+
+将工程命名为*RWPickFlavor*并且选择*Swift*语言,点击*Next*
+
+
+这个教程的工程将创建在*\~/Documents/Libraries*,在你的主目录中选择*Documents*文件夹。如果你没有*Libraries*文件夹,在底部点击新建文件夹按钮并且创建它。
+
+
+
+完成,选择*Libraries*文件夹并且单价创建
+
+
+
+保存你开发的pods这个目录非常重要,因为你将引用这个目录在你本地开发你的Podfile期间
+
+
+
+正常情况下,当你使用CocoaPods,你在你的Podfile包含的关系
+
+ ```
+ pod ' PodName ', '~> 1.0'
+ ```
+
+
+
+ ```
+pod ' MyPodName ', :path => ' ~/Path/To/Folder/Containing/My/Pod '
+ ```
+
+但是当你开发你自己的CocoaPod,你讲在本地建立一个特殊的路径,像这样:
+
+
+
+这种方法有两种好处:
+
+
+
+ 1.他使用你本地机器上的文件对于你的pod,而不是从远程仓库中获取。
+
+ 2.正常情况下,你不需要选择一个pod包含到你的app中,因为这些选择将在你下一次运行pod install 时被覆盖,pod将会重新从远程仓库读取并且覆盖更改的文件。
+
+
+你可以使用不同的位置对你的pods进行开发,一般情况下我建议还是将他们放在*\~/Documents/Libraries*。如果你和一个团队合作开发,CocoaPods扩展作为扩展主目录。因此你不需要努力构建绝对路径在Podfile目录中。
+,
+
+
+你也能使用其他的CocoaPods在你的工程中作为项目的依赖对于你创建CocoaPod。你只需要一个Podfile来管理你的CocoaPod依赖关系。
+
+
+
+关闭Xcode,然后在终端输入以下命令
+
+
+ ```
+ cd ~/Documents/Libraries/RWPickFlavor
+ pod init
+ open -a Xcode Podfile
+
+ ```
+
+
+
+这是创建一个新的Podfile并且在Xcode中打开它。
+
+
+使用下面的命令对Podfile进行更新:
+
+```
+ platform :ios, '8.0'
+ use_frameworks!
+
+ target 'RWPickFlavor' do
+ pod 'Alamofire', '~> 1.2'
+ pod 'MBProgressHUD', '~> 0.9.0'
+ end
+```
+
+
+这声明*RWPickFlavor*将在[Alamofire](https://github.com/Alamofire/Alamofire) 和
+[MBProgressHUD](https://github.com/jdg/MBProgressHUD)提供依赖关系
+
+
+保存并且关闭Podfile,然后在终端输入以下命令
+
+```
+pod install
+```
+
+
+
+正如你所期望的,这将创建一个工作区间并且安装你所需要的各种文件。
+
+
+
+注意:如果*pod install* 给你任何警告,这样的话你可能使用的是旧版本。Swift基于CocoaPods,尤其是Alamofire,需要 0.36 CocoaPods版本或更新。根据以下命令,你可以检测你已经安装的CocoaPods版本。
+
+```
+pod --version
+
+```
+
+
+如果是版本过低的原因,输入以下终端命令来安装最新的版本。
+
+```
+sudo gem install CocoaPods
+
+```
+
+
+
+输入以下命令,打开并且创建一个新的*RWPickFlavor*工作区间。
+
+```
+open RWPickflavor.xcworkspace
+```
+
+
+你的项目导航器看起来应该是这个样子。
+
+[](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/rwpickflavor_new_pod_files.png)
+
+
+你现在需要从*IceCreamShop*复制一些已经存在的文件在*RWPickFlavor*工作区间。
+
+
+
+首先在*RWPickFlavor.xcworkspace*创建一个组,把你需要的文件复制进去。
+
+Categories
+
+Controllers
+
+Factories
+
+Models
+
+Views
+
+- Ice Cream
+- Storyboards & Nibs
+
+
+
+现在拖拽每一个文件,除了 *AppDelegate.swift*和*LaunchScreen.xib*,从*IceCreamShop.xcworkspace*拖拽进*RWPickFlavor.xcworkspace*,就像这样
+
+[](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/copy_workspace_files.gif)
+
+
+当被提示时,确保每一个需要被复制的条目被复制安装了而不是简单的链接。
+
+
+
+当你完成了,*RWPickFlavor*应该有以下文件:
+
+*RWPickFlavor*
+
+RWPickFlavor.h
+
+*Categories*
+
+- RGBAColorFromString.swift
+
+*Controllers*
+
+- PickFlavorDataSource.swift
+- PickFlavorViewController.swift
+
+*Factories*
+
+*Models*
+
+*Supporting Files*
+
+*Views*
+
+*Ice Cream*
+
+- IceCreamView.swift
+- ScoopCell.swift
+- ScoopView.swift
+
+*Storyboard & Nibs*
+
+- Main.storyboard
+
+Once you’re sure all the files have been copied over, *delete* the
+originals and any empty groups from *IceCreamShop*, leaving just the
+files in *RWPickFlavor*. Take care not to delete any of the following:
+
+- AppDelegate.swift
+- LaunchScreen.xib
+- Images.xcassets
+- Anything under the *Supporting Files* group
+
+
+现在打开*Info.plist*,找到*Supporting Files*组,并且删除*Main storyboard file base name*
+
+
+
+构建并运行,你不应该看见任何错误,并且你会看见“Ice Cream Shop”的图标和黑色的背景。
+
+图像呢?
+----------------------
+
+
+
+你可能会注意,你并没有复制*Resources*组,这是因为你需要复制的仅仅是*background.jpg*他自己本身到*Resources*文件夹中。不完整的*Images.xcassets*资源文件。
+
+
+首先在*RWPickFlavor*创建一个*Resources*组
+
+
+
+下一步,选择*Images.xcassets*在*IceCreamShop*,点击右侧*background*,并且选择*Show In Finder*,像这样。
+
+[](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/background_show_in_finder.png)
+
+
+
+现在拖拽*background.jpg*到*RWPickFlavor*的 *Resources*组中。当提示的时候依然检测每一个条目是否被安装。当你复制后,从*IceCreamShop*中删除*Images.xcassets*原来的背景
+
+
+
+最后,在*RWPickFlavor*更新视图的图像上选择你的场景在*Main.storyboard*,所以引用*background.jpg*替代*background*
+
+[](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/update_pickflavorviewcontroller_imageview.png)
+
+
+信不信,最难创造的是你的pod。
+
+[](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/high_five_kitten.jpg)
+
+CocoaPods and Git
+-----------------
+
+
+自从CocoaPods傻瓜式引导,每一个pod将需要有他自己的git仓库。如果你已经有一个首选的git托管,好的,你可以用它来作为你的仓库。
+
+
+
+如果没有,[GitHub](https://github.com)是一个很好的的选择因为它是众所周知的开发平台,与很多自由的开源项目。
+
+
+这个教程使用[GitHub](https://github.com),但你也可以使用你的git仓库。
+
+ GitHub设置
+-----------------
+
+First, [Sign up](https://github.com/join) or
+[Login](https://github.com/login) to your GitHub account.
+
+首先[创建](https://github.com/join)或[登陆](https://github.com/login)你的GitHub账户。
+
+
+
+下一步,点击顶部右侧*+* (create new)图标并且选择 New repository展示如下:
+
+ [](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/github_new_repository.png)
+
+
+
+进入*RWPickFlavor*仓库,并且选择*Create repository*
+
+
+
+Github将创建一个新的仓库在你的账户下。然后你会看到下面的屏幕与一个快速设置,现实您储存的仓库网址。
+
+[](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/github_quick_setup.png)
+
+
+
+所以距离打开到现在你需要这个URL在短短的一瞬间。
+
+
+
+现在你需要第二个仓库涌来存放你所有私有的pod,你将在本教程中使用。
+
+
+打开[github.com](http://github.com)在你个新的栏目。再一次点击*Create new*图标,并且选择*New repository*。仓库名为*RWPodSpecs*,并且选择*Create repository*
+
+
+
+离开这个标签,这样当你需要使用它的时候很简单去使用了。
+
+Podspec 设置
+-------------
+
+
+
+现在你需要创建*RWPickFlavor.podspec*文件对于*RWPickFlavor*,*Podspec*包含基础信息,尤其是pod的名字,版本和Git下载的URL
+
+
+在终端输入以下命令。
+
+```
+cd ~/Documents/Libraries/RWPickFlavor
+pod spec create RWPickFlavor
+open -a Xcode RWPickFlavor.podspec
+
+```
+
+
+
+这是创建的*RWPickFlavor.podspec*并且打开它在Xcode
+
+
+
+默认情况下有很多优秀的例子和文档,然而你并不需要大部分的他们。
+
+
+
+以下*RWPickFlavor.podspec*代替了一切。
+
+```
+Pod::Spec.new do |s|
+
+ # 1
+ s.platform = :ios
+ s.ios.deployment_target = '8.0'
+ s.name = "RWPickFlavor"
+ s.summary = "RWPickFlavor lets a user select an ice cream flavor."
+ s.requires_arc = true
+
+ # 2
+ s.version = "0.1.0"
+
+ # 3
+ s.license = { :type => "MIT", :file => "LICENSE" }
+
+ # 4 - Replace with your name and e-mail address
+ s.author = { "[Your Name Goes Here]" => "[Your_Email@Your_Email_Domain.com]" }
+
+ # For example,
+ # s.author = { "Joshua Greene" => "jrg.developer@gmail.com" }
+
+
+ # 5 - Replace this URL with your own Github page's URL (from the address bar)
+ s.homepage = "[Your RWPickFlavor Homepage URL Goes Here]"
+
+ # For example,
+ # s.homepage = "https://github.com/JRG-Developer/RWPickFlavor"
+
+
+ # 6 - Replace this URL with your own Git URL from "Quick Setup"
+ s.source = { :git => "[Your RWPickFlavor Git URL Goes Here]", :tag => "#{s.version}"}
+
+ # For example,
+ # s.source = { :git => "https://github.com/JRG-Developer/RWPickFlavor.git", :tag => "#{s.version}"}
+
+
+ # 7
+ s.framework = "UIKit"
+ s.dependency 'Alamofire', '~> 1.1'
+ s.dependency 'MBProgressHUD', '~> 0.9.0'
+
+ # 8
+ s.source_files = "RWPickFlavor/**/*.{swift}"
+
+ # 9
+ s.resources = "RWPickFlavor/**/*.{png,jpeg,jpg,storyboard,xib}"
+end
+
+
+```
+
+
+就像一个Podfile,Podspec是用Ruby写的。要格外小心,不要做任何错误,否则将可能无法验证安装后是否成功.
+
+
+
+这是发生了什么?
+
+
+1. 首先你详细说明了pod的基本信息。Swift必须最低基于CocoaPods8.0版本或者更高。
+ 如果你指定了较低的版本,pod无法正常安装。
+
+
+2. 对于你指定的CocoaPod版本好,本质上Podspec是一个快照。当你更新一个pod,你也将需要更
+ 新Podspec的版本。所有的高版本度遵循[Semantic Versioning](http://semver.org/)。
+ 如果你并没有相似的版本,看[如何使用CocoaPods在Swift中]
+ (http://www.raywenderlich.com/97014)
+
+
+3. 所有pods必须指定一个许可证。如果你没有,CocoaPods将在你安装的时候提出警告。并且你
+ 不能把它上传到分支。
+
+
+4. 在这里,您可以指定关于自己的信息,pod作者。输入你的名字和e-mail地址。替代那些占
+ 位符文本
+
+
+5. 在这里你需要指定你的pod的网页和网址。就是简单的从浏览器地址栏复制和粘贴到Github
+ 主页
+
+
+6. 从上面创建第一个“Quick Setup”,那部分的git下载网址替代URL。最好使用一个HTTP或
+ HTTPS:URL更容易被其他用户处理。如果你想你也可以使用SSH URL,但是你需要确保你团
+ 队的每一个人-无论谁需要访问CocoaPod-已经有自己的公钥/私钥针对你的Git host
+
+
+7. 这里你需要说明你的框架和pod依赖关系。
+
+
+
+8. 在这里你可以说明指定基于文件扩展的公共源文件,在这种情况下,你可以说明‘.swift’作
+ 为扩展
+
+
+
+ 9. 最后你可以根据指定的文件扩展名指定资源。
+
+
+
+ 推送Git
+-----------
+
+
+
+你终于准备推送*RWPickFlavor* pod GitHub主页
+
+
+在终端输入以下命令,
+
+```
+cd ~/Documents/Libraries/RWPickFlavor
+git init
+git add .
+git commit -m "Initial commit"
+git tag 0.1.0
+git remote add origin [Your RWPickFlavor Git URL]
+git push -u origin master --tags
+
+```
+
+
+
+然后跳出提示,输入你的用户名和密码到Github
+
+
+
+这所有文件在*RWPickFlavor*目录中,创建一个新的*0.1.0*标签,并且推送你每一个东西到你的远程仓库
+
+
+
+祝贺你,你已经创建你的第一个CocoaPod!
+
+[](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/victory_dance.png)
+
+
+
+你已经创建了第一个CocoaPod,但是你能真正的使用吗?并不能很快使用。
+
+
+
+你首先需要添加你的Podspec去私有的specs repo,当你尝试去安装它这让CocoaPods发现pod。
+对于这个你已经创建一个Git repo,所以这最后一步比较简单。
+
+在终端输入以下命令,确保你仍在*RWPickFlavor*目录中。
+
+```
+pod repo add RWPodSpecs [Your RWPodSpecs Git URL]
+pod repo push RWPodSpecs RWPickFlavor.podspec
+
+```
+
+
+这将创建一个本地引用 *RWPodSpecs*在你的机器上,并推送*RWPickFlavor.podspec*到它上。
+
+
+你现在有一个私有的pod specs repo被建立!比你想象的容易,对吗?
+
+使用你的新CocoaPod
+-----------------------
+
+
+最后来使用你创建的pod
+
+
+对于*IceCreamShop*打开Podfile,输入以下命令
+
+```
+platform :ios, '8.0'
+
+source 'https://github.com/CocoaPods/Specs.git'
+source '[Your RWPodSpecs Git URL Goes Here]'
+
+use_frameworks!
+
+target 'IceCreamShop' do
+ pod 'RWPickFlavor', :path => '~/Documents/Libraries/RWPickFlavor'
+end
+
+```
+
+
+
+确保你替换的*[Your RWPodSpecs Git URL Goes Here]*Git URL是你的 *RWPodSpecs* repo
+
+
+
+然后运行`pod install`在终端
+
+
+最后,用下面的替换`AppDelegate.swift`。
+
+```
+import UIKit
+import RWPickFlavor
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ var window: UIWindow?
+ var rootViewController: UIViewController!
+
+ func application(application: UIApplication, didFinishLaunchingWithOptions
+ launchOptions: [NSObject : AnyObject]?) -> Bool {
+
+ setupRootViewController()
+
+ window = UIWindow(frame: UIScreen.mainScreen().bounds)
+ window?.rootViewController = rootViewController
+ window?.makeKeyAndVisible()
+
+ return true
+ }
+
+ func setupRootViewController() {
+ let bundle = NSBundle(forClass: PickFlavorViewController.self)
+ let storyboard = UIStoryboard(name: "Main", bundle: bundle)
+ rootViewController = storyboard.instantiateInitialViewController() as! UIViewController
+ }
+}
+
+```
+
+
+
+在In `setupRootViewController()`,你得到一个可参考的`RWPickFlavor`“bundle”,这实际上是一个动态绑定框架,创建一个`Main.storyboard`,并且实例化其根视图控制器。
+
+
+构建并且运行。你将看到你熟悉的“Choose Your Flavor”界面。
+
+
+
+ 抽象所有事物
+---------------------------
+
+
+
+如果你像我一样,你可能会想,“哇,App Delegate一定知道很多关于RWPickFlavor结构”
+
+
+
+幸运的是,有一些事情你可以做来减少这种耦合性,使用[BetterBaseClasses](https://github.com/JRG-Developer/BetterBaseClasses)– a CocoaPod使其创建CocoaPods更容易
+
+
+添加一下代码带你的*RWPickFlavor*,:
+
+```
+pod 'BetterBaseClasses', '~> 1.0'
+
+```
+
+
+
+同样添加到*RWPickFlavor.podspec*:
+
+```
+s.dependency 'BetterBaseClasses', '~> 1.0'
+
+```
+
+
+
+现在替换版本:
+
+```
+s.version = "0.2.0"
+
+```
+
+
+在这里,你声明*BetterBaseClasses*作为一个关系,然后加入你的CocoaPod
+
+
+
+现在你在终端运行*pod install*安装这个新的依赖关系。
+
+
+
+接下来添加提取`PickFlavorViewController`
+
+```
+import BetterBaseClasses
+
+```
+
+
+然后替换类的定义
+
+```
+public class PickFlavorViewController: BaseViewController, UICollectionViewDelegate {
+
+```
+
+
+这种`PickFlavorViewController`,所以它是`BaseViewController`是*BetterBaseClasses*一部分
+
+
+
+现在你需要推送这些改变到你的*RWPickFlavor* and*RWPodSpecs*仓库。运行以下命令
+
+```
+cd ~/Documents/Libraries/RWPickFlavor
+git add .
+git commit -m "Added BetterBaseClasses dependency"
+git tag 0.2.0
+git push origin master --tags
+pod repo push RWPodSpecs RWPickFlavor.podspec
+
+```
+
+
+接下来你需要提交这些改变到你的*IceCreamShop*.
+
+
+
+更新*IceCreamShop* Podfile,替换`pod 'RWPickFlavor',
+
+```
+pod 'RWPickFlavor', '~> 0.2.0
+
+```
+
+
+
+在这里更新Podfile请求的版本就是你刚刚推送的。
+
+
+
+在终端执行`pod install`去更新这种依赖关系在*IceCreamShop*.
+
+
+
+最后用以下内容替换*AppDelegate.swift*
+
+```
+import UIKit
+import RWPickFlavor
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ var window: UIWindow?
+
+ func application(application: UIApplication, didFinishLaunchingWithOptions
+ launchOptions: [NSObject : AnyObject]?) -> Bool {
+
+ window = UIWindow(frame: UIScreen.mainScreen().bounds)
+ window?.rootViewController = UINavigationController(rootViewController:
+ PickFlavorViewController.instanceFromStoryboard())
+ window?.makeKeyAndVisible()
+
+ return true
+ }
+}
+
+```
+
+ 这很简单
+
+
+
+[BetterBaseClasses](https://github.com/JRG-Developer/BetterBaseClasses)添加到
+`UIViewController`,`UITableViewController`和其他的UIkit类。这里包括一个`UIViewController+BetterBaseClasses`,增加了更加方便的方法,尤其是`instanceFromStoryboard()`使它更容易实例化视图控制器。无论他们在main bundle活着其他一些地方,尤其是在框架中
+
+
+
+构建并且运行,又一次,你应该看见了熟悉的界面“Choose Your Flavor”
+
+ 何去何从?
+----------------------
+
+
+你能在[这](http://cdn2.raywenderlich.com/wp-content/uploads/2015/06/IceCreamShop_Final2.zip)和[这](http://cdn4.raywenderlich.com/wp-content/uploads/2015/06/RWPickFlavor_Final.zip)完成IceCreamShop project和RWPickFlavor pod工程的下载,
+
+
+
+
+你现在开始准备制作自己的CocoaPods!然而,本教程谈到的CocoaPods知识冰山一角。
+学习[CocoaPods
+Guides](http://guides.cocoapods.org/)去学习你需要了解的创建CocoaPods的知识。
+
+
+
+最后创建一个CocoaPod,你可能考虑添加[CocoaPods Master Specs Repo](),因此他将可能通过[CocoaPods
+Trunk](http://blog.cocoapods.org/CocoaPods-Trunk)博客到世界各地的开发商手里,
+
+
+
+关于本教程如果你有任何问题或评论,欢迎加入讨论
+
+
diff --git "a/issue-14/\345\246\202\344\275\225\345\201\232\344\270\200\344\270\252iOS\345\210\206\345\275\242App.md" "b/issue-14/\345\246\202\344\275\225\345\201\232\344\270\200\344\270\252iOS\345\210\206\345\275\242App.md"
new file mode 100644
index 0000000..431a1a0
--- /dev/null
+++ "b/issue-14/\345\246\202\344\275\225\345\201\232\344\270\200\344\270\252iOS\345\210\206\345\275\242App.md"
@@ -0,0 +1,786 @@
+如何做一个iOS分形App
+---
+
+> * 原文链接 : [如何做一个iOS分形App](https://www.weheartswift.com/make-ios-fractal-app/)
+* 原文作者 : [Silviu Pop](https://www.weheartswift.com/author/tspop/)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [alier1226](https://github.com/alier1226)
+* 校对者: [MrLoong](https://github.com/MrLoong)
+* 状态 : 完成
+
+## 介绍
+
+在这个教程中,我们会做一个可以渲染Mandelbrot Set的应用程序,我们可以缩放和平铺它来看分形那令人惊叹的复杂之美。最终的结果如下:
+
+
+
+
+着色程序的代码
+
+```
+void main() {
+ #define iterations 128
+ vec2 position = v_tex_coord; // gets the location of the current pixel in the intervals [0..1] [0..1]
+ vec3 color = vec3(0.0,0.0,0.0); // initialize color to black
+
+ vec2 z = position; // z.x is the real component z.y is the imaginary component
+
+
+ // Rescale the position to the intervals [-2,1] [-1,1]
+ z *= vec2(3.0,2.0);
+ z -= vec2(2.0,1.0);
+
+ vec2 c = z;
+
+ float it = 0.0; // Keep track of what iteration we reached
+ for (int i = 0;i < iterations; ++i) {
+ // zn = zn-1 ^ 2 + c
+
+ // (x + yi) ^ 2 = x ^ 2 - y ^ 2 + 2xyi
+ z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
+ z += c;
+
+ if (dot(z,z) > 4.0) { // dot(z,z) == length(z) ^ 2 only faster to compute
+ break;
+ }
+
+ it += 1.0;
+ }
+
+ if (it < float(iterations)) {
+ color.x = sin(it / 3.0);
+ color.y = cos(it / 6.0);
+ color.z = cos(it / 12.0 + 3.14 / 4.0);
+ }
+
+ gl_FragColor = vec4(color,1.0);
+}
+```
+
+
+
+你可以下载起始版本跟着教程一起做,也可以在本文结尾找到最终版本的代码。
+
+##项目设置
+
+
+
+`Gamescene.sks`文件里包含一个名为 `fractal` 的子画面,它填充了整个界面并且着色程序程序 `Fractal.fsh`也附在它上。
+
+
+
+`Fractal.fsh`包含了上面着色程序的代码
+
+
+
+`GameViewController.swift`包含了设置游戏场景的代码
+
+
+`GameScene.swift` 为空
+
+
+
+如果你现在运行代码,你将会得到如下的结果:
+
+
+
+
+
+
+请注意纵横比固定为3/2,我们需要先根据屏幕大小调节它。
+
+
+
+并且由于画面是静态的,所以你不可能与它有任何方式的交互。
+
+
+##设置界面
+
+
+我们将用一个透明的scrollview来处理平铺缩放。scrollview将自动跟踪我们的位置以及我们在分形中的缩放程度。
+
+
+
+打开`Main.storyboard`文件,拖进去一个scrollview。将scrollview设置成fill the view,并对它的宽度,到顶部距离,到底部距离设置限制。
+
+
+
+将scrollview的最大缩放程度设置为100000,意味着我们将可以把分享放大到**十万倍**!我们不能再放大更多了因为已经接近了`float`类型的准确极限。
+
+
+
+拖一个view(画面)到scrollview里,它将用作处理缩放。这个view本身不会展示任何东西,我们将用到它的`contentOffset`和scrollView的`zoom`属性来更新我们的着色程序。要确保这个画面可以填满scrollView,并且设定好宽度,到顶部底部左右距离的限制。将画面的背景色设置为 Clear Color (透明色)。
+
+
+
+接下来我们将连接我们所需要的outlet和scrollView的代理。
+
+
+给scrollView和scrollView的contentView拖进outlet。
+
+```
+class GameViewController: UIViewController, UIScrollViewDelegate {
+
+ @IBOutlet weak var contentView: UIView!
+ @IBOutlet weak var scrollView: UIScrollView!
+ ...
+}
+```
+
+
+
+接下来我们去掉代理方法,并且实现 `viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? ` 这个方法
+
+```
+class GameViewController: UIViewController, UIScrollViewDelegate {
+
+ ...
+
+ func scrollViewDidScroll(scrollView: UIScrollView) {
+
+ }
+
+ func scrollViewDidZoom(scrollView: UIScrollView) {
+
+ }
+
+ func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
+ return contentView
+ }
+
+ ...
+}
+```
+
+
+##向着色程序发送数据
+
+
+
+着色程序可以从你的swift代码里的uniform变量里获得数据。uniform变量可以在SpriteKit编辑器里声明。那现在我们来声明一下uniform变量。
+
+
+
+
+打开 `GameScene.sks` 文件,选择 mandelbrote sprite。将insepctor拖到底部,在“Custom shader Uniforms”里添加两项:`float` 类型的 `zoom` ,值为`1`, 以及 `vec2`类型的`offset`。我们将用这两项uniform变量储存scrollView的 `contentOffset` 以及 `zoom` 属性。
+
+
+
+
+
+**警告**:Xcode 6.3的uniform变量有bug。它不能直接在编辑器里赋值初始化,你必须在代码里初始化它们。
+
+
+
+我们可以通过shader属性来获取节点上(node)着色程序,用 `theuniformedName()`方法来从着色程序得到uniform变量。以下是我们获取zoom uniform变量的例子:
+
+```
+let zoomUniform = node.shader!.uniformNamed("zoom")!
+```
+
+Once we have a uniform we can change its value via one of of the properties
+
+当我们有了uniform变量后,我们可以通过它的属性来改变它的值。
+
+```
+var textureValue: SKTexture!
+var floatValue: Float
+var floatVector2Value: GLKVector2
+var floatVector3Value: GLKVector3
+var floatVector4Value: GLKVector4
+var floatMatrix2Value: GLKMatrix2
+var floatMatrix3Value: GLKMatrix3
+var floatMatrix4Value: GLKMatrix4
+```
+
+We’re only interested in using `floatValue` and `floatVector2Value` for this tutorial.
+
+在本教程里,我们只对 `floatValue`和`floatVector2Value`感兴趣。
+
+Ex: to set the zoom to 2 we use
+
+例子:将zoom的值设置成2
+
+```
+zoomUniform.floatValue = 2
+```
+
+##Coordinate systems and mapping intervals
+## 坐标系以及映射出区间
+
+
+我们将在保持比例的基础上映射不同的坐标系。我们将用这个来转化scrollview的坐标到复平面。
+
+
+
+让我们先看一下一维的情况:
+
+
+
+将x从区间[0,a]映射到区间[0,1],我们只需要除以区间长度 `x' = x / a`。
+
+
+将x从区间[0,1]映射到区间[a,b],我们可以乘上区间长度,然后再加上区间起始值,`x' = x * (b - a) + a`。
+
+
+举个例子,比如iPhone4的x坐标,x坐标为0到480之间。映射 `x` 到`[0,1]`, 我们用 `x' = x / 480`。映射`x'` 从 `[0,1]` 到`[-2,2]`,我们用 `x'' = x' * 4 - 2`
+
+
+如果我们屏幕上有一点x,坐标值为120,那么对应到区间`[0,1]` 将成为 `120 / 480 = 0.25`,以及在区间 `[-2,2]`,如下所见它将成为 `0.25 * 4 - 2 = -1`。
+
+
+
+
+##Mapping between the scrollview and the complex plane
+
+##scrollView及复平面互相映射
+
+
+
+我们需要讲scrollView上的点转换到复平面。第一步,先将scrollView上的点转换到区间`[0,1]`。通过将`contentOffset` 除以`contentSize`可以将 `contentOffset`转换到区间`[0,1]`。
+
+```
+var offset = scrollView.contentOffset
+
+offset.x /= scrollView.contentSize.width
+offset.y /= scrollView.contentSize.height
+```
+
+
+我们着色程序x,y坐标都有点在区间`[0,1]`,所以我们要在scrollView的contentView里映射出这些店。
+
+
+标准化过的contentView为 `1.0 / zoom` ,所以contentView里标准化过的点坐标讲在区间`[contentOffset / contentSize,contentOffset / contentSize + 1.0 / zoom]`。
+
+
+还有我们必须牢记的是,y轴的点在GLSL上,而点(0,0)在**左下**角,所以我们必须翻转y轴来对应我们的scrollView。
+
+
+下面的GLSL代码转换scrollView的contentView里点的位置。
+
+```
+// Fractal.fsh
+void main {
+
+ vec2 position = v_tex_coord;
+ position.y = 1.0 - position.y; // flip y coordinate
+
+ vec2 z = offset + position / zoom;
+
+...
+}
+```
+
+
+如下你可以看见蓝色的scrollView的contentView在标准化与未标准化过的边框。`contentSize = (960,640)`,`contentOffset = (240,160)`,`zoom = 2.0`
+
+
+
+ScrollView
+
+
+
+标准化过的ScrollView
+
+
+
+
+
+最后我们将点映射到复平面。为了在mandelbrot里得到好看的效果,我们将希望映射区域`[-1.5,0.5] x [-1,1]`复平面。
+
+
+
+我们还想使纵横比正确。现在我们的x、y轴的比例一样,我们要乘以x和纵横比使得图片不会变形。
+
+
+
+纵横比是什么
+
+
+纵横比是屏幕宽度和高度的比例。
+
+```
+// Fractal.fsh
+
+void main {
+...
+ z *= 2.0;
+ z -= vec2(1.5,1.0);
+
+ float aspectRatio = u_sprite_size.x / u_sprite_size.y;
+ z.x *= aspectRatio;
+...
+}
+```
+
+
+下面你可以看到我们scrollView的contentView映射到的平复面以及纠正过纵横比的结果。
+
+
+
+
+
+为了整合上面所有代码,我们建了一个新的方法叫updateShader,它可以传一个contentView坐标到着色程序。我们所需要做的就是在scrollView的代理方法里调用updateShader方法。
+
+```
+class GameViewController: UIViewController, UIScrollViewDelegate {
+
+...
+
+ func updateShader(scrollView: UIScrollView) {
+ let zoomUniform = node.shader!.uniformNamed("zoom")!
+
+ let offsetUniform = node.shader!.uniformNamed("offset")!
+
+ var offset = scrollView.contentOffset
+
+ offset.x /= scrollView.contentSize.width
+ offset.y /= scrollView.contentSize.height
+
+ zoomUniform.floatValue = Float(scrollView.zoomScale)
+ offsetUniform.floatVector2Value = GLKVector2Make(Float(offset.x), Float(offset.y))
+ }
+
+ func scrollViewDidScroll(scrollView: UIScrollView) {
+ updateShader(scrollView)
+ }
+
+ func scrollViewDidZoom(scrollView: UIScrollView) {
+ updateShader(scrollView)
+ }
+...
+}
+```
+
+
+同时也别忘了当view出现时调用updateShader方法,这样你才可以初始化uniform变量。
+
+```
+class ViewController {
+...
+ override func viewDidAppear(animated: Bool) {
+ super.viewDidAppear(animated)
+
+ updateShader(scrollView)
+ }
+...
+}
+```
+
+
+
+最终着色程序的如下所示:
+
+```
+void main() {
+#define iterations 128
+
+ vec2 position = v_tex_coord; // gets the location of the current pixel in the intervals [0..1] [0..1]
+
+ position.y = 1.0 - position.y;
+
+ vec2 z = offset + position / zoom;
+
+ z *= 2.0;
+ z -= vec2(1.5,1.0);
+
+ float aspectRatio = u_sprite_size.x / u_sprite_size.y;
+ z.x *= aspectRatio;
+
+ vec2 c = z;
+
+ float it = 0.0; // Keep track of what iteration we reached
+ for (int i = 0;i < iterations; ++i) {
+ // zn = zn-1 ^ 2 + c
+
+ // (x + yi) ^ 2 = x ^ 2 - y ^ 2 + 2xyi
+ z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
+ z += c;
+
+ if (dot(z,z) > 4.0) { // dot(z,z) == length(z) ^ 2 only faster to compute
+ break;
+ }
+
+ it += 1.0;
+ }
+
+ vec3 color = vec3(0.0,0.0,0.0); // initialize color to black
+
+ if (it < float(iterations)) {
+ color.x = sin(it / 3.0);
+ color.y = cos(it / 6.0);
+ color.z = cos(it / 12.0 + 3.14 / 4.0);
+ }
+
+ gl_FragColor = vec4(color,1.0);
+}
+```
+
+[Complete Source Code](https://www.weheartswift.com/wp-content/uploads/2015/06/MandelbrotTutorial-Complete.zip)
+
+[完整代码](https://www.weheartswift.com/wp-content/uploads/2015/06/MandelbrotTutorial-Complete.zip)
+
+
+
+
+##挑战
+
+
+
+1 . 优化
+
+
+黑色部分渲染的最慢。幸好根据下图,我们可以很快知道一个点是否在两块黑色部分之一里 (心形部分或者区域2)。[这里](http://en.wikibooks.org/wiki/Fractals/Iterations_in_the_complex_plane/Mandelbrot_set#Cardioid_and_period-2_checking)你可以找到如何判断点是否在两块黑色区域之一里的方法。加上这些代码来改进着色程序,它们只会在点不在这两个区域里执行mandelbrot循环。这将大幅度提高app在这些区域可见时的表现。
+
+
+
+见下图,主要的心形为红色,区域2为绿色。
+
+
+
+**Hint**
+
+**提示**
+
+
+只当点在这些区域中的一个以外的时候执行mandelbrot循环。
+
+**Solution**
+
+**答案**
+
+```
+void main() {
+ #define iterations 128
+
+ vec2 position = v_tex_coord; // gets the location of the current pixel in the intervals [0..1] [0..1]
+
+ position.y = 1.0 - position.y;
+
+ vec2 z = offset + position / zoom;
+
+ z *= 2.0;
+ z -= vec2(1.5,1.0);
+
+ float aspectRatio = u_sprite_size.x / u_sprite_size.y;
+ z.x *= aspectRatio;
+
+ vec2 c = z;
+
+
+ bool skipPoint = false;
+
+ // cardioid checking
+ if ((z.x + 1.0) * (z.x + 1.0) + z.y * z.y < 0.0625) {
+ skipPoint = true;
+ }
+
+ // period 2 checking
+ float q = (z.x - 0.25) * (z.x - 0.25) + z.y * z.y;
+
+ if (q * (q + (z.x - 0.25)) < 0.25 * z.y * z.y) {
+ skipPoint = true;
+ }
+
+ float it = 0.0; // Keep track of what iteration we reached
+
+ if (!skipPoint) {
+ for (int i = 0;i < iterations; ++i) {
+ // zn = zn-1 ^ 2 + c
+
+ // (x + yi) ^ 2 = x ^ 2 - y ^ 2 + 2xyi
+ z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
+ z += c;
+
+ if (dot(z,z) > 4.0) { // dot(z,z) == length(z) ^ 2 only faster to compute
+ break;
+ }
+
+ it += 1.0;
+ }
+ }
+
+ vec3 color = vec3(0.0,0.0,0.0); // initialize color to black
+
+ if (it < float(iterations) && !skipPoint) {
+ color.x = sin(it / 3.0);
+ color.y = cos(it / 6.0);
+ color.z = cos(it / 12.0 + 3.14 / 4.0);
+ }
+
+ gl_FragColor = vec4(color,1.0);
+ }
+
+ ```
+
+
+ [完整代码](https://www.weheartswift.com/wp-content/uploads/2015/06/MandelbrotTutorial-Challenge-1.zip)
+
+
+2 . 做一个类似的app,可以让你探索Julia set 的某点c。
+
+
+例子: vec2 c = vec2(-0.76, 0.15);
+
+
+
+**Solution**
+
+**答案**
+
+ void main() {
+ #define iterations 128
+
+ vec2 position = v_tex_coord; // gets the location of the current pixel in the intervals [0..1] [0..1]
+
+ position.y = 1.0 - position.y;
+
+ vec2 z = offset + position / zoom;
+
+ z *= 2.0;
+ z -= vec2(1.0,1.0);
+
+ float aspectRatio = u_sprite_size.x / u_sprite_size.y;
+ z.x *= aspectRatio;
+
+ vec2 c = vec2(-0.76, 0.15);
+
+
+ float it = 0.0; // Keep track of what iteration we reached
+
+ for (int i = 0;i < iterations; ++i) {
+ // zn = zn-1 ^ 2 + c
+
+ // (x + yi) ^ 2 = x ^ 2 - y ^ 2 + 2xyi
+ z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
+ z += c;
+
+ if (dot(z,z) > 4.0) { // dot(z,z) == length(z) ^ 2 only faster to compute
+ break;
+ }
+
+ it += 1.0;
+ }
+
+ vec3 color = vec3(0.0,0.0,0.0); // initialize color to black
+
+ if (it < float(iterations)) {
+ color.x = sin(it / 3.0);
+ color.y = cos(it / 6.0);
+ color.z = cos(it / 12.0 + 3.14 / 4.0);
+ }
+
+ gl_FragColor = vec4(color,1.0);
+ }
+
+
+[完整代码](https://www.weheartswift.com/wp-content/uploads/2015/06/MandelbrotTutorial-Challenge-2.zip)
+
+
+3 . 添加一个点c的uniform变量,使用户可以用两个手指改变其值。
+
+
+**提示**
+
+
+用UIPanGestureRecognizer来检测两个手指的范围。你需要标准化手势识别器传来的结果。
+
+
+**答案**
+
+
+```
+class GameViewController: UIViewController, UIScrollViewDelegate {
+ ...
+ var c: GLKVector2 = GLKVector2Make(0, 0)
+
+ override func viewDidLoad() {
+ ...
+ let panGr = UIPanGestureRecognizer(target: self, action: "didPan:")
+
+ panGr.minimumNumberOfTouches = 2
+
+ view.addGestureRecognizer(panGr)
+ }
+
+ func didPan(panGR: UIPanGestureRecognizer) {
+
+ var translation = panGR.translationInView(view)
+
+ translation.x /= view.frame.size.width
+ translation.y /= view.frame.size.height
+
+ c = GLKVector2Make(Float(translation.x) + c.x, Float(translation.y) + c.y)
+
+ let cUniform = node.shader!.uniformNamed("c")!
+
+ cUniform.floatVector2Value = c
+ panGR.setTranslation(CGPointZero, inView: view)
+ }
+ }
+```
+
+[完整代码](https://www.weheartswift.com/wp-content/uploads/2015/06/MandelbrotTutorial-Challenge-3.zip)
+
+
+4 . 用一个图片来给julia分形涂色。有很多方法都可以实现,其中有一个很有意思的方法如下:
+
+
+
+
+1. 每一次循环都从图片里得到对应z的颜色。如果颜色不是透明的就跳出循环。
+2. 如果跑完所有循环,得到的颜色依旧不是透明的,那么就用它来填色对应的像素。
+3. 如果是透明的,那么就用另外一个公式来填点的颜色。比如标准化过的循环次数。
+
+
+下面是一个用兔子照片来填色的julia分形。
+
+
+
+
+
+
+
+
+**Hint**
+
+**提示**
+
+
+你需要再添加一个Texture类型的uniform变量,命名为image。你可以用`vec4 color = texture2D(image,p)` 来得到texture在p位置的颜色。
+
+
+**答案**
+
+```
+ class GameViewController: UIViewController, UIScrollViewDelegate {
+ ...
+
+ override func viewDidLoad() {
+ ...
+ let imageUniform = node.shader!.uniformNamed("image")!
+
+ imageUniform.textureValue = SKTexture(imageNamed: "bunny")
+ }
+ ...
+ }
+
+```
+
+```
+ vec4 getColor(vec2 p) {
+
+ if (p.x > 0.99 || p.y > 0.99 || p.x < 0.01 || p.y < 0.01) {
+ return vec4(0.0);
+ }
+
+ return texture2D(image,p);
+ }
+
+ void main() {
+ #define iterations 128
+
+ vec2 position = v_tex_coord; // gets the location of the current pixel in the intervals [0..1] [0..1]
+
+ position.y = 1.0 - position.y;
+
+ vec2 z = offset + position / zoom;
+
+ z *= 2.0;
+ z -= vec2(1.0,1.0);
+
+ float aspectRatio = u_sprite_size.x / u_sprite_size.y;
+ z.x *= aspectRatio;
+
+ vec2 c = vec2(-0.76, 0.15);
+
+ vec4 color = vec4(0.0); // initialize color to black
+
+ float it = 0.0; // Keep track of what iteration we reached
+ for (int i = 0;i < iterations; ++i) {
+ // zn = zn-1 ^ 2 + c
+
+ // (x + yi) ^ 2 = x ^ 2 - y ^ 2 + 2xyi
+ z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
+ z += c;
+
+ color = getColor(z);
+
+ if (dot(z,z) > 4.0 || color.w > 0.1) { // dot(z,z) == length(z) ^ 2 only faster to compute
+ break;
+ }
+
+ it += 1.0;
+ }
+
+ if (color.w < 0.1) {
+
+ float s = it / 80.0;
+ color = vec4(s,s,s,1.0);
+ }
+
+ gl_FragColor = color;
+ }
+
+```
+
+[完整代码](https://www.weheartswift.com/wp-content/uploads/2015/06/MandelbrotTutorial-Challenge-4.zip)
+
+
+5 .类比分形,实验一下Mandelbrot的公式。**这个是开放性的挑战。**以下提供了两个例子
+
+
+**燃烧之船的分形**
+
+
+
+**Formula** `zn = abs(zn-12 + c)`
+
+**公式** `zn = abs(zn-12 + c)`
+
+**GLSL**
+
+**GLSL**
+```
+ z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
+ z += c;
+
+ z = abs(z);
+ ```
+
+
+[源代码](https://www.weheartswift.com/wp-content/uploads/2015/06/MandelbrotTutorial-Challenge-5-1.zip)
+
+
+**Sierpinski Julia**
+
+
+
+
+**公式** `zn = zn-12 + 0.5 * c / (zn-12)`
+
+**GLSLS**
+
+
+```
+vec2 powc(vec2 z,float p) {
+ vec2 polar = vec2(length(z),atan(z.y,z.x));
+
+ polar.x = pow(polar.x,p);
+ polar.y *= p;
+
+ return vec2(polar.x * cos(polar.y),polar.x * sin(polar.y));
+}
+
+void main() {
+...
+ z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
+ z += 0.5 * c * powc(z,-2.0);
+...
+}
+```
+
+ [源代码](https://www.weheartswift.com/wp-content/uploads/2015/06/MandelbrotTutorial-Challenge-5-2.zip)
+
+
+
diff --git "a/issue-15/ReactNavtive\346\241\206\346\236\266\346\225\231\347\250\213.md" "b/issue-15/ReactNavtive\346\241\206\346\236\266\346\225\231\347\250\213.md"
new file mode 100644
index 0000000..c0e0f83
--- /dev/null
+++ "b/issue-15/ReactNavtive\346\241\206\346\236\266\346\225\231\347\250\213.md"
@@ -0,0 +1,1410 @@
+React Navtive框架教程
+---
+
+> * 原文链接 : [React Native Tutorial: Building Apps with JavaScript](http://www.raywenderlich.com/99473/introducing-react-native-building-apps-javascript)
+* 原文作者 : [ColinEberhardt](http://www.raywenderlich.com/u/ColinEberhardt)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [kmyhy](https://github.com/kmyhy)
+* 校对者: [这里校对者的github用户名](github链接)
+* 状态 : 校对中
+
+
+A few months ago Facebook announced React Native, a framework that lets you build native iOS applications with JavaScript – and the official repository just came out of beta today!
+
+几个月前Facebook推出了React Native框架,允许开发者用JavaScript编写本地iOS App——今天,官方代码库的beta版本终于放出!
+
+People have been using JavaScript and HTML5 to create iOS applications using the PhoneGap wrapper for a number of years, so is React Native really such a big deal?
+
+早在几年前,开发者就已经在使用JavaScript和HTML5加上PhoneGap编译器来编写iOS App了,因此React Native框架是不是有点多余?
+
+React Native is a big deal, and people are getting very excited about it for two main reasons:
+1. With React Native your application logic is written and runs in JavaScript, whereas your application UI is fully native; therefore you have none of the compromises typically associated with HTML5 UI.
+2. React introduces a novel, radical and highly functional approach to constructing user interfaces. In brief, the application UI is simply expressed as a function of the current application state.
+
+但React Native确实是一个很了不起的东东,开发者为之欢欣鼓舞,这是因为:
+1. 通过React Native框架,你可以用JavaScript来编写和运行应用程序逻辑,而UI却可以是真正的本地代码编写的;因此,你完全不需要一个HTML5编写的UI。
+2. React框架采用了一种新颖的、激进的和高度函数式的方式来构建UI。简单说,应用程序的UI可以简单地用一个函数来表示应用程序当前的状态。
+
+The key point with React Native is that it aims to primarily bring the power of the React programming model to mobile app development. It is not aiming to be a cross platform, write-once run-anywhere, tool. It is aiming to be learn-once write-anywhere. An important distinction to make. This tutorial only covers iOS, but once you’ve learned the concepts here you could port that knowledge into creating an Android app very quickly.
+
+React Native的重点是把React编程模型引进到移动App的开发中去。它的目的并不是跨平台,一次编写到处运行。它真正的目标是“一次学习多处编写”。这是一个重大的区别。本教程只涉及iOS,但一旦你学会了它的思想,你就可以快速将同样的知识应用到Android App的编写上。
+
+If you have only ever written applications in Objective-C or Swift, you might not be particularly excited about the prospect of using JavaScript instead. Although, as a Swift developer, the second point above should pique your interest!
+如果你过去只使用O-C或Swift编写过iOS应用程序,你可能不会因为可以用JavaScript编写iOS App而激动不已。但是,作为一个Swift开发者,上面所说的第二点原因肯定会激起你的兴趣!
+
+Through Swift, you’ve no doubt been learning new and more functional ways to encode algorithms, and techniques that encourage transformation and immutability. However, the way in which you construct your UI is very much the same as it was when developing with Objective-C: it’s still UIKit-based and imperative.
+
+通过Swift,你毫无疑问已经学习到那些新颖的、函数式的编写代码的方法;以及一些和过去不同或相同的技术。但是,你构建UI的方式仍然和O-C时代没有太大的不同:仍然要离不开UIKit。
+
+Through intriguing concepts such as a virtual DOM and reconciliation, React brings functional programming directly to the UI layer.
+This tutorial takes you through the process of building an application for searching UK property listings:
+
+通过一些有趣的概念,比如虚拟DOM和reconciliation,React直接将函数式编程的理念用到了UI层面。
+这个教程带你一起构建一个搜索英国房产信息的应用:
+
+
+
+If you’ve never written any JavaScript before, fear not; this tutorial leads you through each and every step of the coding. React uses CSS properties for styling which are generally easy to read and understand, but if you need to, you can always refer to the excellent Mozilla Developer Network reference.
+Want to learn more? Read on!
+
+如果你之前从未写过任何 JavaScript ,别担心;这篇教程带着你一点一点编写代码。React 使用 CSS 属性来定义样式,这些样式通常都很易于阅读和理解,但是如果你想进一步了解,可以参考 Mozilla Developer Network reference。
+要想学习更多内容,请往下看!
+
+##Getting Started
+##开始
+The React Native framework is available via GitHub. You can grab the framework by either cloning the repository using git, or you can choose download it as a zip file. If you are not interested in the source code, you can also use the command line interface (CLI) to create your React Native project, which is the approach this tutorial will take.
+
+React Native 框架托管在GitHub。要获得这个框架,你可以使用git命令克隆项目到本地,或者直接下载zip包。 如果你不想使用源代码,也可以用命令行界面(CLI)创建React Native项目,本文将使用这种方式。
+
+React Native uses Node.js, a JavaScript runtime, to build your JavaScript code. If you don’t already have Node.js installed, it’s time to get it!
+First install Homebrew using the instructions on the Homebrew website, then install Node.js by executing the following in a Terminal window:
+
+React Native 使用了 Node.js,如果你的机器上没有安装Node.js,请先安装它。
+首先需要安装 Homebrew,安装指南请参考Homebrew网站。然后用brew命令来安装Node.js:
+
+```
+brew install node
+```
+
+Next, use homebrew to install watchman, a file watcher from Facebook:
+
+然后安装 watchman(Facebook推出的文件改动监听器):
+
+```
+brew install watchman
+```
+This is used by React Native to figure out when your code changes and rebuild accordingly. It’s like having Xcode do a build each time you save your file.
+Next use npm to install the React Native CLI tool:
+
+React Native通过watchman来监视代码文件的改动并适时进行编译。这就好比Xcode,它会在每次文件被保存时对文件进行编译。
+然后用npm命令安装React Native 的CLI工具:
+
+```
+npm install -g react-native-cli
+```
+This uses the Node Package Manager to fetch the CLI tool and install it globally; npm is similar in function to CocoaPods or Carthage.
+Navigate to the folder where you would like to develop your React Native application, and use the CLI tool to construct the project:
+
+这个命令通过Node Package Manager来下载和安装CLI工具,npm是一个类似CocoPods或Carthage工具。
+定位到要创建React Native 项目的文件夹,使用CLI工具创建一个新的React Native项目:
+
+```
+react-native init PropertyFinder
+```
+This creates a starter-project containing everything you need to build and run a React Native application.
+If you look at the created folders and files you will find a node_modules folder, which contains the React Native framework. You will also find an index.ios.js file, which is the skeletal app created by the CLI tool. Finally, there is an Xcode project file and an iOS folder, containing the small amount of code required to ‘bootstrap’ your application.
+Open the Xcode project file then build and run. The simulator will start and display the following greeting:
+
+这将创建一个默认的React Native项目,其中包含有能够让React Native项目编译运行的必要内容。
+在React Native项目文件夹中,有一个node_modules文件夹,它包含React Native 框架文件。此外还有一个 index.ios.js 文件,这是CLI创建的脚手架代码。最后,还有一个Xcode项目文件及一个iOS文件夹,后者会有一些iOS代码用于引导React Navtive App。
+打开Xcode项目文件,build&run。模拟器启动并显示一句问候语:
+
+
+
+You might also have noticed that a terminal window has popped up, displaying the following:
+
+与此同时Xcode还会打开一个终端窗口,并显示如下信息:
+> ===============================================================
+ | Running packager on port 8081.
+ | Keep this packager running while developing on any JS
+ | projects. Feel free to close this tab and run your own
+ | packager instance if you prefer.
+ |
+ | https://github.com/facebook/react-native
+ |
+ ===============================================================
+>
+> Looking for JS files in /Users/colineberhardt/Temp/TestProject
+> React packager ready.
+
+This is the React Native packager, running under node. You’ll find out what it does shortly.
+Don’t close the Terminal window; just keep it running in the background. If you do close it by mistake, simply stop and re-run the project via Xcode.
+
+这是React Navtive Packager,它在node容器中运行。你待会就会发现它的用处。
+千万不要关闭这个窗口,让它一直运行在后面。如果你意外关闭它,可以在Xcode中先停止程序,再重新运行程序。
+
+> Note: One final thing before you get too deep in the code — you’re
+> going to be writing a lot of JavaScript code in this tutorial, and
+> Xcode is certainly not the best tool for this job! I use Sublime Text,
+> which is a cheap and versatile editor, but atom, brackets or any other
+> lightweight editor will do the job.
+>
+> 注意:
+> 在开始接触具体的代码之前(在本教程中,主要是js代码),我们将推荐 Sublime
+> Text这个文本编辑工具,因为Xcode并不适合用于编写js代码的。当然,你也可以使用 atom, brackets
+> 等其他轻量级的工具来替代。
+
+##Hello React Native
+##你好, React Native
+Before getting started on the property search application, you’re going to create a very simple Hello World app. You’ll be introduced to the various components and concepts as you go along.
+
+在开始编写这个房产搜索App之前,我们先来创建一个简单的Hello World项目。我们将通过这个例子来演示React Native的各个组件和概念。
+用你喜欢的文本编辑器(例如Sublime Text)打开index.ios.js ,删除所有内容。然后加入以下语句:
+
+```
+'use strict';
+```
+This enables Strict Mode, which adds improved error handling and disables some less-than-ideal JavaScript language features. In simple terms, it makes JavaScript better!
+
+这将开启严谨模式,这会改进错误的处理并禁用某些js语法特性,这将让JavaScript表现得更好。
+
+> Note: For a more detailed overview of Strict Mode, I’d encourage you
+> to read Jon Resig’s article “ECMAScript 5 Strict Mode, JSON, and
+> More”.
+>
+> 注意: 关于严谨模式,读者可以参考 Jon Resig的文章:“ECMAScript 5 Strict Mode, JSON, and More”。
+
+Next, add the following line:
+然后加入这一句:
+
+```
+var React = require('react-native');
+```
+
+这将加载 react-native 模块,并将其保存为React变量。React Native 使用和Node.js 一样的 require 函数来加载模块,类似于Swift中的import语句。
+
+> Note: For more information about JavaScript modules I’d recommend
+> reading this article by Addy Osmani on writing modular JavaScript.
+>
+> 注意:
+> 关于JavaScript 模块的概念,请参考 Addy Osmani的[这篇文章](http://addyosmani.com/writing-modular-js/)。
+
+Just below the require statement, add the following:
+
+然后加入如下语句:
+
+```
+var styles = React.StyleSheet.create({
+ text: {
+ color: 'black',
+ backgroundColor: 'white',
+ fontSize: 30,
+ margin: 80
+ }
+});
+```
+This defines a single style that you will shortly apply to your “Hello World” text. If you’ve done any web development before, you’ll probably recognize those property names; React Native uses Cascading Style Sheets (CSS) to style the application UI.
+Now for the app itself! Still working in the same file, add the following code just beneath the style declaration above:
+
+这将定义一个css样式,我们将在显示“Hello World”字符串时应用这个样式。
+在React Native 中,我们可以使用 [Cascading Style Sheets (CSS)](http://www.w3schools.com/css/) 语法来格式化UI样式。
+接下来敲入如下代码:
+
+```
+class PropertyFinderApp extends React.Component {
+ render() {
+ return React.createElement(React.Text, {style: styles.text}, "Hello World!");
+ }
+}
+```
+Yes, that’s a JavaScript class!
+Classes were introduced in ECMAScript 6 (ES6). Although JavaScript is constantly evolving, web developers are restricted in what they can use due to the need to maintain compatibility with older browsers. React Native runs within JavaScriptCore; as a result, you can use modern language features without worrying about supporting legacy browsers.
+
+这里我们定义了一个JavaScript 类。JavaScript类的概念出现自ECMAScript 6。由于JavaScript是一门不断演变的语言,因此web开发者必须保持与浏览器的向下兼容。由于React Native基于JavaScriptCore,因此我们完全可以放心使用它的现代语法特性,而不需要操心与老版本浏览器兼容的问题。
+
+> Note: If you are a web developer, I’d thoroughly encourage you to use
+> modern JavaScript, and then convert to older JavaScript using tools
+> such as Babel to maintain support for older and incompatible browsers.
+>
+> 注意:如果你是Web开发人员,我建议你使用新的JavaScript语法。有一些工具比如 Babel,可以将现代JavaScript语法转变为传统JavaScript语法,这样就能和老式浏览器进行兼容。
+
+PropertyFinderApp extends React.Component, the basic building block of the React UI. Components contain immutable properties, mutable state variables and expose a method for rendering. Your current application is quite simple and only requires a render method.
+React Native components are not UIKit classes; instead they are a lightweight equivalent. The framework takes care of transforming the tree of React components into the required native UI.
+
+PropertyFinderApp 类继承自 React.Componen,后者是React UI中的基础。Components包含了一些不可变属性、可变属性和一些渲染方法。当然,这个简单App中,我们就用到一个render方法。
+React Native 的Components不同于UIKit 类,它们是轻量级的对象。框架会将React Components转换为对应的本地UI对象。
+最后敲入如下代码:
+
+```
+React.AppRegistry.registerComponent('PropertyFinder', function() { return PropertyFinderApp });
+```
+AppRegistry defines the entry point to the application and provides the root component.
+Save your changes to index.ios.js and then return to Xcode. Ensure the PropertyFinder scheme is selected with one of the iPhone simulators, and then build and run your project. After a few seconds you’ll see your “Hello World” app in action:
+
+AppRegistry 代表了App的入口以及根组件。保存文件,返回Xcode。确保当前Scheme为PropertyFinder ,然后在模拟器运行App。你将看到如下效果:
+
+
+That’s a JavaScript application running in the simulator, rendering a native UI, without a browser in sight!
+Still don’t trust me? :] Verify it for yourself: within Xcode, select Debug\View Debugging\Capture View Hierarchy and take a look at the native view hierarchy. You will see no UIWebView instances anywhere! Just a proper, real, view! Neat :]!
+
+看到了吧,模拟器将JavaScript代码渲染为本地UI组件,你不会看到任何浏览器的痕迹。
+你可以这样来确认一下:
+在Xcode中,选中 Debug\View Debugging\Capture View Hierarchy,查看本地视图树。你将找不到任何UIWebView实例。
+
+
+Curious as to how it all works? In Xcode open AppDelegate.m and locate application:didFinishLaunchingWithOptions:. This method constructs a RCTRootView, which loads the JavaScript application and renders the resultant view.
+When the application starts, the RCTRootView loads the application from the following URL:
+
+你一定会惊奇这一切是怎么发生的。在 Xcode 中打开 AppDelegate.m,找到application:didFinishLaunchingWithOptions:方法。
+在这个方法中,创建了一个RCTRootView,该对象负责加载JavaScript App并渲染相关视图。
+App一启动,RCTRootView会加载如下URL的内容:
+
+```
+http://localhost:8081/index.ios.bundle
+```
+Recall the Terminal window that was opened when you ran this application; this starts a packager and server that handles the above request.
+Open this URL in Safari; you’ll see the JavaScript code for your app. You should be able to find your “Hello World” code embedded among the React Native framework.
+When your app starts, this code is loaded and executed by the JavaScriptCore framework. In the case of your application, it loads the PropertyFinderApp component, then constructs the native UIKit view. You’ll learn a bit more about this later in the tutorial.
+
+还记得App启动时弹出的终端窗口吗?终端窗口中运行的packager和server会处理上述请求。
+你可以用Safari来打开上述URL,你将会看到一些JavaScript代码。在React Native 框架代码中你会找到“Hello World”相关代码。
+当App打开时,这些代码会被加载并执行。以我们的App来说,PropertyFinderApp组件会被加载,然后创建相应的本地UI组件。
+
+##Hello World JSX
+##你好, JSX
+Your current application uses React.createElement to construct the simple UI for your application, which React turns into the native equivalent. While your JavaScript code is perfectly readable in its present form, a more complex UI with nested elements would rapidly become quite a mess.
+Make sure the app is still running, then return to your text editor to edit index.ios.js. Modify the return statement of your component’s render method as follows:
+
+前面我们用React.createElement构建了一个简单的UI ,React 会将之转换为对应的本地对象。但对于复杂UI来说(比如那些组件嵌套的UI),代码会变得非常难看。
+确保App保持运行,回到文本编辑器,修改index.ios.js中的return语句为:
+
+```
+return Hello World (Again);
+```
+This is JSX, or JavaScript syntax extension, which mixes HTML-like syntax directly in your JavaScript code; if you’re already a web developer, this should feel rather familiar. You’ll use JSX throughout this article.
+
+这里使用了JSX语法,即JavaScript 语法扩展,它基本上是将JavaScript代码混合了HTML风格。如果你是一个web开发人员,对此你应该不会陌生。 在本文中,JSX随处可见。
+
+Save your changes to index.ios.js and return to the simulator. Press Cmd+R, and you’ll see your application refresh to display the updated message “Hello World (Again)”.
+
+保存 index.ios.js回到iPhone模拟器,按下快捷键 Cmd+R,你会看到App的显示变成了 “Hello World (Again)”。
+
+Re-running a React Native application is really as simple as refreshing a web browser! :]
+
+重新运行React Navtive App如同刷新Web页面一样简单。
+
+Since you’ll be working with the same set of JavaScript, you can leave the app running and simply refresh the app in this fashion after modifying and saving index.ios.js.
+
+因为你实际上是在和JavaScript打交道,所以只需修改并保存index.ios.js,即可让App内容得到更新,同时不中断App的运行。
+
+> Note: If you are feeling inquisitive, take a look at your ‘bundle’ in
+> the browser to see what the JSX is transformed into.
+>
+> 注意:
+> 如果你还有疑问,你可以用浏览器在看一下你的“Bundle”内容,它应该也发生了变化。
+
+Okay, enough of this “Hello World” fun; it’s time to build the real application!
+好了,“Hello World" 的演示就到此为止;接下来我们要编写一个真正的React App了!
+
+##Adding Navigation
+##实现导航
+
+这个demo使用了标准的UIKit中的导航控制器来提供”栈式导航体验“。接下来我们就来实现这个功能。
+
+Within index.ios.js, rename the PropertyFinderApp class to HelloWorld:
+
+在 index.ios.js, 将 PropertyFinderApp 类修改为 HelloWorld:
+
+```
+class HelloWorld extends React.Component {
+```
+
+You’ll keep the “Hello World” text display around a little longer, but it won’t be the root component of your app anymore.
+Next add the following class below the HelloWorld component:
+
+我们仍然要显示“Hello World”字样,但不再将它作为App的根视图。
+然后为HelloWorld加入以下代码:
+
+```
+class PropertyFinderApp extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+}
+```
+This constructs a navigation controller, applies a style and sets the initial route to the HelloWorld component. In web development, routing is a technique for defining the navigation structure of an application, where pages — or routes — are mapped to URLs.
+Within the same file, update the styles declaration to include the container style as shown below:
+
+这将创建一个导航控制器,并指定了它的外观样式和初始route(相对于HelloWorld视图)。在web开发中,routing是一种技术,用于表示应用程序的导航方式,即哪个一页面(或route)对应哪一个URL。
+然后修改css样式定义,在其中增加一个container样式:
+
+```
+var styles = React.StyleSheet.create({
+ text: {
+ color: 'black',
+ backgroundColor: 'white',
+ fontSize: 30,
+ margin: 80
+ },
+ container: {
+ flex: 1
+ }
+});
+```
+
+You’ll find out what flex: 1 means a bit later in this tutorial.
+Head back to the simulator and press Cmd+R to see your new UI in action:
+
+flex: 1的意思稍后解释。
+回到模拟器,按 Cmd+R 查看效果:
+
+
+
+There’s the navigation controller with its root view, which is currently the “Hello World” text. Excellent — you now have the basic navigation structure for your application in place. It’s time to add the ‘real’ UI!
+这个导航控制器有一个根视图,即图中显示的”Hello World“文本。非常好——我们的App已经具备了基本的导航功能。是时候显示一些”真正的“UI了!
+##Building the Search Page
+##实现查找
+Add a new file to the project named SearchPage.js and place it in the same folder as index.ios.js. Add the following code to this file:
+
+新建一个 SearchPage.js 文件,保存在 index.ios.js同一目录。在这个文件中加入代码:
+
+```
+'use strict';
+
+var React = require('react-native');
+var {
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+ TouchableHighlight,
+ ActivityIndicatorIOS,
+ Image,
+ Component
+} = React;
+```
+
+You’ve already seen the strict mode and the react-native import before, but the assignment statement that follows it is something new.
+This is a destructuring assignment, which lets you extract multiple object properties and assign them to variables using a single statement. As a result, the rest of your code can drop the React prefix; for example, you can refer directly to StyleSheet rather than React.StyleSheet. Destructuring is also useful for manipulating arrays and is well worth learning more about.
+Still working in the same file, SearchPage.js, add the following style:
+
+这里使用了一个解构赋值(destructuring assignment),可以将多个对象属性一次性赋给多个变量。这样,在后面的代码中,我们就可以省略掉React前缀,比如用StyleSheet 来代替 React.StyleSheet。解构赋值对于数组操作来说尤其方便,请参考[well worth learning more about.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)
+然后定义如下Css样式:
+
+var styles = StyleSheet.create({
+ description: {
+ marginBottom: 20,
+ fontSize: 18,
+ textAlign: 'center',
+ color: '#656565'
+ },
+ container: {
+ padding: 30,
+ marginTop: 65,
+ alignItems: 'center'
+ }
+});
+
+Again, these are standard CSS properties. Setting up styles like this is less visual than using Interface Builder, but it’s better than setting view properties one by one in your viewDidLoad() methods! :]
+Add the component itself just below the styles you added above:
+
+这里,再次使用了标准的 CSS 属性。虽然用CSS设置样式在可视化方面比起在IB中要差一些,但总比在viewDidLoad()方法中用代码写要好一些。
+然后加入以下代码:
+
+```
+class SearchPage extends Component {
+ render() {
+ return (
+
+
+ Search for houses to buy!
+
+
+ Search by place-name, postcode or search near your location.
+
+
+ );
+ }
+}
+```
+
+render is a great demonstration of JSX and the structure it provides. Along with the style, you can very easily visualize the UI constructed by this component: a container with two text labels.
+Finally, add the following to the end of the file:
+
+在render方法中使用了大量的JSX语法来构造UI组件。通过这种方式,你可以非常容易地构造出如下组件:在一个Container View中包含了两个label。
+在源文件的最后加入这一句:
+
+```
+module.exports = SearchPage;
+```
+
+This exports the SearchPage class, which permits its use in other files.
+这一句将使 SearchPage 类可被其他js文件引用。
+
+The next step is to update the application routing in order to make this the initial route.
+
+然后需要修改App的导航。
+
+Open index.ios.js and add the following just after the current require import near the top of the file:
+
+打开 index.ios.js 在文件头部、现有的require 语句后加入 require 语句:
+
+```
+var SearchPage = require('./SearchPage');
+```
+
+Within the render function of the PropertyFinderApp class, update initialRoute to reference the newly added page as shown below:
+
+在 PropertyFinderApp 类的 render 函数中,修改 initialRoute 为:
+
+```
+component: SearchPage
+```
+
+At this point you can remove the HelloWorld class and its associated style, if you like. You won’t be needing that code any longer.
+
+这里我们可以将HelloWorld类和它对应的样式移除了,我们不再需要它。
+
+Return to the simulator, hit Cmd+R and check out the new UI:
+
+回到模拟器,按下 Cmd+R 查看效果:
+
+
+
+This is using the new component, SearchPage, which you added.
+
+你新创建的组件SearchPage显示在屏幕中。
+##Styling with Flexbox
+##弹性盒子模型
+So far, you’ve seen basic CSS properties that deal with margins, paddings and color. However, you might not be familiar with flexbox, a more recent addition to the CSS specification that is very useful for application UI layout.
+
+一直以来,我们都在用原始的CSS属性来设置外边距、内边距和颜色。但是,最新的CSS规范中增加了弹性盒子的概念,非常利于我们对App的UI进行布局,虽然你可能还不太熟悉它。
+
+React Native uses the css-layout library, a JavaScript implementation of the flexbox standard which is transpiled into C (for iOS) and Java (for Android).
+
+React Native 使用了 css-layout 库,在这个库中实现了弹性盒子,而这种模型无论对iOS还是Android来说都很好理解。
+
+It’s great that Facebook has created this as a separate project that targets multiple languages, since this allows for novel applications, such as applying flexbox layout to SVG (yes, that was written by me … and no, I don’t sleep much!).
+
+更幸运的是,Facebook针对许多语言单独实现了这个项目,这就引申出了许多新颖的用法,比如[在SVG中应用弹性盒子布局](http://blog.scottlogic.com/2015/02/02/svg-layout-flexbox.html)(是的,这篇文章也是我写的,为此我不得不熬到深夜)。
+
+In your app, the container has the default flow direction of column, which means its children are arranged in a vertical stack, like so:
+
+在这个App中,采用了默认的垂直流式布局,即容器中的子元素按照从上到下的顺序进行布局。比如:
+
+
+This is termed the main axis, and can run either vertically or horizontally.
+
+这被称作主轴, 主轴可能是水平方向,也可能是垂直方向。
+
+The vertical position of each child is determined from a combination of its margin, height and padding. The container also sets the alignItems property to center, which determines the placement of children on the cross axis. In this case, it results in center-aligned text.
+
+每个子元素的纵向位置由它们的边距(margin)、间距(padding)和高决定。容器的alignItems属性会被设置为居中(center),这决定了子元素在交叉轴上的位置。在本例里,将导致子元素水平居中对齐。
+
+It’s time to add the input field and buttons. Open SearchPage.js and insert the following just after the closing tag of the second Text element:
+
+接下来我们添加一些文本输入框和按钮。打开SearchPage.js 在第二个 Text 元素后添加:
+
+```
+
+
+
+ Go
+
+
+
+ Location
+
+```
+You’ve added two top-level views here: one to hold a text input and a button, and one with only a button. You’ll read about how these elements are styled in a little bit.
+
+这段代码添加了两个顶级的视图:一个文本输入框外加一个按钮,以及一个单独的按钮。它们所使用的样式待会我们再介绍。
+
+Next, add the accompanying styles to your styles definition:
+
+接着,在styles中增加如下样式:
+
+```
+flowRight: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ alignSelf: 'stretch'
+},
+buttonText: {
+ fontSize: 18,
+ color: 'white',
+ alignSelf: 'center'
+},
+button: {
+ height: 36,
+ flex: 1,
+ flexDirection: 'row',
+ backgroundColor: '#48BBEC',
+ borderColor: '#48BBEC',
+ borderWidth: 1,
+ borderRadius: 8,
+ marginBottom: 10,
+ alignSelf: 'stretch',
+ justifyContent: 'center'
+},
+searchInput: {
+ height: 36,
+ padding: 4,
+ marginRight: 5,
+ flex: 4,
+ fontSize: 18,
+ borderWidth: 1,
+ borderColor: '#48BBEC',
+ borderRadius: 8,
+ color: '#48BBEC'
+}
+```
+Take care with the formatting; each style property should be separated by a comma. That means you’ll need to add a trailing comma after the container selector.
+
+不同样式属性间以逗号分隔,这样你在container选择器后必须以一个逗号结尾。
+
+These styles are used by the text input and buttons which you just added.
+
+这些样式将被文本输入框和按钮所用。
+
+Return to the simulator and press Cmd+R to see the updated UI:
+
+回到模拟器,按下Cmd+R ,你将看到如下效果:
+
+
+
+The text field and ‘Go’ button are on the same row, so you’ve wrapped them in a container that has a flexDirection: 'row' style. Rather than explicitly specifying the widths of the input field and button, you give each a flex value instead. The text field is styled with flex: 4, while the button has flex: 1; this results in their widths having a 4:1 ratio.
+
+Go按钮和其紧随的文本框在同一行,因此我们将它们用一个容器装在一起,同时容器的flexDirection: 样式属性设置为'row' 。我们没有显式指定文本框和按钮的宽度,而是分别指定它们的flex样式属性为4和1。也就是说,它们的宽度在整个宽度(屏幕宽度)中所占的份额分别为4和1。
+
+You might have noticed that your buttons…aren’t actually buttons! :] With UIKit, buttons are little more than labels that can be tapped, and as a result the React Native team decided it was easier to construct buttons directly in JavaScript. The buttons in your app use TouchableHighlight, a React Native component that becomes transparent and reveals the underlay colour when tapped.
+
+而且,视图中的两个按钮都不是真正的按钮。对于UIKit,按钮不过是可以点击的标签而已,因此React Native开发团队能够用JavaScript以一种简单的方式构建按钮:TouchableHighlight是一种React Native组件,当它被点击时,它的前景会变得透明,从而显示其隐藏在底部的背景色。
+
+The final step to complete the search screen of the application is to add the house graphic. The images are available to download as a zip file. Download and unzip the file.
+
+最后我们还要在视图中添加一张图片。这些图片可以在[此处](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/ReactNative-HouseImage.zip)下载。下载后解压缩zip文件。
+
+Within Xcode open the Images.xcassets file and tap the plus button to add a new image set. Next drag the required images into the correct ‘slots':
+
+在 Xcode 打开 Images.xcassets 文件,点击加号按钮,添加一个新的image set。然后将需要用到的图片拖到image set右边窗口对应的位置。
+
+
+
+You’ll have to stop the running application and restart so that these images become available to your React Native app.
+
+要让这些图片显示,必须停止你的 React Native App并重新启动。
+
+Add the following beneath the TouchableHighlight component for the location button:
+
+在location按钮对应的 TouchableHighlight 组件下加入:
+
+``
+
+Now, add the image’s corresponding style to the end of the style list, remembering to add a trailing comma to the previous style:
+
+然后,为图片添加适当的样式定义,记得在上一个样式之后添加一个逗号结尾:
+
+```
+image: {
+ width: 217,
+ height: 138
+}
+```
+
+The require('image!house') statement is used to reference an image located within your application’s asset catalogue. Within Xcode, if you open up Images.xcassets you will find the ‘house’ icon that the above code refers to.
+
+由于我们将图片添加到了Images.xcasset资源包中,我们需要用require('image!house')语句获得图片在App包中的正确路径。在Xcode中,打开Images.xcassets ,你可以找到名为house的image set。
+
+Returning to the simulator, hit Cmd+R and admire your new UI:
+
+回到模拟器,按下Cmd+R 查看运行效果:
+
+
+
+> Note: If you don’t see the house image at this point and see an image
+> that “image!house” cannot be found instead, try restarting the
+> packager (i.e. the “npm start” command you have in the terminal).
+>
+> 注意: 如果图片没有显示,却看到一个““image!house” cannot be
+> found”的提示,则可以重启packager(在终端中输入npm start命令)。
+
+Your current app looks good, but it’s somewhat lacking in functionality. Your task now is to add some state to your app and perform some actions.
+
+到目前为止,我们的App看上去有模有样,但它还缺少很多实际的功能。接下来的任务就是为App增加一些状态,执行一些动作。
+##Adding Component State
+##组件状态
+Each React component has its own state object, which is used as a key-value store. Before a component is rendered you must set the initial state.
+
+每个React组件都有一个state对象,它是一个键值存储对象。在组件被渲染之前,我们可以设置组件的state对象。
+
+Within SearchPage.js, add the following code to the SearchPage class, just before render():
+
+打开SearchPage.js,在 SearchPage 类的 render()方法前,加入以下代码:
+
+```
+constructor(props) {
+ super(props);
+ this.state = {
+ searchString: 'london'
+ };
+}
+```
+
+Your component now has a state variable, with searchString set to an initial value of london.
+
+现在组件就有一个state变量了,同时我们在state中存放了一个 searchString:'london' 的键值对象。
+
+Time to make use of this component state. Within render, change the TextInput element to the following:
+
+然后我们来使用这个state变量。在render方法中,修改TextInput元素为:
+
+```
+
+```
+
+This sets the TextInput value property — that is, the text displayed to the user — to the current value of the searchString state variable. This takes care of setting the initial state, but what happens when the user edits this text?
+
+这将改变 TextInput 的value 属性,即在TextInput中显示一个london的文本——即state变量中的searchString。这个值在我们初始化state时指定的,但如果用户修改了文本框中文本,那又怎么办?
+
+The first step is to create a method that acts as an event handler. Within the SearchPage class add the following method:
+
+ 首先创建一个事件处理方法。在 SearchPage 类中增加一个方法:
+
+onSearchTextChanged(event) {
+ console.log('onSearchTextChanged');
+ this.setState({ searchString: event.nativeEvent.text });
+ console.log(this.state.searchString);
+}
+
+This takes the value from the event’s text property and uses it to update the component’s state. It also adds some logging code that will make sense shortly.
+
+首先从事件参数event中获得text属性,然后将它保存到组件的state中,并用控制台输出一些感兴趣的内容。
+
+To wire up this method so it gets called when the text changes, return to the TextInput field within the render method and add an onChange property so the tag looks like the following:
+
+为了让文本内容改变时这个方法能被调用,我们需要回到TextInput的onChange事件属性中,绑定这个方法,即新加一个onChange属性,如以下代码所示:
+
+``
+
+Whenever the user changes the text, you invoke the function supplied to onChange; in this case, it’s onSearchTextChanged.
+
+一旦用户改变了文本框中的文本,这个函数立即就会被调用。
+
+> Note: You might be wondering what the bind(this) statement is for.
+> JavaScript treats the this keyword a little differently than most
+> other languages; its counterpart in Swift is self. The use of bind in
+> this context ensures that inside the onSearchTextChanged method, this
+> is a reference to the component instance. For more information, see
+> the MDN page on this.
+>
+> 注意: bind(this) 的使用有点特殊。JavaScript 中 this
+> 关键字的含义其实和大部分语言都不相同,它就好比Swift语言中的self。bind方法的调用使得onSearchTextChanged
+> 方法中能够引用到this,并通过this引用到组件实例。更多内容请参考 MDN page on this。
+
+There’s one final step before you refresh your app again: add the following logging statement to the top of render(), just before return:
+
+然后,我们在render方法顶部、return语句之前加一条Log语句:
+
+```
+console.log('SearchPage.render');
+```
+
+ou are about to learn something quite intriguing from these log statements! :]
+
+通过这些log语句,你应该能明白大致发生了什么事情!
+
+Return to your simulator, then press Cmd+R. You should now see that the text input has an initial value of ‘london’ and that editing the text causes some statements to be logged to the Xcode console:
+
+返回模拟器,按下Cmd+R,我们将看到文本框中一开始就有了一个london的字样,当你编辑这段文本后,控制台中的内容将显示:
+
+
+
+Looking at the screenshot above, the order of the logging statement seems a little odd:
+
+ 1. This is the initial call to render() to set up the view.
+ 2. You invoke onSearchTextChanged() when the text changes.
+ 3. You then update the component state to reflect the new input text,which triggers another render.
+ 4. onSearchTextChanged() then wraps things up by logging the new search string.
+
+查看上面的截屏,log语句输出的顺序似乎有点问题:
+1. 组件初始化后调用 render() 方法
+2. 当文本被改变, onSearchTextChanged() 被调用
+3. 我们在代码中改变了组件的state 属性,因此render()方法会被调用
+4. onSearchTextChanged() 打印新的search string.
+
+Whenever the app updates the state of any React component, this triggers an entire UI re-rendering that in turn calls render of all of your components. This is a great idea, as it entirely de-couples the rendering logic from the state changes that affect the UI.
+
+当React 组件的状态被改变时,都会导致整个UI被重新渲染——所有组件的render方法都会被调用。这样做的目的,是为了将渲染逻辑和组件状态的改变完全进行分离。
+
+With most other UI frameworks, it is either your responsibility to manually update the UI based on state changes, or use of some kind of binding framework which creates an implicit link between the application state and its UI representation; see, for example, my article on implementing the MVVM pattern with ReactiveCocoa.
+
+在其他所有的UI框架中,要么程序员在状态改变时自己手动刷新UI,要么使用一种绑定机制在程序状态和UI之间进行联系。就像我另一篇文章 [MVVM pattern with ReactiveCocoa](http://www.raywenderlich.com/74106/mvvm-tutorial-with-reactivecocoa-part-1)所讲。
+
+With React, you no longer have to worry about which parts of the UI might be affected by a state change; your entire UI is simply expressed as a function of your application state.
+
+而在React中,我们不再操心状态的改变会导致那一部分UI需要刷新,因为当状态改变所有的UI都会刷新。
+
+At this point you’ve probably spotted a fundamental flaw in this concept. Yes, that’s right — performance!
+
+当然,你也许会担心性能问题。
+
+Surely you can’t just throw away your entire UI and re-build it every time something changes? This is where React gets really smart. Each time the UI renders itself, it takes the view tree returned by your render methods, and reconciles — or diffs — it with the current UIKit view. The output of this reconciliation process is a simple list of updates that React needs to apply to the current view. That means only the things that have actually changed will re-render.
+
+难道每次状态改变时,整个UI都会被舍弃然后重新创建吗?
+这就是React真正智能的地方。每当UI要进行渲染时,它会遍历整个视图树并计算render方法,对比与当前UIKit视图是否一致,并将需要改变的地方列出一张列表,然后在此基础上刷新视图。也就是说,只有真正发生变化的东西才会被重新渲染。
+
+It’s amazing to see the novel concepts that make ReactJS so unique — the virtual-DOM (Document Object Model, the visual-tree of a web document) and reconciliation — applied to an iOS app.
+You can wrap your head around all that later; you still have some work to do in the app. Remove the logging code you just added above, since it’s no longer necessary and just adds cruft to the code.
+
+ReactJS将一些新奇的概念应用到了iOS App中,比如虚拟DOM(Document Object Modal,web文档可视树)和一致性。这些概念我们可以稍后再讨论,先来看下这个App接下来要做的工作。删除上面添加的Log语句。
+
+##Initiating a Search
+##开始搜索
+n order to implement the search functionality you need handle the ‘Go’ button press, create a suitable API request, and provide a visual indication to the user that a query is in progress.
+Within SearchPage.js, update the initial state within the constructor:
+
+为了实现搜索功能,我们需要处理Go按钮点击事件,创建对应的API请求,显示网络请求的状态。
+打开SearchPage.js, 在constructor方法中修改state的初始化代码:
+
+```
+this.state = {
+ searchString: 'london',
+ isLoading: false
+};
+```
+
+The new isLoading property will keep track of whether a query is in progress.
+Add the following logic to the start of render:
+
+isLoading 属性用于表示查询是否正在进行。
+在render方法最上面增加:
+
+```
+var spinner = this.state.isLoading ?
+ ( ) :
+ ( );
+```
+
+这里用了一个三目运算,这是一个if语句的简化形式。如果isLoading为true,显示一个网络指示器,否则显示一个空的view。
+在return语句中,在Image下增加:
+
+```
+{spinner}
+```
+
+Now, add the following property within the TouchableHighlight tag that renders the ‘Go’ text:
+
+在Go按钮对应的 TouchableHighlight 标签中增加如下属性:
+
+```
+onPress={this.onSearchPressed.bind(this)}
+```
+
+Next, add the following methods to the SearchPage class:
+
+在 SearchPage 类中新增如下方法:
+
+```
+_executeQuery(query) {
+ console.log(query);
+ this.setState({ isLoading: true });
+}
+
+onSearchPressed() {
+ var query = urlForQueryAndPage('place_name', this.state.searchString, 1);
+ this._executeQuery(query);
+}
+```
+
+_executeQuery() will eventually run the query, but for now it simply logs a message to the console and sets isLoading appropriately so the UI can show the new state.
+
+_executeQuery() 目前仅仅是在控制台中输出一些信息,同时设置isLoading属性为true,剩下的功能我们留到后面完成。
+
+> Note: JavaScript classes do not have access modifiers, so they have no concept of ‘private’. As a result you often see developers prefixing methods with an underscore to indicate that they should be considered private.
+>
+> 注意: JavaScript 类没有访问器,因此也就没有私有的概念。因此我们会在方法名前加一个下划线,以表示该方法视同为私有方法。
+
+You’ll invoke onSearchPressed() and initiate the query when the ‘Go’ button is pressed.
+Finally, add the following utility function just above the SearchPage class declaration:
+
+当Go按钮被点击, onSearchPressed() 即被调用。
+然后,在 SearchPage 类声明之前,声明如下实用函数:
+
+```
+function urlForQueryAndPage(key, value, pageNumber) {
+ var data = {
+ country: 'uk',
+ pretty: '1',
+ encoding: 'json',
+ listing_type: 'buy',
+ action: 'search_listings',
+ page: pageNumber
+ };
+ data[key] = value;
+
+ var querystring = Object.keys(data)
+ .map(key => key + '=' + encodeURIComponent(data[key]))
+ .join('&');
+
+ return 'http://api.nestoria.co.uk/api?' + querystring;
+};
+```
+
+This function doesn’t depend on SearchPage, so it’s implemented as a free function rather than a method. It first creates the query string based on the parameters in data. Following this, it transforms the data into the required string format, name=value pairs separated by ampersands. The => syntax is an arrow function, another recent addition to the JavaScript language that provides a succinct syntax for creating anonymous functions.
+
+这个函数不依赖SearchPage类,因此被定义为函数而不是方法。它首先将key\value参数以键值对形式放到了data集合中,然后将data集合转换成以&符分隔的“键=值”形式。=>语法是箭头函数的写法,一种创建匿名函数简洁写法,具体请参考[recent addition to the JavaScript language](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) 。
+
+Head back to the simulator, press Cmd+R to reload the application and tap the ‘Go’ button. You’ll see the activity indicator spin; take a look at the Xcode console to see what it’s telling you:
+
+回到模拟器,按下 Cmd+R 重启App,然后点击‘Go’ 按钮。你将看到网络指示器开始转动。同时控制台将输出:
+
+
+
+The activity indicator renders and the URL for the required query appears in the log. Copy and paste that URL into your browser to see the result. You’ll see a massive JSON object. Don’t worry — you don’t need to understand that! You’ll add code to parse that now.
+
+网络指示器显示,同时要请求的URL也打印出来了。拷贝并粘贴URL到Safari,查看搜索结果。你将看到一堆JSON对象。我们将用代码解析这些JSON对象。
+
+> Note: This app makes use of the Nestoria API for searching property listings. The JSON response coming back from the API is pretty straightforward, but you can have a look at the documentation for all the details on the expected request URL and response formats. The next step is to make the request from within your application.
+>
+> 注意: 这个App使用 [Nestoria API 来查找房子](http://www.nestoria.co.uk/help/api)。查找结果以JSON格式返回。官方文档中列出了所有请求的URL规范及响应格式。
+
+##Performing an API Request
+##发送请求
+Still within SearchPage.js, update the initial state in the class constructor to add a message variable:
+
+打开 SearchPage.js,在初始化状态过程中增加一个message属性:
+
+```
+this.state = {
+ searchString: 'london',
+ isLoading: false,
+ message: ''
+};
+```
+
+Within render, add the following to the bottom of your UI:
+
+在render方法中,在UI元素的最后加入:
+
+```
+{this.state.message}
+```
+
+You’ll use this to display a range of messages to the user.
+
+这个Text用于向用户显示一些文本。
+
+Within the SearchPage class, add the following code to the end of _executeQuery():
+
+在 SearchPage 类中,在 _executeQuery()方法最后加入:
+
+```
+fetch(query)
+ .then(response => response.json())
+ .then(json => this._handleResponse(json.response))
+ .catch(error =>
+ this.setState({
+ isLoading: false,
+ message: 'Something bad happened ' + error
+ }));
+```
+
+This makes use of the fetch function, which is part of the Web API, and provides a vastly improved API versus XMLHttpRequest. The asynchronous response is returned as a promise, with the success path parsing the JSON and supplying it to a method which you are going to add next.
+
+fetch 函数在 [Fetch API](https://fetch.spec.whatwg.org/)中定义,这个新的JavaScript规范被Firefox 39(Nightly版)以及Chrome 42(开发版)支持,它在XMLHttpRequest的基础上进行了极大的改进。结果是异步返回的,同时使用了 [promise](http://www.html5rocks.com/en/tutorials/es6/promises/)规范,如果response中包含有效的JSON对象则将JSON对象的response成员(另一个JSON)传到_handleResponse方法(后面实现)。
+
+The final step is to add the following function to SearchPage:
+
+然后在 SearchPage类中增加方法:
+
+```
+_handleResponse(response) {
+ this.setState({ isLoading: false , message: '' });
+ if (response.application_response_code.substr(0, 1) === '1') {
+ console.log('Properties found: ' + response.listings.length);
+ } else {
+ this.setState({ message: 'Location not recognized; please try again.'});
+ }
+}
+```
+
+This clears isLoading and logs the number of properties found if the query was successful.
+
+如果查询结果成功返回,我们重置 isLoading 属性为false,然后打印结果集的总行数。
+
+> Note: Nestoria has a number of non-1** response codes that are potentially useful. For example, 202 and 200 return a list of best-guess locations. When you’ve finished building your app, why not try handling these and present a list of options to the user?
+>
+> 注意: Nestoria 有 不以1开头的响应码, 这些代码都非常有用。例如202 和 200表示返回一个推荐位置的列表。当完成这个实例后,你可以尝试处理这些返回码,并将列表提供给用户选择。
+
+ Save your work, then in the simulator press Cmd+R and try searching for london’; you should see a log message saying that 20 properties were found. Next try a non-existent location, such as ‘narnia’ (*sniff*), and you’ll be greeted by the following message:
+
+保存,返回模拟器,按下Cmd+R ,然后搜索 ‘london’你将在控制台看到一条消息,表示搜索到20条房子信息。尝试输入一个不存在的地名,比如 ‘narnia’ 你将看到如下信息:
+
+
+
+It’s time to see what those 20 properties in real places such as London look like!
+
+接下来我们在伦敦或者别的什么城市搜索20座房子。
+
+##Displaying the Results
+##显示搜索结果
+Create a new file SearchResults.js, and add the following:
+
+新建一个文件: SearchResults.js, 编写如下代码:
+
+```
+'use strict';
+
+var React = require('react-native');
+var {
+ StyleSheet,
+ Image,
+ View,
+ TouchableHighlight,
+ ListView,
+ Text,
+ Component
+} = React;
+```
+
+Yes, that’s right, it’s a require statement that includes the react-native module, and a destructuring assignment. You’ve been paying attention haven’t you? :]
+
+你注意到了吗?一切都是老样子,一条requires语句和一个结构赋值。
+
+Next add the component itself:
+
+然后定义一个Componet子类:
+
+```
+class SearchResults extends Component {
+
+ constructor(props) {
+ super(props);
+ var dataSource = new ListView.DataSource(
+ {rowHasChanged: (r1, r2) => r1.guid !== r2.guid});
+ this.state = {
+ dataSource: dataSource.cloneWithRows(this.props.listings)
+ };
+ }
+
+ renderRow(rowData, sectionID, rowID) {
+ return (
+
+
+ {rowData.title}
+
+
+ );
+ }
+
+ render() {
+ return (
+
+ );
+ }
+
+}
+```
+
+The above code makes use of a more specialized component — ListView — which displays rows of data within a scrolling container, similar to UITableView. You supply data to the ListView via a ListView.DataSource, and a function that supplies the UI for each row.
+
+上述代码中使用了一个专门的组件——ListView ——该组件非常像ITableView。通过ListView.DataSource, 我们可以向ListView提供数据。renderRow函数则用于为每个行提供UI。
+
+When constructing the data source, you provide a function that compares the identity of a pair of rows. The ListView uses this during the reconciliation process, in order to determine the changes in the list data. In this instance, the properties returned by the Nestoria API have a guid property, which is a suitable check for this purpose.
+
+在构建数据源的时候,我们使用箭头函数对不同的行进行识别。这个函数在ListView进行“一致化”的时候被调用,以便判断列表中的数据是否被改变。在本例中,Nestoria API有一个guid属性,刚好可以用来作为判断的标准。
+
+Now add the module export to the end of the file:
+
+最后,加入一条模块输出语句:
+
+```
+module.exports = SearchResults;
+```
+
+Add the following to SearchPage.js near the top of the file, underneath the require call for React:
+
+在SearchPage.js 头部,require 下方加入:
+
+```
+var SearchResults = require('./SearchResults');
+```
+
+This allows us to use the newly added SearchResults class from within the SearchPage class.
+Modify the current _handleResponse method by replacing the console.log statement with the following:
+
+这样我们就可以 SearchPage 类中使用SearchResults类了。
+在_handleResponse 方法,将console.log 一句替换为:
+
+```
+this.props.navigator.push({
+ title: 'Results',
+ component: SearchResults,
+ passProps: {listings: response.listings}
+});
+```
+
+The above code navigates to your newly added SearchResults component and passes in the listings from the API request. Using the push method ensures the search results are pushed onto the navigation stack, which means you’ll get a ‘Back’ button to return to the root.
+
+上述代码将导航至SearchResults 页面,并将请求到的列表数据传递给它。Push方法可以将页面添加到导航控制器的ViewController堆栈中,同时你的导航栏上将出现一个Back按钮,点击它可以返回到上一页面。
+
+Head back to the simulator, press Cmd+R and try a quick search. You’ll be greeted by a list of properties:
+
+回到模拟器, 按下Cmd+R ,进行一个查找动作。你将看到搜索结果如下:
+
+
+
+好了,房子清单已经列出来了,不过列表有一点丑陋。接下来我们会让它变得漂亮一点。
+##A Touch of Style
+##表格样式
+This React Native code should be starting to look familiar by now, so this tutorial is going to pick up the pace.
+
+现在,React Native的代码对我们来说已经不陌生了,接下来我们的教程可以稍微加快一点节奏了。
+
+Add the following style definition just after the destructuring assignment in SearchResults.js:
+
+在 SearchResults.js文件的解构赋值语句之后,添加样式定义:
+
+```
+var styles = StyleSheet.create({
+ thumb: {
+ width: 80,
+ height: 80,
+ marginRight: 10
+ },
+ textContainer: {
+ flex: 1
+ },
+ separator: {
+ height: 1,
+ backgroundColor: '#dddddd'
+ },
+ price: {
+ fontSize: 25,
+ fontWeight: 'bold',
+ color: '#48BBEC'
+ },
+ title: {
+ fontSize: 20,
+ color: '#656565'
+ },
+ rowContainer: {
+ flexDirection: 'row',
+ padding: 10
+ }
+});
+```
+
+This defines all the styles that you are going to use to render each row.
+
+这些代码中的样式将在渲染单元格时用到。
+
+Next replace renderRow() with the following:
+
+修改renderRow() 方法如下:
+
+```
+renderRow(rowData, sectionID, rowID) {
+ var price = rowData.price_formatted.split(' ')[0];
+
+ return (
+ this.rowPressed(rowData.guid)}
+ underlayColor='#dddddd'>
+
+
+
+
+ £{price}
+ {rowData.title}
+
+
+
+
+
+ );
+}
+```
+
+This manipulates the returned price, which is in the format ‘300,000 GBP’, to remove the GBP suffix. Then it renders the row UI using techniques that you are by now quite familiar with. This time, the data for the thumbnail image is supplied via a URL, and React Native takes care of decoding this off the main thread.
+
+其中价格将以‘300,000 GBP’的格式显示,记得将GBP 后缀删除。上述代码用你已经很熟悉的方式来渲染单元格UI。缩略图以URL方式提供,React Native 自动将其解码(主线程中)。
+
+Also, note the use of an arrow function in the onPress property of the TouchableHighlight component; this is used to capture the guid for the row.
+
+在TouchableHightlight组件的onPress属性中再次使用了箭头函数,并将该行数据的guid作为传递的参数。
+
+The final step is to add this method to the class to handle the press:
+
+最后一个方法,用于处理点击事件
+
+```
+rowPressed(propertyGuid) {
+ var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];
+}
+```
+
+This method locates the property that was tapped by the user. It doesn’t do anything with it yet, but you’ll fix that shortly. But right now, it’s time to admire your handiwork.
+Head back to the simulator, press Cmd+R and check out your results:
+
+这里,当用户点击某行时,通过guid去房产列表中找到对应的房屋信息。
+回到模拟器,按下 Cmd+R ,观察运行结果:
+
+
+
+这下看起来好多了——只不过,那些住在London的人居然住得起这么贵房子?真是令人难以置信!
+
+Time to add the final view to the application.
+
+接下来,我们就来实现App的最后一个界面了。
+##Property Details View
+##查看房屋详情
+
+Add a new file PropertyView.js to the project, then add the following to the top of the file:
+
+新建一个 PropertyView.js 文件到项目中,编辑如下内容:
+
+```
+'use strict';
+
+var React = require('react-native');
+var {
+ StyleSheet,
+ Image,
+ View,
+ Text,
+ Component
+} = React;
+```
+
+Surely you can do this in your sleep by now! :]
+
+确保进行到这一步的时候,你还没有睡着!:]
+
+Next add the following styles:
+
+继续添加如下样式:
+
+```
+var styles = StyleSheet.create({
+ container: {
+ marginTop: 65
+ },
+ heading: {
+ backgroundColor: '#F8F8F8',
+ },
+ separator: {
+ height: 1,
+ backgroundColor: '#DDDDDD'
+ },
+ image: {
+ width: 400,
+ height: 300
+ },
+ price: {
+ fontSize: 25,
+ fontWeight: 'bold',
+ margin: 5,
+ color: '#48BBEC'
+ },
+ title: {
+ fontSize: 20,
+ margin: 5,
+ color: '#656565'
+ },
+ description: {
+ fontSize: 18,
+ margin: 5,
+ color: '#656565'
+ }
+});
+```
+
+Then add the component itself:
+
+然后将组件加入视图:
+
+```
+class PropertyView extends Component {
+
+ render() {
+ var property = this.props.property;
+ var stats = property.bedroom_number + ' bed ' + property.property_type;
+ if (property.bathroom_number) {
+ stats += ', ' + property.bathroom_number + ' ' + (property.bathroom_number > 1
+ ? 'bathrooms' : 'bathroom');
+ }
+
+ var price = property.price_formatted.split(' ')[0];
+
+ return (
+
+
+
+ £{price}
+ {property.title}
+
+
+ {stats}
+ {property.summary}
+
+ );
+ }
+}
+```
+
+The first part of render() performs some manipulation on the data. As is often the case, the data returned by the API is of mixed quality and often has missing fields. This code applies some simple logic to make the data a bit more presentable.
+
+render() 方法的第一步,是封装数据。因为从API获得的数据经常不太规范而且某些字段不全。代码采用简单手段让数据变得更便于展示一些。
+
+The rest of render is quite straightforward; it’s simply a function of the immutable state of this component.
+
+剩下来的事情就非常简单了,填充组件的状态到UI上。
+
+Finally add the following export to the end of the file:
+
+在文件最后加入export语句:
+
+```
+module.exports = PropertyView;
+```
+
+Head back to SearchResults.js and add the require statement to the top of the file, just underneath the React require line:
+
+回到SearchResults.js 在文件头部加入 require 语句:
+
+```
+var PropertyView = require('./PropertyView');
+```
+
+Next update rowPressed() to navigate to your newly added PropertyView:
+
+修改 rowPressed() 方法,调用 PropertyView类:
+
+```
+rowPressed(propertyGuid) {
+ var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];
+
+ this.props.navigator.push({
+ title: "Property",
+ component: PropertyView,
+ passProps: {property: property}
+ });
+}
+```
+
+You know the drill: head back to the Simulator, press Cmd+R, and go all the way to the property details by running a search and tapping on a row:
+
+老规矩:返回模拟器,按下 Cmd+R, 点击搜索结果列表中的某行:
+
+
+
+Affordable living at it’s finest — that’s a fancy looking pad!
+
+能住得起的房子才是最好的房子——在Pad上看到的这个房子确实很有吸引力!
+
+Your app is almost complete; the final step is to allow users to search for nearby properties.
+
+你的App快接近完成了,最后一步是让用户能够查找距离他们最近的房子。
+##Geolocation Search
+##根据位置查找
+Within Xcode, open Info.plist and add a new key, by right clicking inside the editor and selecting Add Row. Use NSLocationWhenInUseUsageDescription as the key name and use the following value:
+
+在Xcode中打开 Info.plist ,右键,Add Row,增加一个key。 使用NSLocationWhenInUseUsageDescription 作为键名,使用
+下列字符串作为键值:
+
+> PropertyFinder would like to use your location to find nearby
+> properties
+
+Here’s how your plist file will look once you’ve added the new key:
+添加完新值后,你的Plist文件将如下图所示:
+
+
+
+This key details the prompt that you’ll present to the to the user to request access to their current location.
+
+这将在询问用户是否允许App使用他们的当前位置时,以这串文本作为提示信息。
+
+Open SearchPage.js, locate the TouchableHighlight that renders the ‘Location’ button and add the following property value:
+
+打开 SearchPage.js, 找到TouchableHighlight 中渲染 ‘Location’ 按钮的代码,加入下列属性值:
+
+```
+onPress={this.onLocationPressed.bind(this)}
+```
+
+When you tap the button, you ‘ll invoke onLocationPressed — you’re going to add that next.
+
+这样,当点击Location按钮,会调用 onLocationPressed 方法。
+
+Add the following within the body of the SearchPage class:
+
+在SearchPage 类中,增加方法:
+
+```
+onLocationPressed() {
+ navigator.geolocation.getCurrentPosition(
+ location => {
+ var search = location.coords.latitude + ',' + location.coords.longitude;
+ this.setState({ searchString: search });
+ var query = urlForQueryAndPage('centre_point', search, 1);
+ this._executeQuery(query);
+ },
+ error => {
+ this.setState({
+ message: 'There was a problem with obtaining your location: ' + error
+ });
+ });
+}
+```
+
+The current position is retrieved via navigator.geolocation; this is an interface defined by the Web API, so it should be familiar to anyone who has used location services within the browser. The React Native framework provides its own implementation of this API using the native iOS location services.
+
+navigator.geolocation可获取当前位置。这个方法定义在 Web API中,这对于曾经在浏览器中使用过位置服务的人来说并不陌生。React Native 框架用本地iOS服务重新实现了这个API。
+
+If the current position is successfully obtained, you invoke the first arrow function; this sends a query to Nestoria. If something goes wrong, you’ll display a basic error message instead.
+
+如果当前位置获取成功,我们将调用第一个箭头函数,否则调用第二个箭头函数简单显示一下错误信息。
+
+Since you’ve made a change to the plist, you’ll need to relaunch the app to see your changes. No Cmd+R this time — sorry. Stop the app in Xcode, and build and run your project.
+
+因为我们修改了plist文件,因此我们需要重新启动App,而不能仅仅是Cmd+R了。请在Xcode中终止App,然后重新编译运行App。
+
+Before you use the location-based search, you need to specify a location that is covered by the Nestoria database. From the simulator menu, select Debug\Location\Custom Location … and enter a latitude of 55.02 and a longitude of -1.42, the coordinates of a rather nice seaside town in the North of England that I like to call home!
+
+在使用基于地理定位的搜索之前,我们需要指定一个Nestoria数据库中的默认位置。在模拟器菜单中,选择Debug\Location\Custom Location … 然后输入 55.02的纬度和-1.42的经度。这是一个位于英格兰北部的非常优美的海边小镇,我的家。
+
+
+
+它远没有伦敦那么繁华——但住起来真的很便宜!:]
+##Where To Go From Here?
+##接下来做什么
+Congratulations on completing your first React Native application! You can find the complete project on GitHub with a commit for each ‘run’ step in the tutorial, very useful if you get lost along the way :]
+
+恭喜你,你的第一个React Native App终于完成了!你可以在GitHub上找到每一个”可运行的“步骤的项目源文件,如果你搞不定的时候它们会非常有用的 :]
+
+If you come from the web world, you can see how easy it is to define your interface and navigation with JavaScript and React to get a fully native UI from your code. If you work mainly on native apps, I hope you’ve gained a feel for the benefits of React Native: fast app iteration, modern JavaScript and clear style rules with CSS.
+
+如果你来自Web领域,你可能觉得在代码中用JS和React框架建立基于本地化UI的App的界面并实现导航不过是小菜一碟。但如果你主要开发的是本地App,我希望你能从中感受到React Native的优点:快速的App迭代,现代JavaScript语法的支持和清晰的CSS样式规则。
+
+Perhaps you might write your next app using this framework? Or then again, perhaps you will stick with Swift or Objective-C?
+
+在你的下一个App中,你是会使用这个框架,还是会继续顽固不化地使用Swift和O-C呢?
+
+Whichever path you take, I hope you have learned something new and interesting in this article, and can carry some of the principles forward into your next project.
+
+无论你怎么选择,我都希望你能从本文的介绍中学习到一些有趣的新东西,并把其中一些原理应用到你的下一个项目中。
+
+If you have any questions or comments on this tutorial, feel free to join the discussion in the forums below!
+
+如果你有任何问题及建议,请参与到下面的讨论中来!
+
+
+
+
+
diff --git "a/issue-15/Strings \345\234\250 Swift 2\344\270\255\345\255\227\347\254\246\344\270\262\350\256\276\350\256\241\347\232\204\350\203\214\345\220\216\346\200\235\346\203\263.md" "b/issue-15/Strings \345\234\250 Swift 2\344\270\255\345\255\227\347\254\246\344\270\262\350\256\276\350\256\241\347\232\204\350\203\214\345\220\216\346\200\235\346\203\263.md"
new file mode 100644
index 0000000..c283fb9
--- /dev/null
+++ "b/issue-15/Strings \345\234\250 Swift 2\344\270\255\345\255\227\347\254\246\344\270\262\350\256\276\350\256\241\347\232\204\350\203\214\345\220\216\346\200\235\346\203\263.md"
@@ -0,0 +1,87 @@
+* 原文链接:[Strings in Swift 2](https://developer.apple.com/swift/blog/?id=30)
+* 原文作者:[ Apple ](https://developer.apple.com/)
+* [译文出自:开发者前线 www.devtf.cn](www.devtf.cn)
+* 译者:[MrLoong](https://github.com/MrLoong)
+* 校对者:MrLoong
+* 状态:完成
+
+#Strings 在 Swift 2中字符串设计的背后思想
+
+swift提供高性能,兼容Unicode的string作为标准库的一部分,在 Swift 2.0,**String**类型不再符合**CollectionType**协议,曾经**String**是字符的集合,类似于array。现在,**String**提供了一个公开字符集视图的**charactes**属性
+
+为什么会变?虽然模拟一个字符串作为字符集合他看起来很自然,但是**String**类型的行为还是与**Array**,**Set**,**Dictionary**这种集合类型有很大的区别的。这一直存在着,但是随着Swift 2协议扩展的增加,这些差异使得有必要做出一些基本的改变。
+
+##与之不同的Sum
+
+当你添加一一个元素到集合中,你期望集合将包含这个元素。就是说,当你将一个值添加到数组中时,该数组包含该值。就像应用dictionary或set。然而,当你在一个string中添加一个组合标记字符串,字符串本身的内容被更改。
+
+考虑字符串cafe,其中有四个字符串:c,a,f和e:
+
+```
+var letters: [Character] = ["c", "a", "f", "e"]
+var string: String = String(letters)
+
+print(letters.count) // 4
+print(string) // cafe
+print(string.characters.count) // 4
+
+```
+
+如果你添加一个组合字符**U+0301**和**´**,字符串仍然有四个字符,但是现在最后一位时**é**
+
+```
+let acuteAccent: Character = "\u{0301}" // ´ COMBINING ACUTE ACCENT' (U+0301)
+
+string.append(acuteAccent)
+print(string.characters.count) // 4
+print(string.characters.last!) // é
+
+```
+在刚刚这个字符串的字符属性不包涵原始的小写字母**e**,也不包涵**´**,知识附加。相反字符串中现在包涵一个小写“e”,带有**é**:
+
+```
+string.characters.contains("e") // false
+string.characters.contains("´") // false
+string.characters.contains("é") // true
+
+```
+
+如果我们像其它集合那样处理字符串,这个结果将会让我们惊讶,添加**UIColor.redColor()**和**UIColor.greenColor()**然后设置他包涵**UIColor.yellowColor().**
+
+##根据文字内容判断
+
+字符串和集合之间的另一个区别是他们确定相等的方式
+
+* 如果两个array都有相同的数目,并且每对元素在相应的指数是相等的,两个array相等。
+* 如果两个sets都有相同的数目,并且并且每个元素包含在第一个元素也包含在第二个。
+* 如果两个dictionaries有相同的key,value,则两个dictionaries相等
+
+然而**string**基于正则等价平等。如果有相同的语言意思和外观特征,甚至如果他们由不同的Unicode组成,则表示等效。
+
+考虑下韩语协作系统,由24个字母组成,它由24个字母组成,或是Jamo,代表个别的辅音和元音。当把这些字母写出来的时候,每个音节都是字母组合起来的。字符“가”([GA])是由字母“ᄀ”([ ])和“ᅡ”[一]。在swift,字符串是相同的无论是由分解或预作字符序列:
+
+```
+let decomposed = "\u{1100}\u{1161}" // ᄀ + ᅡ
+let precomposed = "\u{AC00}" // 가
+
+decomposed == precomposed // true
+```
+同样,这种行为与任何快速的收集类型有很大的不同。这将是令人惊讶的价值🐟和🍚被认为等于🍣阵列。
+
+##取决于你的观点
+
+
+* characters is a collection of Character values, or [extended grapheme clusters](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Strings/Articles/stringsClusters.html).
+* unicodeScalars is a collection of [Unicode scalar values](http://www.unicode.org/glossary/#unicode_scalar_value).
+* utf8 is a collection of [UTF–8](http://www.unicode.org/glossary/#UTF_8) code units.
+* utf16 is a collection of [UTF–16](http://www.unicode.org/glossary/#UTF_16) code units.
+
+
+如果我们把“CAFé”前面的例子,由分解的特征【C,A,F,E ]和[´],这里有各种字符串的视图将包括:
+
+
+
+* **characters**属性段的文本扩展字形集群,这是一个近似的用户感知的字符(在这种情况下,c++,f)。因为一个字符串必须遍历它的每一个位置(每个位置称为一个代码点)为了确定字符边界.访问此属性的线性输出**O(n)**时间。当处理字符串包含人类可读的文本,高级语言环境敏感的Unicode编码算法,尤其使用localizedstandardcompare用(_:)方法和localizedlowercasestring财产,应优先采用字符处理的特点。
+* UTF8,UTF16性质为UTF 8和16表示–utf–提供代码.这些值对应于将实际的字节写入到一个文件,当翻译到和从一个特定的编码。UTF-8编码单元被许多POSIX字符串处理API.而UTF-16代码单元中使用的Cocoa & Cocoa Touch表示字符串的长度和偏移量。
+
+有关字符串和字符的快速工作的更多信息, 阅读 [The Swift Programming Language](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/StringsAndCharacters.html#//apple_ref/doc/uid/TP40014097-CH7-ID285) 和 [the Swift Standard Library Reference](https://developer.apple.com/library/prerelease/ios//documentation/Swift/Reference/Swift_String_Structure/index.html#//apple_ref/swift/struct/s:SS).
diff --git a/issue-15/readme.md b/issue-15/readme.md
new file mode 100644
index 0000000..af03db4
--- /dev/null
+++ b/issue-15/readme.md
@@ -0,0 +1,3 @@
+add issue-15
+
+
diff --git "a/issue-16/ReactNavtive\346\241\206\346\236\266\346\225\231\347\250\213.md" "b/issue-16/ReactNavtive\346\241\206\346\236\266\346\225\231\347\250\213.md"
new file mode 100644
index 0000000..a7257c3
--- /dev/null
+++ "b/issue-16/ReactNavtive\346\241\206\346\236\266\346\225\231\347\250\213.md"
@@ -0,0 +1,1314 @@
+React Navtive框架教程
+---
+
+> * 原文链接 : [React Native Tutorial: Building Apps with JavaScript](http://www.raywenderlich.com/99473/introducing-react-native-building-apps-javascript)
+* 原文作者 : [ColinEberhardt](http://www.raywenderlich.com/u/ColinEberhardt)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [kmyhy](https://github.com/kmyhy)
+* 校对者:[lastdays](https://github.com/MrLoong)
+* 状态:完成
+
+
+
+
+几个月前Facebook推出了React Native框架,允许开发者用JavaScript编写本地iOS App——今天,官方代码库的beta版本终于放出!
+
+
+
+早在几年前,开发者就已经在使用JavaScript和HTML5加上PhoneGap编译器来编写iOS App了,因此React Native框架是不是有点多余?
+
+
+
+但React Native确实是一个很了不起的东东,开发者为之欢欣鼓舞,这是因为:
+1. 通过React Native框架,你可以用JavaScript来编写和运行应用程序逻辑,而UI却可以是真正的本地代码编写的;因此,你完全不需要一个HTML5编写的UI。
+2. React框架采用了一种新颖的、激进的和高度函数式的方式来构建UI。简单说,应用程序的UI可以简单地用一个函数来表示应用程序当前的状态。
+
+
+React Native的重点是把React编程模型引进到移动App的开发中去。它的目的并不是跨平台,一次编写到处运行。它真正的目标是“一次学习多处编写”。这是一个重大的区别。本教程只涉及iOS,但一旦你学会了它的思想,你就可以快速将同样的知识应用到Android App的编写上。
+
+
+如果你过去只使用O-C或Swift编写过iOS应用程序,你可能不会因为可以用JavaScript编写iOS App而激动不已。但是,作为一个Swift开发者,上面所说的第二点原因肯定会激起你的兴趣!
+
+
+
+通过Swift,你毫无疑问已经学习到那些新颖的、函数式的编写代码的方法;以及一些和过去不同或相同的技术。但是,你构建UI的方式仍然和O-C时代没有太大的不同:仍然要离不开UIKit。
+
+
+通过一些有趣的概念,比如虚拟DOM和reconciliation,React直接将函数式编程的理念用到了UI层面。
+这个教程带你一起构建一个搜索英国房产信息的应用:
+
+
+
+
+如果你之前从未写过任何 JavaScript ,别担心;这篇教程带着你一点一点编写代码。React 使用 CSS 属性来定义样式,这些样式通常都很易于阅读和理解,但是如果你想进一步了解,可以参考 Mozilla Developer Network reference。
+要想学习更多内容,请往下看!
+
+
+##开始
+
+
+React Native 框架托管在GitHub。要获得这个框架,你可以使用git命令克隆项目到本地,或者直接下载zip包。 如果你不想使用源代码,也可以用命令行界面(CLI)创建React Native项目,本文将使用这种方式。
+
+
+
+React Native 使用了 Node.js,如果你的机器上没有安装Node.js,请先安装它。
+首先需要安装 Homebrew,安装指南请参考Homebrew网站。然后用brew命令来安装Node.js:
+
+```
+brew install node
+```
+
+
+
+然后安装 watchman(Facebook推出的文件改动监听器):
+
+```
+brew install watchman
+```
+
+
+React Native通过watchman来监视代码文件的改动并适时进行编译。这就好比Xcode,它会在每次文件被保存时对文件进行编译。
+然后用npm命令安装React Native 的CLI工具:
+
+```
+npm install -g react-native-cli
+```
+
+
+这个命令通过Node Package Manager来下载和安装CLI工具,npm是一个类似CocoPods或Carthage工具。
+定位到要创建React Native 项目的文件夹,使用CLI工具创建一个新的React Native项目:
+
+```
+react-native init PropertyFinder
+```
+
+
+这将创建一个默认的React Native项目,其中包含有能够让React Native项目编译运行的必要内容。
+在React Native项目文件夹中,有一个node_modules文件夹,它包含React Native 框架文件。此外还有一个 index.ios.js 文件,这是CLI创建的脚手架代码。最后,还有一个Xcode项目文件及一个iOS文件夹,后者会有一些iOS代码用于引导React Navtive App。
+打开Xcode项目文件,build&run。模拟器启动并显示一句问候语:
+
+
+
+
+与此同时Xcode还会打开一个终端窗口,并显示如下信息:
+> ===============================================================
+ | Running packager on port 8081.
+ | Keep this packager running while developing on any JS
+ | projects. Feel free to close this tab and run your own
+ | packager instance if you prefer.
+ |
+ | https://github.com/facebook/react-native
+ |
+ ===============================================================
+>
+> Looking for JS files in /Users/colineberhardt/Temp/TestProject
+> React packager ready.
+
+
+
+这是React Navtive Packager,它在node容器中运行。你待会就会发现它的用处。
+千万不要关闭这个窗口,让它一直运行在后面。如果你意外关闭它,可以在Xcode中先停止程序,再重新运行程序。
+
+
+>
+> 注意:
+> 在开始接触具体的代码之前(在本教程中,主要是js代码),我们将推荐 Sublime
+> Text这个文本编辑工具,因为Xcode并不适合用于编写js代码的。当然,你也可以使用 atom, brackets
+> 等其他轻量级的工具来替代。
+
+
+##你好, React Native
+
+
+在开始编写这个房产搜索App之前,我们先来创建一个简单的Hello World项目。我们将通过这个例子来演示React Native的各个组件和概念。
+用你喜欢的文本编辑器(例如Sublime Text)打开index.ios.js ,删除所有内容。然后加入以下语句:
+
+```
+'use strict';
+```
+
+这将开启严谨模式,这会改进错误的处理并禁用某些js语法特性,这将让JavaScript表现得更好。
+
+
+>
+> 注意: 关于严谨模式,读者可以参考 Jon Resig的文章:“ECMAScript 5 Strict Mode, JSON, and More”。
+
+
+然后加入这一句:
+
+```
+var React = require('react-native');
+```
+
+这将加载 react-native 模块,并将其保存为React变量。React Native 使用和Node.js 一样的 require 函数来加载模块,类似于Swift中的import语句。
+
+
+>
+> 注意:
+> 关于JavaScript 模块的概念,请参考 Addy Osmani的[这篇文章](http://addyosmani.com/writing-modular-js/)。
+
+
+
+然后加入如下语句:
+
+```
+var styles = React.StyleSheet.create({
+ text: {
+ color: 'black',
+ backgroundColor: 'white',
+ fontSize: 30,
+ margin: 80
+ }
+});
+```
+
+
+这将定义一个css样式,我们将在显示“Hello World”字符串时应用这个样式。
+在React Native 中,我们可以使用 [Cascading Style Sheets (CSS)](http://www.w3schools.com/css/) 语法来格式化UI样式。
+接下来敲入如下代码:
+
+```
+class PropertyFinderApp extends React.Component {
+ render() {
+ return React.createElement(React.Text, {style: styles.text}, "Hello World!");
+ }
+}
+```
+
+
+这里我们定义了一个JavaScript 类。JavaScript类的概念出现自ECMAScript 6。由于JavaScript是一门不断演变的语言,因此web开发者必须保持与浏览器的向下兼容。由于React Native基于JavaScriptCore,因此我们完全可以放心使用它的现代语法特性,而不需要操心与老版本浏览器兼容的问题。
+
+
+>
+> 注意:如果你是Web开发人员,我建议你使用新的JavaScript语法。有一些工具比如 Babel,可以将现代JavaScript语法转变为传统JavaScript语法,这样就能和老式浏览器进行兼容。
+
+
+
+PropertyFinderApp 类继承自 React.Componen,后者是React UI中的基础。Components包含了一些不可变属性、可变属性和一些渲染方法。当然,这个简单App中,我们就用到一个render方法。
+React Native 的Components不同于UIKit 类,它们是轻量级的对象。框架会将React Components转换为对应的本地UI对象。
+最后敲入如下代码:
+
+```
+React.AppRegistry.registerComponent('PropertyFinder', function() { return PropertyFinderApp });
+```
+
+
+AppRegistry 代表了App的入口以及根组件。保存文件,返回Xcode。确保当前Scheme为PropertyFinder ,然后在模拟器运行App。你将看到如下效果:
+
+
+
+
+看到了吧,模拟器将JavaScript代码渲染为本地UI组件,你不会看到任何浏览器的痕迹。
+你可以这样来确认一下:
+在Xcode中,选中 Debug\View Debugging\Capture View Hierarchy,查看本地视图树。你将找不到任何UIWebView实例。
+
+
+
+
+你一定会惊奇这一切是怎么发生的。在 Xcode 中打开 AppDelegate.m,找到application:didFinishLaunchingWithOptions:方法。
+在这个方法中,创建了一个RCTRootView,该对象负责加载JavaScript App并渲染相关视图。
+App一启动,RCTRootView会加载如下URL的内容:
+
+```
+http://localhost:8081/index.ios.bundle
+```
+
+
+还记得App启动时弹出的终端窗口吗?终端窗口中运行的packager和server会处理上述请求。
+你可以用Safari来打开上述URL,你将会看到一些JavaScript代码。在React Native 框架代码中你会找到“Hello World”相关代码。
+当App打开时,这些代码会被加载并执行。以我们的App来说,PropertyFinderApp组件会被加载,然后创建相应的本地UI组件。
+
+
+##你好, JSX
+
+
+前面我们用React.createElement构建了一个简单的UI ,React 会将之转换为对应的本地对象。但对于复杂UI来说(比如那些组件嵌套的UI),代码会变得非常难看。
+确保App保持运行,回到文本编辑器,修改index.ios.js中的return语句为:
+
+```
+return Hello World (Again);
+```
+
+
+这里使用了JSX语法,即JavaScript 语法扩展,它基本上是将JavaScript代码混合了HTML风格。如果你是一个web开发人员,对此你应该不会陌生。 在本文中,JSX随处可见。
+
+
+
+保存 index.ios.js回到iPhone模拟器,按下快捷键 Cmd+R,你会看到App的显示变成了 “Hello World (Again)”。
+
+
+
+重新运行React Navtive App如同刷新Web页面一样简单。
+
+
+
+因为你实际上是在和JavaScript打交道,所以只需修改并保存index.ios.js,即可让App内容得到更新,同时不中断App的运行。
+
+
+> 注意:
+> 如果你还有疑问,你可以用浏览器在看一下你的“Bundle”内容,它应该也发生了变化。
+
+
+好了,“Hello World" 的演示就到此为止;接下来我们要编写一个真正的React App了!
+
+
+##实现导航
+
+这个demo使用了标准的UIKit中的导航控制器来提供”栈式导航体验“。接下来我们就来实现这个功能。
+
+
+在 index.ios.js, 将 PropertyFinderApp 类修改为 HelloWorld:
+
+```
+class HelloWorld extends React.Component {
+```
+
+
+我们仍然要显示“Hello World”字样,但不再将它作为App的根视图。
+然后为HelloWorld加入以下代码:
+
+```
+class PropertyFinderApp extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+}
+```
+
+
+这将创建一个导航控制器,并指定了它的外观样式和初始route(相对于HelloWorld视图)。在web开发中,routing是一种技术,用于表示应用程序的导航方式,即哪个一页面(或route)对应哪一个URL。
+然后修改css样式定义,在其中增加一个container样式:
+
+```
+var styles = React.StyleSheet.create({
+ text: {
+ color: 'black',
+ backgroundColor: 'white',
+ fontSize: 30,
+ margin: 80
+ },
+ container: {
+ flex: 1
+ }
+});
+```
+
+
+flex: 1的意思稍后解释。
+回到模拟器,按 Cmd+R 查看效果:
+
+
+
+
+这个导航控制器有一个根视图,即图中显示的”Hello World“文本。非常好——我们的App已经具备了基本的导航功能。是时候显示一些”真正的“UI了!
+
+##实现查找
+
+
+新建一个 SearchPage.js 文件,保存在 index.ios.js同一目录。在这个文件中加入代码:
+
+```
+'use strict';
+
+var React = require('react-native');
+var {
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+ TouchableHighlight,
+ ActivityIndicatorIOS,
+ Image,
+ Component
+} = React;
+```
+
+
+
+这里使用了一个解构赋值(destructuring assignment),可以将多个对象属性一次性赋给多个变量。这样,在后面的代码中,我们就可以省略掉React前缀,比如用StyleSheet 来代替 React.StyleSheet。解构赋值对于数组操作来说尤其方便,请参考[well worth learning more about.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)
+然后定义如下Css样式:
+
+var styles = StyleSheet.create({
+ description: {
+ marginBottom: 20,
+ fontSize: 18,
+ textAlign: 'center',
+ color: '#656565'
+ },
+ container: {
+ padding: 30,
+ marginTop: 65,
+ alignItems: 'center'
+ }
+});
+
+
+
+这里,再次使用了标准的 CSS 属性。虽然用CSS设置样式在可视化方面比起在IB中要差一些,但总比在viewDidLoad()方法中用代码写要好一些。
+然后加入以下代码:
+
+```
+class SearchPage extends Component {
+ render() {
+ return (
+
+
+ Search for houses to buy!
+
+
+ Search by place-name, postcode or search near your location.
+
+
+ );
+ }
+}
+```
+
+
+
+在render方法中使用了大量的JSX语法来构造UI组件。通过这种方式,你可以非常容易地构造出如下组件:在一个Container View中包含了两个label。
+在源文件的最后加入这一句:
+
+```
+module.exports = SearchPage;
+```
+
+这一句将使 SearchPage 类可被其他js文件引用。
+
+
+
+然后需要修改App的导航。
+
+
+
+打开 index.ios.js 在文件头部、现有的require 语句后加入 require 语句:
+
+```
+var SearchPage = require('./SearchPage');
+```
+
+
+
+在 PropertyFinderApp 类的 render 函数中,修改 initialRoute 为:
+
+```
+component: SearchPage
+```
+
+
+这里我们可以将HelloWorld类和它对应的样式移除了,我们不再需要它。
+
+
+回到模拟器,按下 Cmd+R 查看效果:
+
+
+
+
+
+你新创建的组件SearchPage显示在屏幕中。
+
+##弹性盒子模型
+
+
+一直以来,我们都在用原始的CSS属性来设置外边距、内边距和颜色。但是,最新的CSS规范中增加了弹性盒子的概念,非常利于我们对App的UI进行布局,虽然你可能还不太熟悉它。
+
+
+
+React Native 使用了 css-layout 库,在这个库中实现了弹性盒子,而这种模型无论对iOS还是Android来说都很好理解。
+
+
+
+更幸运的是,Facebook针对许多语言单独实现了这个项目,这就引申出了许多新颖的用法,比如[在SVG中应用弹性盒子布局](http://blog.scottlogic.com/2015/02/02/svg-layout-flexbox.html)(是的,这篇文章也是我写的,为此我不得不熬到深夜)。
+
+
+在这个App中,采用了默认的垂直流式布局,即容器中的子元素按照从上到下的顺序进行布局。比如:
+
+
+
+
+这被称作主轴, 主轴可能是水平方向,也可能是垂直方向。
+
+
+每个子元素的纵向位置由它们的边距(margin)、间距(padding)和高决定。容器的alignItems属性会被设置为居中(center),这决定了子元素在交叉轴上的位置。在本例里,将导致子元素水平居中对齐。
+
+
+接下来我们添加一些文本输入框和按钮。打开SearchPage.js 在第二个 Text 元素后添加:
+
+```
+
+
+
+ Go
+
+
+
+ Location
+
+```
+
+这段代码添加了两个顶级的视图:一个文本输入框外加一个按钮,以及一个单独的按钮。它们所使用的样式待会我们再介绍。
+
+
+
+接着,在styles中增加如下样式:
+
+```
+flowRight: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ alignSelf: 'stretch'
+},
+buttonText: {
+ fontSize: 18,
+ color: 'white',
+ alignSelf: 'center'
+},
+button: {
+ height: 36,
+ flex: 1,
+ flexDirection: 'row',
+ backgroundColor: '#48BBEC',
+ borderColor: '#48BBEC',
+ borderWidth: 1,
+ borderRadius: 8,
+ marginBottom: 10,
+ alignSelf: 'stretch',
+ justifyContent: 'center'
+},
+searchInput: {
+ height: 36,
+ padding: 4,
+ marginRight: 5,
+ flex: 4,
+ fontSize: 18,
+ borderWidth: 1,
+ borderColor: '#48BBEC',
+ borderRadius: 8,
+ color: '#48BBEC'
+}
+```
+
+
+不同样式属性间以逗号分隔,这样你在container选择器后必须以一个逗号结尾。
+
+
+这些样式将被文本输入框和按钮所用。
+
+
+
+回到模拟器,按下Cmd+R ,你将看到如下效果:
+
+
+
+
+
+Go按钮和其紧随的文本框在同一行,因此我们将它们用一个容器装在一起,同时容器的flexDirection: 样式属性设置为'row' 。我们没有显式指定文本框和按钮的宽度,而是分别指定它们的flex样式属性为4和1。也就是说,它们的宽度在整个宽度(屏幕宽度)中所占的份额分别为4和1。
+
+
+
+而且,视图中的两个按钮都不是真正的按钮。对于UIKit,按钮不过是可以点击的标签而已,因此React Native开发团队能够用JavaScript以一种简单的方式构建按钮:TouchableHighlight是一种React Native组件,当它被点击时,它的前景会变得透明,从而显示其隐藏在底部的背景色。
+
+
+
+最后我们还要在视图中添加一张图片。这些图片可以在[此处](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/ReactNative-HouseImage.zip)下载。下载后解压缩zip文件。
+
+
+
+在 Xcode 打开 Images.xcassets 文件,点击加号按钮,添加一个新的image set。然后将需要用到的图片拖到image set右边窗口对应的位置。
+
+
+
+
+
+要让这些图片显示,必须停止你的 React Native App并重新启动。
+
+
+在location按钮对应的 TouchableHighlight 组件下加入:
+
+``
+
+
+然后,为图片添加适当的样式定义,记得在上一个样式之后添加一个逗号结尾:
+
+```
+image: {
+ width: 217,
+ height: 138
+}
+```
+
+
+
+由于我们将图片添加到了Images.xcasset资源包中,我们需要用require('image!house')语句获得图片在App包中的正确路径。在Xcode中,打开Images.xcassets ,你可以找到名为house的image set。
+
+
+
+回到模拟器,按下Cmd+R 查看运行效果:
+
+
+
+
+>
+> 注意: 如果图片没有显示,却看到一个““image!house” cannot be
+> found”的提示,则可以重启packager(在终端中输入npm start命令)。
+
+
+到目前为止,我们的App看上去有模有样,但它还缺少很多实际的功能。接下来的任务就是为App增加一些状态,执行一些动作。
+
+##组件状态
+
+
+每个React组件都有一个state对象,它是一个键值存储对象。在组件被渲染之前,我们可以设置组件的state对象。
+
+
+打开SearchPage.js,在 SearchPage 类的 render()方法前,加入以下代码:
+
+```
+constructor(props) {
+ super(props);
+ this.state = {
+ searchString: 'london'
+ };
+}
+```
+
+
+
+现在组件就有一个state变量了,同时我们在state中存放了一个 searchString:'london' 的键值对象。
+
+
+然后我们来使用这个state变量。在render方法中,修改TextInput元素为:
+
+```
+
+```
+
+
+
+这将改变 TextInput 的value 属性,即在TextInput中显示一个london的文本——即state变量中的searchString。这个值在我们初始化state时指定的,但如果用户修改了文本框中文本,那又怎么办?
+
+
+ 首先创建一个事件处理方法。在 SearchPage 类中增加一个方法:
+
+onSearchTextChanged(event) {
+ console.log('onSearchTextChanged');
+ this.setState({ searchString: event.nativeEvent.text });
+ console.log(this.state.searchString);
+}
+
+
+首先从事件参数event中获得text属性,然后将它保存到组件的state中,并用控制台输出一些感兴趣的内容。
+
+
+
+为了让文本内容改变时这个方法能被调用,我们需要回到TextInput的onChange事件属性中,绑定这个方法,即新加一个onChange属性,如以下代码所示:
+
+``
+
+
+
+一旦用户改变了文本框中的文本,这个函数立即就会被调用。
+
+
+>
+> 注意: bind(this) 的使用有点特殊。JavaScript 中 this
+> 关键字的含义其实和大部分语言都不相同,它就好比Swift语言中的self。bind方法的调用使得onSearchTextChanged
+> 方法中能够引用到this,并通过this引用到组件实例。更多内容请参考 MDN page on this。
+
+
+
+然后,我们在render方法顶部、return语句之前加一条Log语句:
+
+```
+console.log('SearchPage.render');
+```
+
+
+
+通过这些log语句,你应该能明白大致发生了什么事情!
+
+
+
+返回模拟器,按下Cmd+R,我们将看到文本框中一开始就有了一个london的字样,当你编辑这段文本后,控制台中的内容将显示:
+
+
+
+
+
+查看上面的截屏,log语句输出的顺序似乎有点问题:
+1. 组件初始化后调用 render() 方法
+2. 当文本被改变, onSearchTextChanged() 被调用
+3. 我们在代码中改变了组件的state 属性,因此render()方法会被调用
+4. onSearchTextChanged() 打印新的search string.
+
+
+当React 组件的状态被改变时,都会导致整个UI被重新渲染——所有组件的render方法都会被调用。这样做的目的,是为了将渲染逻辑和组件状态的改变完全进行分离。
+
+
+
+在其他所有的UI框架中,要么程序员在状态改变时自己手动刷新UI,要么使用一种绑定机制在程序状态和UI之间进行联系。就像我另一篇文章 [MVVM pattern with ReactiveCocoa](http://www.raywenderlich.com/74106/mvvm-tutorial-with-reactivecocoa-part-1)所讲。
+
+
+
+而在React中,我们不再操心状态的改变会导致那一部分UI需要刷新,因为当状态改变所有的UI都会刷新。
+
+
+
+当然,你也许会担心性能问题。
+
+
+
+难道每次状态改变时,整个UI都会被舍弃然后重新创建吗?
+这就是React真正智能的地方。每当UI要进行渲染时,它会遍历整个视图树并计算render方法,对比与当前UIKit视图是否一致,并将需要改变的地方列出一张列表,然后在此基础上刷新视图。也就是说,只有真正发生变化的东西才会被重新渲染。
+
+ReactJS将一些新奇的概念应用到了iOS App中,比如虚拟DOM(Document Object Modal,web文档可视树)和一致性。这些概念我们可以稍后再讨论,先来看下这个App接下来要做的工作。删除上面添加的Log语句。
+
+
+##开始搜索
+
+
+为了实现搜索功能,我们需要处理Go按钮点击事件,创建对应的API请求,显示网络请求的状态。
+打开SearchPage.js, 在constructor方法中修改state的初始化代码:
+
+```
+this.state = {
+ searchString: 'london',
+ isLoading: false
+};
+```
+
+
+
+isLoading 属性用于表示查询是否正在进行。
+在render方法最上面增加:
+
+```
+var spinner = this.state.isLoading ?
+ ( ) :
+ ( );
+```
+
+这里用了一个三目运算,这是一个if语句的简化形式。如果isLoading为true,显示一个网络指示器,否则显示一个空的view。
+在return语句中,在Image下增加:
+
+```
+{spinner}
+```
+
+
+在Go按钮对应的 TouchableHighlight 标签中增加如下属性:
+
+```
+onPress={this.onSearchPressed.bind(this)}
+```
+
+
+
+在 SearchPage 类中新增如下方法:
+
+```
+_executeQuery(query) {
+ console.log(query);
+ this.setState({ isLoading: true });
+}
+
+onSearchPressed() {
+ var query = urlForQueryAndPage('place_name', this.state.searchString, 1);
+ this._executeQuery(query);
+}
+```
+
+
+
+_executeQuery() 目前仅仅是在控制台中输出一些信息,同时设置isLoading属性为true,剩下的功能我们留到后面完成。
+
+
+> 注意: JavaScript 类没有访问器,因此也就没有私有的概念。因此我们会在方法名前加一个下划线,以表示该方法视同为私有方法。
+
+
+当Go按钮被点击, onSearchPressed() 即被调用。
+然后,在 SearchPage 类声明之前,声明如下实用函数:
+
+```
+function urlForQueryAndPage(key, value, pageNumber) {
+ var data = {
+ country: 'uk',
+ pretty: '1',
+ encoding: 'json',
+ listing_type: 'buy',
+ action: 'search_listings',
+ page: pageNumber
+ };
+ data[key] = value;
+
+ var querystring = Object.keys(data)
+ .map(key => key + '=' + encodeURIComponent(data[key]))
+ .join('&');
+
+ return 'http://api.nestoria.co.uk/api?' + querystring;
+};
+```
+
+
+
+这个函数不依赖SearchPage类,因此被定义为函数而不是方法。它首先将key\value参数以键值对形式放到了data集合中,然后将data集合转换成以&符分隔的“键=值”形式。=>语法是箭头函数的写法,一种创建匿名函数简洁写法,具体请参考[recent addition to the JavaScript language](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) 。
+
+
+
+回到模拟器,按下 Cmd+R 重启App,然后点击‘Go’ 按钮。你将看到网络指示器开始转动。同时控制台将输出:
+
+
+
+
+网络指示器显示,同时要请求的URL也打印出来了。拷贝并粘贴URL到Safari,查看搜索结果。你将看到一堆JSON对象。我们将用代码解析这些JSON对象。
+
+
+>
+> 注意: 这个App使用 [Nestoria API 来查找房子](http://www.nestoria.co.uk/help/api)。查找结果以JSON格式返回。官方文档中列出了所有请求的URL规范及响应格式。
+
+
+##发送请求
+
+
+打开 SearchPage.js,在初始化状态过程中增加一个message属性:
+
+```
+this.state = {
+ searchString: 'london',
+ isLoading: false,
+ message: ''
+};
+```
+
+
+
+在render方法中,在UI元素的最后加入:
+
+```
+{this.state.message}
+```
+
+
+
+这个Text用于向用户显示一些文本。
+
+
+
+在 SearchPage 类中,在 _executeQuery()方法最后加入:
+
+```
+fetch(query)
+ .then(response => response.json())
+ .then(json => this._handleResponse(json.response))
+ .catch(error =>
+ this.setState({
+ isLoading: false,
+ message: 'Something bad happened ' + error
+ }));
+```
+
+
+
+fetch 函数在 [Fetch API](https://fetch.spec.whatwg.org/)中定义,这个新的JavaScript规范被Firefox 39(Nightly版)以及Chrome 42(开发版)支持,它在XMLHttpRequest的基础上进行了极大的改进。结果是异步返回的,同时使用了 [promise](http://www.html5rocks.com/en/tutorials/es6/promises/)规范,如果response中包含有效的JSON对象则将JSON对象的response成员(另一个JSON)传到_handleResponse方法(后面实现)。
+
+
+
+然后在 SearchPage类中增加方法:
+
+```
+_handleResponse(response) {
+ this.setState({ isLoading: false , message: '' });
+ if (response.application_response_code.substr(0, 1) === '1') {
+ console.log('Properties found: ' + response.listings.length);
+ } else {
+ this.setState({ message: 'Location not recognized; please try again.'});
+ }
+}
+```
+
+
+
+如果查询结果成功返回,我们重置 isLoading 属性为false,然后打印结果集的总行数。
+
+
+>
+> 注意: Nestoria 有 不以1开头的响应码, 这些代码都非常有用。例如202 和 200表示返回一个推荐位置的列表。当完成这个实例后,你可以尝试处理这些返回码,并将列表提供给用户选择。
+
+
+
+保存,返回模拟器,按下Cmd+R ,然后搜索 ‘london’你将在控制台看到一条消息,表示搜索到20条房子信息。尝试输入一个不存在的地名,比如 ‘narnia’ 你将看到如下信息:
+
+
+
+
+
+接下来我们在伦敦或者别的什么城市搜索20座房子。
+
+
+##显示搜索结果
+
+
+新建一个文件: SearchResults.js, 编写如下代码:
+
+```
+'use strict';
+
+var React = require('react-native');
+var {
+ StyleSheet,
+ Image,
+ View,
+ TouchableHighlight,
+ ListView,
+ Text,
+ Component
+} = React;
+```
+
+
+你注意到了吗?一切都是老样子,一条requires语句和一个结构赋值。
+
+
+然后定义一个Componet子类:
+
+```
+class SearchResults extends Component {
+
+ constructor(props) {
+ super(props);
+ var dataSource = new ListView.DataSource(
+ {rowHasChanged: (r1, r2) => r1.guid !== r2.guid});
+ this.state = {
+ dataSource: dataSource.cloneWithRows(this.props.listings)
+ };
+ }
+
+ renderRow(rowData, sectionID, rowID) {
+ return (
+
+
+ {rowData.title}
+
+
+ );
+ }
+
+ render() {
+ return (
+
+ );
+ }
+
+}
+```
+
+h
+上述代码中使用了一个专门的组件——ListView ——该组件非常像ITableView。通过ListView.DataSource, 我们可以向ListView提供数据。renderRow函数则用于为每个行提供UI。
+
+
+
+在构建数据源的时候,我们使用箭头函数对不同的行进行识别。这个函数在ListView进行“一致化”的时候被调用,以便判断列表中的数据是否被改变。在本例中,Nestoria API有一个guid属性,刚好可以用来作为判断的标准。
+
+
+
+最后,加入一条模块输出语句:
+
+```
+module.exports = SearchResults;
+```
+
+
+
+在SearchPage.js 头部,require 下方加入:
+
+```
+var SearchResults = require('./SearchResults');
+```
+
+
+
+这样我们就可以 SearchPage 类中使用SearchResults类了。
+在_handleResponse 方法,将console.log 一句替换为:
+
+```
+this.props.navigator.push({
+ title: 'Results',
+ component: SearchResults,
+ passProps: {listings: response.listings}
+});
+```
+
+
+
+上述代码将导航至SearchResults 页面,并将请求到的列表数据传递给它。Push方法可以将页面添加到导航控制器的ViewController堆栈中,同时你的导航栏上将出现一个Back按钮,点击它可以返回到上一页面。
+
+
+
+回到模拟器, 按下Cmd+R ,进行一个查找动作。你将看到搜索结果如下:
+
+
+
+好了,房子清单已经列出来了,不过列表有一点丑陋。接下来我们会让它变得漂亮一点。
+
+##表格样式
+
+
+现在,React Native的代码对我们来说已经不陌生了,接下来我们的教程可以稍微加快一点节奏了。
+
+在 SearchResults.js文件的解构赋值语句之后,添加样式定义:
+
+```
+var styles = StyleSheet.create({
+ thumb: {
+ width: 80,
+ height: 80,
+ marginRight: 10
+ },
+ textContainer: {
+ flex: 1
+ },
+ separator: {
+ height: 1,
+ backgroundColor: '#dddddd'
+ },
+ price: {
+ fontSize: 25,
+ fontWeight: 'bold',
+ color: '#48BBEC'
+ },
+ title: {
+ fontSize: 20,
+ color: '#656565'
+ },
+ rowContainer: {
+ flexDirection: 'row',
+ padding: 10
+ }
+});
+```
+
+
+
+这些代码中的样式将在渲染单元格时用到。
+
+
+
+修改renderRow() 方法如下:
+
+```
+renderRow(rowData, sectionID, rowID) {
+ var price = rowData.price_formatted.split(' ')[0];
+
+ return (
+ this.rowPressed(rowData.guid)}
+ underlayColor='#dddddd'>
+
+
+
+
+ £{price}
+ {rowData.title}
+
+
+
+
+
+ );
+}
+```
+
+
+
+其中价格将以‘300,000 GBP’的格式显示,记得将GBP 后缀删除。上述代码用你已经很熟悉的方式来渲染单元格UI。缩略图以URL方式提供,React Native 自动将其解码(主线程中)。
+
+
+在TouchableHightlight组件的onPress属性中再次使用了箭头函数,并将该行数据的guid作为传递的参数。
+
+
+
+最后一个方法,用于处理点击事件
+
+```
+rowPressed(propertyGuid) {
+ var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];
+}
+```
+
+
+
+这里,当用户点击某行时,通过guid去房产列表中找到对应的房屋信息。
+回到模拟器,按下 Cmd+R ,观察运行结果:
+
+
+
+这下看起来好多了——只不过,那些住在London的人居然住得起这么贵房子?真是令人难以置信!
+
+
+
+接下来,我们就来实现App的最后一个界面了。
+
+##查看房屋详情
+
+
+
+新建一个 PropertyView.js 文件到项目中,编辑如下内容:
+
+```
+'use strict';
+
+var React = require('react-native');
+var {
+ StyleSheet,
+ Image,
+ View,
+ Text,
+ Component
+} = React;
+```
+
+
+确保进行到这一步的时候,你还没有睡着!:]
+
+
+
+继续添加如下样式:
+
+```
+var styles = StyleSheet.create({
+ container: {
+ marginTop: 65
+ },
+ heading: {
+ backgroundColor: '#F8F8F8',
+ },
+ separator: {
+ height: 1,
+ backgroundColor: '#DDDDDD'
+ },
+ image: {
+ width: 400,
+ height: 300
+ },
+ price: {
+ fontSize: 25,
+ fontWeight: 'bold',
+ margin: 5,
+ color: '#48BBEC'
+ },
+ title: {
+ fontSize: 20,
+ margin: 5,
+ color: '#656565'
+ },
+ description: {
+ fontSize: 18,
+ margin: 5,
+ color: '#656565'
+ }
+});
+```
+
+
+
+然后将组件加入视图:
+
+```
+class PropertyView extends Component {
+
+ render() {
+ var property = this.props.property;
+ var stats = property.bedroom_number + ' bed ' + property.property_type;
+ if (property.bathroom_number) {
+ stats += ', ' + property.bathroom_number + ' ' + (property.bathroom_number > 1
+ ? 'bathrooms' : 'bathroom');
+ }
+
+ var price = property.price_formatted.split(' ')[0];
+
+ return (
+
+
+
+ £{price}
+ {property.title}
+
+
+ {stats}
+ {property.summary}
+
+ );
+ }
+}
+```
+
+
+
+render() 方法的第一步,是封装数据。因为从API获得的数据经常不太规范而且某些字段不全。代码采用简单手段让数据变得更便于展示一些。
+
+剩下来的事情就非常简单了,填充组件的状态到UI上。
+
+
+在文件最后加入export语句:
+
+```
+module.exports = PropertyView;
+```
+
+
+
+回到SearchResults.js 在文件头部加入 require 语句:
+
+```
+var PropertyView = require('./PropertyView');
+```
+
+
+
+修改 rowPressed() 方法,调用 PropertyView类:
+
+```
+rowPressed(propertyGuid) {
+ var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];
+
+ this.props.navigator.push({
+ title: "Property",
+ component: PropertyView,
+ passProps: {property: property}
+ });
+}
+```
+
+
+
+老规矩:返回模拟器,按下 Cmd+R, 点击搜索结果列表中的某行:
+
+
+
+
+
+能住得起的房子才是最好的房子——在Pad上看到的这个房子确实很有吸引力!
+
+
+
+你的App快接近完成了,最后一步是让用户能够查找距离他们最近的房子。
+
+##根据位置查找
+
+
+在Xcode中打开 Info.plist ,右键,Add Row,增加一个key。 使用NSLocationWhenInUseUsageDescription 作为键名,使用
+下列字符串作为键值:
+
+> PropertyFinder would like to use your location to find nearby
+> properties
+
+
+添加完新值后,你的Plist文件将如下图所示:
+
+
+
+
+
+这将在询问用户是否允许App使用他们的当前位置时,以这串文本作为提示信息。
+
+
+
+打开 SearchPage.js, 找到TouchableHighlight 中渲染 ‘Location’ 按钮的代码,加入下列属性值:
+
+```
+onPress={this.onLocationPressed.bind(this)}
+```
+
+
+
+这样,当点击Location按钮,会调用 onLocationPressed 方法。
+
+
+
+在SearchPage 类中,增加方法:
+
+```
+onLocationPressed() {
+ navigator.geolocation.getCurrentPosition(
+ location => {
+ var search = location.coords.latitude + ',' + location.coords.longitude;
+ this.setState({ searchString: search });
+ var query = urlForQueryAndPage('centre_point', search, 1);
+ this._executeQuery(query);
+ },
+ error => {
+ this.setState({
+ message: 'There was a problem with obtaining your location: ' + error
+ });
+ });
+}
+```
+
+
+
+navigator.geolocation可获取当前位置。这个方法定义在 Web API中,这对于曾经在浏览器中使用过位置服务的人来说并不陌生。React Native 框架用本地iOS服务重新实现了这个API。
+
+
+
+如果当前位置获取成功,我们将调用第一个箭头函数,否则调用第二个箭头函数简单显示一下错误信息。
+
+
+
+因为我们修改了plist文件,因此我们需要重新启动App,而不能仅仅是Cmd+R了。请在Xcode中终止App,然后重新编译运行App。
+
+
+
+在使用基于地理定位的搜索之前,我们需要指定一个Nestoria数据库中的默认位置。在模拟器菜单中,选择Debug\Location\Custom Location … 然后输入 55.02的纬度和-1.42的经度。这是一个位于英格兰北部的非常优美的海边小镇,我的家。
+
+
+
+它远没有伦敦那么繁华——但住起来真的很便宜!:]
+
+##接下来做什么
+
+
+恭喜你,你的第一个React Native App终于完成了!你可以在GitHub上找到每一个”可运行的“步骤的项目源文件,如果你搞不定的时候它们会非常有用的 :]
+
+
+
+如果你来自Web领域,你可能觉得在代码中用JS和React框架建立基于本地化UI的App的界面并实现导航不过是小菜一碟。但如果你主要开发的是本地App,我希望你能从中感受到React Native的优点:快速的App迭代,现代JavaScript语法的支持和清晰的CSS样式规则。
+
+
+
+在你的下一个App中,你是会使用这个框架,还是会继续顽固不化地使用Swift和O-C呢?
+
+
+
+无论你怎么选择,我都希望你能从本文的介绍中学习到一些有趣的新东西,并把其中一些原理应用到你的下一个项目中。
+
+
+如果你有任何问题及建议,请参与到下面的讨论中来!
+
+
+
+
+
diff --git a/issue-16/readme.md b/issue-16/readme.md
new file mode 100644
index 0000000..abd3845
--- /dev/null
+++ b/issue-16/readme.md
@@ -0,0 +1,3 @@
+add issue-16
+
+
diff --git "a/issue-16/\344\273\213\347\273\215iOS\350\256\276\350\256\241\346\250\241\345\274\2171:2(Swift).md" "b/issue-16/\344\273\213\347\273\215iOS\350\256\276\350\256\241\346\250\241\345\274\2171:2(Swift).md"
new file mode 100644
index 0000000..0ae4279
--- /dev/null
+++ "b/issue-16/\344\273\213\347\273\215iOS\350\256\276\350\256\241\346\250\241\345\274\2171:2(Swift).md"
@@ -0,0 +1,836 @@
+介绍iOS设计模式 (1/2)
+---
+
+>* 原文链接 : [Introducing iOS Design Patterns in Swift – Part 1/2](http://www.raywenderlich.com/86477/introducing-ios-design-patterns-in-swift-part-1)
+* 译者 : [Alier Hu](https://github.com/alier1226)
+* 校对者:[lastdays](https://github.com/MrLoong)
+* 状态:完成
+
+
+
+**2015/04/22更新:** 最新更新到Xcode6.3和Swfit 1.2。
+
+
+
+
+**更新日志:** 这个教程由Vicent Ngo最新更新到iOS 8和Swift。原教程由教程组组员Eli Ganem发表。
+
+
+
+
+**iOS设计模式** - 你很可能听说过这个词,但是你真的知道它什么意思么?虽然大多数开发者都同意设计模式非常重要,但于此同时,并没有很多文章写这个内容,而且我们作为开发者在写代码时也有时不在意设计模式。
+
+
+
+设计模式是在软件设计中常见问题的重复使用的解决方法。它们是可以让你的代码清晰易懂可重复使用度高的模版。它们也可以帮你完成一些松散的代码,你不用很麻烦就可以更改代码或者替换里面的组成部分。
+
+
+
+如果你并不熟悉设计模式,那我有一个好消息给你!首先, 由于Cocoa的建立方式,你现在就可以使用很多iOS的设计模式了!其次,这个教程会让你快速学所有的主流(以及不那么主流)的被广泛应用在Cocoa里的iOS设计模式。
+
+
+
+教程分为两部分,你将会做一个音乐库的app,它可以显示你的专辑以及它们相关的信息。
+
+
+
+在开发这个app的过程中,你将会了解大部分常见的Cocoa设计模式:
+
+
+* 创建型:单例模式(Singleton)
+
+* 结构型:MVC,修饰模式(Decorator),适配器模式(Adapter),外观模式(Facade)
+
+* 行为型:观察者模式(Observer),备忘录模式(Memento)
+
+
+
+别以为这篇文章是关于理论的;你将会在你的音乐app中知道怎么运用大多数这些设计模式。在本教程最后你的app最终会是这样:
+
+
+
+
+
+
+
+让我们开始吧!
+
+
+
+## 开始
+
+
+
+下载并解压 [起始项目](http://cdn1.raywenderlich.com/wp-content/uploads/2014/11/BlueLibrarySwift-Starter.zip), 在xCode里打开 `BlueLibrarySwift.xcodeproj` .
+
+
+
+在这个项目里,有三件事需要注意:
+
+
+
+1. `ViewController` 有两个`IBOutlet`连接了表格(table view)和storyboard的工具栏
+
+2. 为了让你更方便,storyboard里已经有三个设置好限制的部分了。最上面的部分是显示专辑封面的,在它下面会有一个表格显示对应专辑封面的信息。最后那个工具栏有两个按钮,一个可以撤销操作,另一个可以删除你所选择的专辑。Storyboard如下所示:
+
+
+3.一个还没写的HTTP Client类 (`HTTPClient `)需要你之后完成。
+
+
+
+**注意:** 你知道当你新建一个Xcode项目时,你的代码已经有设计模式了么?MVC,代理,原型,单例 - 你可以免费得到它们所有!
+
+
+在你深入第一种设计模式之前,你需要先新建两个类来存以及展示专辑数据。
+
+
+去到`“File\New\File…”`(或者直接按`Command+N`)。选择`iOS > Cocoa Touch Class`然后点击`Next`。将类的名字设置成`Album`,子类设置成`NSObject`。最后选择`Swift`语言。点击`Next` 和`Create`。
+
+
+打开`Album.swift`并且在类定义里加上以下这些属性。
+
+```
+var title : String!
+var artist : String!
+var genre : String!
+var coverUrl : String!
+var year : String!
+```
+
+
+这样你就建立了五个属性。`Album`类会记录标题,艺术家,类别,专辑封面,以及专辑年份。
+接下来在属性下面加上对象初始化方法。
+
+```
+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和年份。
+
+
+
+接下来加上以下这段代码:
+
+```
+override var description: String {
+ return "title: \(title)" +
+ "artist: \(artist)" +
+ "genre: \(genre)" +
+ "coverUrl: \(coverUrl)" +
+ "year: \(year)"
+}
+```
+
+
+`description()`方法返回一个代表专辑属性的字符串。
+
+
+
+再次去`File\New\File…`,选择`Cocoa Touch Class`并且点击`Next`。将这个类的名称改为`AlbumView`,不过这次将它的子类设置成`UIView `。确认语言为`Swift`然后点击`Next`和`Create`。
+
+
+打开`AlbumView.swift`,然后在这个类的定义里加上以下属性:
+
+```
+private var coverImage: UIImageView!
+private var indicator: UIActivityIndicatorView!
+```
+
+
+`coverImage`代表了专辑封面图片。第二个属性是指示物,表明专辑封面正在下载这么一个活动。
+
+
+
+这些属性被设置成private因为没有除了`AlbumView`的类需要知道这些属性的存在;它们只需要在这个类的功能里面被用到。当你要建立一个库给别的开发者用,你要把隐藏的状态信息隐藏,这个惯例是非常重要的。
+
+
+
+接下来,给这个类加上初始化方法:
+
+```
+required init(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ commonInit()
+}
+
+init(frame: CGRect, albumCover: String) {
+ super.init(frame: frame)
+ commonInit()
+}
+
+func commonInit() {
+ backgroundColor = UIColor.blackColor()
+ coverImage = UIImageView(frame: CGRect(x: 5, y: 5, width: frame.size.width - 10, height: frame.size.height - 10))
+ addSubview(coverImage)
+ indicator = UIActivityIndicatorView()
+ indicator.center = center
+ indicator.activityIndicatorViewStyle = .WhiteLarge
+ indicator.startAnimating()
+ addSubview(indicator)
+}
+
+The `NSCoder` initializer is required because UIView conforms to NSCoding.
+
+`NSCoder`初始化方法是必须的,因为UIView遵守NSCoding.
+
+
+
+`commonInit`是一个这两个初始化里都被用到的帮助方法:你将在这个项目的剩余部分也用到它,你将会给专辑界面设置一些好的默认值。把背景设置为黑,建立一个5像素边框的图片界面,然后建立并添加到活动指示器。
+
+
+
+最后,添加一下方法:
+
+```
+func highlightAlbum(#didHighlightView: Bool) {
+ if didHighlightView == true {
+ backgroundColor = UIColor.whiteColor()
+ } else {
+ backgroundColor = UIColor.blackColor()
+ }
+}
+```
+
+
+
+如果专辑被高亮,它将设置专辑的背景色为白,如果没有被高亮,则被设置成黑。
+
+
+建造你的项目(`Command+B`)来确认所有东西都正常。都好了?那么就准备好来做你第一个设计模式吧!:]
+
+
+MVC - 设计模式之王
+
+
+
+
+
+MVC是Cocoa中的一个组成部件,它无可厚非是所有设计模式中最常用的。它将对象按照你程序的角色分类,并且根据角色清晰地分离代码。
+
+
+这三个角色为:
+
+
+Model: 这个对象存着你程序的数据并且定义怎么控制它。举个例子,在你的程序中,专辑这个类就是Model。
+
+
+View: 这个对象负责Model的视觉描述以及用户可以交互的控制;总而言之,所有UIVIew有关的对象。在你的程序里,你的AlbumVIew类就代表着View。
+
+
+Controller: Controller 是一个“调解人”,协调所有的工作。它从model里面得到数据,然后在view里面展示它们,监听事件并且如果需要会控制数据。你能猜到哪个类是你的controller么?没错,是ViewController.
+
+
+在你的程序中执行一个好的设计模式意味着每个对象都可以被分到其中的一个组里。
+
+
+
+通过Controller的View 和Model之间的交流可由下图表示:
+
+
+
+
+如果数据有变化Model会通知Controller,然后Controller会在View中更新显示数据。View可以通知Controller用户的行为,然后Controller会根据需要更新Model或者得到需要的数据。
+
+
+你可能会疑惑为什么你不能直接抛弃Controll二,然后在一个类里面运行View和Model,这样看起来会更简单。这都因为要分离代码以及重复利用性。理想中,View应该完全脱离Model。如果View完全不依赖一个特定的Model,那么它可以在另一个model中再次被利用来展示其他的数据。
+
+
+举例说,如果以后你想在你的库中添加电影或者书籍,你依然可以用`AlbumView`来展示你的电影和书籍。更多的是,如果你想新创建一个有关专辑的项目,你还可以重复利用`Album`类,因为它并不依赖于任何View。这就是MVC的厉害之处!
+
+
+
+## 怎么用MVC模式
+
+
+
+首先,你需要确保你的项目中的每一个类必须是Controller,Model或者View;不要在一个类里混杂了两个角色的功能。到现在你建立了`Album`类和`AlbumView`类,做得好!
+
+
+
+其次,为了确保你遵守了这种工作的方法,你要创建三个组(group)来放你的代码,每一个种类有一个组。
+
+
+选择`File\New\Group` (或者直接按`Command+Option+N`)并且给你的组命名为`Model`。重复这个过程来创建`View` 和 `Controller` 组。
+
+
+现在把`Album.swift` 拖到Model组。把`AlbumView.swift`拖到View组,最后把`ViewController.swift` 拖到Controller组。
+
+
+现在项目应该长这样:
+
+
+
+
+
+你的项目已经看起来很好了,所有文件都没有到处乱放。很明显你也可以有别的组合类,但是这个程序的核心都在这三个种类里。
+现在你项目的组成部分已经很有序了,你需要从某个地方得到专辑的数据。你需要创建一个API类来管理数据,它将在你整个代码中都被使用。这也意味着我们有机会来聊下下一个需要你学习的设计模式-单例。
+
+##The Singleton Pattern
+
+
+单例设计模式确保了制定类里只有一个实例并且有一个全局的访问指向这个实例。当它第一次被用的时候,一般是以惰性加载的方式来创建单例。
+
+
+```
+注:苹果用了很多这个方法。比如:NSUserDefaults.standardUserDefaults(), UIApplication.sharedApplication(), UIScreen.mainScreen(), NSFileManager.defaultManager() 他们都是返回一个单例的对象。
+```
+
+
+
+你很可能能想为什么需要关心一个类中是否不止一个实例。代码和内存都很容易获得啊,不是吗?
+
+
+
+在一些情况下,类里只有一个实例是比较符合逻辑的。比如说, 你的程序里只有一个实例,设备有一个主要的屏幕,所以你只需一个实例。或者处理全局配置类:实现一个线程安全的访问指向到单个分享的资源,比如配置文件,比起很多类可能在同一时间改这个配置文件,会更简单些。
+
+
+
+## 单例模式怎么用
+
+
+看下下图:
+
+
+
+
+
+上面的图显示了一个Logger类有一个属性(一个单例)和两个方法:`sharedInstance` 和 `init`。
+
+
+
+当用户第一次请求sharedInstance,单例属性还没有被初始化,所以你需要先创建一个类的实例,然后返回它的参考(reference)。
+
+
+
+下一次你调用`sharedInstance`,`instance`(实例)就将立即返回不再需要其他的初始化。这个逻辑确保了每次只有一个实例存在。
+
+
+你将通过创建一个实例类来管理全部数据来实现这个模式。
+
+
+
+你讲发现,在这个项目中有一个组叫`API`;这就是你将把所有给你程序提供服务的类放置的地方。右键这个组然后选择`New File`来在这个组里创建一个新的文件。选`iOS > Cocoa Touch Class` 然后点击`Next`。将这个类的名字设置为`LibraryAPI`, 子类为 `NSObject`。最后选择 `Swift`作为编程语言,点击`Next` 然后点 `Create`。
+
+
+
+现在去`LibraryAPI.swift` 然后把这段代码添加到类定义里。
+
+```
+//1
+class var sharedInstance: LibraryAPI {
+ //2
+ struct Singleton {
+ //3
+ static let instance = LibraryAPI()
+ }
+ //4
+ return Singleton.instance
+}
+```
+
+
+
+这个很短的方法里干了很多事
+
+
+
+它首先创建了一个类变量为计算类型属性。这个类变量,就是OC里的类方法,是一个不需要实例化`LibarayAPI`类就可以调用的东西。想要更多关于类型属性的信息,请去参考Apple Swift关于属性的文档。
+
+
+
+在这个类变量里嵌入一个结构称为`Singleton`
+
+
+
+`Singleton`包含了一个叫instance的静态常量。把一个属性声明为`static`(静态)意味着这个属性只出现一次。同时也要注意静态属性在Swift中为绝对惰性,当它被创造以后,它不会再一次被创造。这也是单例设计模式必须的,当它被初始化过后,这个初始化方法永远不会再被调用。
+
+
+返回计算类型属性
+
+```
+**Note**: To learn more about different ways to create a singleton in Swift refer to this: [Github page](https://github.com/hpique/SwiftSingleton)
+```
+
+```
+**注意`**: 想要了解在swift中更多创建一个单例的方法,请去[GitHub网页](https://github.com/hpique/SwiftSingleton)
+```
+
+
+
+你现在又了一个单例对象作为管理专辑的开始。让我更进一步然后创建一个类来处理你的库里数据的永久性。
+
+
+现在在`API`组里创建一个新文件。选`iOS > Cocoa Touch class`然后点击`Next`。把这个类的名字改成PersistencyManager,然后把它弄成`NSObject`的子类。
+
+
+打开`PersistencyManager.swift`然后将下面的代码放到大括号里。
+
+```
+private var albums = [Album]()
+```
+
+
+
+现在你声明了一个私有属性来放置专辑的数据。这个数组是可变的,你可以很方便的添加或者删除专辑。
+
+
+
+现在讲下面的初始化方法添加到类:
+
+```
+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]
+}
+
+```
+
+
+在这个初始化方法里,你往数组里添加了五个样本专辑。如果你不喜欢上面的专辑,你也可以自己换成你喜欢的音乐:]
+
+
+
+现在把下面的方法添加到类:
+
+```
+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)
+}
+```
+
+.
+
+这些方法让你可以得到,添加以及删除专辑。
+
+
+
+运行你的项目来确保所有东西都能正常编译。
+
+
+
+到现在这个时候,你可以会想什么时候为什么`PersistencyManager`会介入因为它并不是一个单例。你将在下一个章节中看到`LibraryAPI` 与 `PersistencyManager`之间的关系,下一个章节是关于外观模式的。
+
+
+#外观设计模式
+
+
+
+
+外观设计模式给复杂的子系统提供一个借口。你只会有一个整合的API暴露给你,而不是一堆类和它们各自的API。
+
+
+下面这个图解释了这个概念:
+
+
+
+
+API的用户并不完全知道API有多复杂。这个模式是当要用到大量类时,特别是那些非常复杂难懂难用的,非常理想好用的。
+
+
+
+外观模式将那些从接口处用系统的代码和你要隐藏的执行类的代码分离开;它并且减少你子系统内部对外面代码的依赖性。如果外观模式下的代码很有可能变,那么这个就非常有用,因为外观类可以保持同样的CPI当其它东西在幕后变化。
+
+
+
+举个例子,如果你有一天想要换你的后端服务,你并不需要改变你的代码因为它的API并不会改变。
+
+
+##怎么用外观模式
+
+
+现在你有`PersistencyManager `来本地储存专辑数据,和`HTTPClient` 来处理远端通讯。在你的项目中,其它的类不应该知道这个逻辑,因为它们会被`LibraryAPI`隐藏在外观后。
+
+
+
+为了实现这个模式,只有`LibraryAPI`拥有`PersistencyManager` 和 `HTTPClient`。并且,`LibraryAPI`会暴露给一个简单的API使它可以获得这些服务。
+
+
+
+这个模式长这样:
+
+
+
+
+
+`LibraryAPI`会暴露给其他的代码,但是会对app中其它部分隐藏`HTTPClient` 和`PersistencyManager`的复杂性。
+
+打开 `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()
+}
+```
+
+
+HTTP Client 并不真正与真实的服务器工作而且只在这里需要声明外观模式的运用,所以`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:)`。这个类首先本地更新数据,如果有网络连接,它再更新远程服务器。这才是外观模式真正的厉害之处;当你的系统之外的什么类添加了一个专辑,它并会不知道-也不需要知道-其底下的复杂性。
+
+
+
+```
+**注:** 当给你的子系统的类设计一个外观模式时,记住什么都不能阻止客户直接进入那些被“隐藏”的类。不要吝啬地加上防守的代码,也不要假设所有客户都会像外观模式那样用你的类。
+```
+
+
+编译运行你的app。你应该会看到两个空的界面和一个工具栏。上面的界面会用来展示你的专辑封面,下面那个界面将用来展示专辑相关信息的表格。
+
+
+
+
+
+你现在需要一些可以展示专辑数据到屏幕上的东西-是一个好机会用你下一个设计模式了:修饰模式。
+
+
+#修饰设计模式
+
+
+修饰模式动态添加行为和责任到一个对象而不改变它的代码。这是一个用来创建子类的另一种方法:当你通过把一个类包装到另一个对象来更改类的时候。在swift中有两个非常常见的方法来实施这个模式:`Extensions` 和 `Delegation`
+
+
+##扩建
+
+
+
+添加一个扩建是非常有用的技巧,它使你可以给现有类、结构、枚举类型添加新功能而不用让它们成为子类。这个的绝妙之处在于你可以扩展你的那些你没有权限的代码,并且增加它们的功能性。这个意味着你可以Cocoa类里,比如UIView和UIImage里加入你自己的方法。
+
+
+
+比如说,在编译时添加的新方法可以想扩展类里的普通方法一样被执行。这个与典型的修饰模式还是有一点点的区别,因为一个扩建不不拥有它扩建的那个类的实例。
+
+
+## 怎么用扩建
+
+
+
+想象一下当你有一个专辑对象,然后你想在表单里展示这个。
+
+
+
+
+
+专辑标题从哪来呢?`Album`是一个在model里的对象,所以它不关心你怎么展示数据。你只需要有一些外部的代码来添加功能到Album类而不用直接更改这个类。
+
+
+你首先创建一个扩展Album类的扩建,它会定义一个新方法,这个方法会返回一个方便与`UITableView`用的数据结构。
+
+
+
+这个数据结构看上去像下面这样:
+
+
+
+
+
+想要给Album添加一个扩展,去到`File\New\File… `然后选择 `iOS > Source > Swift File` 模版, 然后点击 `Next`. 键入 `AlbumExtensions` 然后点击 `Create`
+
+Go to `AlbumExtensions.swift` and add the following extension:
+去`AlbumExtensions.swift`然后添加下面的扩展:
+
+```
+extension Album {
+ func ae_tableRepresentation() -> (titles:[String], values:[String]) {
+ return (["Artist", "Album", "Genre", "Year"], [artist, title, genre, year])
+ }
+}
+```
+
+
+
+注意在这个方法名字的开头有一个`ae_`,这是`AlbumExtension`的缩写。这样的规则会避免在原始执行中的方法有冲突(包括那些你可以不知道的私有的方法)。
+
+
+
+```
+注意:类当然可以是父类的方法无效,不过扩展的话是不可以的。扩展里方法和属性是不能与原先的类里的方法和属性有同样的名字。
+```
+
+
+
+想一下这个模式有多么的厉害:
+
+
+* 你直接用`Album`里的属性。
+* 你已经添加到了`Album`类里了,但是你还没有让它变为子类。如果你想要让它变为`Album` 子类,你依旧可以这么做。
+* 这步简单的添加可以让你返回一个类似UITableView的专辑描述,而不用更改Album里的代码。
+
+
+
+##代理
+
+
+.
+另外一个修饰模式,代理,是一个对象代理或者与另一个对象合作的机制。比如说,当你用`UITableView`时,其中一个你必须用到的方法是`tableView(_:numberOfRowsInSection:)`。
+
+
+
+你不能指望`UITableView`来指导每个部分里有几行,因为这具体到了应用。所以,`UITableView`的代理就有了计算行数的任务。这使得`UITableView`类独立于它要展示的数据。
+
+
+这里是一个大概的解释当你创建一个`UITableView`时发生了什么。
+
+
+
+
+
+这个`UITableView`对象要展示一个表单。不过,最终它会需要一些它没有的信息。然后,它转向他的代理们然后发出需要其他信息的请求。在Objective-C实施代理模式里,一个类可以通过协议声明非强制的和必须的方法。在这个教程的后面一点你会学到协议的。
+
+
+
+只是加个子类然后覆盖必须的方法看上去很简单一下,不过要注意,对一个子类只能有一个父类,如果你想要这个对象成为2个及以上对象的代理,你将不能用子类的方法。
+
+
+
+```
+注意:这是一个非常重要的模式。Apple在大多UIKit类里用这个方法:UITableView, UITextView, UITextField, UIWebView, UIAlert, UIActionSheet, UICollectionView, UIPickerView, UIGestureRecognizer, UIScrollView. 这个单子持续更长。
+```
+
+
+##怎么使用代理模式
+
+
+
+打开`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那里得到所有专辑的列表。记住,我们的计划使用LibraryAPI的外观模式而不是直接用 `PersistencyManager`
+3. 现在这步就是要建立`UITableView`了。你要声明view controller是`UITableView`代理/数据的源头;所以,所有`UITableView`需要的信息都会由view controller提供。注意你其实可以在storyboard设置代理和数据源,如果你的表单是在那里创建的。
+
+Now, add the following method to `ViewController.swift`:
+
+现在,将下面的方法添加到`ViewController.swift`:
+
+```
+func showDataForAlbum(albumIndex: Int) {
+ // defensive code: make sure the requested index is lower than the amount of albums
+ if (albumIndex < allAlbums.count && albumIndex > -1) {
+ //fetch the album
+ let album = allAlbums[albumIndex]
+ // save the albums data to present it later in the tableview
+ currentAlbumData = album.ae_tableRepresentation()
+ } else {
+ currentAlbumData = nil
+ }
+ // we have the data we need, let's refresh our tableview
+ dataTable!.reloadData()
+}
+```
+
+
+`showDataForAlbum()`从专辑数组里得到需要的专辑数据。当你想要展现新的数据时,你只需要调用`reloadData`。这个会使`UITableView` 问他的代理,比如这个表单里需要几个部分,每个部分里有多少行,每个单格应该长什么样之类的问题。
+
+
+把下面这行添加到`viewDidLoad`的最后。
+
+```
+self.showDataForAlbum(currentAlbumIndex)
+```
+
+
+这个会在打开app的时候载入当前的专辑。而且因为`currentAlbumIndex`之前被设置成0,这会展示列表里的第一个专辑
+
+
+现在是执行数据源协议的时候了!你先添加由类执行的在类声明那行的协议列表。或者,为了使东西看起来干净点你可以把他们添加成扩展,你应该已经对扩展很熟悉了。
+
+
+
+将下面的扩展添加到文件的尾部。你要确保把这些添加在类定义的大括号之前!
+
+```
+extension ViewController: UITableViewDataSource {
+}
+
+extension ViewController: UITableViewDelegate {
+}
+```
+
+
+
+这就是怎么使你的代理凑手协议 - 把它想象成这是代理做出的满足方法合同的承诺。这样,你表明了`ViewController`会遵守 `UITableViewDataSource` 和 `UITableViewDelegate`的下移。这样` UITableView`可以百分百确保这些需要的方法由代理执行了。
+
+
+将下面的代码添加到`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]
+ cell.detailTextLabel!.text = albumData.values[indexPath.row]
+ }
+ return cell
+}
+```
+
+
+
+`tableView(_:numberOfRowsInSection:)` 返回表单里的行数,而且这与数据结构里的标题数量一致。
+
+
+
+`tableView(_:cellForRowAtIndexPath:) `创建并且返回了单元的标题和值。
+
+
+
+```
+注:你实际需要添加方法到主类的声明里或者到扩展:编译器不会在乎数据源方法到底是不是在UITableViewDataSource扩展里。当然对于别人读代码来说,这样的结构相当帮助可读性。
+```
+
+
+
+编译运行你的程序。你的app应该启动而且展现下面的屏幕:
+
+
+
+
+表单数据源成功!
+
+
+
+##现在能去哪里?
+
+
+
+现在东西看上去不错!你的MVC模式在它该在的地方,而且你也看见过了单例,外观,修饰模式运作时的样子。你也可以看他们在由Apple写的Cocoa里样子,也能看这些模式如何能被运用到妮子的代码。
+
+
+这里是到现在为止的[项目文件](http://cdn1.raywenderlich.com/wp-content/uploads/2014/12/BlueLibrarySwift-Part11.zip)
+
+
+
+还有很多没学过的:比如适配器,观察者和备忘录模式将在[本教程的第二部分](http://www.raywenderlich.com/90773/introducing-ios-design-patterns-in-swift-part-2)提到。而且这还不是全部,我们将会有一个复查的教程,里面有更多的设计模式来让你写一个简单的iOS游戏。
+
+
+
+如果你还有其他的问题或者只是想提到你最喜欢的设计模式,加入到下面的讨论!
\ No newline at end of file
diff --git "a/issue-16/\344\273\213\347\273\215iOS\350\256\276\350\256\241\346\250\241\345\274\2172:2(Swift).md" "b/issue-16/\344\273\213\347\273\215iOS\350\256\276\350\256\241\346\250\241\345\274\2172:2(Swift).md"
new file mode 100644
index 0000000..c56cf79
--- /dev/null
+++ "b/issue-16/\344\273\213\347\273\215iOS\350\256\276\350\256\241\346\250\241\345\274\2172:2(Swift).md"
@@ -0,0 +1,1062 @@
+* 原文链接:[Introducing iOS Design Patterns in Swift – Part 2/2](http://www.raywenderlich.com/90773/introducing-ios-design-patterns-in-swift-part-2)
+* 原文作者:[Vincent Ngo](http://www.raywenderlich.com/u/jomoka)
+* [译文出自:开发者前线 www.devtf.cn](www.devtf.cn)
+* 译者:[lastdays](https://github.com/MrLoong)
+* 校对者:[lastdays](https://github.com/MrLoong)
+* 状态:完成
+
+
+##Getting Started 让我们开始
+
+
+
+你可以下载[the project source from the end of part 1](http://cdn1.raywenderlich.com/wp-content/uploads/2014/12/BlueLibrarySwift-Part11.zip)与我们共同来探索
+
+
+这是你在第一部分结束时完成的音乐库App样品
+
+
+
+
+应用程序的最初设计包括在屏幕的顶端上上水平滚动条的专辑切换。但是为什么不重写它适配所有view,而不是单一的编辑一个简单的滚动条?
+
+
+
+
+为了使这个view可重用。关于其内容的所有决定都应该留给下一个对象:一个委托。水平滚动条要声明一个delegate implements为了scroller的工作。类似于UITableView delegate。当我们讨论设计模式的时候我们会实现这个。
+
+##适配器模式
+
+
+
+Adapter允许classes与不兼容的接口一起工作。它围绕一个对象进行封装,并公开一个标准接口与该对象进行交互。
+
+
+
+如果你很熟悉适配器你将注意到,App使用了略微不同的方式去实现它--App通过协议来实现它。
+你可能会感觉它很像UITableViewDelegate,UIScrollViewDelegate, NSCoding 和 NSCopying。作为一个示例,随着NSCopying协议发展,任何class都能提供一个标准的copy方法。
+
+## 怎样使用适配器
+
+之前提到的horizontal scroller看起来像这个样子
+
+
+
+
+
+开始实现它,在Project Navigator点击View group 选择New File…并且选择iOS > Cocoa Touch class然后点击Next。建立类名为HorizontalScroller并且继承于UIView。
+
+打开HorizontalScroller.swift并且加入下面的代码类:
+
+```
+@objc protocol HorizontalScrollerDelegate {
+}
+
+```
+
+这定义了一个协议名为HorizontalScrollerDelegate。在声明协议之前你包括了@objc所以你能使用@optional delegate 方法。像在 Objective-C。
+
+你定义了所需要的并且选中的委托方法将在大括号之间实现。所以添加以下协议方法
+
+```
+// ask the delegate how many views he wants to present inside the horizontal scroller
+func numberOfViewsForHorizontalScroller(scroller: HorizontalScroller) -> Int
+// ask the delegate to return the view that should appear at
+func horizontalScrollerViewAtIndex(scroller: HorizontalScroller, index:Int) -> UIView
+// inform the delegate what the view at has been clicked
+func horizontalScrollerClickedViewAtIndex(scroller: HorizontalScroller, index:Int)
+// ask the delegate for the index of the initial view to display. this method is optional
+// and defaults to 0 if it's not implemented by the delegate
+optional func initialViewIndex(scroller: HorizontalScroller) -> Int
+
+```
+
+
+
+这里有需求和可选择的方法。所需的方法必须由委托来执行,通常包含一些数据,这是绝对必须的。
+在这种情况下,所需的细节是view数量,特殊的索引视图,并且挖掘试图的行为。这里的可选方法是初始视图。如果没有实现,HorizontalScroller讲默认为第一个索引。
+
+
+在HorizontalScroller.swift,
+
+将下面的代码添加到HorizontalScroller的定义。
+
+```
+weak var delegate: HorizontalScrollerDelegate?
+
+```
+
+
+你上面创建的创建的属性被定义为弱引用。这是必要的,以防止保留一个周期。如果一类对它的委托有强引用的话,该委托保持强引用并且返回一个标准的类,你的app将发生内存泄露,因为这两个类将释放分配给其他的内存。所有的属性在swift中被默认为强引用
+
+
+
+委托是可选的,所以有可能使用这个类的人不提供一个委托。但如果他们这样做,它将使HorizontalScrollerDelegate一致并且你可以确保协议方法在那里实现。
+
+Add a few more properties to the class:添加一些属性到类中:
+
+```
+// 1
+private let VIEW_PADDING = 10
+private let VIEW_DIMENSIONS = 100
+private let VIEWS_OFFSET = 100
+
+// 2
+private var scroller : UIScrollView!
+// 3
+var viewArray = [UIView]()
+
+```
+
+
+滚动每一个评论块:
+
+* 定义常亮,使其易于在设计时修改布局。视图的尺寸内的滚动条是100 x 100的矩形。
+
+* 创建一个包含试图的滚动视图
+* 创建一个拥有专辑封面的数组
+
+Next you need to implement the initializers. Add the following methods:
+下一步你需要执行初始化。添加以下方法:
+
+```
+override init(frame: CGRect) {
+ super.init(frame: frame)
+ initializeScrollView()
+}
+
+required init(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ initializeScrollView()
+}
+
+func initializeScrollView() {
+ //1
+ scroller = UIScrollView()
+ addSubview(scroller)
+
+ //2
+ scroller.setTranslatesAutoresizingMaskIntoConstraints(false)
+ //3
+ self.addConstraint(NSLayoutConstraint(item: scroller, attribute: .Leading, relatedBy: .Equal, toItem: self, attribute: .Leading, multiplier: 1.0, constant: 0.0))
+ self.addConstraint(NSLayoutConstraint(item: scroller, attribute: .Trailing, relatedBy: .Equal, toItem: self, attribute: .Trailing, multiplier: 1.0, constant: 0.0))
+ self.addConstraint(NSLayoutConstraint(item: scroller, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1.0, constant: 0.0))
+ self.addConstraint(NSLayoutConstraint(item: scroller, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1.0, constant: 0.0))
+
+ //4
+ let tapRecognizer = UITapGestureRecognizer(target: self, action:Selector("scrollerTapped:"))
+ scroller.addGestureRecognizer(tapRecognizer)
+}
+
+```
+
+
+initializers delegate必须工作在initializeScrollView()。这是实现方法:
+
+* 创建一个新的UIScrollView实例并将其添加到父视图。
+* 关闭自动调整尺寸。就是这样,你可以应用你自己的限制。
+* 应用约束到scrollview。完全填满HorizontalScroller视图
+* 创建一个gesture收识别。这个收拾识别检测涉及的滚动试图。并且检查相册封面是否已经被窃听。如果是这样的话,他会通知 HorizontalScroller delegate
+
+现在添加这个方法。
+
+```
+func scrollerTapped(gesture: UITapGestureRecognizer) {
+ let location = gesture.locationInView(gesture.view)
+ if let delegate = delegate {
+ for index in 0.. UIView {
+ return viewArray[index]
+}
+
+```
+
+viewatindex仅仅返回视图在一个特定的指数。使用此方法,以突出显示专辑封面。
+
+Now add the following code to reload the scroller:
+现在,添加下面的代码加载滚动:
+
+```
+func reload() {
+ // 1 - Check if there is a delegate, if not there is nothing to load.
+ if let delegate = delegate {
+ //2 - Will keep adding new album views on reload, need to reset.
+ viewArray = []
+ let views: NSArray = scroller.subviews
+
+ // 3 - remove all subviews
+ for view in views {
+ view.removeFromSuperview()
+ }
+
+ // 4 - xValue is the starting point of the views inside the scroller
+ var xValue = VIEWS_OFFSET
+ for index in 0.. (Int) {
+ return allAlbums.count
+}
+
+```
+
+
+
+正如你所认识的,这是一种在滚动视图中返回视图的方法。由于滚动视图将显示所有的专辑数据的封面,count是专辑记录的数量。
+
+Now, add this code:、
+现在,添加此代码:
+
+```
+func horizontalScrollerViewAtIndex(scroller: HorizontalScroller, index: Int) -> (UIView) {
+ let album = allAlbums[index]
+ let albumView = AlbumView(frame: CGRect(x: 0, y: 0, width: 100, height: 100), albumCover: album.coverUrl)
+ if currentAlbumIndex == index {
+ albumView.highlightAlbum(didHighlightView: true)
+ } else {
+ albumView.highlightAlbum(didHighlightView: false)
+ }
+ return albumView
+ }
+
+```
+
+
+
+在这里,你创建一个新的albumview,接下来检查用户是否选择这张专辑。然后,您可以将其设置为突出显示或不取决于是否选择相册。最后,你通过它的horizontalscroller。
+
+
+这是它!仅仅三个短的方法显示一个漂亮的水平滚动条的方法。
+
+
+是的,你还需要创建滚动条,并把它添加到你的主要观点,但在这之前,将下面的方法添加到主类的定义:
+
+```
+func reloadScroller() {
+ allAlbums = LibraryAPI.sharedInstance.getAlbums()
+ if currentAlbumIndex < 0 {
+ currentAlbumIndex = 0
+ } else if currentAlbumIndex >= allAlbums.count {
+ currentAlbumIndex = allAlbums.count - 1
+ }
+ scroller.reload()
+ showDataForAlbum(currentAlbumIndex)
+ }
+
+```
+
+
+
+此方法加载相册数据通过libraryapi然后设置当前显示基于当前视图索引的当前值。如果当前视图索引小于0,则表示当前没有选择视图,然后在列表中显示的第一张专辑。否则,最后一张专辑被显示。
+
+```
+scroller.delegate = self
+reloadScroller()
+
+```
+
+由于horizontalscroller是创建在storyboard中,所有你需要做的是设置代理,叫reloadscroller(),将负荷的滚动条来显示专辑数据视图。
+
+
+由于horizontalscroller是创建在storyboard中,所有你需要做的是设置代理,叫reloadscroller(),将负荷的滚动条来显示专辑数据视图。
+
+编译和运行你的项目,新的水平滚动条,看看:
+
+
+
+##The Observer Pattern
+
+在观察者模式,一个对象的状态变化通知其他任何对象。所涉及的对象不需要知道彼此的,从而鼓励一个解耦设计。当一个属性发生改变时,这个模式最常用于通知感兴趣的对象。
+
+
+
+通常的实现要求一个观察者在另一个对象的状态寄存器。当状态发生改变时,所有的观察对象都会被通知。
+
+
+
+如果你想坚持MVC的概念(提示:你这样做),你需要让模型对象和视图对象沟通,但他们之间没有直接的参考。这就是观察者模式的所在。
+
+
+
+Cocoa在两个熟悉的方式实现观察者模式:通知和键值观察(KVO)。
+
+##Notifications
+
+
+不要被混淆与推送本地通知,通知是基于订阅和发布模式,允许对象(发行商)发送消息到其他对象(用户/听众)。出版商从不需要了解有关用户的任何事情。
+
+
+
+通知被苹果严重使用。例如,当键盘显示/隐藏系统发送uikeyboardwillshownotification / uikeyboardwillhidenotification,分别。当你的应用程序进入后台,系统将一个uiapplicationdidenterbackgroundnotification通知。
+
+##How to Use Notifications
+
+
+去albumview.swift在初始化结束中插入下面的代码(框架:CGRect,albumcover:初始化字符串):
+
+```
+NSNotificationCenter.defaultCenter().postNotificationName("BLDownloadImageNotification", object: self, userInfo: ["imageView":coverImage, "coverUrl" : albumCover])
+
+```
+
+
+
+这条线穿过NSNotificationCenter发送通知单。通知信息包含在UIImageView和封面图像被下载的URL。这是所有的信息,您需要执行的封面下载任务。
+
+
+在libraryapi.swift init中,直接在super.init()后面添加下面一行
+
+```
+NSNotificationCenter.defaultCenter().addObserver(self, selector:"downloadImage:", name: "BLDownloadImageNotification", object: nil
+
+```
+
+
+
+这是等式的另一边:观察者。每一次albumview类岗位bldownloadimagenotification通知,自libraryapi注册同一通知观察者,系统会通知libraryapi。然后libraryapi通知downloadimage()响应。
+
+
+
+然而,在你实现downloadimage()你要记得退订此通知时候释放。如果你不正确的退订通知你们班登记,通知会发送到回收实例。这可能会导致应用程序崩溃。
+
+Add the following method to LibraryAPI.swift:
+添加下面的方法到libraryapi.swift:
+
+```
+deinit {
+ NSNotificationCenter.defaultCenter().removeObserver(self)
+}
+
+```
+
+
+当这个对象被释放,它使自己从所有通知已注册的观察者。
+
+
+
+还有一件事要做。这可能是一个好主意,以节省下载资源并且覆盖本地,所以应用程序将不需要下载相同的盖过一遍又一遍。
+
+
+
+打开persistencymanager.swift并添加下面的方法:
+
+```
+func saveImage(image: UIImage, filename: String) {
+ let path = NSHomeDirectory().stringByAppendingString("/Documents/\(filename)")
+ let data = UIImagePNGRepresentation(image)
+ data.writeToFile(path, atomically: true)
+}
+
+func getImage(filename: String) -> UIImage? {
+ var error: NSError?
+ let path = NSHomeDirectory().stringByAppendingString("/Documents/\(filename)")
+ let data = NSData(contentsOfFile: path, options: .UncachedRead, error: &error)
+ if let unwrappedError = error {
+ return nil
+ } else {
+ return UIImage(data: data!)
+ }
+}
+
+```
+
+
+这个代码非常简单。下载的图片将被保存在文件目录,并将getimage()如果匹配的文件不在文件目录中找到返回nil。
+
+Now add the following method to LibraryAPI.swift:
+现在添加下面的方法来libraryapi.swift:
+
+```
+ func downloadImage(notification: NSNotification) {
+ //1
+ let userInfo = notification.userInfo as! [String: AnyObject]
+ var imageView = userInfo["imageView"] as! UIImageView?
+ let coverUrl = userInfo["coverUrl"] as! String
+
+ //2
+ if let imageViewUnWrapped = imageView {
+ imageViewUnWrapped.image = persistencyManager.getImage(coverUrl.lastPathComponent)
+ if imageViewUnWrapped.image == nil {
+ //3
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
+ let downloadedImage = self.httpClient.downloadImage(coverUrl as String)
+ //4
+ dispatch_sync(dispatch_get_main_queue(), { () -> Void in
+ imageViewUnWrapped.image = downloadedImage
+ self.persistencyManager.saveImage(downloadedImage, filename: coverUrl.lastPathComponent)
+ })
+ })
+ }
+ }
+ }
+
+```
+
+
+再次,你使用的是隐藏的复杂性,从其他类下载图像的外观模式。通知发件人不关心图像来自网络或文件系统。
+
+
+
+建立并运行你的应用程序看看美丽的覆盖在你的horizontalscroller:
+
+
+
+
+
+停止应用程序和运行它。注意,有没有延迟加载的封面,因为他们已经保存在本地。你甚至可以断开互联网和您的应用程序将正常工作。然而,有一个奇怪的点这里:微调从未停止旋转!发生了什么事?
+
+
+你开始旋转时下载的图像,但是你还没有实现的逻辑映像下载完成后停止旋转。你可以发送一个通知,每一次的图像已被下载,但相反的,你会使用其他观察者模式,KVO。
+
+##Key-Value Observing (KVO) 键值观察(KVO)
+
+
+在KVO,对象可以被通知到一个特定的财产的任何变化;要么自己或另一个对象。如果你有兴趣,你可以更多地了解这个[Apple’s KVO Programming Guide](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html)。
+
+##How to Use the KVO Pattern 如何使用KVO模式
+
+
+如上所述,该KVO机制允许一个对象观察变化的属性。在你的情况,你可以使用KVO观察到保存图像的UIImageView图像属性。
+
+打开albumview.swift并添加以下代码以init(框架:albumcover:),只在你添加载体图像作为子视图:
+
+```
+coverImage.addObserver(self, forKeyPath: "image", options: nil, context: nil)
+
+```
+
+
+这增加了self,这是当前类,对载体图像图像特性观察。
+
+
+你还需要注销作为观察者,仍在albumview.swift,添加以下代码:
+
+```
+deinit {
+ coverImage.removeObserver(self, forKeyPath: "image")
+}
+
+```
+
+
+最后添加此方法
+
+```
+override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer) {
+ if keyPath == "image" {
+ indicator.stopAnimating()
+ }
+}
+
+```
+
+
+
+你必须在每一个类中实现这种方法作为一个观察者。系统每一次都会有一次观测到的性能变化来执行这个方法。在上面的代码中,你停止旋转时的“形象”性质的变化。这样,当一个图像被加载,微调将停止转动。
+
+
+建设和运行您的项目。微调应该消失:
+
+
+
+
+
+如果你适当使用和终止它,你会注意到,你的应用程序的状态没有保存。你看的最后一张专辑当应用程序启动时不会是默认的相册。
+
+##The Memento Pattern 备忘录模式
+
+
+
+
+备忘录模式捕捉和表现对象的内部状态。换句话说,它可以节省你的东西。后来,这种外在的状态可以在不破坏封装恢复;即,私有数据保密。
+
+##如何使用备忘录模式
+
+
+在viewcontroller.swift中添加下面的两种方法:
+
+```
+//MARK: Memento Pattern
+func saveCurrentState() {
+ // When the user leaves the app and then comes back again, he wants it to be in the exact same state
+ // he left it. In order to do this we need to save the currently displayed album.
+ // Since it's only one piece of information we can use NSUserDefaults.
+ NSUserDefaults.standardUserDefaults().setInteger(currentAlbumIndex, forKey: "currentAlbumIndex")
+}
+
+func loadPreviousState() {
+ currentAlbumIndex = NSUserDefaults.standardUserDefaults().integerForKey("currentAlbumIndex")
+ showDataForAlbum(currentAlbumIndex)
+}
+
+```
+
+
+saveCurrentState保存当前专辑指数NSUserDefaults – NSUserDefaults是一种标准的数据存储提供的iOS应用程序特定的设置和数据保存。
+
+
+
+loadpreviousstate加载以前保存的指标。这不是该备忘录模式实现比较充分的,但是你要有。
+
+
+
+现在,添加下面一行在viewcontroller.swift viewDidLoad前scroller.delegate =self:
+
+```
+loadPreviousState()
+
+```
+
+
+
+当应用程序启动时加载先前保存的状态。但是,你从哪里来拯救这个应用程序的当前状态?你会使用通知来做这个。iOS发送uiapplicationdidenterbackgroundnotification通知当应用程序进入后台。你可以使用该通知称savecurrentstate。那不方便吗?
+
+Add the following line to the end of viewDidLoad:
+添加下面一行到viewDidLoad:
+
+```
+NSNotificationCenter.defaultCenter().addObserver(self, selector:"saveCurrentState", name: UIApplicationDidEnterBackgroundNotification, object: nil)
+
+```
+
+
+
+现在,当应用程序即将进入后台,视图会自动调用的savecurrentstate保存当前状态。
+
+
+向类添加下面的代码:
+
+```
+deinit {
+ NSNotificationCenter.defaultCenter().removeObserver(self)
+}
+
+```
+
+
+
+这将确保你将类作为一个观察者的时候释放视图。
+
+
+
+构建和运行您的应用程序。导航到一个相册,把应用程序的主页按钮的背景(命令+ Shift +如果你在模拟器),然后关闭你的应用程序从Xcode。重新启动,并检查先前选定的专辑的中心:
+
+
+
+
+
+它看起来像这张专辑的数据是正确的,但不是以正确版本的专辑。给什么?
+
+
+
+这是可选的方法initialviewindexforhorizontalscroller的意思是!因为这方法不在委托执行,在这种情况下,视图,初始视图总是设置为第一视角。
+
+
+
+为了解决这个问题,将下面的代码添加到viewcontroller.swift:
+
+```
+func initialViewIndex(scroller: HorizontalScroller) -> Int {
+ return currentAlbumIndex
+}
+
+```
+
+
+
+现在horizontalscroller第一视角设置为任何专辑由currentalbumindex。这是为了确保应用程序的经验仍然是个人和恢复一个的方式。
+
+
+
+再次运行您的应用程序。滚动到一张专辑之前,把应用程序的背景下,停止应用程序,然后重新启动以保证问题是固定的:
+
+
+
+
+
+如果你看persistencymanager的初始化,你会注意到这张专辑是硬编码的数据并重新创建每一次persistencymanager创建。但最好在一个文件中创建一个相册列表。如何将相册数据保存到文件中?
+
+
+
+然后他们需要重现重新创建专辑的情况时,一种选择是遍历专辑的性质,将它们保存到一个plist文件。这不是最好的选择,因为它需要你写特定的代码,根据什么数据/属性是在每个类。例如,如果你创建了一个具有不同性质的电影类,保存和加载该数据将需要新的代码。
+
+
+此外,您将无法为每个类实例保存私有变量,因为它们不能访问外部类。这正是苹果创造的归档机制的原因。
+
+##Archiving
+
+
+
+苹果的一个专门的implementations是归档模式实现。这将一个对象转换为一个流,可以保存和稍后恢复,而不暴露私有属性到外部类。你可以阅读更多关于这个功能在iOS 16的6章的教程书。[Apple’s Archives and Serializations Programming Guide](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Archiving/Archiving.html).
+
+## 如何使用Archiving
+
+
+ 打开album.swift改变班线如下:
+
+```
+class Album: NSObject, NSCoding {
+
+```
+
+
+
+在album.swift添加下面的两种方法:
+
+```
+required init(coder decoder: NSCoder) {
+ super.init()
+ self.title = decoder.decodeObjectForKey("title") as! String
+ self.artist = decoder.decodeObjectForKey("artist") as! String
+ self.genre = decoder.decodeObjectForKey("genre") as! String
+ self.coverUrl = decoder.decodeObjectForKey("cover_url") as! String
+ self.year = decoder.decodeObjectForKey("year") as! String
+}
+
+
+func encodeWithCoder(aCoder: NSCoder) {
+ aCoder.encodeObject(title, forKey: "title")
+ aCoder.encodeObject(artist, forKey: "artist")
+ aCoder.encodeObject(genre, forKey: "genre")
+ aCoder.encodeObject(coverUrl, forKey: "cover_url")
+ aCoder.encodeObject(year, forKey: "year")
+}
+
+```
+
+
+在NSCoding协议的一部分,encodewithcoder会给你打电话的时候,问一个专辑实例进行归档。相反,init(编码器)初始化将用来重建或解压缩从保存的实例。它虽然简单,但强大。
+
+现在,该相册类可以被归档,添加的代码,实际上保存和加载的专辑列表。
+
+
+
+添加下面的方法到persistencymanager.swift:
+
+```
+func saveAlbums() {
+ var filename = NSHomeDirectory().stringByAppendingString("/Documents/albums.bin")
+ let data = NSKeyedArchiver.archivedDataWithRootObject(albums)
+ data.writeToFile(filename, atomically: true)
+}
+
+```
+
+
+这将是一种被称为保存专辑的方法。nskeyedarchiver档案专辑阵列为一个名为albums.bin。
+
+
+
+
+当你archive的对象包含其他对象,该文档会自动尝试递归archive的子对象和孩子任何子对象等。在这种情况下,archive开始与专辑,这是一个数组的相册实例。由于数组和专辑都支持NSCopying接口,数组中的每件事都是archive。
+
+
+现在替换init在persistencymanager.swift用下面的代码:
+
+```
+override init() {
+ super.init()
+ if let data = NSData(contentsOfFile: NSHomeDirectory().stringByAppendingString("/Documents/albums.bin")) {
+ let unarchiveAlbums = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! [Album]?
+ if let unwrappedAlbum = unarchiveAlbums {
+ albums = unwrappedAlbum
+ }
+ } else {
+ createPlaceholderAlbum()
+ }
+}
+
+func createPlaceholderAlbum() {
+ //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]
+ saveAlbums()
+}
+
+```
+
+
+
+你有移动占位符创作专辑代码可读性的一个单独的方法createplaceholderalbum()。在新的代码,如果它存在的话,nskeyedunarchiver加载相册数据从文件。如果它不存在,它创建的相册数据,并立即保存它为下一次推出的应用程序。
+
+
+你还想保存相册数据,每次应用程序进入背景。这似乎不是必要的,但如果你后来添加的选项来改变专辑的数据?然后,你会希望这个,以确保所有的变化被保存。
+
+
+
+由于主要的应用程序访问所有服务通过libraryapi,这就是应用程序将让persistencymanager知道它需要保存相册数据。
+
+
+
+现在添加以下实现方法到 LibraryAPI.swift中
+
+```
+func saveAlbums() {
+ persistencyManager.saveAlbums()
+}
+
+```
+
+
+此代码只会在调用libraryapi保存相册上persistencymangaer。
+
+
+
+将下面的代码添加到saveCurrentState在ViewController.swift结束:
+
+```
+LibraryAPI.sharedInstance.saveAlbums()
+
+```
+
+
+和上面的代码使用libraryapi触发数据视图专辑时保存其状态的保存。
+
+##Final Touches
+
+
+您将通过允许用户执行删除操作来删除一个相册,或撤消操作以使其改变自己的想法,从而为您的音乐应用程序添加最后的触摸!
+
+
+添加以下属性视图:
+
+```
+// We will use this array as a stack to push and pop operation for the undo option
+var undoStack: [(Album, Int)] = []
+
+```
+
+
+
+这将创建一个空的撤销堆栈。的undoStack将举行一个元组的两参数。第一张是一张专辑,二是这张专辑的索引。
+
+
+
+在viewDidLoad中的reloadscroller()后添加以下代码:
+
+```
+let undoButton = UIBarButtonItem(barButtonSystemItem: .Undo, target: self, action:"undoAction")
+undoButton.enabled = false;
+let space = UIBarButtonItem(barButtonSystemItem: .FlexibleSpace, target:nil, action:nil)
+let trashButton = UIBarButtonItem(barButtonSystemItem: .Trash, target:self, action:"deleteAlbum")
+let toolbarButtonItems = [undoButton, space, trashButton]
+toolbar.setItems(toolbarButtonItems, animated: true)
+
+```
+
+
+上面的代码创建了一个工具栏,其中有2个按钮和一个灵活的空间。撤消按钮在这里被禁用,因为撤消堆栈开始空。注意工具栏已经在故事情节中,所以你需要做的是设置工具栏的项目。
+
+
+你会加入三个方法在viewcontroller.swift,专辑管理动作处理:添加,删除,和撤销。
+
+
+第一个是增加新专辑的方法:
+
+```
+func addAlbumAtIndex(album: Album,index: Int) {
+ LibraryAPI.sharedInstance.addAlbum(album, index: index)
+ currentAlbumIndex = index
+ reloadScroller()
+}
+
+```
+
+
+
+在这里你添加相册,将其设置为当前专辑索引,并重新加载滚动。
+
+
+
+下一步是删除方法:
+
+```
+func deleteAlbum() {
+ //1
+ var deletedAlbum : Album = allAlbums[currentAlbumIndex]
+ //2
+ var undoAction = (deletedAlbum, currentAlbumIndex)
+ undoStack.insert(undoAction, atIndex: 0)
+ //3
+ LibraryAPI.sharedInstance.deleteAlbum(currentAlbumIndex)
+ reloadScroller()
+ //4
+ let barButtonItems = toolbar.items as! [UIBarButtonItem]
+ var undoButton : UIBarButtonItem = barButtonItems[0]
+ undoButton.enabled = true
+ //5
+ if (allAlbums.count == 0) {
+ var trashButton : UIBarButtonItem = barButtonItems[2]
+ trashButton.enabled = false
+ }
+}
+
+```
+
+考虑下面的每一个部分:
+
+* 获得这张专辑删除.
+* 创建一个变量称为undoaction存储一个元组的专辑,这张专辑的指标。然后将元组添加到堆栈中
+* 使用libraryapi从数据结构中删除专辑和重载滚动。.
+* 因为在撤消堆栈中有一个动作,您需要启用撤消按钮.
+
+
+
+
+最后,添加撤消操作的方法:
+
+```
+func undoAction() {
+ let barButtonItems = toolbar.items as! [UIBarButtonItem]
+ //1
+ if undoStack.count > 0 {
+ let (deletedAlbum, index) = undoStack.removeAtIndex(0)
+ addAlbumAtIndex(deletedAlbum, index: index)
+ }
+ //2
+ if undoStack.count == 0 {
+ var undoButton : UIBarButtonItem = barButtonItems[0]
+ undoButton.enabled = false
+ }
+ //3
+ let trashButton : UIBarButtonItem = barButtonItems[2]
+ trashButton.enabled = true
+}
+
+```
+
+
+
+最后考虑上述方法的意见:
+
+* 该方法将对象从堆栈中弹出,给你一个包含已删除的相册及其索引的元组。然后你继续增加专辑的背面。
+* 自从你在堆栈中的最后一个对象被删除时,你就需要检查堆栈是否为空。如果是,那就意味着没有更多的动作来撤消。所以你禁用了撤消按钮
+* 你也知道,既然你毁掉了一个行动,至少应该有一张专辑封面。因此你启用了垃圾桶。
+
+
+建立和运行你的应用程序来测试你的撤销机制,删除一张专辑(或两者),并点击撤消按钮看到它的动作:
+
+
+
+
+
+这也是一个很好的地方,以测试是否更改您的相册数据保留在会话之间。现在,如果你删除了一张专辑,把应用程序发送到后台,然后终止应用程序,下一次你启动应用程序的显示相册列表应该反映删除。
+
+
+
+如果你想得到所有的专辑回来,只是删除应用程序并运行它再从Xcode安装一个新的副本的入门资料。
+
+
diff --git "a/issue-16/\344\275\277\347\224\250\344\270\200\344\270\252MVC.md" "b/issue-16/\344\275\277\347\224\250\344\270\200\344\270\252MVC.md"
new file mode 100644
index 0000000..dd88fbc
--- /dev/null
+++ "b/issue-16/\344\275\277\347\224\250\344\270\200\344\270\252MVC.md"
@@ -0,0 +1,504 @@
+* 原文链接:[Brigade’s Experience Using an MVC Alternative](https://medium.com/brigade-engineering/brigades-experience-using-an-mvc-alternative-36ef1601a41f)
+* 原文作者: [Ryan Quan](https://medium.com/@ryanquan)
+* 译文出自:[开发者前线 www.devtf.cn](www.devtf.cn)
+* 译者:[Quzhiyu](https://github.com/Quzhiyu)
+* 校对者:[lastdays](https://github.com/MrLoong)
+* 状态:完成
+
+#Brigade’s Experience Using an MVC Alternative
+
+
+任何iOS开发者都会告诉你iOS应用程序是建立于[模型视图控制器](https://developer.apple.com/library/ios/documentation/general/conceptual/devpedia-cocoacore/MVC.html)(MVC)设计模式。他们也可能告诉你有些人叫它“Massive View Controller”设计模式。因为,你会经常发现你的代码有明显的不适合视图或者模型,所以它被抛到了控制器,导致臃肿的控制器很难维持,因为你有一个巨大的类做的一切。
+
+
+
+
+在发展中旅的iOS应用程序,我们想建筑师以这样一种方式,干净利落地解决了几个关键点:
+
+
+
+ * 易于迭代
+ * 友好合作
+ * 分离关注
+ * 易于测试
+
+
+
+我们偶然发现了一个建筑叫VIPER,这似乎刚好符合要求。VIPER是发展与朋友之间手机互动,他们有一个很好的介绍博客的职位以及更多参与的博客。我们决定离开苹果的标准给VIPER一枪。
+
+
+
+#什么是VIPER,我们要怎么使用它。
+
+
+
+正如它的创造者所说, VIPER提供指导,构建应用程序的架构和可调整以适应个人应用。这将是一个VIPER是什么讨论我们如何调整以适应我们的需要。
+
+
+
+为了最好的理解VIPER,忘记所有你知道的MVC。VIPER是一个新型的野兽(或者爬行动物),如果你的思维仍在MVC的地方,你会很难接受VIPER。想象你从起点开始,不知道iOS应用城西的结构。没有MVC。
+
+
+
+VIPER打算分离应用程序几个角色之一的关注:
+
+
+
+* 视图/用户界面
+* 交互器
+* 提出者/事件处理程序
+* 实体
+* 路由器/线框
+
+
+
+注:其中一些另类的名字,你会看到如果你看看上面提到的其他VIPER参考上文的博客文章。我已经添加了这两个名字在这里头。
+
+
+
+在实践中,我们发现,我们使用了一些额外的角色,增加了数据管理器和服务层。
+
+
+下面是一个图一个典型的VIPER“栈”(稍后)在我们的应用程序和描述了我们如何看待VIPER。每一个方块代表一个单独的类,每一行代表一个参考实例之间的各自的类。
+
+
+
+
+
+
+把每一个类当作一个工人在流水线中思考。每个类只知道如何进行一组有限的操作,并依赖于其他类来帮助完成手头的任务。
+
+
+让我们先去把每个类的责任,然后我们会给出一个例子,从用户交互,向用户显示数据。
+
+
+
+#视图/用户界面
+
+
+
+
+
+
+查看责任:
+
+
+
+
+* 向用户展示信息
+* 检测用户交互
+
+
+
+该视图是由提出者告诉由什么来显示,它告诉提出者,当事件需要发生。
+
+
+
+#向用户展示信息
+
+
+
+如果视图应该向用户显示错误,提出者可能会调用视图的方法:
+
+
+
+
+
+然后,它可以显示错误,如它所喜悦的,它可以在一个警告视图,标签,或任何其他方式。这里的关键问题是,提出者不关心如何显示错误的细节。它只是关心它会显示。
+
+###Detecting user interaction
+
+###检测用户交互
+
+
+如果一个事件发生如用户点击“登录”按钮,视图可能会调用一个类似的方法:
+
+
+
+
+
+正如我们以前所做的那样,该对象的方法只是在有秩序下不干涉任务给下一个工人,一旦它完成了它的任务。就这一观点而言,它完成了它的工作来检测用户的交互,现在是向演示者处理事件。
+
+
+
+#演示/事件处理程序
+
+
+
+
+
+提出者职责:
+
+
+
+* 告诉视图该显示什么
+* 处理事件
+
+
+
+提出者会告诉视图该显示什么,并相应地处理相应的事件。
+
+###Telling the view what to display
+
+###告诉视图该显示什么
+
+
+
+
+
+正如我们刚才看到的那样,一个演示者的职责就是告诉我们要展示什么。为了完成这一任务,它也作为一个设计师,还是一个“提出者”,格式化数据的视图,它可以以一种有意义的方式显示。
+
+
+
+
+回顾我们以前的错误例子,演示者可能会以一个错误对象的形式收到错误。而不是直接从错误的角度出发,它决定了恰当的信息来描述错误并将其传递给视图。
+
+
+
+
+
+###处理事件
+
+
+
+演示者通常会收到事件类型的方法调用两个来源:视图和交互。
+
+
+
+##查看事件
+
+
+
+由视图和部分的工作是通知的事件的报告,相应地处理这些事件。这通常意味着要求关联检索一些信息或执行某些任务。
+
+
+
+在我们的日志中,该演示者将被通知的视图,试图事件已发生与特定的用户名和密码。提出者会问的关联进行尝试通过它调用适当的方法:
+
+
+
+
+
+
+
+你可能猜到现在提出者在做什么;什么都没有,它的责任时事件的处理并且,它现在到了关联进行任务。
+
+###Interactor events
+
+###关联事件
+
+
+提出者也可以从关联接受事件。这将发生在交互执行完成任务和用户应该知道结果。
+
+
+例如在尝试登陆失败,交际者会告诉提出者:
+
+
+提出者会把这个错误,把它转换成一个有意义的字符串,然后告诉使用该字符串显示一个错误。
+
+
+
+
+#交互器
+
+
+
+
+
+交互器的责任:
+
+* 执行业务逻辑
+
+
+关联是什么执行的应用程序,都是围绕着数据的业务逻辑。
+
+###Performing business logic
+
+###执行业务逻辑
+
+
+
+
+关联是一个知道如何开展,查看通知提出者的事件。
+
+
+
+例如,假设在应用程序中,需要向后端应用程序的同步网络请求。那是,请求B不能执行请求,请求B直到完成需要的信息,是在响应请求这会开始从提出者到关联的电话:
+
+
+
+有一件事我们必须提出的是这里的关联本身并不直接处理网络请求。事实上,它甚至不知道网络请求正在发生。它所知道的是,它可以从数据管理器中的实体的形式(我们将详细说明这个概念很快)。记住,交际者的相应的方法调用上会是这个样子:
+
+
+在这里,我们看到,关联调用的数据管理的必要方法,然后调用返回结果的提出者。这里的关键是交际者知道` performmytask `需要` fetchfoowithcallback:`和` fetchbarwithfoo:回调:`被称为,` fetchbarwithfoo:回调:`必须在回调块` fetchfoowithcallback `。这样,关联处理的“业务逻辑”的应用。
+
+
+
+
+
+#数据管理器
+
+
+
+
+
+
+数据管理职责:
+
+
+* 检索数据
+
+* 存储数据(可选)
+
+
+
+
+数据管理器是一个知道在哪里检索数据的人,如果它应该坚持或不。
+
+#Retrieving data
+
+#检索数据
+
+
+我们看到在关联的例子,我们应该能够查询数据的数据管理,就得到正确的实体回来。我们不关心数据来自何处,因为数据管理器处理。
+
+
+
+数据管理器清楚地知道在哪里检索特定的数据或执行某些请求。例如,关联可以从数据管理器请求用户对象:
+
+
+
+如果这是一个应用程序的后端服务器的支持,数据管理器会知道它必须最终使网络请求检索用户数据。如果这是一个应用程序没有后台服务器的支持,数据管理器可以从本地持久存储中检索用户数据。对于网络请求,我们喜欢有数据管理查询服务对象:
+
+
+
+我们很快就要进入服务对象,但这里的关键是,数据管理器知道要使用什么服务来检索特定的信息。一旦接收到信息,它的信息反馈给团员的继电器。
+
+
+
+###Storing data
+
+###存储数据
+
+
+
+持续的数据也是数据管理者关注的问题。它知道什么时候它应该存储数据,如数据从服务器端,它可以存储缓存的目的。这里的主要原因是,没有其他类知道任何数据是持久的,因为数据管理器摘要。一个简单的例子,这是一个修改上面的代码:
+
+
+让我们打破这个:
+
+
+* 数据管理器首先检查是否已缓存版本的用户给出了相应的` userid `。
+
+* 如果用户发现,关联的通知和方法返回。
+
+* 如果未找到用户,则数据管理器必须从后端服务器检索用户,并使用一个服务来做到这一。
+
+* 一旦用户从服务器检索,用户首先是持久的,那么关联的通知。
+
+
+坚持数据是一个巨大的话题,所有的,可以实现许多方式。这个例子只是作为一个简单的教学模式,但这个想法将在这一切的背后,一个数据管理是一种VIPER的点。
+
+
+
+
+#服务
+
+
+
+
+
+服务职责:
+
+* 向特定实体执行网络请求
+
+
+
+服务对象的VIPER是没有必要的,但是我们已经发现非常有用。
+
+
+
+向特定实体执行网络请求
+
+
+
+我们已经找到服务是一个很好的方式来保持代码与网络请求干燥。其思想是一个单一的服务只处理一个实体类型,并且知道需要对该实体类型进行各种修改的网络请求。
+
+
+例如,您可能有一个用户实体的服务。这个类的头文件可能看起来如下:
+
+
+此服务知道如何创建用户、登录用户、获取用户对象。对于一个给定的身份证的用户对象的一些类似的东西,是一种很可能被用于在多个地方的应用程序,这是服务对象是方便的,在保持事情干。
+
+#实体
+
+
+
+
+
+实体职责:
+
+
+* 代表数据
+
+
+
+
+实体是非常直接的向前和你所期望的。它们体现了某种类型的数据,并作为一种“负载”,通过周围的其他类。例如,数据管理器返回一个实体的交互,它返回一个实体的提出者,然后使用该实体告诉视图显示什么信息。
+
+
+#路由器/线框
+
+
+
+
+线框职责:
+
+* 初始化所有其他类
+
+* 处理路由到其他视图的应用程序
+
+
+
+
+路由器/线框是什么胶水,其他所有的VIPER组件彼此并处理从一个视图导航到另一个应用程序。
+
+
+
+###初始化所有其他课程
+
+
+
+你可能会想知道所有这些VIPER类被实例化和有线互相交谈。这是路由器/线框的地方来。
+
+
+
+在VIPER,每一“堆”由一个观点,提出者,交互,数据管理和服务(我们无论如何),和实体。这一“堆”是什么VIPER是指作为一个模块。VIPER模块对应一个用例。一个用例是你的应用程序应该为用户执行的功能。
+
+
+
+例如,一个用于许多应用程序共同使用的案例是允许用户登录帐户。为此,我们将有一个VIPER模块专门为“登录”的应用程序屏幕。这个模块将有:
+
+
+* 显示“登录”屏幕。
+
+* 一个演示者,处理事件,如用户要求登录在一个特定的用户名和密码。
+
+* 一个团员知道叫上喜欢尝试登录用户在此类事件中的数据管理方法。
+
+* 一个知道用以检索或发送信息到服务器的数据管理器。
+
+* 服务知道HTTP URL请求。
+
+* 实体,您的服务器响应被转换成这样的信息是有用的,在您的应用程序。
+
+
+
+你会注意到,看来,提出者,关联,和数据管理是这个模块非常具体的。也就是说,他们只知道如何处理与日志记录有关的事情。另一方面的服务和实体是非常一般的东西,可以用在许多不同的模块。在这种情况下,该服务可能是一个知道如何将所有端点与服务器上的用户关联的服务。这可能包括登录、注册,或只是检索一般用户数据。你可能在这里使用的是一个用户实体,它只代表用户对象。很容易看到,这个实体可以在许多地方使用的应用程序。
+
+
+
+
+现在我们已经解释了什么是VIPER“栈”或模块,我们可以解释的线框的部分责任。线框图是什么实例化实例这些VIPER组件和连接他们互相交谈。那是,它给出了视图和另一个提出者参考,提出者和彼此关联的引用,等等。实例化一个线框相当于实例化整个VIPER模块。
+
+
+
+###处理路由到其他视图的应用程序
+
+
+
+他线框的别名,路由器,是什么使得在VIPER的R。线框图知道如何浏览和其他模块时要求。这意味着线框也会有其他的框架参考。
+
+
+
+例如,假设你有一个初始屏幕在你的应用程序,这是你的典型的家庭屏幕的应用程序,需要用户帐户。这个屏幕上有一个按钮,“注册”和“登录”按钮。这个屏幕是一个模块,让我们称之为注册提示(命名是硬的)。这里使用的情况是,用户应该能够看到他们的选项之前登录到您的应用程序。当用户按“登录”按钮时,典型的流程是:
+
+
+* 视图将通知用户已请求登录的提示。
+
+* 主持人会意识到这需要一个单独的模块,它将通知框。
+
+
+
+现在,注册框线框将实例化的日志,从而实例化整个VIPER模块,和现在的日志视图在登记的观点(也许作为一个模态)。
+
+
+
+#VIPER的利益
+
+
+
+使用后的VIPER,我们发现它在许多方面是非常有益的。让我们回到我们出发去完成构建我们的应用程序时看看它们的地址列表中的VIPER。
+
+* 易于迭代
+
+* 友好合作
+
+* 分离关注
+
+* 规格能力
+
+
+###易于迭代
+
+
+
+一件事是非常有用的,在添加功能的应用程序是知道新的代码应该生活在旧的代码。VIPER与每个组件有一个明确的责任,不排成一个很好的工作,这使得它很容易决定,把新的代码。
+
+
+我经常发现当我在一个模块中添加新功能时,它几乎感觉像是一个日常的例程,因为我已经知道每个代码应该放在哪里了,这只是一个完成的问题。
+
+
+也就是说,我们遇到的情况,我们确定VIPER在放置一块代码,我们必须作出判断。例如,如果用户可以执行所有行动前选择列表中的多个项目,我们应该在视图,保持这个状态的主持人,或其他类的VIPER堆栈?一旦你弄清楚什么对你最有意义,你可以绕过这些问题,在未来很容易,因为你已经解决了他们。然后一切又变得例行。
+
+
+
+###友好合作
+
+
+
+VIPER在团队非常容易的工作。由于用例被分成不同的模块,你不踩别人的脚趾,因为所有的代码的一个特点是通常划分为自己的模块。
+
+
+
+在实践中,每一个VIPER组件之间的交互通过接口。在Objective-C中这只是一个协议。很好的一点是,你可以定义一个类的接口,然后单独的人可以单独对这些类进行单独的工作。我们已经取得了很大成功,这被定义的视图演示界面提前对视图做纯粹的UI的工作一个人工作,一个人工作,其余的VIPER栈做纯粹的“后台”工作。
+
+
+
+
+###分离关注
+
+
+
+每个VIPER模块遵循单一责任原则,VIPER自然分离出来的关注类。这是什么使前两点(和最后一个点)工作了这么好。
+
+
+
+###易于测试
+
+
+
+通过分离元件,遵循单一责任原则,这也使事情容易说明它给你而掐灭其他依赖规格特异功能的能力。例如,如果你想测试你的交互逻辑,你所要做的就是找出提出者和数据管理器来消除可以使写作规格复杂的依赖关系。那么你的规格只会测试的关联,你可以写为提出者和数据管理单独的规格。
+
+
+#结论
+
+
+
+整体上我们发现使切换VIPER为上述原因是非常有用的,对我们有益。当然有许多的障碍,你会用VIPER需要模具VIPER需要你的遭遇,但那是真的任何建筑的你选择去。
+
+
+我肯定会尝试VIPER推荐别人的同时也要在其他博客上看它。希望这个帖子一直帮助和得到一些你兴奋地尝试VIPER。
+
+
+
+附加读数:
+
+* [Introduction to VIPER](http://mutualmobile.github.io/blog/2013/12/04/viper-introduction/)
+* [Architecting iOS Apps with VIPER](https://www.objc.io/issues/13-architecture/viper/)
+
+
+
+
+
+
diff --git a/issue-17/1.md b/issue-17/1.md
new file mode 100644
index 0000000..d23a844
--- /dev/null
+++ b/issue-17/1.md
@@ -0,0 +1,156 @@
+##objective - 在LLDB中的调用
+
+
+* 原文链接:[Printing Objective-C Invocations in LLDB](http://arigrant.com/blog/2014/2/18/chisels-print-invocation-command)
+* 原文作者:[]()
+* [译文出自:开发者前线 www.devtf.cn](www.devtf.cn)
+* 译者:[lastdays](https://github.com/MrLoong)
+* 校对者:[lastdays](https://github.com/MrLoong)
+* 状态:完成
+
+不久前Facebook对Chisel开放源码,Chisel 是在LLDB中协助调试iOS应用程序的一系列命令和函数的集合。想要全面了解使用LLDB的技巧(包括使用Chisel的技巧),请阅读我关于调试的objc。Io系列文章:“在调试器中灵活舞蹈——一支同LLDB的华尔兹”,如果你有时间的话,你应该阅读objc。Io的整个版本。因为它实在是太棒了。
+
+有一个命令,我觉得特别有趣,但直到这篇文章,我才终于有机会写一写它。
+
+这个命令就是pinvocation (。。。),这是在Apple的代码中(或者任何你没有访问源和符号)最有用的调试objective - c方法,。Pinvocation可以发现self和_cmd的值,打包成一个NSInvocation然后显示出来给你,包括参数。这里有一个关于其使用的例子:
+
+
+目前为止,Chisel只支持32位x86的 pinvocation,这意味着它只能在32位Mac OS X和32位iOS模拟器使用。这并不是说命令不能在其他体系结构中执行,但它会稍微复杂一些。
+X86 CALLING CONVENTION
+X86调用协定
+
+pinvocation的核心功能是它可以记录关于调用的和你在IOS系统()中会遇到的所有架构的参数,32位x86有着最容易记住和使用的调用协定(这就是为什么它是目前唯一能被pinvocation执行的)。x86的调用协定很简单:
+
+
+所有的参数都在堆栈上。
+
+
+是的,就是这样!
+
+这里有一个例子说明如何调用objc_msgSend函数。
+
+
+这可能看起来有点令人生畏,但不要担心,我们会经历的。
+
+注意:反汇编程序会把寄存器写成% reg,但我要把它写成$reg,因为在LLDB中我们是这样定义它的。
+
+在指令中首先要注意的是$esp的使用,也就是包含堆栈开头地址的堆栈指针寄存器。在x86中,堆栈实际上开始于小地址,向下增加记录(这意味着$esp确实可以记录堆栈的底部)。
+
+x86 mov *命令遵循mov源目的地的形式。第一行是将xmm1寄存器(浮点寄存器中的一个)的内容移动并压入堆栈,16字节。将
+movsd %xmm1, 16(%esp)
+movsd % xmm1,16(% esp)
+as
+译为
+
+* (%esp + 16) = %xmm1;
+* (% esp + 16)= % xmm1;
+
+如果你发现这段代码并不是移动堆栈指针的命令,你可能期望会在堆栈找到一组参数。因为每移动一次堆栈指针,就会在堆栈出现一组相应的参数。但这其实很浪费,所以堆栈指针实际上只在开始一个函数时被移动一次,来为函数使用所需的最大堆栈空间创造足够的空间,然后在函数结束时,它把堆栈指针拨回到函数开始的地方(这叫做弹出堆栈帧)。
+
+上面的代码片段将四个值加载到堆栈的顶部: $esi, $eax, $xmm0, $xmm1(在现实中,这些实际上是两个32位整数寄存器和两个64位的寄存器,分别将两个指针和四个浮动压入堆栈),然后通过调用执行objc_msgSend指令。指令的顺序可能起初似乎与已显示部分顺序相反,但由于堆栈向下记录数据, $esp是在堆栈上的位置是高于$esp + 4的,因此即使第一个指令加载到$esp + 16,它也会是在4 mov指令中堆栈的最低部分。
+
+每一个objective – c的前两个参数是self和_cmd,因为这种情况下的参数是$esi, $eax, $xmm0, $xmm1,如上所述,我们可以推断出, 调用(self)接收器是在$esi缓存器中,选择器(_cmd)在$eax缓存器中。四个浮点参数必须位于xmm0和 xmm1中。
+
+上面这个调用实际上是——(UIView setFrame:),所以参数(4个漂浮)很可能是有意义的。
+
+你需要知道的x86调用协定的最后细节是,调用指令将当前指令指针压入堆栈。因此,在执行调用命令之前,接收者的地址是in *($esp),但是在执行调用指令后,地址就成为 in *($esp + 4),因为堆栈向上增长四个字节,有足够的空间来存放先前指令指针(记住,我们是在一个32位架构!)。
+X86 PREAMBLE
+X86序言
+
+在x86种, objc_msgSend总是使用拯救公约来调用。这意味着在一个函数中,如果你想要使用一个缓存器,你必须将它的内容保存到一边;当你使用完毕时,你必须把内容恢复到你发现它时的样子。这通常通过将值放入堆栈中,使用所需的缓存器, 当你完成时,从堆栈中取出值再输回到缓存器中。所有x86中的objective – c都被编译为callee-save,因此他们都开始于完全相同的方式,有着相同的“序言”。
+
+一个函数必须先移动堆栈指针为其所有工作腾出足够的堆栈空间。在移动堆栈指针之前,$esp需要保存它的数值(这样才可以把它放回去!)。在x86中,这是通过将堆栈指针寄存器的内容存储到基指针寄存器($esp的内容到$ebp)来实现的。哦,那将失去基指针的内容!那让我们先把基指针的内容放到堆栈上吧。
+
+记住,在调用之前,arg0 是在$esp上的,因此在一个函数开始时,它在$esp + 4上。我们现在可以构建标准的函数序言了:
+
+把$ebp压入堆栈(arg0现在$esp + 8上,因为这会让移动堆栈指针向上移动4字节)。
+Move $esp into $ebp (arg0 is now in $esp+8 and $ebp+8).
+将$esp移动到$ebp上(arg0现在在$esp + 8 和$ebp + 8上)。
+
+(根据callee save)将所有会用到的寄存器压入堆栈 (以保留他们的内容)。
+
+为可能用到的所有调用和操作分配足够的堆栈空间(从$esp中减去某个常数X,因为堆栈向下增加记录)。
+
+当一个函数完成后,它必须被拆解。收尾程序执行的是顺序相反的一系列相反动作:
+
+平仓栈(将X加回到$esp)。
+
+对所有用到的寄存器使用POP 指令(按照与他们被压入堆栈时相反的顺序)。
+
+将堆栈的开头数值POP回到$ebp。
+
+下面是以某一函数为例的一组序言和收尾的指令,分别在函数的开始和结尾:
+
+THE PROLOGUE.
+序言。
+
+
+##后记
+
+注意到当这两者连在一起时,会完全返回到函数时开始时的样子。
+
+函数参数在LLDB上
+
+上面所有的信息告诉我们以下信息:
+
+执行调用之前(在函数被调用之前)接收器在esp美元。
+
+执行调用后,接收器在$esp + 4上(这对于函数的第一个指令来说是没有问题的)。
+
+在将$ebp压入堆栈之后,接收器在esp + 8上(真正的第二个指令的功能)。
+
+一旦$esp 被移到$ebp,接收器在$esp + 8和$ebp + 8上(真在第三个指令)。
+
+在$esp被递减后,接收器在$ebp + 8上(某地在函数危机爆发之初就不一定是第四个指令)。
+
+听起来很复杂,不是吗?
+
+如果我们关注函数开始时,接收器是在$esp + 4上,这很简单。在上面提到过的[UIView setFrame:]中,参数在开始时会如下面所示:
+self = *(id *)($esp + 4)
+_cmd = *(SEL *)($esp + 8)
+frame = *(CGRect *)($esp + 12)
+
+计算过程是必要的,因为我们知道如果堆栈充满void *,它是不能引用的。也请注意,如果有更多的参数,下一个会在$esp + 28上,因为框架是16字节数据宽度的(它是一个CGRect,也就是一个CGPoint和 一个CGSize,总共包含4个32位浮点数)。
+
+这里有一个检查setFrame参数的例子,调用,然后使执行停在第一个指令 (这正是一个象征性的断点会带你去的地方;)。
+
+
+在一个objective - c调用中,找到所有的参数。
+
+注意, 在LLDB中,使用美元符号而不是百分号的标志来标记一个寄存器。:- p
+
+构建一个NSINVOCATION堆栈帧
+
+现在,我们知道所有的参数都在x86的哪个位置,我们可以用它们构建一个NSInvocation。为什么我们要构建NSInvocation呢?因为它包含了从寄存器和堆栈获得抓取参数到将其打包很好地呈现给我们(这可以用于转发!)的所有的逻辑。如果你想要了解关于如何以及为什么它能够实现的更多信息,请到核心基础的转发路径中深入研究。
+
+在帖中,你将看到一个私人NSInvocation选择器
+
+[NSInvocation _invocationWithMethodSignature:methodSignature
+[NSInvocation _invocationWithMethodSignature:methodSignature
+frame:frameStackPointer];
+框架:frameStackPointer];
+
+它用于建立传递forwardInvocation的调用。让我们用这个方法!我们需要的是一个方法签名和存储有所有参数的堆栈上的地址。
+So, first we construct the method signature, which is easy. We already know where self and _cmd are.
+所以,我们首先构造方法签名,这很容易。我们已经知道self和_cmd在哪里。
+
+[*(id *)($esp +4) methodSignatureForSelector:*(SEL *)($esp + 8)];
+
+我们需要传递的堆栈指针是$esp + 4 (也就是arg0,是接收器,其余的都要低于它!)。这就是我们所要做的。从那里我们可以使用私人方法(在调试器)来构造一个NSInvocation !
+PUTTING IT ALL TOGETHER
+把它放在一起
+
+至此,只剩下编写Python逻辑,然后把它加载到LLDB(之后你可以放松聚会)。有了Chisel, 这真的很简单。Subclass FBCommand, 运行 name() 以返回到 "pinvocation" ,然后运行 run(arguments, options)来做所有我们上面描述的所有操作。Chisel使得简化分析的参数和选项变得简单!:D
+
+如果你了解它的实际应用。欢迎您阅读的《实现pinvocation》
+OTHER ARCHITECTURES
+其他体系结构
+
+32位x86体系结构将所有参数压入堆栈,使它很容易及时找到所有参数 ($esp + 8,在第一次2指令之后)。
+
+在arm(32位和64位)和x84 – 64中, 根据一些complex-ish规则,参数在寄存器中传递。因为它们是在寄存器中,他们可能会在一个方法的实现中四处移动,这基本上使得我们无法确定它们能否被找到。然而,方法一开始时,所有参数方法都是可用的(根据架构的调用协定),并且pinvocation可以运行。您可能想要更多地利用转发路径(它可以在其他体系结构上将所有参数压入堆栈),所以你仍然可以使用+[NSInvocation _invocationWithMethodSignature:框架:]它需要一个包含所有参数的堆栈上的地址
+
+如果你不相信我,这是x86 – 64的转发路径,它在调用__forwarding__之前将所有参数压入堆栈 (这样就可以在需要时打包一个NSInvocation,如此来输入forwardInvocation:分支,也被称为“慢路径”)。
+
+CORE FOUNDATION'S FORWARDING HANDLER PUSHING EVERY REGISTER ONTO THE STACK BEFORE CALLING INTO __FORWARDING__.
+核心基础的转发处理程序将在调用__FORWARDING__之前把每个寄存器压入堆栈。
diff --git a/issue-17/2.md b/issue-17/2.md
new file mode 100644
index 0000000..ee21f41
--- /dev/null
+++ b/issue-17/2.md
@@ -0,0 +1,688 @@
+#如何实现iOS图书动画:第1部分
+
+
+ * 原文链接 : [How to Create an iOS Book Open Animation: Part 1](http://www.raywenderlich.com/94565/how-to-create-an-ios-book-open-animation-part-1)
+* 原文作者 : [Vincent Ngo](http://www.raywenderlich.com/u/jomoka)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [](https://github.com/kmyhy)
+* 校对者: [lastdays](https://github.com/MrLoong)
+* 状态 : 校对中
+
+
+
+
+本教程分为2个部分,教你开发一个漂亮的iOS图书打开和翻页动画,就像你在Paper 53中所见到的一样:
+
+
+在第1部分,你将学习到如何定制化Collection View Layout,并通过使用深度和阴影使App看起来更真实。
+
+
+在第2部分,你将学习如何以一种合理的方法在两个不同的控制器之间创建自定义的过渡特效,以及利用手势在两个视图间创建自然的、直观的过渡效果。
+
+
+本教程适用于中级-高级的开发者;你将使用自定义过渡动画和自定义Collection View Layout。如果你从来没有用过Colleciton View,请先参考[其他iOS教程](http://www.raywenderlich.com/tutorials)。
+
+
+>
+> 注意:感谢[Attila Hegdüs](https://twitter.com/hegedus90)创建了本教程中的示例项目。
+
+##Getting Started
+##开始
+
+从[此处](http://cdn2.raywenderlich.com/wp-content/uploads/2015/05/Starter-Paper1.zip)下载本教程的开始项目;解开zip压缩包,用Xcode打开Paper.xcodeproj。
+
+编译项目,在模拟器中运行App;你将看到如下画面:
+
+
+
+
+这个App的功能已经很完善了,你可以在你的书库中滚动,查看图书,选中某本图书进行浏览。但当你读一本书的时候,为什么它的书页都是并排放置的?通过一些UICollectionView的知识,你可以让这些书页看起来更好一些!
+
+##The Project Structure
+##项目结构
+
+Here’s a quick rundown of the most important bits of the starter project:
+
+关于这个开始项目,有几个重要的地方需要解释:
+
+
+Data Models文件夹包含3个文件:
+
+- Books.plist 中包含了几本用于演示的图书信息。每本图书包含一张封面图片,以及一个表示每一页的内容的图片的数组。
+- BookStore.swift实现了单例,在整个App声明周期中只能创建一次对象。BookStore的职责是从Books.plist中加载数据并创建Book类实例。
+- Book.swift用于存放图书相关信息的类,比如图书的封面,每一页的图片,以及页号。
+
+
+
+Books文件夹包含了两个文件:
+
+- BooksViewController.swift是一个UICollectionViewController子类。负责以水平方式显式图书列表。
+- BookCoverCell.swift负责显示图书的封面,这个类被BooksViewController类所引用。
+
+
+
+在Book文件夹中则包括:
+
+- BookViewController.swift也是UICollectionViewController的子类。当用户在BooksViewController中选定的一本书后,它负责显示图书中的书页。
+- BookPageCell.swift被BookViewController用于显示图书中的书页。
+
+
+在最后一个文件夹Helper中包含了:
+
+- UIImage+Helpers.swift是UIImage的扩展。该扩展包含了两个实用方法,一个用于让图片呈圆角显示,一个用于将图片缩放到指定大小。
+
+
+这就是整个开始项目的大致介绍——接下来该是我们写点代码的时候了!
+
+
+##定制化图书界面
+
+
+首先我们需要在BooksViewController中覆盖Collection View的默认布局方式。但当前的布局是在屏幕上显示3张图书封面的大图。为了美观,我们将这些图片缩减到一定大小,如下图所示:
+
+
+
+
+当我们滑动图片,移动到屏幕中心的图片将被放大,以表示该图书为选中状态。如果继续滑动,该图书的封面又会缩小到一边,表示我们放弃选择该图书。
+
+
+在App\Books文件夹下新建一个文件夹组:Layout。在Layout上点击右键,选择New File...,然后选择iOS\Source\Cocoa Touch Class模板,并点击Next。类名命名为BooksLayout,继承UICollectionViewFlowLayout类,语言设置为Swift。
+
+
+然后需要告诉BooksViewController中的Collection View,适用我们新建的BooksLayout。
+
+
+打开Main.storyboard,展开BooksViewController对象,然后选择Collection View。在属性面板中,设置Layout 属性为 Custom,设置Class属性为BooksLayout,如下图所示:
+
+
+
+
+打开BooksLayout.swift,在BooksLayout类声明之上加入以下代码:
+
+```
+private let PageWidth: CGFloat = 362
+private let PageHeight: CGFloat = 568
+```
+
+
+这个两个常量将用于设置单元格的的大小。
+现在,在类定义内部定义如下初始化方法:
+
+```
+required init(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+
+ scrollDirection = UICollectionViewScrollDirection.Horizontal //1
+ itemSize = CGSizeMake(PageWidth, PageHeight) //2
+ minimumInteritemSpacing = 10 //3
+}
+```
+
+
+
+上述代码作用如下:
+
+ 1. 设置Collectioin View的滚动方向为水平方向。
+ 2. 设置单元格的大小为PageWidth和PageHeight,即362x568。
+ 3. 设置两个单元格间距10。
+
+
+然后,在init(coder:)方法中加入代码:
+
+```
+override func prepareLayout() {
+ super.prepareLayout()
+
+ //The rate at which we scroll the collection view.
+ //1
+ collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
+
+ //2
+ collectionView?.contentInset = UIEdgeInsets(
+ top: 0,
+ left: collectionView!.bounds.width / 2 - PageWidth / 2,
+ bottom: 0,
+ right: collectionView!.bounds.width / 2 - PageWidth / 2
+ )
+}
+```
+
+
+prepareLayout()方法允许我们在每个单元格的布局信息生效之前可以进行一些计算。
+
+
+对应注释中的编号,以上代码分别说明如下:
+
+
+ 1. 设置当用户手指离开屏幕后,Collection
+ View停止滚动的速度。默认的设置为UIScrollViewDecelerationRateFast,这是一个较快的速度。你可以尝试着设置为Normal 和 Fast,看看二者之间有什么区别。
+ 2. 设置Collection View的contentInset,以使第一本书的封面位于Collection View的中心。
+
+
+现在我们需要处理每一个单元格的布局信息。
+在prepareLayout()方法下面,加入以下代码:
+
+```
+override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
+ //1
+ var array = super.layoutAttributesForElementsInRect(rect) as! [UICollectionViewLayoutAttributes]
+
+ //2
+ for attributes in array {
+ //3
+ var frame = attributes.frame
+ //4
+ var distance = abs(collectionView!.contentOffset.x + collectionView!.contentInset.left - frame.origin.x)
+ //5
+ var scale = 0.7 * min(max(1 - distance / (collectionView!.bounds.width), 0.75), 1)
+ //6
+ attributes.transform = CGAffineTransformMakeScale(scale, scale)
+ }
+
+ return array
+}
+```
+
+
+
+layoutAttributesForElementsInRect(_:) 方法返回一个UICollectionViewLayoutAttributes对象数组,其中包含了每一个单元格的布局属性。以上代码稍作说明如下:
+
+ 1. 调用父类的layoutAttributesForElementsInRect方法,已获得默认的单元格布局属性。
+ 2. 遍历数组中的每个单元格布局属性。
+ 3. 从单元格布局属性中读取frame。
+ 4. 计算两本书的封面之间的间距——即两个单元格之间的间距——以及屏幕的中心点。
+ 5. 以0.75~1之间的比率缩放封面,具体的比率取决于前面计算出来的间距。然后为了美观,将所有的封面都缩放70%。
+ 6. 最后,应用仿射变换。
+
+
+接下来,在layoutAttributesForElementsInRect(_:)方法后增加如下代码:
+
+```
+override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
+ return true
+}
+```
+
+
+返回true表示每当Collection View的bounds发生改变时都强制重新计算布局属性。Collection View在滚动时会改变它的bounds,因此我们需要重新计算单元格的布局属性。
+
+
+编译运行程序,我们将看到位于中央的封面明显比其他封面要大上一圈:
+
+
+
+
+拖动Colleciton View,查看每本书放大、缩小。但仍然有一点稍显不足,为什么不让书本能够卡到固定的位置呢?
+接下来我们介绍的这个方法就是干这个的。
+
+Snapping to a Book
+
+##对齐书本
+
+
+targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:)方法用于计算每本书应该在对齐到哪个位置,它返回一个偏移位置,可用于设置Collection View的contentOffset。如果你不覆盖这个方法,它会返回一个默认的值。
+
+
+在shouldInvalidateLayoutForBoundsChange(_:)方法后添加如下代码:
+
+```
+override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
+ // Snap cells to centre
+ //1
+ var newOffset = CGPoint()
+ //2
+ var layout = collectionView!.collectionViewLayout as! UICollectionViewFlowLayout
+ //3
+ var width = layout.itemSize.width + layout.minimumLineSpacing
+ //4
+ var offset = proposedContentOffset.x + collectionView!.contentInset.left
+
+ //5
+ if velocity.x > 0 {
+ //ceil returns next biggest number
+ offset = width * ceil(offset / width)
+ } else if velocity.x == 0 { //6
+ //rounds the argument
+ offset = width * round(offset / width)
+ } else if velocity.x < 0 { //7
+ //removes decimal part of argument
+ offset = width * floor(offset / width)
+ }
+ //8
+ newOffset.x = offset - collectionView!.contentInset.left
+ newOffset.y = proposedContentOffset.y //y will always be the same...
+ return newOffset
+}
+```
+
+
+这段代码计算当用户手指离开屏幕时,封面应该位于哪个偏移位置:
+
+
+
+ 1. 声明一个CGPoint。
+ 2. 获得Collection View的当前布局。
+ 3. 获得单元格的总宽度。
+ 4. 计算相对于屏幕中央的currentOffset。
+ 5. 如果velocity.x>0,表明用户向右滚动,用offset除以width,得到书的索引,并滚动到相应的位置。
+ 6. 如果velocity.x=0,表明用户是无意识的滚动,原来的选择不会发生改变。
+ 7. 如果velocity.x<0,表明用户向左滚动。
+ 8. 修改newOffset.x,然后返回newOffset。这样就保证书本总是对齐到屏幕的中央。
+
+
+
+编译运行程序;再次滚动封面,你会注意到滚动动作将变得更整齐了。
+
+
+
+要完成这个布局,我们还需要使用一种机制,以限制用户只能点击位于中央的封面。目前,不管哪个位置的封面都是可点击的。
+
+
+
+打开BooksViewController.swift,在注释"//MARK:Helpers"下面加入以下代码:
+
+```
+func selectedCell() -> BookCoverCell? {
+ if let indexPath = collectionView?.indexPathForItemAtPoint(CGPointMake(collectionView!.contentOffset.x + collectionView!.bounds.width / 2, collectionView!.bounds.height / 2)) {
+ if let cell = collectionView?.cellForItemAtIndexPath(indexPath) as? BookCoverCell {
+ return cell
+ }
+ }
+ return nil
+}
+```
+
+
+selectedCell()方法返回位于中央的那个单元格。
+替换openBook(_:)方法的代码如下:
+
+```
+func openBook() {
+ let vc = storyboard?.instantiateViewControllerWithIdentifier("BookViewController") as! BookViewController
+ vc.book = selectedCell()?.book
+ // UICollectionView loads it's cells on a background thread, so make sure it's loaded before passing it to the animation handler
+ dispatch_async(dispatch_get_main_queue(), { () -> Void in
+ self.navigationController?.pushViewController(vc, animated: true)
+ return
+ })
+}
+```
+
+
+
+这里,直接调用新的selectedCell方法,并用它的book属性代替原来的book参数。
+然后,将collectionView(_:didSelectItemAtIndexPath:)方法替换为:
+
+```
+override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
+ openBook()
+}
+```
+
+
+这里,我们简单地删除了原来的打开某个索引处的图书的代码,而直接打开了当前位于屏幕中央的图书。
+编译运行程序,我们将看到每次打开的图书总是位于屏幕中央的那本。
+
+
+BooksLayout的工作就到这里了。后面我们将使这本电子书显得更真实,能够让用户”翻动“书里的每一页!
+
+##翻页布局
+
+
+最终实现的效果如下:
+
+
+
+这看起来就像是一本真正的书! :]
+
+
+在Book文件夹下新建一个Layout文件夹。在Layout文件夹上右键,选择New File...,然后适用iOS\Source\Cocoa Touch Class模板,然后点Next。类名命名为BookLayout,继承于UICollectionViewFlowLayout,语言选择Swift。
+
+
+同前面一样,图书所使用的Collection View需要适用新的布局。打开Main.storyboard,然后选择Book View Controller场景,展开并选中其中的Collection View,然后设置Layout属性为Custom。
+
+然后,将Layout属性下的 Class属性设置为BookLayout:
+
+
+
+
+打开BookLayout.swift,在类声明之上加入如下代码:
+
+```
+private let PageWidth: CGFloat = 362
+private let PageHeight: CGFloat = 568
+private var numberOfItems = 0
+```
+
+
+这几个常量将用于设置单元格的大小,以及记录整本书的页数。
+接着,在类声明内部加入代码:
+
+```
+override func prepareLayout() {
+ super.prepareLayout()
+ collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
+ numberOfItems = collectionView!.numberOfItemsInSection(0)
+ collectionView?.pagingEnabled = true
+}
+```
+
+
+这段代码和我们在BooksLayout中所写的差不多,仅有以下几处差别:
+
+
+ 1. 将减速速度设置为UIScrollViewDecelerationRateFast,以加快Scroll View滚动速度变慢的节奏。
+ 2. 记住本书的页数。
+ 3. 启用分页,这样Scroll View滚动时将以其宽度的固定倍数滚动(而不是持续滚动)。
+
+
+仍然在BookLayout.swift中,加入以下代码:
+
+```
+override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
+ return true
+}
+```
+
+
+跟前面一样,返回true,以表示用户每次滚动都会重新计算布局。
+
+
+然后,覆盖collectionViewContentSize() ,以指定Collection View的contentSize:
+
+```
+override func collectionViewContentSize() -> CGSize {
+ return CGSizeMake((CGFloat(numberOfItems / 2)) * collectionView!.bounds.width, collectionView!.bounds.height)
+}
+```
+
+
+这个方法返回了内容区域的整个大小。内容区域的高度总是不变的,但宽度是随着页数变化的——即书的页数除以2倍,再乘以屏幕宽度。除以2是因为书页有两面,内容区域一次显示2页。
+
+
+
+就如我们在BooksLayout中所做的一样,我们还需要覆盖layoutAttributesForElementsInRect(_:)方法,以便我们能够在单元格上增加翻页效果。
+
+
+在collectionViewContentSize()方法后加入:
+
+```
+override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
+ //1
+ var array: [UICollectionViewLayoutAttributes] = []
+
+ //2
+ for i in 0 ... max(0, numberOfItems - 1) {
+ //3
+ var indexPath = NSIndexPath(forItem: i, inSection: 0)
+ //4
+ var attributes = layoutAttributesForItemAtIndexPath(indexPath)
+ if attributes != nil {
+ //5
+ array += [attributes]
+ }
+ }
+ //6
+ return array
+}
+```
+
+
+
+不同于在BooksLayout中的将所有计算布局属性的代码放在这个方法里面,我们这次将这个任务放到layoutAttributesForItemAtIndexPath(_:)中进行,因为在图书的实现中,所有单元格都是同时可见的。
+
+Here's a line by line explanation:
+
+以上代码解释如下:
+
+ 1. 声明一个数组,用于保存所有单元格的布局属性。
+ 2. 遍历所有的书页。
+ 3. 对于CollecitonView中的每个单元格,都创建一个NSIndexPath。
+ 4. 通过每个NSIndexPath来获得单元格的布局属性,在后面,我们会覆盖layoutAttributesForItemAtIndexPath(_:)方法。
+ 5. 把每个单元格布局属性添加到数组。
+ 6. 返回数组。
+
+##Handling the Page Geometry
+##处理页面的几何计算
+
+在我们开始实现layoutAttributesForItemAtIndexPath(_:)方法之前,花几分钟好好思考一下布局的问题,它是怎样实现的,如果能够写几个助手方法将会让我们的代码更漂亮和模块化。:]
+
+
+
+
+上图演示了翻页时以书籍为轴旋转的过程。图中书页的”打开度“用-1到1来表示。为什么?你可以想象一下放在桌子上的一本书,书脊所在的位置代表0.0。当你从左向右翻动书页时,书页张开的程度从-1(最左)到1(最右)。因此,我们可以用下列数字表示“翻页”的过程:
+
+ 1. 0.0表示一个书页翻成90度,与桌面成直角。
+ 2. +/-0.5表示书页翻至于桌面成45度角。
+ 3. +/-1.0表示书页翻至与桌面平行。
+
+
+注意,因为角度是按照反时针方向增加的,因此角度的符号和对应的打开度是相反的。
+
+
+首先,在layoutAttributesForElementsInRect(_:)方法后加入助手方法:
+
+```
+//MARK: - Attribute Logic Helpers
+
+func getFrame(collectionView: UICollectionView) -> CGRect {
+ var frame = CGRect()
+
+ frame.origin.x = (collectionView.bounds.width / 2) - (PageWidth / 2) + collectionView.contentOffset.x
+ frame.origin.y = (collectionViewContentSize().height - PageHeight) / 2
+ frame.size.width = PageWidth
+ frame.size.height = PageHeight
+
+ return frame
+}
+```
+
+
+
+对于每一页,我们都可以计算出相对于Collection View中心的frame。getFrame(_:)方法会将每一页的一边对齐到书脊。唯一会变的是Collectoin View的contentOffset在x方向上的改变。
+
+
+
+然后,在getFrame(_:)方法后添加如下方法:
+
+```
+func getRatio(collectionView: UICollectionView, indexPath: NSIndexPath) -> CGFloat {
+ //1
+ let page = CGFloat(indexPath.item - indexPath.item % 2) * 0.5
+
+ //2
+ var ratio: CGFloat = -0.5 + page - (collectionView.contentOffset.x / collectionView.bounds.width)
+
+ //3
+ if ratio > 0.5 {
+ ratio = 0.5 + 0.1 * (ratio - 0.5)
+
+ } else if ratio < -0.5 {
+ ratio = -0.5 + 0.1 * (ratio + 0.5)
+ }
+
+ return ratio
+}
+```
+
+
+
+上面的方法计算书页翻开的程度。对每一段有注释的代码分别说明如下:
+
+ 1. 算出书页的页码——记住,书是双面的。 除以2就是你真正在翻读的那一页。
+ 2. 算出书页的打开度。注意,这个值被我们加了一个权重。
+ 3. 书页的打开度必须限制在-0.5到0.5之间。另外乘以0.1的作用,是为位了在页与页之间增加一条细缝,以表示它们是上下叠放在一起的。
+
+一旦我们计算出书页的打开度,我们就可以将之转变为旋转的角度。
+在getRation(_:indexPath:)方法后面加入代码:
+
+```
+func getAngle(indexPath: NSIndexPath, ratio: CGFloat) -> CGFloat {
+ // Set rotation
+ var angle: CGFloat = 0
+
+ //1
+ if indexPath.item % 2 == 0 {
+ // The book's spine is on the left of the page
+ angle = (1-ratio) * CGFloat(-M_PI_2)
+ } else {
+ //2
+ // The book's spine is on the right of the page
+ angle = (1 + ratio) * CGFloat(M_PI_2)
+ }
+ //3
+ // Make sure the odd and even page don't have the exact same angle
+ angle += CGFloat(indexPath.row % 2) / 1000
+ //4
+ return angle
+}
+```
+
+
+这个方法中有大量计算,我们一点点拆开来看:
+
+ 1. 判断该页是否是偶数页。如果是,则该页将翻到书脊的右边。翻到右边的页是反手翻转,同时书脊右边的页其角度必然是负数。注意,我们将打开度定义为-0.5到0.5之间。
+ 2. 如果当前页是奇数,则该页将位于书脊左边,当书页被翻到左边时,它的按正手翻转,书脊左边的页其角度为正数。
+ 3. 每页之间加一个小夹角,使它们彼此分离。
+ 4. 返回旋转角度。
+
+
+得到旋转角度之后,我们可以操纵书页使其旋转。增加如下方法:
+
+```
+func makePerspectiveTransform() -> CATransform3D {
+ var transform = CATransform3DIdentity
+ transform.m34 = 1.0 / -2000
+ return transform
+}
+```
+
+修改转换矩阵的m34属性,已达到一定的立体效果。
+
+
+然后应用旋转动画。实现下面的方法:
+
+```
+func getRotation(indexPath: NSIndexPath, ratio: CGFloat) -> CATransform3D {
+ var transform = makePerspectiveTransform()
+ var angle = getAngle(indexPath, ratio: ratio)
+ transform = CATransform3DRotate(transform, angle, 0, 1, 0)
+ return transform
+}
+```
+
+在这个方法中,我们用到了刚才创建的两个助手方法去计算旋转的角度,然后通过一个CATransform3D对象让书页在y轴上旋转。
+
+所有的助手方法都实现了,我们最终需要配置每个单元格的属性。在layoutAttributesForElementsInRect(_:)方法后加入以下方法:
+
+```
+override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
+ //1
+ var layoutAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
+
+ //2
+ var frame = getFrame(collectionView!)
+ layoutAttributes.frame = frame
+
+ //3
+ var ratio = getRatio(collectionView!, indexPath: indexPath)
+
+ //4
+ if ratio > 0 && indexPath.item % 2 == 1
+ || ratio < 0 && indexPath.item % 2 == 0 {
+ // Make sure the cover is always visible
+ if indexPath.row != 0 {
+ return nil
+ }
+ }
+ //5
+ var rotation = getRotation(indexPath, ratio: min(max(ratio, -1), 1))
+ layoutAttributes.transform3D = rotation
+
+ //6
+ if indexPath.row == 0 {
+ layoutAttributes.zIndex = Int.max
+ }
+
+ return layoutAttributes
+}
+```
+
+
+在Collection View的每个单元格上,都会调用这个方法。这个方法做了如下工作:
+
+ 1. 创建一个UICollectionViewLayoutAttributes对象layoutAttributes,供IndexPath所指的单元格使用。
+ 2. 调用我们先前定义的getFrame方法设置layoutAttributes的frame,确保单元格对齐于书脊。
+ 3. 调用先前定义的getRatio方法算出单元格的打开度。
+ 4. 判断当前页是否位于正确的打开度范围之内。如果不,不显示该单元格。为了优化(也是为了符合常理),除了正面向上的书页,我们不应当显示书页的背面——书的封面例外,那个不管什么时候都需要显示。
+ 5. 应用旋转动画,使用前面算出的打开度。
+ 6. 判断是否是第一页,如果是,将它的zIndex放在其他页的上面,否则有可能出现画面闪烁的Bug。
+
+编译,运行。打开书,翻动每一页……呃?什么情况?
+
+
+
+书被错误地从中间装订了,而不是从书的侧边装订。
+
+
+
+
+如图中所示,每个书页的锚点默认是x轴和y轴的0.5倍处。现在你知道怎么做了吗?
+
+
+很显然,我们需要修改书页的锚点为它的侧边缘。如果这个书页是位于书的右边,则它的锚点应该是(0,0.5)。如果书页是位于书的左边,测锚点应该是(1,0.5)。
+
+
+打开BookePageCell.swift,添加如下代码:
+
+```
+override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
+ super.applyLayoutAttributes(layoutAttributes)
+ //1
+ if layoutAttributes.indexPath.item % 2 == 0 {
+ //2
+ layer.anchorPoint = CGPointMake(0, 0.5)
+ isRightPage = true
+ } else { //3
+ //4
+ layer.anchorPoint = CGPointMake(1, 0.5)
+ isRightPage = false
+ }
+ //5
+ self.updateShadowLayer()
+}
+```
+
+
+我们重写了applyLayoutAttributes(_:)方法,这个方法用于将BookLoayout创建的布局属性应用到第一个。
+上述代码非常简单:
+
+ 1. 检查当前单元格是否是偶数,也就是说书脊是否位于书页的左边。
+ 2. 如果是,将书页的锚点设置为单元格的左边缘,同时isRightPage设置为true。isRightPage变量可用于决定书页的圆角样式应当在那一边应用。
+ 3. 如果是奇数页,书脊应当位于书页的右边。
+ 4. 这只书页的锚点为单元格的右边缘,isRightPage设为false。
+ 5. 最后,设置当前书页的阴影层。
+
+
+编译,运行。翻动书页,这次的效果已经好了许多:
+
+
+
+
+本教程第一部分就到此为止了。你是不是觉得自己干了一件了不起的事情呢——效果做得很棒,不是吗?
+
+##接下来做什么
+
+
+第一部分的完整代码在此处下载:http://cdn1.raywenderlich.com/wp-content/uploads/2015/05/Part-1-Paper-Completed.zip
+
+
+我们从一个默认的Collection View布局开始,学习了如何定制自己的layout,并用它取得了令人叫绝的效果!用户在用这个App时,会觉得自己真的是在翻一本书。其实,将一个普通的电子阅读App变成一个让用户体验更加真实的带翻页效果的App,只需要做很小的改动。
+
+当然,我们还没有完成这个App。在本教程的第二部分,我们将使App变得更加完善和生动,我们将在打开书和关上书的一瞬间加入自定义动画。
+
+
+在开发App过程中,你也可能曾经有过一些关于布局的“疯狂”想法。如果你对本文有任何问题、意见或想法,请加入到下面的讨论中来!
+
+
+
diff --git a/issue-17/readme.md b/issue-17/readme.md
new file mode 100644
index 0000000..ab6fcc4
--- /dev/null
+++ b/issue-17/readme.md
@@ -0,0 +1,4 @@
+add issue-17
+
+
+
diff --git "a/issue-17/\345\210\233\345\273\272\350\207\252\346\263\250\345\206\214\347\232\204Swift UI \346\216\247\344\273\266.md" "b/issue-17/\345\210\233\345\273\272\350\207\252\346\263\250\345\206\214\347\232\204Swift UI \346\216\247\344\273\266.md"
new file mode 100644
index 0000000..af05f46
--- /dev/null
+++ "b/issue-17/\345\210\233\345\273\272\350\207\252\346\263\250\345\206\214\347\232\204Swift UI \346\216\247\344\273\266.md"
@@ -0,0 +1,187 @@
+> * 原文链接 : [Swift Programming 101: Creating Self-Registering Swift UI Controls](http://www.iphonelife.com/blog/31369/swift-programming-101-creating-self-registering-swift-ui-controls)
+* 原文作者 : [Kevin McNeish](http://www.iphonelife.com/blog/kevin-mcneish)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [kmyhy](https://github.com/kmyhy)
+
+What’s the best way to create controls that respond to notifications without interfering with existing messaging mechanisms? In this article I demonstrate a Notification Proxy class that helps you add functionality to existing iOS UI controls using a practical example: Adding placeholder text to the text view control.
+
+对于自定义控件来说,在不破坏原有的消息机制的前提下,如何响应事件通知?在本文中,我将演示一个通知代理类,通过一个简单的例子,我们用该类向已有的iOS UI控件中增加了自己的新功能:为Text View控件增加placeholder文本。
+
+##The Problem: Missing Placeholders
+##问题:缺失的Placeholder
+Placeholder text is a great way to let your users known the type of content you intend them to enter in a particular control. UIKit’s UITextField control has a placeholder attribute that lets you do this. For example, Figure 1 shows placeholder text in the Twitter settings screen. The placeholder text in the User Name field indicates you should enter your Twitter handle, including the @ sign. The placeholder text in the Password field indicates it is required.
+
+Placeholder文本是用于在某些控件中提示用户输入指定类型的数据的良好方式。UIKit的UITextField控件的placeholder属性就是用来干这个的。例如,下图中,在Twitter的选项设置中,User Name字段就使用了placeholder文本。通过这个placeholder文本,用户可以知道应当在这里输入一个包含了@符号的Twitter用户名。而在Password字段的placeholder文本则标明该字段是必填的。
+
+
+图 1 - 设置程序中的Placeholder 文本
+
+Although the text field has a placeholder attribute, its cousin the text view control, does not.
+
+
+尽管Text Filed有placeholder属性,但它的兄弟控件Text View却没有这个属性。
+
+##Solving the Problem
+##问题的解决
+
+Here’s the big picture of how I planned to solve this problem:
+
+ 我准备这样解决这个问题:
+
+1. Create an extension that adds a calculated property named placeholder to the UITextView class
+2. In the extension, display the label when there is no text in the text view, and hide it when there is text.
+
+---
+
+1. 创建一个UITextView的扩展,添加一个placeholder的计算属性
+2. 在扩展中,当TextView中没有输入任何文字的情况下,显示一个Label,如果输入有文字,则隐藏这个Label。
+
+As mentioned in my previous post, I prefer using extensions rather than subclasses because it allows me to use the out-of-the-box UIKit controls in my apps. All I have to do is add the extension code file to my project and all my classes automatically gain the new functionality.
+
+就像我在前面的文章中提到的,我更喜欢使用扩展,而不是子类。因为扩展允许我在App中使用“盒子之外”的UIKit控件。我需要做的仅仅是在项目中加入某个类的扩展文件,然后这个类会自动获得新的特性。
+
+##Solution 1: UITextViewDelegate
+##方式1:UITextViewDelegate
+
+There are a few ways iOS can notify you when the user types text in a UITextView control. One of these is to adopt the UITextViewDelegate protocol in the extension and store a reference to the text view in its own delegate property. When the user types or deletes characters in the text view at run time, the text view’s delegate method is automatically called. You can then add code to the delegate method that hides or shows the placeholder label.
+
+当用户在TextView中输入文本时,iOS有几种通知App的方式。其中一种就是通过UITextViewDelegate协议。在这个扩展中有一个对TextView的引用保存在它自己的delegate属性中。当用户输入或删除字符时,TextView的协议方法会被调用。我们可以在协议方法中加入隐藏和显示placeholder标签的代码。
+
+
+The problem with this approach is that it ties up the text view’s delegate property, since you can only register a single delegate object with a UI control. If you need to add other delegates to the text view in the future, your hands are tied.
+
+这个解决方法的缺点是,Text View太过于依赖delegate属性,因为在一个UIControl中你只能注册一个委托对象。如果你需要向TextView中加入更多的委托对象,你就会比较麻烦。
+
+##Solution 2: NSNotificationCenter
+##方式2:NSNotificationCenter
+
+NSNotificationCenter has a UITextViewTextDidChangeNotification that alerts you when the user types or deletes characters in a text view. This is a better choice because it doesn’t tie up the text view’s delegate property. The downside of this approach is unregistering from NSNotificationCenter. Typically, you deregister an object from NSNotificationCenter by adding code to its deinit. However, in Swift you can’t add a deinit to an extension. How can we get around this limitation?
+
+NSNotificationCenter通过UITextViewTextDidChangeNotification通知来告诉你用户在TextView中输入或删除了某些字符。这是一种更好的选择,因为它不再依赖于delegate属性。缺点是需要向通知中心进行注销。一般,我们在对象的deinit方法中向NSNotificationCenter注销该对象。但是在Swift中,我们无法在扩展中使用deinit方法。那我们怎样才能突破这个限制呢?
+
+##Creating a Notification Proxy
+##创建一个通知代理
+We can get around the limitations of Swift extensions by creating a lightweight proxy object that is registered with NSNotificationCenter in place of the text view.
+
+我们通过创建一个轻量级的代理对象来突破这个限制,这个代理对象代替TextView来向通知中心进行注册。
+
+
+I’ve created a UITextView extension along with a proxy class and included it in a project you can download from this link.
+
+我已经在我的项目中创建了一个UITextView扩展以及一个代理类,你可以从[这里](http://www.iosappsfornonprogrammers.com/media/blog/NotificationProxyDemo.zip)下载。
+
+You can open the project in Xcode by double-clicking the .xcodeproj file. To see the Notification Proxy class, select the mmTextViewExtensions.swift class file in the Project Navigator (as you can see, I included the mmDynamicTypeExtensions class from my previous article).
+
+双击.xcodeproj文件,用Xcode打开这个项目。在项目导航窗口中选中mmTextViewExtensions.swift文件,你可以找到这个通知代理类(如你所见,其中也包含了上一篇文章中的mmDyamicTypeExtensions类)。
+
+Here is the Notification Proxy class located at the top of the code file:
+
+下面是位于文件头部的通知代理类:
+
+
+
+As you can see, it’s a subclass of UIView. This allows us to add it as a subview to the text view control. The addObserverForName:usingBlock: method has the same signature (name, number, and type of parameters) as NSNotificationCenter’s method. There is just a single line of code in this method that registers the text view with NSNotificationCenter and passes through the parameters including the text view’s block to be executed when a notification occurs. NSNotificationCenter returns an NSObjectProtocol object that can be later used to unregister from notifications.
+
+你可以看到,它是UIView子类。这样我们就可以将它当成subview添加到TextView中。addObserverForName:usingBLock:方法通知中心的方法拥有一样的签名。这个方法中只有一行代码,就是将TextView注册到通知中心并将参数传递过去,包括TextView的闭包,这样当通知发生时该闭包被执行。通知中心会返回一个NSObjectProtocol对象,稍后我们会用来进行通知的注销操作。
+
+The deinit has a single line of code that simply deregisters the protocol object from NSNotificationCenter.
+
+deinit方法也只有一行代码,仅仅是向通知中心进行对象的注销。
+
+The Notification Proxy class is reusable. You can use it for any notification and with any class you want to receive notifications.
+
+通知代理类是可重用的。你可以用于任何类型的通知以及你想接收通知的任何类型的对象。
+
+Below the Notification Proxy in the code file is the UITextView extension. It has a placeholderLabel property that, as you might guess, is used to contain a reference to the placeholder label.
+
+然后是UITextView的扩展,在扩展中有一个placeholderLabel属性,如你所想,它引用了一个placeholder标签对象。
+
+It also has a placeholder String property. As you will see in the next section, all the magic happens when the placeholder property is set at run time and the computed property's code is executed.
+
+此外还有一个placeholder的字符串属性。在后面你将看到,所有的奇迹将在运行时发生,当placeholder属性被设置,这个计算属性的代码就会执行。
+
+Now select the Main.storyboard file in the Project Navigator. As shown in Figure 2, there is a single scene in the storyboard with a text view located near the top of the scene.
+
+现在打开Main.storyboard文件。如图2所示,故事板中只有一个Scene,在这个Scene的顶部,有一个Text View。
+
+
+图 2 - 主 scene 中包含了一个 text view
+
+If you select the text view and go to the Attributes Inspector, you can see the Placeholder attribute, which is added by the extension and its value is set to “Enter your text here!”. The placeholder text doesn’t appear in the design surface (we would need to take a number of extra steps to make this happen, and that’s a topic for another article) but it does appear at run time.
+
+当你选择Text View,然后打开属性面板,你将看见一个Placeholder属性,它是扩展中增加的属性,这个属性的默认值是“Enter your text here!”。在设计时不会显示这段文本(要在设计时可见,我们需要花费大量的工作,因此这不是本文的主题),但在运行时它会显示。
+
+##The Notification Proxy at Run Time
+##运行时的通知代理
+
+The UML sequence diagram in Figure 3 shows the order of messages passed between all objects involved in this architecture.
+
+图3中的UML序列图显示了所有对象之间发生的消息传递的顺序。
+
+
+
+图 3 - 运行时对象间消息传递的顺序
+
+Here is an explanation of each step:
+
+每一步的解释如下:
+
+1. When the UITextView placeholder text is set at run time it fires the extension’s placeholder computed property’s code.
+2. The UITextView extensions’s addPlaceholderLabel method is called.
+3. The addPlaceholderLabel method creates a placeholder label and sets its text.
+4. The placeholder label is added to the UITextView.
+5. A Notification Proxy object is created.
+6. The UITextView extension calls the Notification Proxy’s addObserverForName:withBlock: method, specifying UITextViewTextDidChangeNotification as the name of the notification it wants to observe. It also passes a block of code to be executed when the notification occurs.
+7. The Notification Proxy object registers the UITextView with NSNotificationCenter, also passing through the notification event to be observed and the UITextView block to be executed when the notification occurs.
+8. The UITextView object adds the Notification Proxy to itself as a subview.
+9. When a UITextViewTextDidChangeNotification occurs, NSNotificationCenter calls the block on the UITextView extension.
+10. The UITextView extension sets the placeholder label’s hidden property to true or false, depending on whether or not there is any text entered in the text view.
+11. When the UITextView is deallocated at run time, the placeholder label and notification proxy object are deallocated.
+12. When the Notification Proxy object is deallocated, it unregisters the UITextView from NSNotificationCenter.
+
+---
+
+1. 当运行时,UITextView的placeholder文本被改变时,该扩展的placeholder计算属性中的代码被触发。
+2. 扩展的addPlaceholderLabel方法被调用。
+3. addPlaceholderLabel方法创建一个placeholder标签并设置标签文本。
+4. placeholder标签被添加进UITextView。
+5. 一个通知代理对象被创建。
+6. 扩展调用通知代理的addObserverForName:withBlock:方法,并指定对UITextViewTextDidChangeNotification通知感兴趣。此外闭包代码会在通知发生时执行。
+7. 通知代理对象将UITextView注册到通知中心,同时将通知时间传递给TextView,当通知发生时,UITextView的闭包代码将被执行。
+8. UITextView以subview的方式添加通知代理对象。
+9. 当UITextViewTextDidChangeNotification通知发生,通知中心调用TextView扩展的闭包。
+10. UITextView扩展将placeholder标签的hidden属性设置为true和false,取决于TextView中的文本是否有内容。
+11.当TextView在运行时解构时,placeholder标签和通知代理对象都被解构。
+11. 当通知代理对象解构时,将TextView对象从通知中心中注销。
+
+##Let’s Take it for a Test Run!
+##运行测试
+
+1. In Xcode’s Scheme control, select one of the simulators such as iPhone 6.
+在Xcode的Scheme下拉列表中,选择一个模拟器,例如iPhone6。
+
+2. Click Xcode’s Run button.
+点击Xcode的Run按钮。
+
+3. When the app appears in the Simulator, you can see the placeholder text (Figure 4).
+当App在模拟器中运行后,我们可以看到placeholder文本(图4)。
+
+
+
+图 4 - 运行时的 placeholder 文本
+
+ 4.Now type some code in the text view. As soon as you begin typing, the placeholder label disappears (Figure 5).
+ 现在在TextView中输入任意字符。此时,placeholder将消失(图5)。
+
+
+
+图 5 -placeholder 文字消失了
+
+ 5. If you delete all the characters you typed, the placeholder reappears.
+ 如果将输入的字符删除干净,placeholder文字将重新出现。
+
+##Conclusion
+##结束语
+There are many ways to solve a coding problem. It’s best to take a good look at your options up front and think about the pros and cons of each. I hope through these articles you’re also learning the beauty of sequence diagrams. They allow you to document and understand object interactions that occur at run time. I don’t just use these for my articles. I use them for my real-world coding designs. You’ll find that they can help you identify potential problems as well as help you visualize new and more efficient solutions to your coding challenges.
+
+要解决编程问题有许多方法。最好的方法是先看一下摆在你目前的选择,然后逐个分析其中的利弊。我希望你能看到本文中序列图的好处。它能让你将对象之间在运行时的交互画出来。我在编写代码时也使用了这个方法。你会发现它们能帮助你找出潜在的问题,更能帮你找出编程问题中的新的、更高效的解决方案。
\ No newline at end of file
diff --git "a/issue-17/\345\246\202\344\275\225\345\256\236\347\216\260iOS\345\233\276\344\271\246\345\212\250\347\224\273-\347\254\2541\351\203\250\345\210\206.md" "b/issue-17/\345\246\202\344\275\225\345\256\236\347\216\260iOS\345\233\276\344\271\246\345\212\250\347\224\273-\347\254\2541\351\203\250\345\210\206.md"
new file mode 100644
index 0000000..ef06e19
--- /dev/null
+++ "b/issue-17/\345\246\202\344\275\225\345\256\236\347\216\260iOS\345\233\276\344\271\246\345\212\250\347\224\273-\347\254\2541\351\203\250\345\210\206.md"
@@ -0,0 +1,847 @@
+如何实现iOS图书动画:第1部分
+---
+
+> * 原文链接 : [How to Create an iOS Book Open Animation: Part 1](http://www.raywenderlich.com/94565/how-to-create-an-ios-book-open-animation-part-1)
+* 原文作者 : [Vincent Ngo](http://www.raywenderlich.com/u/jomoka)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [kmyhy](https://github.com/kmyhy)
+* 校对者: [这里校对者的github用户名](github链接)
+* 状态 : 校对中
+
+In this two-part tutorial series, you’ll develop a nifty iOS book open animation and page flip animation similar to Paper by FiftyThree:
+
+
+
+本教程分为2个部分,教你开发一个漂亮的iOS图书打开和翻页动画,就像你在Paper 53中所见到的一样:
+
+In Part 1 you’ll learn how to customize your collection view layouts and apply depth and shadow to make the app look realistic.
+
+在第1部分,你将学习到如何定制化Collection View Layout,并通过使用深度和阴影使App看起来更真实。
+
+In Part 2, you’ll learn to create custom transitions between different controllers in a sensible way and integrate gestures to create natural, intuitive transitions between views.
+
+在第2部分,你将学习如何以一种合理的方法在两个不同的控制器之间创建自定义的过渡特效,以及利用手势在两个视图间创建自然的、直观的过渡效果。
+
+This tutorial is for intermediate to advanced developers; you’ll be working with custom transitions and custom collection view layouts.
+If you’ve never worked with a collection view before, start with some of our other iOS tutorials first.
+
+本教程适用于中级-高级的开发者;你将使用自定义过渡动画和自定义Collection View Layout。如果你从来没有用过Colleciton View,请先参考[其他iOS教程](http://www.raywenderlich.com/tutorials)。
+
+> Note: Full credit goes to Attila Hegedüs for creating this awesome
+> sample project.
+>
+> 注意:感谢[Attila Hegdüs](https://twitter.com/hegedus90)创建了本教程中的示例项目。
+
+##Getting Started
+##开始
+Download the starter project for this tutorial here; extract the contents of the zip file, and open Paper.xcodeproj in Xcode.
+
+从[此处](http://cdn2.raywenderlich.com/wp-content/uploads/2015/05/Starter-Paper1.zip)下载本教程的开始项目;解开zip压缩包,用Xcode打开Paper.xcodeproj。
+
+编译项目,在模拟器中运行App;你将看到如下画面:
+
+
+
+The app is pretty much fully built; you can scroll through your library of books and select one of your favorite books to view. But when was the last time you read a book which had its pages side-by-side? With a bit of UICollectionView know-how, you can dress up the page view quite a bit!
+
+这个App的功能已经很完善了,你可以在你的书库中滚动,查看图书,选中某本图书进行浏览。但当你读一本书的时候,为什么它的书页都是并排放置的?通过一些UICollectionView的知识,你可以让这些书页看起来更好一些!
+
+##The Project Structure
+##项目结构
+
+Here’s a quick rundown of the most important bits of the starter project:
+
+关于这个开始项目,有几个重要的地方需要解释:
+
+The Data Models folder contains three files:
+
+ - Books.plist contains sample book data. Each book contains an image
+ cover along with an array of images to represent pages.
+ - BookStore.swift is a singleton that is only created once in the life
+ cycle of the app. The BookStore’s job is to load data from
+ Books.plist and create Book objects.
+ - Book.swift is a class that stores information related to the book,
+ such as retrieving the book cover, the image for each page index, and
+ the number of pages.
+
+Data Models文件夹包含3个文件:
+
+- Books.plist 中包含了几本用于演示的图书信息。每本图书包含一张封面图片,以及一个表示每一页的内容的图片的数组。
+- BookStore.swift实现了单例,在整个App声明周期中只能创建一次对象。BookStore的职责是从Books.plist中加载数据并创建Book类实例。
+- Book.swift用于存放图书相关信息的类,比如图书的封面,每一页的图片,以及页号。
+
+The Books folder contains two files:
+
+ - BooksViewController.swift is a subclass of
+ UICollectionViewController. This class is responsible for displaying
+ your list of books horizontally.
+ - BookCoverCell.swift displays all your book covers; it’s used by
+ BooksViewController.
+
+Books文件夹包含了两个文件:
+
+- BooksViewController.swift是一个UICollectionViewController子类。负责以水平方式显式图书列表。
+- BookCoverCell.swift负责显示图书的封面,这个类被BooksViewController类所引用。
+
+In the Book folder you’ll find the following:
+
+- BookViewController.swift is also a subclass of UICollectionViewController. Its purpose is to display the pages of the book when you select a book from BooksViewController.
+- BookPageCell.swift is used by BookViewController to display all the pages in a book.
+
+在Book文件夹中则包括:
+
+- BookViewController.swift也是UICollectionViewController的子类。当用户在BooksViewController中选定的一本书后,它负责显示图书中的书页。
+- BookPageCell.swift被BookViewController用于显示图书中的书页。
+
+Here’s what’s in the last folder, Helpers:
+
+- UIImage+Helpers.swift is an extension for UIImage. The extension contains two utility methods, one to round the corners of an image, and another to scale an image down to a given size.
+
+在最后一个文件夹Helper中包含了:
+
+- UIImage+Helpers.swift是UIImage的扩展。该扩展包含了两个实用方法,一个用于让图片呈圆角显示,一个用于将图片缩放到指定大小。
+
+That’s all! Enough of the review — it’s time to lay down some code!
+
+这就是整个开始项目的大致介绍——接下来该是我们写点代码的时候了!
+
+##Customizing the Book Layout
+##定制化图书界面
+First you need to to override the default layout for BooksViewController‘s collection view. The existing layout shows three big book covers that takes up the whole screen. You’ll scale it down a bit to make it look more pleasant, like so:
+
+首先我们需要在BooksViewController中覆盖Collection View的默认布局方式。但当前的布局是在屏幕上显示3张图书封面的大图。为了美观,我们将这些图片缩减到一定大小,如下图所示:
+
+
+
+As you scroll, the cover image nearest the center of the screen grows in size to indicate it’s the active selection. As you keep scrolling, the book cover shrinks in size to indicate you’re setting it aside.
+
+当我们滑动图片,移动到屏幕中心的图片将被放大,以表示该图书为选中状态。如果继续滑动,该图书的封面又会缩小到一边,表示我们放弃选择该图书。
+
+Create a group named Layout under the App\Books group. Next right-click the Layout folder and select New File…, then select the iOS\Source\Cocoa Touch Class template and click Next. Name the class BooksLayout, make it a subclass of UICollectionViewFlowLayout, and set Language to Swift.
+
+在App\Books文件夹下新建一个文件夹组:Layout。在Layout上点击右键,选择New File...,然后选择iOS\Source\Cocoa Touch Class模板,并点击Next。类名命名为BooksLayout,继承UICollectionViewFlowLayout类,语言设置为Swift。
+
+Next you need to instruct BooksViewController‘s collection view to use your new layout.
+
+然后需要告诉BooksViewController中的Collection View,适用我们新建的BooksLayout。
+
+Open Main.storyboard, click on BooksViewController then click on the Collection View. In the Attributes Inspector, set Layout to Custom and Class to BooksLayout as shown below:
+
+打开Main.storyboard,展开BooksViewController对象,然后选择Collection View。在属性面板中,设置Layout 属性为 Custom,设置Class属性为BooksLayout,如下图所示:
+
+
+
+Open BooksLayout.swift and add the following code above the BooksLayout class declaration.
+
+打开BooksLayout.swift,在BooksLayout类声明之上加入以下代码:
+
+```
+private let PageWidth: CGFloat = 362
+private let PageHeight: CGFloat = 568
+```
+
+These two constants will be used to set the size of the cell.
+Now add the following initialization method within the class curly braces:
+
+这个两个常量将用于设置单元格的的大小。
+现在,在类定义内部定义如下初始化方法:
+
+```
+required init(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+
+ scrollDirection = UICollectionViewScrollDirection.Horizontal //1
+ itemSize = CGSizeMake(PageWidth, PageHeight) //2
+ minimumInteritemSpacing = 10 //3
+}
+```
+
+Here’s what the code above does:
+Sets the collection view’s scroll view direction to horizontal.
+Sets the size of the cell to the page width of 362 and to a height of 568.
+Set the minimum spacing between cells to 10.
+
+上述代码作用如下:
+
+ 1. 设置Collectioin View的滚动方向为水平方向。
+ 2. 设置单元格的大小为PageWidth和PageHeight,即362x568。
+ 3. 设置两个单元格间距10。
+
+Next, add the following code after init(coder:):
+
+然后,在init(coder:)方法中加入代码:
+
+```
+override func prepareLayout() {
+ super.prepareLayout()
+
+ //The rate at which we scroll the collection view.
+ //1
+ collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
+
+ //2
+ collectionView?.contentInset = UIEdgeInsets(
+ top: 0,
+ left: collectionView!.bounds.width / 2 - PageWidth / 2,
+ bottom: 0,
+ right: collectionView!.bounds.width / 2 - PageWidth / 2
+ )
+}
+```
+
+prepareLayout() gives you the chance to perform any calculations before you come up with any layout information for each cell.
+
+prepareLayout()方法允许我们在每个单元格的布局信息生效之前可以进行一些计算。
+
+Taking each numbered comment in turn:
+
+对应注释中的编号,以上代码分别说明如下:
+
+Sets how fast the collection view will stop scrolling after a user lifts their finger. By setting it to UIScrollViewDecelerationRateFast the scroll view will decelerate much faster. Try playing around with Normal vs Fast to see the difference!
+Sets the content inset of the collection view so that the first book cover will always be centered.
+
+ 1. 设置当用户手指离开屏幕后,Collection
+ View停止滚动的速度。默认的设置为UIScrollViewDecelerationRateFast,这是一个较快的速度。你可以尝试着设置为Normal 和 Fast,看看二者之间有什么区别。
+ 2. 设置Collection View的contentInset,以使第一本书的封面位于Collection View的中心。
+
+Now you need to handle the layout information of each cell.
+Add the following code below prepareLayout():
+
+现在我们需要处理每一个单元格的布局信息。
+在prepareLayout()方法下面,加入以下代码:
+
+```
+override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
+ //1
+ var array = super.layoutAttributesForElementsInRect(rect) as! [UICollectionViewLayoutAttributes]
+
+ //2
+ for attributes in array {
+ //3
+ var frame = attributes.frame
+ //4
+ var distance = abs(collectionView!.contentOffset.x + collectionView!.contentInset.left - frame.origin.x)
+ //5
+ var scale = 0.7 * min(max(1 - distance / (collectionView!.bounds.width), 0.75), 1)
+ //6
+ attributes.transform = CGAffineTransformMakeScale(scale, scale)
+ }
+
+ return array
+}
+```
+
+layoutAttributesForElementsInRect(_:) returns an array of UICollectionViewLayoutAttributes objects, which provides the layout attributes for each cell. Here’s a breakdown of the code:
+
+layoutAttributesForElementsInRect(_:) 方法返回一个UICollectionViewLayoutAttributes对象数组,其中包含了每一个单元格的布局属性。以上代码稍作说明如下:
+
+Calling the superclass of layoutAttributesForElementsInRect returns an array that contains all default layout attributes for each cell.
+Loop through each attribute in the array.
+Grab the frame for the current cell attribute.
+Calculate the distance between the book cover — that is, the cell — and the center of the screen.
+Scale the book cover between a factor of 0.75 and 1 depending on the distance calculated above. You then scale all book covers by 0.7 to keep them nice and small.
+Finally, apply the scale to the book cover.
+
+ 1. 调用父类的layoutAttributesForElementsInRect方法,已获得默认的单元格布局属性。
+ 2. 遍历数组中的每个单元格布局属性。
+ 3. 从单元格布局属性中读取frame。
+ 4. 计算两本书的封面之间的间距——即两个单元格之间的间距——以及屏幕的中心点。
+ 5. 以0.75~1之间的比率缩放封面,具体的比率取决于前面计算出来的间距。然后为了美观,将所有的封面都缩放70%。
+ 6. 最后,应用仿射变换。
+
+Next, add the following code right after layoutAttributesForElementsInRect(_:):
+
+接下来,在layoutAttributesForElementsInRect(_:)方法后增加如下代码:
+
+```
+override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
+ return true
+}
+```
+
+Returning true forces the layout to recalculate its attributes every time the collection view’s bound changes. A UICollectionView changes its bounds while scrolling, which is perfect for recalculating the cell’s attribute.
+
+返回true表示每当Collection View的bounds发生改变时都强制重新计算布局属性。Collection View在滚动时会改变它的bounds,因此我们需要重新计算单元格的布局属性。
+
+Build and run your app; you’ll see that book in the middle of the view is larger than the others:
+
+编译运行程序,我们将看到位于中央的封面明显比其他封面要大上一圈:
+
+
+
+Scroll through the books to see how each book cover scales up and down. But wouldn’t it be great if the book could snap into place, indicating the selection?
+The next method you’ll add will do just that!
+
+拖动Colleciton View,查看每本书放大、缩小。但仍然有一点稍显不足,为什么不让书本能够卡到固定的位置呢?
+接下来我们介绍的这个方法就是干这个的。
+
+Snapping to a Book
+
+##对齐书本
+
+targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:) determines at which point the collection view should stop scrolling, and returns a proposed offset to set the collection view’s contentOffset. If you don’t override this method, it just returns the default offset.
+
+targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:)方法用于计算每本书应该在对齐到哪个位置,它返回一个偏移位置,可用于设置Collection View的contentOffset。如果你不覆盖这个方法,它会返回一个默认的值。
+
+Add the following code after shouldInvalidateLayoutForBoundsChange(_:):
+
+在shouldInvalidateLayoutForBoundsChange(_:)方法后添加如下代码:
+
+```
+override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
+ // Snap cells to centre
+ //1
+ var newOffset = CGPoint()
+ //2
+ var layout = collectionView!.collectionViewLayout as! UICollectionViewFlowLayout
+ //3
+ var width = layout.itemSize.width + layout.minimumLineSpacing
+ //4
+ var offset = proposedContentOffset.x + collectionView!.contentInset.left
+
+ //5
+ if velocity.x > 0 {
+ //ceil returns next biggest number
+ offset = width * ceil(offset / width)
+ } else if velocity.x == 0 { //6
+ //rounds the argument
+ offset = width * round(offset / width)
+ } else if velocity.x < 0 { //7
+ //removes decimal part of argument
+ offset = width * floor(offset / width)
+ }
+ //8
+ newOffset.x = offset - collectionView!.contentInset.left
+ newOffset.y = proposedContentOffset.y //y will always be the same...
+ return newOffset
+}
+```
+
+Here’s how you calculate the proposed offset for your book covers once the user lifts their finger:
+
+这段代码计算当用户手指离开屏幕时,封面应该位于哪个偏移位置:
+
+Create a new CGPoint called newOffset.
+Grab the current layout of the collection view.
+Get the total width of a cell.
+Calculate the current offset with respect to the center of the screen.
+If velocity.x > 0, the user is scrolling to the right. Think of offset/width as the book index you’d like to scroll to.
+If velocity.x = 0, the user didn’t put enough oomph into scrolling, and the same book remains selected.
+If velocity.x < 0, the user is scrolling left.
+Update the new x offset and return. This guarantees that a book will always be centered in the middle.
+
+ 1. 声明一个CGPoint。
+ 2. 获得Collection View的当前布局。
+ 3. 获得单元格的总宽度。
+ 4. 计算相对于屏幕中央的currentOffset。
+ 5. 如果velocity.x>0,表明用户向右滚动,用offset除以width,得到书的索引,并滚动到相应的位置。
+ 6. 如果velocity.x=0,表明用户是无意识的滚动,原来的选择不会发生改变。
+ 7. 如果velocity.x<0,表明用户向左滚动。
+ 8. 修改newOffset.x,然后返回newOffset。这样就保证书本总是对齐到屏幕的中央。
+
+Build and run your app; scroll through them again and you should notice that the scrolling action is a lot snappier:
+
+编译运行程序;再次滚动封面,你会注意到滚动动作将变得更整齐了。
+
+To finish up this layout, you need to create a mechanism to restrict the user to click only the book in the middle. As of right now, you can currently click any book regardless of its position.
+
+要完成这个布局,我们还需要使用一种机制,以限制用户只能点击位于中央的封面。目前,不管哪个位置的封面都是可点击的。
+
+Open BooksViewController.swift and place the following code under the comment // MARK: Helpers:
+
+打开BooksViewController.swift,在注释"//MARK:Helpers"下面加入以下代码:
+
+```
+func selectedCell() -> BookCoverCell? {
+ if let indexPath = collectionView?.indexPathForItemAtPoint(CGPointMake(collectionView!.contentOffset.x + collectionView!.bounds.width / 2, collectionView!.bounds.height / 2)) {
+ if let cell = collectionView?.cellForItemAtIndexPath(indexPath) as? BookCoverCell {
+ return cell
+ }
+ }
+ return nil
+}
+```
+
+selectedCell() will always return the middle cell.
+Next, replace openBook(_:) with the following:
+
+selectedCell()方法返回位于中央的那个单元格。
+替换openBook(_:)方法的代码如下:
+
+```
+func openBook() {
+ let vc = storyboard?.instantiateViewControllerWithIdentifier("BookViewController") as! BookViewController
+ vc.book = selectedCell()?.book
+ // UICollectionView loads it's cells on a background thread, so make sure it's loaded before passing it to the animation handler
+ dispatch_async(dispatch_get_main_queue(), { () -> Void in
+ self.navigationController?.pushViewController(vc, animated: true)
+ return
+ })
+}
+```
+
+This simply uses the new selectedCell method you wrote rather than taking a book as a parameter.
+Next, replace collectionView(_:didSelectItemAtIndexPath:) with the following:
+
+这里,直接调用新的selectedCell方法,并用它的book属性代替原来的book参数。
+然后,将collectionView(_:didSelectItemAtIndexPath:)方法替换为:
+
+```
+override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
+ openBook()
+}
+```
+
+This simply removes the code that opened the book at the selected index; now you'll always open the book in the center of the screen.
+Build and run your app; you'll notice now that the book in the center of the view is always the one that opens.
+
+这里,我们简单地删除了原来的打开某个索引处的图书的代码,而直接打开了当前位于屏幕中央的图书。
+编译运行程序,我们将看到每次打开的图书总是位于屏幕中央的那本。
+
+You're done with BooksLayout. It's time to make the on-screen book more realistic, and let the user flip the pages in the book!
+
+BooksLayout的工作就到这里了。后面我们将使这本电子书显得更真实,能够让用户”翻动“书里的每一页!
+
+##Book Flipping Layout
+##翻页布局
+Here's the final effect you're shooting for:
+
+最终实现的效果如下:
+
+
+
+Now that looks more like a book! :]
+这看起来就像是一本真正的书! :]
+
+Create a group named Layout under the Book group. Next, right-click the Layout folder and select New File..., then select the iOS\Source\Cocoa Touch Class template and click Next. Name the class BookLayout, make it a subclass of UICollectionViewFlowLayout, and set Language to Swift.
+
+在Book文件夹下新建一个Layout文件夹。在Layout文件夹上右键,选择New File...,然后适用iOS\Source\Cocoa Touch Class模板,然后点Next。类名命名为BookLayout,继承于UICollectionViewFlowLayout,语言选择Swift。
+
+Just as before, your book collection view needs to use the new layout. Open Main.storyboard and select the Book View Controller Scene. Select the collection view and set the Layout to Custom.
+
+同前面一样,图书所使用的Collection View需要适用新的布局。打开Main.storyboard,然后选择Book View Controller场景,展开并选中其中的Collection View,然后设置Layout属性为Custom。
+
+Finally, set the layout Class to BookLayout as shown below:
+
+然后,将Layout属性下的 Class属性设置为BookLayout:
+
+
+
+Open BookLayout.swift and add the following code above the BookLayout class declaration:
+
+打开BookLayout.swift,在类声明之上加入如下代码:
+
+```
+private let PageWidth: CGFloat = 362
+private let PageHeight: CGFloat = 568
+private var numberOfItems = 0
+```
+
+You'll use these constant variables to set the size of every cell; as well, you're keeping track of the total number of pages in the book.
+Next, add the following code inside the class declaration:
+
+这几个常量将用于设置单元格的大小,以及记录整本书的页数。
+接着,在类声明内部加入代码:
+
+```
+override func prepareLayout() {
+ super.prepareLayout()
+ collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
+ numberOfItems = collectionView!.numberOfItemsInSection(0)
+ collectionView?.pagingEnabled = true
+}
+```
+
+This is similar to what you did in BooksLayout, with the following differences:
+
+这段代码和我们在BooksLayout中所写的差不多,仅有以下几处差别:
+
+Set the deceleration rate to UIScrollViewDecelerationRateFast to increase the rate at which the scroll view slows down.
+Grab the number of pages in the current book.
+Enable paging; this lets the view scroll at fixed multiples of the collection view's frame width (rather than the default of continuous scrolling).
+
+ 1. 将减速速度设置为UIScrollViewDecelerationRateFast,以加快Scroll View滚动速度变慢的节奏。
+ 2. 记住本书的页数。
+ 3. 启用分页,这样Scroll View滚动时将以其宽度的固定倍数滚动(而不是持续滚动)。
+
+Still in BookLayout.swift, add the following code:
+
+仍然在BookLayout.swift中,加入以下代码:
+
+```
+override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
+ return true
+}
+```
+
+Again, returning true lets the layout update every time the user scrolls.
+
+跟前面一样,返回true,以表示用户每次滚动都会重新计算布局。
+
+Next, give the collection view a content size by overriding collectionViewContentSize() as shown below:
+
+然后,覆盖collectionViewContentSize() ,以指定Collection View的contentSize:
+
+```
+override func collectionViewContentSize() -> CGSize {
+ return CGSizeMake((CGFloat(numberOfItems / 2)) * collectionView!.bounds.width, collectionView!.bounds.height)
+}
+```
+
+This returns the overall size of the content area. The height of the content will always stay the same, but the overall width of the content is the number of items — that is, pages — divided by two multiplied by the screen's width. The reason you divide by two is that book pages are double sided; there's content on both sides of the page.
+
+这个方法返回了内容区域的整个大小。内容区域的高度总是不变的,但宽度是随着页数变化的——即书的页数除以2倍,再乘以屏幕宽度。除以2是因为书页有两面,内容区域一次显示2页。
+
+Just as you did in BooksLayout, you need to override layoutAttributesForElementsInRect(_:) so you can add the paging effect to your cells.
+
+就如我们在BooksLayout中所做的一样,我们还需要覆盖layoutAttributesForElementsInRect(_:)方法,以便我们能够在单元格上增加翻页效果。
+
+Add the following code just after collectionViewContentSize():
+
+在collectionViewContentSize()方法后加入:
+
+```
+override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
+ //1
+ var array: [UICollectionViewLayoutAttributes] = []
+
+ //2
+ for i in 0 ... max(0, numberOfItems - 1) {
+ //3
+ var indexPath = NSIndexPath(forItem: i, inSection: 0)
+ //4
+ var attributes = layoutAttributesForItemAtIndexPath(indexPath)
+ if attributes != nil {
+ //5
+ array += [attributes]
+ }
+ }
+ //6
+ return array
+}
+```
+
+Rather than calculating the attributes within this method like you did in BooksLayout, you leave this task up to layoutAttributesForItemAtIndexPath(_:), as all cells are within the visible rect at any given time in the book implementation.
+
+不同于在BooksLayout中的将所有计算布局属性的代码放在这个方法里面,我们这次将这个任务放到layoutAttributesForItemAtIndexPath(_:)中进行,因为在图书的实现中,所有单元格都是同时可见的。
+
+Here's a line by line explanation:
+
+以上代码解释如下:
+
+Create a new array to hold UICollectionViewLayoutAttributes.
+Loop through all the items (pages) in the collection view.
+For each item in the collection view, create an NSIndexPath.
+Grab the attribute for the current indexPath. You'll override layoutAttributesForItemAtIndexPath(_:) soon.
+Add the attributes to your array.
+Return all the cell attributes.
+
+ 1. 声明一个数组,用于保存所有单元格的布局属性。
+ 2. 遍历所有的书页。
+ 3. 对于CollecitonView中的每个单元格,都创建一个NSIndexPath。
+ 4. 通过每个NSIndexPath来获得单元格的布局属性,在后面,我们会覆盖layoutAttributesForItemAtIndexPath(_:)方法。
+ 5. 把每个单元格布局属性添加到数组。
+ 6. 返回数组。
+
+##Handling the Page Geometry
+##处理页面的几何计算
+
+Before you jump straight into the implementation of layoutAttributesForItemAtIndexPath(_:), take a minute to consider the layout, how it will work, and if you can write any helper methods to keep everything nice and modular. :]
+
+在我们开始实现layoutAttributesForItemAtIndexPath(_:)方法之前,花几分钟好好思考一下布局的问题,它是怎样实现的,如果能够写几个助手方法将会让我们的代码更漂亮和模块化。:]
+
+
+
+The diagram above shows that every page flips with the book's spine as the axis of rotation. The ratios on the diagram range from -1.0 to 1.0. Why? Well, imagine a book laid out on a table, with the spine representing 0.0. When you turn a page from the left to the right, the "flipped" ratio goes from -1.0 (full left) to 1.0 (full right).
+Therefore, you can represent your page flipping with the following ratios:
+
+上图演示了翻页时以书籍为轴旋转的过程。图中书页的”打开度“用-1到1来表示。为什么?你可以想象一下放在桌子上的一本书,书脊所在的位置代表0.0。当你从左向右翻动书页时,书页张开的程度从-1(最左)到1(最右)。因此,我们可以用下列数字表示“翻页”的过程:
+
+0.0 means a page is at a 90 degree angle, perpendicular to the table.
++/- 0.5 means a page is at a 45 degree angle to the table.
++/- 1.0 means a page is parallel to the table.
+
+ 1. 0.0表示一个书页翻成90度,与桌面成直角。
+ 2. +/-0.5表示书页翻至于桌面成45度角。
+ 3. +/-1.0表示书页翻至与桌面平行。
+
+Note that since angle rotation is counterclockwise, the sign of the angle will be the opposite of the sign of the ratio.
+
+注意,因为角度是按照反时针方向增加的,因此角度的符号和对应的打开度是相反的。
+
+First, add the following helper method after layoutAttributesForElementsInRect(_:):
+
+首先,在layoutAttributesForElementsInRect(_:)方法后加入助手方法:
+
+```
+//MARK: - Attribute Logic Helpers
+
+func getFrame(collectionView: UICollectionView) -> CGRect {
+ var frame = CGRect()
+
+ frame.origin.x = (collectionView.bounds.width / 2) - (PageWidth / 2) + collectionView.contentOffset.x
+ frame.origin.y = (collectionViewContentSize().height - PageHeight) / 2
+ frame.size.width = PageWidth
+ frame.size.height = PageHeight
+
+ return frame
+}
+```
+
+For every page, you calculate the frame with respect to the middle of the collection view. getFrame(_:) will align every page's edge to the book's spine. The only variable that changes is the collection view's content offset in the x direction.
+
+对于每一页,我们都可以计算出相对于Collection View中心的frame。getFrame(_:)方法会将每一页的一边对齐到书脊。唯一会变的是Collectoin View的contentOffset在x方向上的改变。
+
+Next, add the following method after getFrame(_:):
+
+然后,在getFrame(_:)方法后添加如下方法:
+
+```
+func getRatio(collectionView: UICollectionView, indexPath: NSIndexPath) -> CGFloat {
+ //1
+ let page = CGFloat(indexPath.item - indexPath.item % 2) * 0.5
+
+ //2
+ var ratio: CGFloat = -0.5 + page - (collectionView.contentOffset.x / collectionView.bounds.width)
+
+ //3
+ if ratio > 0.5 {
+ ratio = 0.5 + 0.1 * (ratio - 0.5)
+
+ } else if ratio < -0.5 {
+ ratio = -0.5 + 0.1 * (ratio + 0.5)
+ }
+
+ return ratio
+}
+```
+
+The method above calculates the page's ratio. Taking each commented section in turn:
+
+上面的方法计算书页翻开的程度。对每一段有注释的代码分别说明如下:
+
+Calculate the page number of a page in the book — keeping in mind that pages in the book are double-sided. Multiplying by 0.5 gives you the exact page you're on.
+Calculate the ratio based on the weighted percentage of the page you're turning.
+You need to restrict the page to a ratio between the range of -0.5 and 0.5. Multiplying by 0.1 creates a gap between each page to make it look like they overlap.
+
+ 1. 算出书页的页码——记住,书是双面的。 除以2就是你真正在翻读的那一页。
+ 2. 算出书页的打开度。注意,这个值被我们加了一个权重。
+ 3. 书页的打开度必须限制在-0.5到0.5之间。另外乘以0.1的作用,是为位了在页与页之间增加一条细缝,以表示它们是上下叠放在一起的。
+
+Once you've calculated the ratio, you'll use it to calculate the angle of the turning page.
+Add the following code after getRatio(_:indexPath:):
+
+一旦我们计算出书页的打开度,我们就可以将之转变为旋转的角度。
+在getRation(_:indexPath:)方法后面加入代码:
+
+```
+func getAngle(indexPath: NSIndexPath, ratio: CGFloat) -> CGFloat {
+ // Set rotation
+ var angle: CGFloat = 0
+
+ //1
+ if indexPath.item % 2 == 0 {
+ // The book's spine is on the left of the page
+ angle = (1-ratio) * CGFloat(-M_PI_2)
+ } else {
+ //2
+ // The book's spine is on the right of the page
+ angle = (1 + ratio) * CGFloat(M_PI_2)
+ }
+ //3
+ // Make sure the odd and even page don't have the exact same angle
+ angle += CGFloat(indexPath.row % 2) / 1000
+ //4
+ return angle
+}
+```
+
+There's a bit of math going on, but it's not so bad when you break it down:
+
+这个方法中有大量计算,我们一点点拆开来看:
+
+Check to see if the current page is even. This means that the page is to the right of the book's spine. A page turn to the right is counterclockwise, and pages on the right of the spine have a negative angle. Recall that the ratio you defined is between -0.5 and 0.5.
+If the current page is odd, the page is to the left of the book's spine. A page turn to the left is clockwise, and pages on the left side of the spine have a positive angle.
+Add a small angle to each page to give the pages some separation.
+Return the angle for rotation.
+
+ 1. 判断该页是否是偶数页。如果是,则该页将翻到书脊的右边。翻到右边的页是反手翻转,同时书脊右边的页其角度必然是负数。注意,我们将打开度定义为-0.5到0.5之间。
+ 2. 如果当前页是奇数,则该页将位于书脊左边,当书页被翻到左边时,它的按正手翻转,书脊左边的页其角度为正数。
+ 3. 每页之间加一个小夹角,使它们彼此分离。
+ 4. 返回旋转角度。
+
+Once you have the angle, you need to transform each page. Add the following method:
+
+得到旋转角度之后,我们可以操纵书页使其旋转。增加如下方法:
+
+```
+func makePerspectiveTransform() -> CATransform3D {
+ var transform = CATransform3DIdentity
+ transform.m34 = 1.0 / -2000
+ return transform
+}
+```
+
+Modifying the m34 of the transform matrix adds a bit of perspective to each page.
+
+修改转换矩阵的m34属性,已达到一定的立体效果。
+
+Now it's time to apply the rotation. Add the following code:
+
+然后应用旋转动画。实现下面的方法:
+
+```
+func getRotation(indexPath: NSIndexPath, ratio: CGFloat) -> CATransform3D {
+ var transform = makePerspectiveTransform()
+ var angle = getAngle(indexPath, ratio: ratio)
+ transform = CATransform3DRotate(transform, angle, 0, 1, 0)
+ return transform
+}
+```
+
+Here you use the two previous helper methods to calculate the transform and the angle, and create a CATransform3D to apply to the page along the y-axis.
+
+在这个方法中,我们用到了刚才创建的两个助手方法去计算旋转的角度,然后通过一个CATransform3D对象让书页在y轴上旋转。
+
+Now that you have all the helper methods set up, you are finally ready to create the attributes for each cell. Add the following method after layoutAttributesForElementsInRect(_:):
+
+所有的助手方法都实现了,我们最终需要配置每个单元格的属性。在layoutAttributesForElementsInRect(_:)方法后加入以下方法:
+
+```
+override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
+ //1
+ var layoutAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
+
+ //2
+ var frame = getFrame(collectionView!)
+ layoutAttributes.frame = frame
+
+ //3
+ var ratio = getRatio(collectionView!, indexPath: indexPath)
+
+ //4
+ if ratio > 0 && indexPath.item % 2 == 1
+ || ratio < 0 && indexPath.item % 2 == 0 {
+ // Make sure the cover is always visible
+ if indexPath.row != 0 {
+ return nil
+ }
+ }
+ //5
+ var rotation = getRotation(indexPath, ratio: min(max(ratio, -1), 1))
+ layoutAttributes.transform3D = rotation
+
+ //6
+ if indexPath.row == 0 {
+ layoutAttributes.zIndex = Int.max
+ }
+
+ return layoutAttributes
+}
+```
+
+You'll call this method for each cell in your collection view:
+
+在Collection View的每个单元格上,都会调用这个方法。这个方法做了如下工作:
+
+Create a UICollectionViewLayoutAttributes object for the cell at the given NSIndexPath.
+Set the frame of the attribute using the getFrame method you created to ensure it's always aligned with the book's spine.
+Calculate the ratio of an item in the collection view using getRatio, which you wrote earlier.
+Check that the current page is within the ratio's threshold. If not, don't display the cell. For optimization purposes (and because of common sense), you won't display the back-side of a page, but only those that are front-facing — except when it's the book's cover, which you display at all times.
+Apply a rotation and transform with the given ratio you calculated.
+Check if indexPath is the first page. If so, make sure its zIndex is always on top of the other pages to avoid flickering effects.
+
+ 1. 创建一个UICollectionViewLayoutAttributes对象layoutAttributes,供IndexPath所指的单元格使用。
+ 2. 调用我们先前定义的getFrame方法设置layoutAttributes的frame,确保单元格对齐于书脊。
+ 3. 调用先前定义的getRatio方法算出单元格的打开度。
+ 4. 判断当前页是否位于正确的打开度范围之内。如果不,不显示该单元格。为了优化(也是为了符合常理),除了正面向上的书页,我们不应当显示书页的背面——书的封面例外,那个不管什么时候都需要显示。
+ 5. 应用旋转动画,使用前面算出的打开度。
+ 6. 判断是否是第一页,如果是,将它的zIndex放在其他页的上面,否则有可能出现画面闪烁的Bug。
+
+Build and run your app, open up one of your books, flip through it and...whoa, what?
+
+编译,运行。打开书,翻动每一页……呃?什么情况?
+
+
+
+书被错误地从中间装订了,而不是从书的侧边装订。
+
+
+
+As the diagram shows, each page's anchor point is set at 0.5 for both x and y. Can you tell what you need to do to fix this?
+
+如图中所示,每个书页的锚点默认是x轴和y轴的0.5倍处。现在你知道怎么做了吗?
+
+It's clear you need to change the anchor point of a pages to its edge. If the page is on the right hand side of a book, the anchor point should be (0, 0.5). But if the page is on the left hand side of a book, the anchor point should be (1, 0.5).
+
+很显然,我们需要修改书页的锚点为它的侧边缘。如果这个书页是位于书的右边,则它的锚点应该是(0,0.5)。如果书页是位于书的左边,测锚点应该是(1,0.5)。
+
+Open BookPageCell.swift and add the following code:
+
+打开BookePageCell.swift,添加如下代码:
+
+```
+override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
+ super.applyLayoutAttributes(layoutAttributes)
+ //1
+ if layoutAttributes.indexPath.item % 2 == 0 {
+ //2
+ layer.anchorPoint = CGPointMake(0, 0.5)
+ isRightPage = true
+ } else { //3
+ //4
+ layer.anchorPoint = CGPointMake(1, 0.5)
+ isRightPage = false
+ }
+ //5
+ self.updateShadowLayer()
+}
+```
+
+Here you override applyLayoutAttributes(_:), which applies the layout attributes created in BookLayout.
+It's pretty straightforward code:
+
+我们重写了applyLayoutAttributes(_:)方法,这个方法用于将BookLoayout创建的布局属性应用到第一个。
+上述代码非常简单:
+
+Check to see if the current cell is even. This means that the book's spine is on the left of the page.
+Set the anchor point to the left side of the cell and set isRightPage to true. This variable helps you determine where the rounded corners of the pages should be.
+If the current cell is odd, then the book's spine is on the right side of the page.
+Set the anchor point to the right side of the cell and set isRightPage to false.
+Finally, update the shadow layer of the current page.
+
+ 1. 检查当前单元格是否是偶数,也就是说书脊是否位于书页的左边。
+ 2. 如果是,将书页的锚点设置为单元格的左边缘,同时isRightPage设置为true。isRightPage变量可用于决定书页的圆角样式应当在那一边应用。
+ 3. 如果是奇数页,书脊应当位于书页的右边。
+ 4. 这只书页的锚点为单元格的右边缘,isRightPage设为false。
+ 5. 最后,设置当前书页的阴影层。
+
+Build and run your app; flip through the pages and things should look a little better:
+
+编译,运行。翻动书页,这次的效果已经好了许多:
+
+
+
+That's it for the first part of this tutorial! Take some time to bask in the glory of what you've created — it's a pretty cool effect! :]
+
+本教程第一部分就到此为止了。你是不是觉得自己干了一件了不起的事情呢——效果做得很棒,不是吗?
+
+##接下来做什么
+##Where to Go From Here?
+You can download the completed project from Part 1 that contains all the source code.
+
+第一部分的完整代码在此处下载:http://cdn1.raywenderlich.com/wp-content/uploads/2015/05/Part-1-Paper-Completed.zip
+
+You started out with the default layouts for a collection view, and learned to customize a new layout to turn it into something truly amazing! Someone using this app will feel like they are flipping through a real book. It's the little things that turn a normal reader app into something that people can feel truly connected with.
+
+我们从一个默认的Collection View布局开始,学习了如何定制自己的layout,并用它取得了令人叫绝的效果!用户在用这个App时,会觉得自己真的是在翻一本书。其实,将一个普通的电子阅读App变成一个让用户体验更加真实的带翻页效果的App,只需要做很小的改动。
+However, you're not done yet! You'll make this app even better and more intuitive in Part 2 of this tutorial, where you'll explore custom transitions between the closed and opened book views.
+
+当然,我们还没有完成这个App。在本教程的第二部分,我们将使App变得更加完善和生动,我们将在打开书和关上书的一瞬间加入自定义动画。
+
+Do you have any crazy layout ideas you are considering for your own app? If you have any questions, comments or other ideas from this tutorial, please join the discussion below!
+
+在开发App过程中,你也可能曾经有过一些关于布局的“疯狂”想法。如果你对本文有任何问题、意见或想法,请加入到下面的讨论中来!
+
+
+
diff --git "a/issue-17/\345\246\202\344\275\225\345\256\236\347\216\260iOS\345\233\276\344\271\246\345\212\250\347\224\273-\347\254\2542\351\203\250\345\210\206.md" "b/issue-17/\345\246\202\344\275\225\345\256\236\347\216\260iOS\345\233\276\344\271\246\345\212\250\347\224\273-\347\254\2542\351\203\250\345\210\206.md"
new file mode 100644
index 0000000..fab5a1a
--- /dev/null
+++ "b/issue-17/\345\246\202\344\275\225\345\256\236\347\216\260iOS\345\233\276\344\271\246\345\212\250\347\224\273-\347\254\2542\351\203\250\345\210\206.md"
@@ -0,0 +1,993 @@
+> * 原文链接 : [How to Create an iOS Book Open Animation: Part 2](http://www.raywenderlich.com/97690/how-to-create-an-ios-book-open-animation-part-2)
+* 原文作者 : [Vincent Ngo](http://www.raywenderlich.com/u/jomoka)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [kmyhy](https://github.com/kmyhy)
+* 校对者: 这里校对者的github用户名
+* 状态 : 校对中
+
+Welcome back to our iOS book open animation tutorial series!
+In the first part of this tutorial series, you learned how to create two custom collection view layouts and applied shadow layers to the book’s pages to create depth and realism in your app.
+In this final part, you’ll learn to create custom navigation transitions and apply interactive gestures to open a book with a pinch gesture.
+
+欢迎回到iOS图书动画系列教程!在第一部分,我们学习了如何创建两个自定义的collection view layout并在图书书页中使用了阴影图层以使我们的App显得更加立体和真实。
+在这一部分,我们将学习如何创建自定义的转场动画并通过捏放手势来打开一本书。
+
+Note: Full credit goes to Attila Hegedüs for creating this awesome sample project.
+
+
+
+> 注意:感谢[Attila Hegedüs](https://twitter.com/hegedus90)创建了本教程的示例程序。
+
+##Getting Started
+##开始
+The tutorial picks up from Part 1. If you didn’t work through the last part, or want to start afresh, simply download the completed sample project from the previous tutorial.
+
+本教程以前一部分的内容为基础。如果你还没有看过第一部分,或者想从一个新项目开始,你可以在[这里](http://cdn1.raywenderlich.com/wp-content/uploads/2015/05/Part-1-Paper-Completed.zip)下载上一部分教程中的完整示例程序。
+
+
+
+Open up the project in Xcode. Right now, when you select a book to read the open pages simply slide in from the right. This is the default transition behavior for a UINavigationController. But by the end of this tutorial, your custom transition will look like the following:
+
+在Xcode中打开项目。现在,你可以选择一本书进行阅读,并从右边滑动进行翻页。这时的转场动画使用的是UINavigationController自带的动画效果。通过本教程的学习,我们将自定义这个动画效果,如下图所示:
+
+
+
+The custom transition will animate the book smoothly between the closed and opened states in a natural manner that users will love.
+Time to get started!
+
+这个动画会在“打开书”和“合起书”两个状态之间一一种更加自然的方式平滑过渡,这将更能获得用户的欢心。让我们马上开始吧!
+
+##Creating your Custom Navigation Controller
+##创建自定义的导航控制器
+To create a custom transition on a push or pop you must create a custom navigation controller and implement the UINavigationControllerDelegate protocol.
+Right-click (or Ctrl-click) on the App group and click New File. Select the iOS\Source\Cocoa Touch Class template and name the new file CustomNavigationController. Make sure it’s a subclass of UINavigationController and set the language to Swift. Click Next and then Create.
+Open CustomNavigationController.swift and replace its contents with the following:
+
+要创建自定义的push动画和pop动画,我们必须创建自定义导航控制器并实现UINavigationControllerDelegate协议。
+在App文件夹上右击(或ctrl+左键)并点击New File。选择iOS\Source\Cocoa Touch Class模板并将类名设置为CustomNavigationController。让它继承自UINavigationController并将语言设置为Swift。点击Next,Create。
+打开CustomNavigationController.swift,编辑其内容为:
+
+```
+import UIKit
+
+class CustomNavigationController: UINavigationController, UINavigationControllerDelegate {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ //1
+ delegate = self
+ }
+
+ //2
+ func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
+ if operation == .Push {
+ return nil
+ }
+
+ if operation == .Pop {
+ return nil
+ }
+
+ return nil
+ }
+}
+```
+
+Here’s what you’re doing in the code above:
+
+上述代码分别解释如下:
+
+1. In viewDidLoad you set the navigation controller as its own delegate.
+2. navigationController(_:animationControllerForOperation:fromViewController:toViewController:) is one of the methods you can implement for UINavigationControllerDelegate. This method executes each time you push or pop between view controllers, and you control which animated transition you return from this method. The code currently returns nil which defaults to the standard transition. You’ll replace it with your own custom transition object shortly.
+
+---
+
+1. 在 viewDidLoad 方法中,设置CustomNavigationController的delegate属性为它自己。
+2. navigationController(_:animationControllerForOperation:fromViewController:toViewController:) 方法属于UINavigationControllerDelegate协议。这个方法在两个View Controller之间发生push或pop导航时调用。你可以在这个方法中分别针对push导航和pop导航返回各自的Transition对象。目前我们都返回了nil,这表明我们将使用UINavigationController内置的标准Transition。稍后我们将替换为自己的Transition对象。
+
+Now that you have your custom navigation controller set up, it’s time to replace the default navigation controller in storyboard.
+Open Main.storyboard and click Navigation Controller in the storyboard’s view hierarchy on the left. Next, click the Identity Inspector and under Custom Class, change UINavigationController to CustomNavigationController, as shown below:
+
+现在我们拥有了自己的Navigation Controller类,接下来在故事板中将默认的UINavigationController替换为我们的CustomNavigationController。
+打开Main.storyboard,在故事板编辑器左侧的对象窗口中选择Navigation Controller对象,打开Identity窗口,在Custom Class下,将Class由UINavigationController修改为CustomNavigationController,如下图所示:
+
+
+
+Build and run to ensure everything still works; nothing will have changed since you’re returning nil in your delegate method, which defaults to the navigation controller’s standard transition.
+
+编译运行,什么变化都没有发生。这是因为在委托方法中我们仍然返回了nil,因此使用的仍然是UINavigationController内置的标准Transition。
+
+##Creating the Custom Transition
+##创建自定义导航动画
+
+Time for the fun part — building your custom transition object! :]
+With a custom transition object, the class you create must conform to the UIViewControllerAnimatedTransitioning protocol, and in particular, the methods below:
+
+ - transitionDuration: Required. Returns the duration of the animation
+ and synchronizes interactive transitions.
+ - animateTransition: Required. Provides the to and from controllers you’re transitioning between. Most of the heavy lifting will be done in this method.
+ - animationEnded: Optional. Informs you when the transition has finished. You can perform any required cleanup in this method.
+
+最有趣的部分来了——创建我们的自定义Transition对象!:]
+要自定义Transition类,我们必须实现UIViewControllerAnimatedTransitioning协议,最主要的是这几个方法:
+
+ - transitionDuration: 必须实现。这个方法返回一个动画时长,使两个动画的播放时间相同(或不同)。
+ - animateTransition: 必须实现。这个方法负责提供参与动画的两个控制器:一个to控制器,一个from控制器。Transiton的大部分工作在这个方法中完成。
+ - animationEnded: 可选的方法。这个方法主要是用来通知你什么时候动画完成了。我们可以在这个方法中进行必要的清理动作。
+
+##Setting up Your Transition
+##创建自定义Transition类
+Right-click (or Ctrl-click) on the App group and click New File. Select the iOS\Source\Cocoa Touch Class template and name the new file BookOpeningTransition. Make sure it’s a subclass of NSObject and set the language to Swift. Click Next and then Create.
+Open BookOpeningTransition.swift and replace its contents with the following:
+
+在App文件夹上右击(或ctrl+左键),然后点击New File。选择iOS\Source\Coca Touch Class 模板,将类名设置为BookOpeningTransition,继承NSObject,语言Swift。然后点击Next,Create。
+打开BookOpeningTransition.swift,编辑代码如下:
+
+```
+import UIKit
+
+//1
+class BookOpeningTransition: NSObject, UIViewControllerAnimatedTransitioning {
+
+ // MARK: Stored properties
+ var transforms = [UICollectionViewCell: CATransform3D]() //2
+ var toViewBackgroundColor: UIColor? //3
+ var isPush = true //4
+
+ //5
+ // MARK: UIViewControllerAnimatedTransitioning
+ func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
+ return 1
+ }
+
+ func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
+
+ }
+}
+```
+
+Taking each commented section in turn:
+
+1. BookOpeningTransition implements the required methods for the UIViewControllerAnimatedTransitioning protocol.
+2. The dictionary transforms stores key value pairs, where the key is a UICollectionViewCell and the value is of type CATransform3D. This dictionary tracks each cell’s page transform when the book is open.
+3. This defines the color you transition to, which helps the fade look much cleaner.
+4. The boolean isPush determines whether the transition is a push, or a pop,
+5. Here you add the required methods for UIViewControllerAnimatedTransitioning to avoid build errors; you’ll implement these methods shortly.
+
+以上代码对应注释中的编号,分别解释如下:
+
+1. 声明 BookOpeningTransition 类实现UIViewControllerAnimatedTransitioning 协议。
+2. 声明一个 transforms 字典,键存储UICollectionViewCell,值则存储对应的CATransiform3D。这个字典保存在书打开后所有cell的翻页动画。
+3. 指定to视图控制器的背景色,它将让淡出淡入动画看起来更加清楚。
+4. 布尔值isPush 用于标识当前动画是Push动画还是Pop动画。
+5. 增加必须实现的 UIViewControllerAnimatedTransitioning 协议方法,以使编译错误不再出现,稍后我们会实现这些方法。
+
+Now that you have your variables set up, it’s time to implement the protocol methods.
+Replace the contents of transitionDuration(_:) with the following:
+
+定义好需要的变量,接下来就是实现协议方法。
+首先是transitionDuration(_:)方法:
+
+```
+if isPush {
+ return 1
+} else {
+ return 1
+}
+```
+
+transitionDuration(_:) returns the duration of the transition animation. In this case, you want it to take 1 second on either a push or a pop. Writing the method this way lets you easily change the timing of the push or pop.
+
+transitionDuration(_:)方法返回了动画播放时长。这里,无论是Push动画还是Pop动画我们都设置为1秒。通过这个方法我们可以很方便地改变Push动画或Pop动画的时长。
+
+Next, you need to implement the second required protocol method — animateTransition — where the magic will happen! :] You’ll implement this in two parts:
+
+1. Implement the helper methods to set up animateTransition for a push.
+2. Implement the helper methods to set up animateTransition for a pop.
+
+然后,是第二个协议方法——animateTransition——这是最核心的部分!:]我们将这个方法分成两部分来介绍:
+
+1. 实现一个助手方法,用于创建Push动画所需的Transition对象。
+2. 实现一个助手方法,用于创建Pop动画所需的Transition对象。
+
+##Creating the Push Transition
+##创建Push动画
+
+Imagine yourself opening a book in real life:
+
+回想你在生活中打开一本书的样子:
+
+
+
+Although it looks complicated, you only need to consider the two states of your animation and let UIView‘s method animateWithDuration handle the animation between the following two states:
+
+1. Stage 1 is when the book is closed.
+2. Stage 2 is when the book is open; this is essentially the transform you created in Part 1 of this tutorial.
+
+虽然看起来复杂,但我们只需要考虑两个状态,同时让UIView的animateWithDuration方法根据这两个状态进行不同的处理:
+
+1. 状态1,书处于合起状态。
+2. 状态2,书处于打开状态;这就是我们在第一部分教程中实现的部分。
+
+First, you’ll implement some helper methods to handle the two states before you implement the animateTransition(_:) protocol method.
+Still in BookOpeningTransition.swift, add the following code to the end of the class:
+
+首先,在实现animateTransition(:_)协议方法之前,我们来实现几个助手方法。
+仍然在BookOpeningTransition.swift中,编写如下方法:
+
+```
+// MARK: Helper Methods
+func makePerspectiveTransform() -> CATransform3D {
+ var transform = CATransform3DIdentity
+ transform.m34 = 1.0 / -2000
+ return transform
+}
+```
+
+This code returns a transform and adds perspective in the z-axis. You’ll use this later to help transform your views during the animation.
+
+这个方法返回了一个Transform对象,并在z轴上增加了一点立体感。在播放动画时,我们将用到这个方法。
+
+###State 1 – Closed Book
+###状态 1 - 合起书
+Next, add the following code just after makePerspectiveTransform:
+
+在makePerspectiveTransform方法后实现如下方法:
+
+func closePageCell(cell : BookPageCell) {
+ // 1
+ var transform = self.makePerspectiveTransform()
+ // 2
+ if cell.layer.anchorPoint.x == 0 {
+ // 3
+ transform = CATransform3DRotate(transform, CGFloat(0), 0, 1, 0)
+ // 4
+ transform = CATransform3DTranslate(transform, -0.7 * cell.layer.bounds.width / 2, 0, 0)
+ // 5
+ transform = CATransform3DScale(transform, 0.7, 0.7, 1)
+ }
+ // 6
+ else {
+ // 7
+ transform = CATransform3DRotate(transform, CGFloat(-M_PI), 0, 1, 0)
+ // 8
+ transform = CATransform3DTranslate(transform, 0.7 * cell.layer.bounds.width / 2, 0, 0)
+ // 9
+ transform = CATransform3DScale(transform, 0.7, 0.7, 1)
+ }
+
+ //10
+ cell.layer.transform = transform
+}
+Recall that the BookViewController is a collection view of pages. You transformed every page to align to the book’s spine, and rotated it on an axis to achieve the page flipping effect. Initially, you want the book to be closed. This method transitions every cell (or page) to be flat and fit behind the book’s cover.
+Here’s a quick illustration of the transform:
+
+回想一下BookViewController,它是一个CollectionView,代表了书中的一页。我们将每一页和书脊对齐,以y轴为心进行旋转实现翻页效果。首先,书是合起(关闭)的。这个方法将每个cell(即书页)放平并置于封面的下面。
+这是动画运行效果:
+
+
+
+Here’s an explanation of the code that makes that happen:
+
+1. Initialize a new transform using the helper method you created earlier.
+2. Check that the cell is a right-hand page.
+3. If it’s a right-hand page, set its angle to 0 to make it flat.
+4. Shift the page be centered behind the cover.
+5. Scale the page on the x and y axes by 0.7. Recall that you scaled the book covers to 0.7 in the previous tutorial, in case you wondered where this magic number came from.
+6. If the cell isn’t a right-hand page, then it must be a left-hand page.
+7. Set the left-hand page’s angle to 180. Since you want the page to be flat, you need to flip it over to the right side of the spine.
+8. Shift the page to be centered behind the cover.
+9. Scale the pages back to 0.7.
+10. Finally, set the cell’s transform.
+
+以上代码解释如下:
+
+1. 用前面创建的助手方法,生成一个Transform对象。
+2. 判断cell是否是右侧页。
+3. 如果是,设置其角度为0,即放置为水平。
+4. 将它移动到封面下方并居中对齐。
+5. 将它缩放为70%。还记得我们前面将封面也缩放为70%吗?这里是同样的意思。
+6. 如果cell不是右侧页,则就是左侧页。
+7. 设置左侧页的角度为180度。要将它水平放置,我们需要将它翻到书脊的右边。
+8. 将它放到封面下方并居中对齐。
+9. 缩放70%。
+10. 最后,赋给cell的transform属性。
+
+Now add the following method below the one you added above:
+
+在上面的方法后添加如下方法:
+
+```
+func setStartPositionForPush(fromVC: BooksViewController, toVC: BookViewController) {
+ // 1
+ toViewBackgroundColor = fromVC.collectionView?.backgroundColor
+ toVC.collectionView?.backgroundColor = nil
+
+ //2
+ fromVC.selectedCell()?.alpha = 0
+
+ //3
+ for cell in toVC.collectionView!.visibleCells() as! [BookPageCell] {
+ //4
+ transforms[cell] = cell.layer.transform
+ //5
+ closePageCell(cell)
+ cell.updateShadowLayer()
+ //6
+ if let indexPath = toVC.collectionView?.indexPathForCell(cell) {
+ if indexPath.row == 0 {
+ cell.shadowLayer.opacity = 0
+ }
+ }
+ }
+}
+```
+
+setStartPositionForPush(_:toVC:) sets up stage 1 of the transition. It takes in two view controllers to animate:
+
+- fromVC, of type BooksViewController, lets you scroll through your list of books.
+- toVC, of type BookViewController, lets you flip through the pages of the book you selected.
+
+setStartPositionForPush(_:toVC:)方法创建状态1的Transition。这个动画涉及到两个ViewController:
+
+- fromVC,类型为BooksViewController,用于滚动浏览图书列表。
+
+- toVC,BookViewController类型,让你可以翻阅选定的书。
+
+Here’s what’s going on in the code above:
+
+1. Store the background color of BooksViewController‘s collection view and set BookViewController‘s collection view background to nil.
+2. Hide the selected book cover. toVC will now handle the display of the cover image.
+3. Loop through the pages of the book.
+4. Save the current transform of each page in its opened state.
+5. Since the book starts from a closed state, you transform the pages to closed and update the shadow layer.
+6. Finally, ignore the shadow of the cover image.
+
+以上代码解释如下:
+
+1. 保存BooksViewController的Cellection View的背景色,然后设置BookViewController的Collection View的背景色为nil。
+2. 隐藏封面。现在toVC将负责处理封面图片的显示。
+3. 遍历书中所有书页。
+4. 保存每一页的当前Transform到transforms字典。
+5. 由于书一开始是合起的,我们将该页转换为合起状态,然后更新阴影图层。
+6. 最后,忽略封面图片的阴影。
+
+###State 2 – Opened Book
+###状态 2 - 打开书
+Now that you’ve finished state 1 of the transitions, you can move on to state 2, where you go from a closed book to an opened book.
+Add the following method below setStartPositionForPush(_:toVC:)):
+
+现在,状态1的动画完成了,我们可以转移到状态2的处理中来。在这里我们将一本合起的书转换成一本打开的书。在setStartPositionForPush(_:toVC:)方法下添加如下方法:
+
+```
+func setEndPositionForPush(fromVC: BooksViewController, toVC: BookViewController) {
+ //1
+ for cell in fromVC.collectionView!.visibleCells() as! [BookCoverCell] {
+ cell.alpha = 0
+ }
+
+ //2
+ for cell in toVC.collectionView!.visibleCells() as! [BookPageCell] {
+ cell.layer.transform = transforms[cell]!
+ cell.updateShadowLayer(animated: true)
+ }
+}
+```
+
+Digging into the code above:
+
+1. Hide all the book covers, since you’re presenting the selected book’s pages.
+2. Go through the pages of the selected book in BookViewController and load the previously saved open transforms.
+
+上述代码解释如下:
+
+1. 隐藏所有书的封面,因为接下来我们要显示所选图书的内容。
+2. 在BookViewController中遍历书中每一页并读取先前保存在transform数组中的的Transform。
+
+
+After you push from BooksViewController to BookViewController, there’s a bit of cleanup to do.
+Add the following method just after the one you added above:
+
+在从BooksViewController导航到BookViewController后,我们还需要进行一些清理工作。
+在上面的方法之后加入如下方法:
+```
+func cleanupPush(fromVC: BooksViewController, toVC: BookViewController) {
+ // Add background back to pushed view controller
+ toVC.collectionView?.backgroundColor = toViewBackgroundColor
+}
+```
+Once the push is complete, you simply set the background color of BookViewController‘s collection view to the background color you saved earlier, hiding everything behind it.
+
+在Push完成时,我们将BookViewController的Collection View的背景色设回原来保存的颜色,隐藏位于它下面的内容。
+
+###Implementing the Book Opening Transition
+###实现打开书的动画
+Now that you have your helper methods in place, you’re ready to implement the push animation! Add the following code to the empty implementation of animateTransition(_:):
+
+现在我们已经实现了助手方法,接下来要实现Push动画了!
+在空的animateTransition(_:)方法中加入以下代码:
+
+```
+//1
+let container = transitionContext.containerView()
+//2
+if isPush {
+ //3
+ let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! BooksViewController
+ let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! BookViewController
+ //4
+ container.addSubview(toVC.view)
+
+ // Perform transition
+ //5
+ self.setStartPositionForPush(fromVC, toVC: toVC)
+
+ UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.7, options: nil, animations: {
+ //6
+ self.setEndPositionForPush(fromVC, toVC: toVC)
+ }, completion: { finished in
+ //7
+ self.cleanupPush(fromVC, toVC: toVC)
+ //8
+ transitionContext.completeTransition(finished)
+ })
+} else {
+ //POP
+}
+```
+
+Here’s what’s happening in animateTransition(_:):
+
+1. Get the container view, which acts as the superview between the transitioning view controllers.
+2. Check that you’re performing a push.
+3. If so, get both fromVC (BooksViewController) and toVC (BookViewController).
+4. Add toVC (the BookViewController) to the containing view.
+5. Set up the starting positions for the to and from view controllers for the closed state.
+6. Next, you animate from the starting position (Closed State) to the ending position (Opened State)
+7. Perform any cleanup.
+8. Notify the system that the transition is complete.
+
+以上代码解释如下:
+
+1. 获取Container View,Container View在两个View Controller发生转场时充当父视图的角色。
+2. 判断当前的转场动作是否是一个Push动作。
+3. 如果是,分别获取fromVC(BooksViewController)和toVC(BookViewController)。
+4. 将toVC(BookViewController)加到Container View。
+5. 设定Push动作的起止点,即toVC和fromVC。
+6. 开始动画。从起始点(书合起的状态)转变到终点(书打开状态)。
+7. 执行清理动作。
+8. 告诉系统,转换完成。
+
+##Applying the Push Transition to the Navigation Controller
+##在Navigation Controller中应用Push动画
+Now that you have your push transition set up, it’s time to apply it to your custom navigation controller.
+Open BooksViewController.swift and add the following property just after the class declaration:
+
+现在我们已经创建好Push动画,接下来就是将它应用到自定义的Navigation Controller中了。
+打开BooksViewController.swift在类声明中增加属性:
+```
+var transition: BookOpeningTransition?
+```
+
+This property keeps track of your transition, letting you know whether the transition is a push or pop.
+Next add the following extension after the ending curly brace:
+
+transition属性用于保存Transition对象,通过它我们可以知道当前动画是Push动画还是Pop动画。
+然后在文件末尾的大括号之后加入一个扩展:
+
+```
+extension BooksViewController {
+func animationControllerForPresentController(vc: UIViewController) -> UIViewControllerAnimatedTransitioning? {
+ // 1
+ var transition = BookOpeningTransition()
+ // 2
+ transition.isPush = true
+ // 3
+ self.transition = transition
+ // 4
+ return transition
+ }
+}
+```
+
+This creates an extension to separate parts of the code’s logic. In this case, you want to group methods related to transitions in one place. This method sets up the transition object and returns it as well.
+Taking a closer look at the code:
+
+1. Create a new transition.
+2. Since you are presenting the controller, or pushing, set isPush to true.
+3. Save the current transition.
+4. Return the transition.
+
+通过扩展,我们将一部分代码分离出来。这里,我们将和转换动画有关的方法放到了一起。上面的这个方法创建并返回了一个Transition对象。
+以上代码解释如下:
+
+1. 创建一个新的Transition。
+2. 因为我们是要弹出或者Push一个Controller,所以将isPush设置为true。
+3. 保存当前Transition对象。
+4. 返回Transition对象。
+
+Now open CustomNavigationController.swift and replace the push if statement with the following:
+
+现在打开CustomNavigationController.swift并将Push的if语句替换为:
+
+```
+if operation == .Push {
+ if let vc = fromVC as? BooksViewController {
+ return vc.animationControllerForPresentController(toVC)
+ }
+}
+```
+
+This checks that the view controller you’re pushing from is a BooksViewController, and presents BookViewController with the transition you created: BookOpeningTransition.
+Build and run your app; click on a book of your choice and you’ll see the book animate smoothly from closed to opened:
+
+上述语句判断当前Push的View Controller是不是一个BooksViewController,如果是,用我们创建的BookOpeningTransition呈现BookViewController。
+编译运行,选择某本书,你将看到书缓缓由合起状态打开:
+
+
+Uh..how come it’s not animating?
+呃...我们的动画效果呢?
+
+
+It’s jumping straight from a closed book to an opened book because you haven’t loaded the pages’ cells!
+
+书直接从合起状态跳到了打开状态,原因在于我们没有加载cell(书页)!
+
+The navigation controller transitions from BooksViewController to BookViewController, which are both UICollectionViewControllers. UICollectionView cells don’t load on the main thread, so your code sees zero cells at the start — and thinks there’s nothing to animate!
+
+导航控制器从BooksViewController切换到BookViewController,这二者都是UICollecitonViewController。UICollectionViewCell没有在主线程中加载,因此代码一开始的时候以为cell的个数为0——这样当然不会有动画产生。
+
+You need to give the collection view enough time to load all the cells.
+Open BooksViewController.swift and replace openBook(_:) with the following:
+
+我们需要让Collection View有足够的时间去加载所有的Cell。
+打开BooksViewController.swift将openBook(_:)方法替换为:
+
+```
+func openBook(book: Book?) {
+ let vc = storyboard?.instantiateViewControllerWithIdentifier("BookViewController") as! BookViewController
+ vc.book = selectedCell()?.book
+ //1
+ vc.view.snapshotViewAfterScreenUpdates(true)
+ //2
+ dispatch_async(dispatch_get_main_queue(), { () -> Void in
+ self.navigationController?.pushViewController(vc, animated: true)
+ return
+ })
+}
+```
+
+Here’s how you solved the problem:
+
+1. You tell BookViewController to create a snapshot after the changes have been incorporated.
+2. Make sure you push BookViewController on the main thread to give the cells time to load.
+
+以上代码解释如下:
+
+1. 告诉BookViewController在动画一开始之前截屏。
+2. 将Push BookViewController的动作放到主线程中进行,这样就有时间去加载cell了。
+
+Build and run your app again; you should see the book animate properly on a push:
+
+编译、运行,这次你将看到正确的Push动画了:
+
+
+
+That looks much better! :]
+Now that you’re done with the push transition, you can move on to the pop transition.
+
+这样看起来是不是好多啦?
+现在,关于Push动画的内容就到此结束,接下来,我们开始实现Pop动画。
+###Implementing the Pop Helper Methods
+###实现Pop动画的助手方法
+Popping the view controller is pretty much the opposite of a push. Stage 1 is now the open book state, and Stage 2 is now the closed book state:
+
+一个View Controller的Pop动作刚好和Push相反。状态1是图书打开的状态,而状态2则变成了书合起的状态:
+
+
+Open up BookOpeningTransition.swift and add the following code:
+
+打开BookOpeningTransition.swift,加入以下方法:
+
+```
+// MARK: Pop methods
+func setStartPositionForPop(fromVC: BookViewController, toVC: BooksViewController) {
+ // Remove background from the pushed view controller
+ toViewBackgroundColor = fromVC.collectionView?.backgroundColor
+ fromVC.collectionView?.backgroundColor = nil
+}
+```
+
+setStartPositionForPop(_:toVC) only stores the background color of BookViewController and removes the background color of BooksViewController‘s collection view. Note that you don’t need to set up any cell transforms, since the book is currently in its opened state.
+
+setStartPositionForPop(_:toVC)方法仅仅是保存BookViewController的背景色并将BooksViewController的Collection View的背景色删除。注意,你不需要创建任何cell动画,因为书在这个时候是打开状态。
+
+Next, add the following code for setEndPositionForPop(_:toVC) immediately after the code you just added above:
+
+接着,在上面的方法后面加入这个方法:
+
+```
+func setEndPositionForPop(fromVC: BookViewController, toVC: BooksViewController) {
+ //1
+ let coverCell = toVC.selectedCell()
+ //2
+ for cell in toVC.collectionView!.visibleCells() as! [BookCoverCell] {
+ if cell != coverCell {
+ cell.alpha = 1
+ }
+ }
+ //3
+ for cell in fromVC.collectionView!.visibleCells() as! [BookPageCell] {
+ closePageCell(cell)
+ }
+}
+```
+
+This method sets up the end state of the pop transition where the book goes from opened to closed:
+
+1. Get the selected book cover.
+2. In the closed book state, loop through all the book covers in BooksViewController and fade them all back in.
+3. Loop through all the pages of the current book in BookViewController and transform the cells to a closed state.
+
+这个方法创建Pop动画的起止点,即从打开变成合起:
+
+1. 获取选择的书的封面。
+2. 在合起状态,在BooksViewController中遍历私有书的封面,然后对所有对象进行一个渐入效果。
+3. 在BookViewController中遍历当前图书的所有页,将所有cell转变成合起状态。
+
+Now add the following method:
+
+现在新建如下方法:
+
+```
+func cleanupPop(fromVC: BookViewController, toVC: BooksViewController) {
+ // Add background back to pushed view controller
+ fromVC.collectionView?.backgroundColor = self.toViewBackgroundColor
+ // Unhide the original book cover
+ toVC.selectedCell()?.alpha = 1
+}
+```
+
+This method performs some cleanup once the pop transition has finished. The cleanup process sets BooksViewController‘s collection view background to its original state and displays the original book cover.
+
+这个方法在Pop动画完成时执行清理动作:将BooksViewController的Collection View的背景色设回它开始的值并显示封面。
+
+Now add the following code within the protocol method animateTransition(_:) inside the else block of the code with the //POP comment:
+
+在animateTransition(_:)方法里面,找到注释有“//POP”的else语句块,添加如下代码:
+
+```
+//1
+let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! BookViewController
+let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! BooksViewController
+
+//2
+container.insertSubview(toVC.view, belowSubview: fromVC.view)
+
+//3
+setStartPositionForPop(fromVC, toVC: toVC)
+UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: {
+ //4
+ self.setEndPositionForPop(fromVC, toVC: toVC)
+}, completion: { finished in
+ //5
+ self.cleanupPop(fromVC, toVC: toVC)
+ //6
+ transitionContext.completeTransition(finished)
+})
+```
+
+Here’s how the pop transition animation works:
+
+1. Grab the view controllers involved in the transition. fromVC is now BookViewController (the opened book state) and toVC is now the BooksViewController (closed book state).
+2. Add BooksViewController below BookViewController within the container view.
+3. setStartPositionForPop(_:toVC) stores the background color before setting it to nil.
+4. Animate from the opened book state to the closed book state.
+5. Clean up the view controller once the animation is done by setting the background color back to it’s original color and showing the book cover.
+6. Notify the transition is complete.
+
+以上代码解释如下:
+
+1. 获取动画中涉及的两个ViewController。现在,fromVC 是BookViewController (打开状态),toVC是BooksViewController(合起状态)。
+2. 向Container View中加入BooksViewController(在BookViewContorller的下方)。
+3. setStartPositionForPop(_:toVC) 方法先保存背景色,再将背景色设为nil。
+4. 执行动画,即从打开状态切换到合起状态。
+5. 动画完成,执行清理动作。将背景色设回原来值,显示封面。
+6. 通知动画完成。
+
+##Applying the Pop Transition to the Navigation Controller
+##在Navigation Controller中应用Pop动画
+Now you need to set up the pop transition just as you did with the push transition.
+Open BooksViewController.swift and add the following method right after animationControllerForPresentController(_:):
+
+现在需要创建Pop动画,就如同我们在Push动画所做一样。
+打开BooksViewController.swift,在animationControllerForPresentController(_:)方法后增加如下方法:
+
+```
+func animationControllerForDismissController(vc: UIViewController) -> UIViewControllerAnimatedTransitioning? {
+ var transition = BookOpeningTransition()
+ transition.isPush = false
+ self.transition = transition
+ return transition
+}
+```
+
+This again creates a new BookOpeningTransition, but the only difference is that the transition is now set to be a pop.
+Now open CustomNavigationController.swift and replace the pop if statement with the following:
+
+这里,我们创建了一个新的BookOpeningTransition对象,但不同的是isPush设置为false。
+打开CustomNavigationController.swift,然后替换Pop部分的if语句为:
+
+```
+if operation == .Pop {
+ if let vc = toVC as? BooksViewController {
+ return vc.animationControllerForDismissController(vc)
+ }
+}
+```
+
+This returns the transition and performs the pop animation to close the book.
+Build and run your app; select a book to see it open and close as shown below:
+
+上述代码返回一个Transition对象,并执行Pop动画,合起书本。
+编译,运行程序,选择一本书,查看它的打开和合起。如下图所示:
+
+
+##Creating an Interactive Navigation Controller
+##创建互动式的Navigation Controller
+The opening and closing transition animations look great — but you can still do better! You can apply intuitive gestures to pinch the book open or closed.
+First, open BookOpeningTransition.swift and add the following property:
+
+打开和合起动画搞定了——但我们还能更进一步!我们为什么不用一个更直观的捏放手势来打开和合起书本呢?
+打开BookOpeningTransition.swift,增加如下属性定义:
+
+```
+// MARK: Interaction Controller
+var interactionController: UIPercentDrivenInteractiveTransition?
+```
+
+Next open CustomNavigationController.swift and add the following code:
+然后打开CustomNavigationController.swift,加入下列代码:
+
+
+```
+func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
+ if let animationController = animationController as? BookOpeningTransition {
+ return animationController.interactionController
+ }
+ return nil
+}
+```
+
+In the above method, you return the interactive animator object from BookOpeningTransition. This lets the navigation controller keep track of the progress of the animation so the user can interactively pinch a book opened or closed.
+
+在这个方法中,我们从BookOpeningTransition对象获得了一个interactionController。这样导航控制器能够跟踪动画进程以便用户可以用捏放手势打开和合起书。
+
+Now open BooksViewController.swift and add the following property under the transition variable:
+
+打开BooksViewController.swift,在trnasitoin变量下增加如下属性:
+
+```
+//1
+var interactionController: UIPercentDrivenInteractiveTransition?
+//2
+var recognizer: UIGestureRecognizer? {
+ didSet {
+ if let recognizer = recognizer {
+ collectionView?.addGestureRecognizer(recognizer)
+ }
+ }
+}
+```
+
+Here’s why you added these variables:
+
+1. interactionController is of class type UIPercentDrivenInteractiveTransition, which manages the custom animation between the view controllers transitioning in and out. The interaction controller also depends on a transition animator, which is a custom object that implements the UIViewControllerAnimatorTransitioning protocol. You’ve created BookOpeningTransition — which does exactly that!
+The iteractionController can control the progress between pushing and popping view controllers. To learn more about this class, read up on Apple’s documentation on how this works.
+2. recognizer is a UIGestureRecognizer. You’ll use the gesture recognizer to pinch the book in and out.
+
+这两个属性的作用分别是:
+
+1. interactionController 是一个UIPercentDrivenInteractiveTransition类,它负责管理View Contorller之间转场的自定义动画。interactionController由一个Transition Animator生成,后者是一个实现了UIViewControllerAnimatorTransitioning协议的对象。而我们已经拥有了BookOpeningTransition——这就是一个实现了UIViewControllerAnimatorTransitioning的对象。interactionController能够控制Push动画和Pop动画之间的进度。关于这个类的更多内容,请参考Apple官方文档。
+2. recognizer 是一个UIGestureRecognizer。我们用这个手势识别器实现以捏放手势开合书本。
+
+Now add the following snippet under the transition.isPush = true line of your BooksViewController extension in animationControllerForPresentController(_:):
+
+在BooksViewController扩展的animationControllerForPresentController(_:)方法中,transition.isPush=true一行下面,加入代码:
+
+```
+transition.interactionController = interactionController
+```
+
+This lets your custom navigation controller know which interaction controller to use.
+Add the same code to animationControllerForDismissController(_:), under transition.isPush = false:
+
+这句代码让CustomNavigationController知道要用哪个interaction controller。
+在animationControllerForDismissController(_:)方法中transition.isPush=false一行下面加入同样的代码:
+
+```
+transition.interactionController = interactionController
+```
+
+Next, add the following code to viewDidLoad():
+
+在viewDidLoad()方法中增加代码:
+
+```
+recognizer = UIPinchGestureRecognizer(target: self, action: "handlePinch:")
+```
+
+This initializes a UIPinchGestureRecognizer, which lets the user perform a pinch gesture with the action method handlePinch(_:).
+Implement the action under viewDidLoad() like so:
+
+这里我们初始化了一个UIPinchGestureRecognizer,允许用户在做出捏放手势时调用handlePinch(_:)方法。
+在viewDidLoad()方法下面实现这个方法:
+
+```
+// MARK: Gesture recognizer action
+func handlePinch(recognizer: UIPinchGestureRecognizer) {
+ switch recognizer.state {
+ case .Began:
+ //1
+ interactionController = UIPercentDrivenInteractiveTransition()
+ //2
+ if recognizer.scale >= 1 {
+ //3
+ if recognizer.view == collectionView {
+ //4
+ var book = self.selectedCell()?.book
+ //5
+ self.openBook(book)
+ }
+ //6
+ } else {
+ //7
+ navigationController?.popViewControllerAnimated(true)
+ }
+ case .Changed:
+ //8
+ if transition!.isPush {
+ //9
+ var progress = min(max(abs((recognizer.scale - 1)) / 5, 0), 1)
+ //10
+ interactionController?.updateInteractiveTransition(progress)
+ //11
+ } else {
+ //12
+ var progress = min(max(abs((1 - recognizer.scale)), 0), 1)
+ //13
+ interactionController?.updateInteractiveTransition(progress)
+ }
+ case .Ended:
+ //14
+ interactionController?.finishInteractiveTransition()
+ //15
+ interactionController = nil
+ default:
+ break
+ }
+}
+```
+
+For the UIPinchGestureRecognizer, you’ll keep track of three different states. The state began lets you know when the pinch has started. The state changed detects changes to the pinch, and ended lets you know when the pinch has ended.
+The rundown of your implementation of handlePinch(_:) code is below:
+
+对于UIPinchGestureRecognizer,我们要关注这3个状态:开始状态,这让你知道捏放手势何时开始;改变状态,检测捏放手势的变化;结束状态,让你知道捏放手势何时结束。
+handlePinch(_:)方法代码解释如下:
+
+**Began State**
+1. Instantiate a UIPercentDrivenInteractiveTransition object.
+2. Check that the scale, which is dependent on the distance between the pinch points, is greater than or equal to 1.
+3. If so, ensure that the view you involved in the gesture is indeed a collection view.
+4. Grab the book being pinched.
+5. Perform a push of BookViewController to show the pages of the book.
+6. If the scale is less than 1…
+7. …perform a pop of BookViewController to show the book cover again.
+**Changed State – While Pinching**
+8. Check to see if the current transition is performing a push.
+9. If you’re pushing to BookViewController, obtain the progress of the user’s pinch gesture. progress must be between 0 and 1. You scale the pinch down to one-fifth of its original value; this gives the user more control over the transition. Otherwise pinching a book open would appear to jump to the opened state immediately.
+10. Update the completed percentage of the transition based on the progress you calculated earlier.
+11. If the current transition is not performing a push, then it must be performing a pop.
+12. While pinching the book closed, the scale must progress from 1 to 0.
+13. Finally, update the progress of the transition.
+**End State – Stop Pinching**
+14. Notify the system that the user interaction of the transition is complete.
+15. Set the interaction controller to nil.
+
+**开始状态**
+1. 创建一个UIPercentDrivenInteractiveTransition 对象。
+2. scale取决于捏合点之间的距离,判断scale值是否大于或者等于1。
+3. 如果是,判断相关的View是否是一个Collection View。
+4. 获取正在被捏合的书。
+5. 执行Push BookViewController的动画,显示书本中的书页。
+6. 如果 scale 小于 1…
+7. …执行Pop BookViewController的动画,显示封面
+**改变状态 – 捏合过程中**
+8. 判断当前是否是Push动画。
+9. 如果正在Push一个BookViewConroller,计算捏放手势的进度。该进度必然是0-1之间的数字。我们将原始值除以5以让用户拥有更好的控制感。否则用双指打开的手势打开一本书时,会突然跳到打开状态。
+10. 基于我们计算的进度,更新动画进度。
+11. 如果当前不是Push动画,则它应该是Pop动画。
+12. 当双指捏合合起一本书时,scale值必然是从1慢慢变到0。
+13. 最后, 更新动画进度。
+**结束状态 – 手势终止**
+14. 告诉系统,用户交互式动画完成。
+15.将interaction controller 设置为 nil。
+
+Finally, you need to implement the pinch-to-closed state. Therefore you have to pass the gesture recognizer to BookViewController so it can pop itself.
+Open up BookViewController.swift, and add the following property under the book variable:
+
+最后,我们需要实现“捏合以合起书本”的状态。当然,我们必须将手势识别器传递给BookViewController以便它会Pop。
+打开BookViewController.swift,在book变量声明下增加一个属性:
+
+```
+var recognizer: UIGestureRecognizer? {
+ didSet {
+ if let recognizer = recognizer {
+ collectionView?.addGestureRecognizer(recognizer)
+ }
+ }
+}
+```
+
+Whenever you set the gesture recognizer in BookViewController, the gesture will be added immediately to the collection view so you can track the pinch gesture as the user closes the book.
+Next you need to pass the gesture recognizer between the BooksViewController and BookViewController.
+Open up BookOpeningTransition.swift. Add the following lines to cleanUpPush(_:toVC) after the point where you set the background color:
+
+当我们将手势识别器传递给BookViewController时,它会被添加到Collection View,因此我们可以跟踪到用户的“关书”手势。
+然后需要在BooksViewController和BookViewController之间传递手势识别器。
+打开BookOpeningTransition.swift。在cleanUpPush(_:toVC)方法中,在设置背景色之后添加如下代码:
+
+```
+// Pass the gesture recognizer
+toVC.recognizer = fromVC.recognizer
+```
+
+Once you’ve pushed from BooksViewController to the BookViewController, you pass the pinch gesture to BookViewController. This automatically adds the pinch gesture to the collection view.
+When you pop from BookViewController to BooksViewController, you have to pass the pinch gesture back.
+Add the following line to cleanUpPop(_:toVC), just after the line where you set the background color:
+
+当我们从BooksViewController Push到BookViewController时,将捏放手势传递给BookViewController。这会导致捏放手势自动添加到Collection View中。
+当我们从BookViewController Pop回BooksViewController时,我们必须将捏放手势又传递回去。
+在cleanUpPop(_:toVC)方法中,在我设置背景色之后添加如下代码:
+
+```
+// Pass the gesture recognizer
+toVC.recognizer = fromVC.recognizer
+```
+
+Build and run your app; select any book and use a pinch gesture to open and close the book:
+
+编译、运行程序,选择一本书,用捏放手势打开和合起书:
+
+
+The pinch gesture is a natural mechanism to open and close a book; it is also an opportunity to clean up your interface. You don’t need that Back button in the navigation bar anymore — time to get rid of it.
+Open Main.storyboard, select Custom Navigation View Controller, open the Attributes Inspector and uncheck Bar Visibility under the Navigation Controller section, like so:
+
+捏放手势是一种天然就适合用于对书本进行“开关”的手势;它让我们的界面显得更加简单。我们不再需要导航栏的Back按钮——因此我们决定去掉它。
+打开Main.storyboard,选择Custom Navigation View Controller,打开属性面板,在Navigation Controller一栏下面,取消Bar Visibility选项,如下所示:
+
+
+Build and run your app again:
+
+再次编译运行程序:
+
+
+Much cleaner! :]
+##Where To Go From Here?
+##接下来做什么
+You can download the final project from this part with all the code from the tutorial above.
+In this tutorial, you learned how to apply custom layouts to collection views to give the user a more natural, and decidedly more interesting, experience with your app. You also created custom transitions and applied smart interactions to pinch a book opened and closed. Your app, while still solving the same basic problem, gives the application much more personality and helps it stand out from the rest.
+
+你可以在[这里](http://cdn3.raywenderlich.com/wp-content/uploads/2015/05/Part-2-Paper-Completed_Final.zip)下载到上面所有步骤完成后的最终项目。
+在本教程中,我们学习如何对Collection View进行自定义布局,让App的用户体验更加自然、也更加有趣。我们还创建了自定义动画,使用智能交互让用户以捏放手势开合一本书。这个App在实现了所有基本功能的同时,让程序显得更加的人性化和与众不同。
+
+Is it easier to go with the default “ease-in/ease-out” animations? Well, you could save a little development time. But the best apps have that extra bit of customized polish that makes them shine. Everyone remembers the apps they downloaded that were a lot of fun to use; you know, the ones that gave you a little UI thrill, but without sacrificing utility.
+
+相比较之下,是默认的“淡入/淡出”动画更简单一些。它能节省你一部分开发时间。但是杰出的应用程序都应当有一些自己特有的地方,从而使它们能够脱颖而出。
+要知道,每个人都喜欢记住那些用起来非常有趣的App,在UI上能让人感到兴奋而同时又没有牺牲功能的App。
+
+I hope you enjoyed this tutorial, and again I would like to thank Attila Hegedüs for creating this awesome project.
+If you have any question about this tutorial, please join the forum discussion below!
+
+希望你能喜欢本教程,再次感谢[Attila Hegedüs](https://twitter.com/hegedus90)提供了这个教程的示例项目。
+如果对本教程有任何问题,请加入到下面的讨论中来!
\ No newline at end of file
diff --git a/issue-18/1.md b/issue-18/1.md
new file mode 100644
index 0000000..8b6b870
--- /dev/null
+++ b/issue-18/1.md
@@ -0,0 +1,346 @@
+* 原文链接:[Dependency Injection: Give Your iOS Code a Shot in the Arm](https://corner.squareup.com/2015/06/dependency-injection-in-objc.html)
+* 原文作者:[ @祈祈祈祈祈祈 ](http://weibo.com/u/2801408731)
+* [译文出自:开发者前线 www.devtf.cn](www.devtf.cn)
+* 译者:[MrLoong](https://github.com/MrLoong)
+* 校对者:MrLoong
+* 状态:完成
+
+
+#什么是Dependency Injection(依赖注入)?
+
+在许多程序设计语言里,比如Java,C#,依赖注入(DI)都是一种较流行的设计模式,但是它在Objective-C中没有得到广泛应用。本文旨在用 Objective-C的例子对依赖注入进行简要介绍,同时介绍 Objective-C 代码中使用依赖注入的实用方法。尽管文章主要针对Objective-C,但是提到的所有概念对Swift同样适用。
+
+依赖注入的概念十分简单:一个对象应该通过依赖传递获得,而不是创建他们本身。推荐Martin Fowler的 [excellent discussion on the subject](http://martinfowler.com/articles/injection.html) 作为背景材料阅读。
+
+依赖可以通过initializer(初始化器)(或者constructor(构造器))或者属性(set方法)传递给对象。它们通常被称为"constructor injection" 和 "setter injection"。(构造器注入和 set方法注入)
+
+```
+Constructor Injection:
+
+- (instancetype)initWithDependency1:(Dependency1 *)d1
+ dependency2:(Dependency2 *)d2;
+Setter Injection:
+
+@property (nonatomic, retain) Dependency1 *dependency1;
+@property (nonatomic, retain) Dependency2 *dependency2;
+
+```
+
+根据Fowler的描述,一般情况下,首选构造器注入,在构造函数注入不适合的情况下才选择setter注入。虽然使用构造函数注入时,很可能还是要给这些依赖定义属性,但你可以给这些属性设置成read only从而简化你的对象API。
+
+##为什么要使用依赖注入?
+
+使用依赖注入有很多优点:
+
+* 依赖申明清晰。 一个对象需要进行的操作变得一目了然,同时也容易消除危险的隐藏依赖,比如全局变量。
+
+* 组件化。 依赖注入提倡composition over inheritance,以提高代码的重用性。
+
+* 更易定制。 当创建对象的时,在特殊情况下更易对对象进行部分的定制。
+
+* 明确从属关系。 特别是在使用构造器依赖注入时,对象所有权规则严格执行--可以建立一个直接非循环的对象图。
+
+* 易测试性。 依赖注入比其他方法更能提高对象的易测试性。因为通过构造器创建这些对象很简单,也没有必要管理隐藏的依赖。此外,模拟依赖变得简单,从而可以把测试集中在被测试的对象上。
+
+##在代码中使用依赖注入
+
+你的代码库可能还没有使用依赖注入设计模式,但是转换一下很简单。依赖注入很好的一点就是你不需要让整个工程的代码全都采取该模式。相反,你可以在代码库的特定区域运用然后从那边扩展开来。
+
+##二级各种类的注入
+
+首先,把类分为两种:基本类型和复杂类型。基本类型是没有依赖的,或者是只依靠其他基本类型。基本类型基本不用被继承,因为他们功能清晰不变,也不需要链接外部资源。许多基本类型都是从Cocoa 自身获得的,比如NSString, NSArray, NSDictionary, and NSNumber.
+
+复杂类型就相反了。它们有复杂的依赖,包括应用级别的逻辑(需要修改的部分),或者访问额外的资源,例如磁盘,网络或者全局内存服务。应用中绝大多数类都是复杂的,包括几乎所有的控制器对象和模型对象。很多cocoa类型也很复杂,例如NSURLConnection or UIViewController.。
+
+根据以上分类情况,想要使用依赖注入模式最简单的方法是先选择应用中一个复杂的类,找到类中的初始化其他复杂对象的地方(找"alloc]init"或者"new"关键字)。将类中引进依赖注入,改变这一实例化对象作为初始化参数在类中传递而不是类初始化对象本身。
+
+##在初始化时分配依赖
+让我们来看一个例子,子对象(依赖)在母体的初始化函数中被初始化。原始的代码如下:
+
+```
+@interface RCRaceCar ()
+
+@property (nonatomic, readonly) RCEngine *engine;
+
+@end
+
+@implementation RCRaceCar
+
+- (instancetype)init
+{
+ ...
+ // Create the engine. Note that it cannot be customized or
+ // mocked out without modifying the internals of RCRaceCar.
+ _engine = [[RCEngine alloc] init];
+
+ return self;
+}
+
+@end
+
+```
+
+依赖注入做了小的修改:
+
+```
+@interface RCRaceCar ()
+
+@property (nonatomic, readonly) RCEngine *engine;
+
+@end
+
+@implementation RCRaceCar
+
+// The engine is created before the race car and passed in
+// as a parameter, and the caller can customize it if desired.
+- (instancetype)initWithEngine:(RCEngine *)engine
+{
+ ...
+
+ _engine = engine;
+
+ return self;
+}
+
+@end
+
+```
+
+惰性初始化依赖
+
+有一些对象可能一段时间后才用到,或者初始化之后才会用到,或者永远也不会用到。没有用依赖注入之前的例子:
+
+```
+@interface RCRaceCar ()
+
+@property (nonatomic) RCEngine *engine;
+
+@end
+
+@implementation RCRaceCar
+
+- (instancetype)initWithEngine:(RCEngine *)engine
+{
+ ...
+
+ _engine = engine;
+ return self;
+}
+
+- (void)recoverFromCrash
+{
+ if (self.fire != nil) {
+ RCFireExtinguisher *fireExtinguisher = [[RCFireExtinguisher alloc] init];
+ [fireExtinguisher extinguishFire:self.fire];
+ }
+}
+
+@end
+
+```
+
+一般情况下赛车一般不会撞车,所以我们永远不会使用我们的灭火器。因为需要这个对象的概率很低,我们不想在初始化方法中立即创建他们从而拖慢了每个赛车的创建。另外,如果我们的赛车需要从多个撞车中恢复过来,这就需要创建多个灭火器。对于这样的情况,我们可以使用工厂设计模式。
+
+工厂设计模式是标准的objectice-c blocks语法,它不需要参数并且返回一个对象的实体。一个对象可以在不需要知道如何创建他们的细节的时候就能使用他们的blocks创建依赖。
+
+这边是一个使用依赖注入也就是使用工厂设计模式来创建我们的灭火器的例子:
+
+```
+typedef RCFireExtinguisher *(^RCFireExtinguisherFactory)();
+
+@interface RCRaceCar ()
+
+@property (nonatomic, readonly) RCEngine *engine;
+@property (nonatomic, copy, readonly) RCFireExtinguisherFactory fireExtinguisherFactory;
+
+@end
+
+@implementation RCRaceCar
+- (instancetype)initWithEngine:(RCEngine *)engine
+ fireExtinguisherFactory:(RCFireExtinguisherFactory)extFactory
+{
+ ...
+
+ _engine = engine;
+ _fireExtinguisherFactory = [extFactory copy];
+ return self;
+}
+
+- (void)recoverFromCrash
+{
+ if (self.fire != nil) {
+ RCFireExtinguisher *fireExtinguisher = self.fireExtinguisherFactory();
+ [fireExtinguisher extinguishFire:self.fire];
+ }
+}
+
+@end
+
+```
+
+工厂模式在我们需要创建未知个数的依赖时也很有用,甚至在初始化器中创建,比如:
+
+```
+@implementation RCRaceCar
+
+- (instancetype)initWithEngine:(RCEngine *)engine
+ transmission:(RCTransmission *)transmission
+ wheelFactory:(RCWheel *(^)())wheelFactory;
+{
+ self = [super init];
+ if (self == nil) {
+ return nil;
+ }
+
+ _engine = engine;
+ _transmission = transmission;
+
+ _leftFrontWheel = wheelFactory();
+ _leftRearWheel = wheelFactory();
+ _rightFrontWheel = wheelFactory();
+ _rightRearWheel = wheelFactory();
+
+ // Keep the wheel factory for later in case we need a spare.
+ _wheelFactory = [wheelFactory copy];
+
+ return self;
+}
+
+@end
+
+```
+
+避免笨重的配置
+
+如果对象不应该在其他对象里被alloc,那它应该在哪边被alloc?是不是这样的依赖都很难去配置?难道每次alloc他们都一样困难?对于这些问题的解决要依靠类型的简洁初始化器(例如+[NSDictionary dictionary]),我们将我们的对象图配置从普通对象中取出,使他们纯净可测试,业务逻辑清晰。
+
+在添加类型简易初始化方法之前,确保它是有必要的。如果一个对象只有少量的参数在init方法,并且这些参数没有合理的地默认值,那么这个类型是不需要简介初始化方法的,就直接调用标准的init方法就可以了。
+
+我们将从4处地方手机我们的依赖去配置我们的对象:
+
+值没有合理的默认值。如每个实例都可能包含不同的布尔值或者数值。这些值应该作为参数传给类型的简洁初始化器。
+现存的共享对象。这些对象应该作为参数传给类型的简洁初始器(例如 一段无线电波)。这些都是之前可能被评估成单例或者通过父类指针的对象。
+新创建的对象。如果我们的对象不能将这些依赖共享给其他对象,那么合作的对象应该在类型简介初始化函数中新建一个实例。这些都是之前在对象的implementation里面直接分配的对象。
+系统单例。这些是cocoa提供的单例和可以直接使用的单例。这些单例的应用,如[NSFileManager defaultManager],在你的app中,预计只需要产生一个实例的类型使用可以使用单例。系统中有很多这样的单例。
+一个赛车类的简洁初始化方法如下:
+
+```
++ (instancetype)raceCarWithPitRadioFrequency:(RCRadioFrequency *)frequency;
+{
+ RCEngine *engine = [[RCEngine alloc] init];
+ RCTransmission *transmission = [[RCTransmission alloc] init];
+
+ RCWheel *(^wheelFactory)() = ^{
+ return [[RCWheel alloc] init];
+ };
+
+ return [[self alloc] initWithEngine:engine
+ transmission:transmission
+ pitRadioFrequency:frequency
+ wheelFactory:wheelFactory];
+}
+
+```
+
+你的类型便利初始化方法应该放在适合的地方。常用的或者可复用的配置文件将作为对象放在.m文件里面,而由一个特殊的Foo 对象使用的配置器应该放在RaceCar的@interface里面。
+系统单例
+
+在Cocoa库里很多对象只有一个实例存在,例如[UIApplication sharedApplication], [NSFileManager defaultManager], [NSUserDefaults standardUserDefaults], [UIDevice currentDevice].如果一个对象依赖于以上这些对象,应该把它放进初始化器的参数。即使你的代码中可能只有一个实例,你的测试想模拟这个实例或创建一个实例的测试避免测试的相互依赖。
+
+建议大家在自己的代码中避免创建全局引用的单例,也不要在一个对象第一次需要或者注入它所有依赖于它的对象时创建他的单个实例。
+
+不可变的构造器
+
+
+偶尔会有这种问题,就是一个类的初始化器/构造器不能被改变,或者直接调用。在这种情况下,应该使用setter injection,例:
+
+```
+// An example where we can't directly call the the initializer.
+RCRaceTrack *raceTrack = [objectYouCantModify createRaceTrack];
+
+// We can still use properties to configure our race track.
+raceTrack.width = 10;
+raceTrack.numberOfHairpinTurns = 2;
+
+```
+
+setter injuection 允许你配置对象,但是这在对象设计上引入了额外的可变性,需要测试和解决。幸运的是,导致初始化不能访问或者不能修改的两种主要场景都可以避免。
+
+类注册
+
+使用类注册工厂模式也就是对象不能修改他们的初始化器。
+
+```
+NSArray *raceCarClasses = @[
+ [RCFastRaceCar class],
+ [RCSlowRaceCar class],
+];
+
+NSMutableArray *raceCars = [[NSMutableArray alloc] init];
+for (Class raceCarClass in raceCarClasses) {
+ // All race cars must have the same initializer ("init" in this case).
+ // This means we can't customize different subclasses in different ways.
+ [raceCars addObject:[[raceCarClass alloc] init]];
+}
+
+```
+
+对于这样的问题可以用工厂模式 blocks简单代替类型申明的列表。
+
+```
+typedef RCRaceCar *(^RCRaceCarFactory)();
+
+NSArray *raceCarFactories = @[
+ ^{ return [[RCFastRaceCar alloc] initWithTopSpeed:200]; },
+ ^{ return [[RCSlowRaceCar alloc] initWithLeatherPlushiness:11]; }
+];
+
+NSMutableArray *raceCars = [[NSMutableArray alloc] init];
+for (RCRaceCarFactory raceCarFactory in raceCarFactories) {
+ // We now no longer care which initializer is being called.
+ [raceCars addObject:raceCarFactory()];
+}
+
+```
+
+Storyboards
+
+storyboards提供便捷的方法来布置我们的用户界面,但是给依赖注入带来了问题。尤其是在storyboard中初始化View Controller不允许你选择调用哪个初始化方法。同样地,当在sytoyboard中定义页面跳转的时候,目标View Controller不会给你自定初始化方法来产生实例。
+
+解决方法就是避免使用storyboard。这听起来是个极端的解决方案,但是我们将发现使用storyboard会产生大量其他问题。另外,不想失去storyboard给我们带来的便利,可以使用XIB,而且XIB可以让你自定义初始化器。
+
+公有 Vs.私有
+
+依赖注入鼓励你在公共接口中暴露更多的对象。如前所述,这有很多优点。搭建框架时候,他能大大的充实你的公共API。而且运用依赖注入,公共对象A可以使用私有对象B(这样轮流过来就可以使用私有对象C),但对象B和C从来没有暴露在框架外面。对象A在初始化器中依赖注入对象B,然后对象B的构造器又创建了公共对象C.
+
+```
+// In public ObjectA.h.
+@interface ObjectA
+// Because the initializer uses a reference to ObjectB we need to
+// make the Object B header public where we wouldn't have before.
+- (instancetype)initWithObjectB:(ObjectB *)objectB;
+@end
+
+@interface ObjectB
+// Same here: we need to expose ObjectC.h.
+- (instancetype)initWithObjectC:(ObjectC *)objectC;
+@end
+
+@interface ObjectC
+- (instancetype)init;
+@end
+你也不希望框架的使用者担心对象B和对象C的实现细节,我们可以通过协议解决这个问题。
+
+
+@interface ObjectA
+- (instancetype)initWithObjectB:(id )objectB;
+@end
+
+// This protocol exposes only the parts of the original ObjectB that
+// are needed by ObjectA. We're not creating a hard dependency on
+// our concrete ObjectB (or ObjectC) implementation.
+@protocol ObjectB
+- (void)methodNeededByObjectA;
+@end
+
+```
\ No newline at end of file
diff --git a/issue-18/readme.md b/issue-18/readme.md
new file mode 100644
index 0000000..8f808d4
--- /dev/null
+++ b/issue-18/readme.md
@@ -0,0 +1,4 @@
+add issue-18
+
+
+
diff --git "a/issue-18/\346\216\214\346\217\241iOS8\344\270\255\347\232\204\345\212\250\346\200\201\346\226\207\346\234\254.md" "b/issue-18/\346\216\214\346\217\241iOS8\344\270\255\347\232\204\345\212\250\346\200\201\346\226\207\346\234\254.md"
new file mode 100644
index 0000000..cfc86fa
--- /dev/null
+++ "b/issue-18/\346\216\214\346\217\241iOS8\344\270\255\347\232\204\345\212\250\346\200\201\346\226\207\346\234\254.md"
@@ -0,0 +1,675 @@
+> * 原文链接 : [Swift Programming 101: Mastering Dynamic Type in iOS 8](http://www.iphonelife.com/blog/31369/swift-programming-101-mastering-dynamic-type-ios)
+* 原文作者 : [Kevin McNeish](http://www.iphonelife.com/blog/kevin-mcneish)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [kmyhy](https://github.com/kmyhy)
+
+Apple has announced they expect third party apps to support Dynamic Type. However, if you have tried to implement it in your apps, you know there are some unexpected land mines along the way (such as static table view cells and custom cell styles). In this article, you will learn how Dynamic Type works under the hood and how to get it working properly in a variety of scenarios. You will also get some Swift code that makes it easier to implement Dynamic Type in your apps.
+
+Apple声称鼓励第三方App能够支持动态文本。但是,如果你尝试在App中实现这个特性,你会发现其中有很多坑(例如静态cell和定制cell样式)。在本文中,我们将介绍动态文本的机理以及它在各种场景中的应用。我们也会介绍一些Swift代码,这将极大地帮助你在自己的App中实现动态文本。
+
+##What is Dynamic Type?
+##什么是动态文本?
+In iOS 7, Apple introduced Dynamic Type technology to iOS devices. It allows users to specify their preferred font size (for apps that support Dynamic Type) in the Settings app.
+
+在iOS7中,Apple引入了动态文本的概念。动态文本允许用户通过设置程序修改App的字体大小(只是针对支持动态文本的App)。
+
+This makes it easy for visually impaired users to increase the size of the text, or, on the other end of the spectrum, those with sharp vision to decrease the font size to fit more information on each screen.
+
+对于视力不好的用户,很容易就能将文本字体增大,另一方面,对于视力较好的用户,则可以将字体改小,以便在同一屏中容纳更多的内容。
+
+To change the Dynamic Type setting, in the Settings App, select General > Accessibility > Larger Text, which displays the Accessibility screen (Figure 1). The user can move the slider to the left to decrease the font size or to the right to increase it. To get even larger font sizes, the user can turn on the Larger Accessibility Sizes option at the top of the screen.
+
+要在设置App中修改动态文本设置,选择通用->辅助功能->更大字体,如图1所示。用户通过拖动滑条来改变字体的大小。要使用更大的字体,可以打开屏幕上方的“辅助功能中的更大字体”开关。
+
+图 1 - 更大字体设置
+The left side of Figure 2 shows the smallest font size in the Contacts app. The right side shows the largest non-accessibility font setting.
+
+图2左边的图显示的是联系人App在最小字体下的显示效果,右边的图是在没有打开“辅助功能中的更大字体"时的最大字体的显示效果。
+
+
+图 2 - 联系人 app 的小字体和大字体
+Here are some of the built-in apps that currently support Dynamic Type:
+
+- Messages
+- Calendar
+- Maps
+- Notes
+- Health
+- Reminders
+- Contacts
+- Weather
+
+下面是系统内置的支持动态文本的App:
+
+- 信息
+- 日历
+- 地图
+- 备忘录
+- 健康
+- 事项提醒
+- 联系人
+- 天气
+
+Since these built-in apps support Dynamic Type, users are beginning to expect the custom apps you create to support it as well. Let's dive in and see what it takes.
+
+正是因为这些App都支持动态文本,用户也会要求第三方的App也支持动态文本。先让我们看看最终的效果。
+
+##Running the Sample App
+##运行示例App
+
+Let's begin by checking out an existing project. You can download the sample app from this link.
+
+1. In Xcode, open the iDeliverMobile.xcodeproj file.
+2. Before running the app, in the menu, select Xcode > Open Developer Tool > iOS Simulator.
+3. In the Simulator's menu, select Hardware > Device > iPhone 5s.
+4. Next, launch the Settings app and navigate to General > Accessibility > Larger Text. Drag the slider all the way to the right to 5. select the largest text.
+6. Go back to Xcode, and in the Scheme control at the top left of the Xcode screen, select iPhone 5s from the list of devices.
+7. Click Xcode's Run button. When the app appears in the Simulator, you can see that the font is still small. Obviously, we need to make a change to support Dynamic Type.
+9. Go back to Xcode and click the Stop button.
+
+
+我们先把项目check out出来。你可以在[这里](http://www.iosappsfornonprogrammers.com/media/blog/iDeliverMobileDynamicType.zip)下载示例项目。
+
+1. 在Xcode中,打开iDeliverMobile.xcodeproj文件。
+2. 运行程序之前,打开Xcode->Open->Developer Tool->iOS Simulator菜单。
+3. 在模拟器菜单中,选择Hardware->Device->iPhone 5S。
+4. 然后,打开设置程序,找到General->Accessibility->Larger Text。将滑块向右拖动至最大。
+5. 回到Xcode,点击Scheme,从设备列表中选择iPhone 5S。
+6. 点击Run,在模拟器中运行程序,你会看到字体仍然是小字体。显然,我们的App还不支持动态文本。
+7. 回到Xcode,退出App。
+
+## Working with Text Styles
+##使用文本样式
+Currently, all the user interface controls in the sample app have hard-coded font names and sizes. To support Dynamic Type, you need to change these hard-coded fonts to text styles.
+
+当前,示例程序中的所有UI控件的字体名称和大小都是硬编码的。要支持动态文本,我们需要将这些硬编码的内容替换成文本样式。
+
+Text styles are similar to styles in a word processing app. They allow you to specify the relative size and weight of the font used for each text element in your app. Figure 3 contains a list of the font styles to choose from.
+
+文本样式是类似文字处理程序中的”样式“的概念。样式能够让我们以相对大小和字重的方式指定某段文本的字体。图3列出了可选的字体风格。
+
+
+
+图 3 - 动态文本中使用的文本样式
+Let's give some of these styles a try.
+
+1. In the Project Navigator, select the Main.storyboard file.
+2. In the Deliveries scene, select the Feng Wong label, then go to the Attributes Inspector and change the Font to Headline (Figure 4).
+Set the Font to Headline
+Figure 4 - Set the Font to Headline.
+3. Select the address label and set its Font to Subhead.
+4. To see the full effect of the Dynamic Type change set the address label's Autoshrink setting to Fixed Font Size (Figure 5). This causes the address label text to be cut off, but we'll fix that later.
+Set the address label
+Figure 5 - Set the address label's Font to Subhead and Autoshrink to Fixed Font Size.
+5. Click Xcode's Run button. When the app appears in the Simulator, the font size has increased to match the new setting (Figure 6).
+
+让我们先来试一试。
+
+1. 在项目导航窗口中,选择Main.storyboard文件。
+2. 在Deliveries场景中,选择Feng Wong标签,打开属性面板,将Font修改为Headline(图4)。
+
+图 4 - 设置Font 为 Headline.
+3. 选择address标签,将Font设置为Subhead。
+4. 要预览全部动态文本效果,将address标签的AutoShrink设为Fixed Font Size(图5)。这会让address标签的text被截断,但随后我们会解决这个问题。
+
+图 5 - 设置address 的 Font 为 Subhead , Autoshrink 为 Fixed Font Size
+5. 点击Run,在模拟器中运行程序,你会发现,字体的大小已经发生改变(图6)。
+
+
+图 6 - 动态文本已经起作用了
+Also, notice the row height has increased slightly to accommodate the larger font size.
+
+注意,为了适应大文本,行高被稍微增高了一点。
+
+What happens if the user changes the font size in Settings while the app is running? Let's find out.
+
+1. If the iDeliverMobileCD app is not currently running, click the Run button in Xcode.
+2. When the app appears in the Simulator, press the Shift+Command+H keys to go to the Home screen.
+3. Launch the Settings app and navigate to the General > Accessibility > Larger Text screen. Drag the slider to the far left to decrease the text size.
+4. Press Shift+Command+H to go back to the Home screen and launch the iDeliverMobileCD app again. Notice the labels' fonts have change to match the new smaller font (Figure 7).
+The labels fonts change dynamically
+Figure 7 - The fonts change dynamically.
+5. Go back to Xcode and click the Stop button.
+
+现在,让我们来看看,当用户在设置程序中改变字体大小后,又会发生什么情况?
+
+1. 如果 iDeliverMobileCD 未启动,请点击Xcode中的Run按钮。
+2. 当App一启动,按 Shift+Command+H ,切到Home屏幕。
+3. 打开设置程序,找到 General -> Accessibility -> Larger Text ,将滑块向左拖动,以减少字体大小。
+4. 按下 Shift+Command+H ,回到Home屏,重新打开iDeliverMobileCD 。注意标签的字体已经变小了(图7)。
+
+Figure 7 - The fonts change dynamically.
+5. 回到Xcode ,退出程序。
+
+##Dynamic Type and Prototype Cells
+##动态文本和模板单元格
+The behavior you are seeing with the labels in this table view is unique to table views with:
+
+1. Table view Content set to Dynamic Prototype
+2. Cells using one of the built-in styles
+
+你看到的例子实际上相当于我们进行了以下动作:
+
+1. 将Table view 的Content 设为Dynamic Prototype(动态模板)
+2. 将Cell的style设为任意一种内置类型
+
+As you can see in Figure 5, the table view in the sample app is using prototype cells.
+
+就如你在图5中所见,示例中的Table View确实使用了模板单元格。
+
+If you select the table view cell in the Deliveries scene and go to the Attributes Inspector, you can see the Style is set to Subtitle (Figure 8).
+
+如果你选择Deliveries场景中的Table View中的单元格,在属性面板中你会看到其style是Subtitle(图8)。
+
+
+
+图 8 - 单元格的Style 为 Subtitle.
+As you will learn, table views with static and custom cells behave differently.
+
+呆会你会明白,Table View的静态单元格和动态单元格是截然不同的。
+
+##Line Wrapping Label Text
+##标签文本的换行
+In some of the built-in iOS apps, Apple allows text to be truncated when the user increases the font size. You can see this in the email address of the Contacts app shown in Figure 9.
+
+在一些iOS内置应用中,苹果允许文本在加大字体后被截断。在联系人应用中,你会在email地址中看到这样的例子(图9)。
+
+
+图 9 - email 地址被截断
+
+You can truncate text in your own apps, or you can have the text wrap to the next line. Let's see how wrapping the text works.
+
+在你的App中,你可以允许文本被截断,或者换到下一行。现在,让我们看看如何换行。
+
+In the Deliveries scene, select the detail text label then go to the Attributes Inspector and set the number of lines to zero. This causes the address text to wrap to the next line (Figure 10).
+
+在Deliveries场景中,选择detail text标签,打开属性面板,将number of lines设置为0。这会导致email地址换行(图10)。
+
+图 10 - 标签文字超出了行高
+Unfortunately, iOS is no longer able to properly determine the height of the row. This brings us to a bigger discussion of dynamic row heights.
+
+然而,iOS却不能正确地计算行高。接下来我们就来讨论动态单元格的行高。
+
+##Dynamic Row Heights
+##动态行高
+When implementing Dynamic Type in a table view, its row height must be dynamic to accommodate changes in font size. Apple provides three strategies to achieve this:
+
+1. Set the table view's rowHeight property.
+2. Implement the tableView:heightForRowAtIndexPath: delegate method.
+3. Self-sizing cells
+
+当在Table View中使用动态文本时,表格的行高必须也能够自动适应字体大小的变化。苹果提供了3种解决办法:
+
+1. 修改表格的rowHeight属性。
+2. 实现tableView:heightForRowAtIndexPath: 协议方法。
+3. 自适应大小的单元格。
+
+###Using the rowHeight Property
+###使用rowHeight属性
+
+Even though your table view row heights need to be dynamic, you can still use the classic table view rowHeight property. Whenever the font size changes (you will learn how to be notified of this later in this article), you can recalculate a new height and store it in the table view's rowHeight property.
+
+尽管你的表格的行高应该是动态计算的,但你仍然可以像过去一样使用rowHeight属性。每当字体大小改变(后面我们会讲到如何获得相应的通知),我们都需要重新计算新的行高,并设置表格的rowHeight属性。
+
+The advantage of using the rowHeight property is speed. It provides the best scrolling performance because no calculations need to be performed on the fly while the user is scrolling.
+
+使用rowHeight属性的优点是速度。它提供了最优的滚动性能,因为当用户滚动表格时,不需要进行任何计算。
+
+The downside of this approach is that you have to perform your own calculations to determine the proper height for rows. In addition, all rows must be the same height.
+
+缺点是我们必须手动计算正确的行高。另外,所有的单元格都必须使用相同的行高。
+
+In iOS 7, the default height of a table view was 44 points In iOS 8, the default height of a table view is UITableViewAutomaticDimension (a constant that equates to -1). If you want to use the rowHeight property, you need to set its initial value in the Attributes Inspector or in your view controller's viewDidLoad method.
+
+在iOS7中,默认行高为44,在iOS8中,默认行高是UITableViewAutomaticDimenssion(一个常量,等于-1)。如果你要使用rowHeight属性,你需要在属性面板中或者viewDidLoad方法中设置它的初始值。
+
+###Implementing heightForRowAtIndexPath
+###实现heightForRowAtIndexPath方法
+You can use the tableView:heightForRowAtIndexPath: view controller method to individually size each row based on your own calculations.
+
+我们可以用tableView:heightForRowAtIndexPath: 方法单独计算每一行的行高。
+
+There is one big disadvantage to this approach. The height of all rows is requested up front, even before the rows have been created. If you have tens of thousands of rows, this can be a real performance drag.
+
+这种方法没有什么明显的优点。每一行的行高都会事先被询问,不管该行是不是已经被创建。如果你的表格有上千行,这会导致性能上的延迟。
+
+###Self-Sizing Table View Cells
+###自适应大小单元格
+When using self-sizing table view cells, rather than setting the rowHeight property, you either set the table view's estimatedRowHeight property or implement the tableView:estimatedHeightForRowAtIndexPath: delegate method.
+
+如果使用自适应大小单元格,而不是使用rowHeight属性,则我们既不用设置estimatedRowHeight属性,也不用实现tableView:estimatedHeightForRowAtIndexPath:协议方法。
+
+Here are the basic steps performed by the runtime to create a self-sizing cell:
+创建自适应大小单元格的步骤大致如下:
+
+1. Before a row comes on screen, it's sized using the estimatedRowHeight property or the associated delegate method.
+在绘制每个单元格之前,它会使用estimatedRowHeight属性或者调用相关的委托方法。
+
+2. When the row is scrolled onto the screen, the cell is created.
+当表格滚动,该行即将显示到屏幕时,单元格被创建。
+
+3. The cell is asked how bit it should be.
+此时单元格会被询问其大小。
+4. If the height is different from the estimated height, it's used to adjust he content size of the table view cell.
+如果这个高度和estimatedHeight不同,则使用该高度进行调整。
+5. The cell is displayed on screen.
+显示单元格。
+
+In step 3, there are two ways a cell can communicate the height it needs to be:
+
+在第3步,又有两种计算单元格高度的方式:
+
+1. Auto Layout
+自动布局
+2. Manual sizing code
+手动计算大小
+
+
+The table view calls systemLayoutSizeFittingSize on your cell. This method knows if you have implemented constraints on your cell, and if so, the Auto Layout engine specifies the cell size.
+
+Table View会调用每个单元格的systemLayoutSizeFittingSize方法。该方法返回单元格是否已经实现了布局约束,如果实现,则自动布局引擎负责指定单元格的大小。
+
+If you haven't implemented Auto Layout constraints, the table view calls the sizeThatFits method on your cell. You can return a cell height from this method based on your own calculations—the cell width is already calculated for you.
+
+如果没有实现自己的布局约束,TableView调用单元格的sizeThatFits方法。在这个方法中我们可以自行计算单元格高度并返回——而单元格的宽度是已经计算好的。
+
+###Using Auto Layout with Dynamic Type
+###在动态文本中使用自动布局
+Let's use Auto Layout in our sample project to see how it works with Dynamic Type. We first need to make sure Auto Layout is turned on for the project's storyboard.
+
+先让我们在示例项目中试下自动布局,看如何在动态文本中使用。首先需要确定故事板是否支持自动布局。
+
+1. Click on the white background of the storyboard to select it.
+点击故事板的白色背景
+2. Next, go to the File Inspector by selecting the button on the far left in the Inspector toolbar. Under the Interface Builder Document section, make sure Use Auto Layout is selected (Figure 11).
+打开文件面板。在Interface Builder Document栏下,确保Use Auto Layout 已选中(图11)。
+
+图 11 -选中 Use Auto Layout
+3. Now we need to change the Deliveries scene's table view cell to use a custom style. To do this, click on the table view cell to select it, go to the Attributes Inspector and change the Style to Custom. This removes both labels from the cell.
+将Deliveries场景的表格单元格的风格修改为自定义。选中表格单元格,打开属性面板,将Sytle设置为Custom。这会将单元格的两个标签删除。
+
+4. Let's change the design-time height of the cell to make it easier to lay out in Interface Builder. Click in the gray area below the table view to select it. Next, go to the Size Inspector by clicking the second button from the right in the Inspector toolbar. Set the Row Height to 60.
+在IB中改变单元格的高度是非常简单的。点击表格的灰色区域,在Size面板,将 Row Height设置为 60。
+
+5. Next, drag a label from the Object Library and position it in the cell so you can see the horizontal and vertical guide lines shown in Figure 12.
+从Object Library中拖一个标签到单元格中,你可以看到它的水平和垂直导线,如图12所示。
+
+图 12 -加一个标签到单元格中
+6. Grab the resizing handle on the right side of the label and drag it to the right side of the cell until the vertical guidelines shown in Figure 13 appears.
+拖住标签右边的resizing手柄,将它向右拖,直到垂直导线到达图13中所示的位置。
+
+图 13 - 设置标签的宽度
+7. With the label still selected, in the Attributes Inspector, set Font to Headline and Tag to 1.
+保持标签选中状态,打开属性面板,设置Font为Headline,Tag为1。
+8. Drag a second label from the Object Library and position it below the first label so you see the guide lines shown in Figure 14.
+再拖一个标签放到第一个标签下方,使其导线如图14所示。
+
+图 14 - 添加第2个标签
+9. Grab the resizing handle on the right side of the label and drag it to the right side of the cell until the vertical guidelines appear, just as with the first label.
+拖住标签右边的resizing手柄,向右拖,直到其垂直导线和第一个标签的垂直导线对齐。
+10. With the label still selected, in the Attributes Inspector set Font to Subhead, Lines to 0 and Tag to 2. Setting Lines to 0 causes the label to wrap to the next line when displaying long text strings.
+保持标签选中,打开属性面板,设置Font为Subhead,Lines为0,Tag为2。将Lines设置为0,这样当标签显示长文本时会自动换行显示。
+11. Click on the Headline label at the top of the cell to select it in the design surface.
+选中单元格上面的Headline标签。
+12. At the bottom right of the Interface Builder editor, click the Pin button (Figure 15). In the popup dialog, uncheck the Constrain to margins check box, and then click on each of the four red I-bars at the top of the dialog. This pins the label's top, bottom, left, and right sides to their nearest neighbors. Next, select the Height check box, and then click the Add 5 Constraints button at the bottom of the dialog.
+点击IB编辑器右下角Pin按钮(图15),在弹出的窗口中,反选Constrain to margin勾选框,然后点击窗口上半部分的4个边距,使其显示为4条红线。这使得标签的上、下、左、右4条边都分别向最近的控件对齐。然后将Height勾选,再点击Add 5 Constraints按钮。
+
+图 15 - 为上端的标签添加布局约束
+13. Select the Subhead label at the bottom of the cell and click the Pin button again to display the Constraints popup dialog.
+选择单元格下方的Subhead标签,点击Pin按钮。
+14. You need to select the same constranst as you did with the Headline label: Uncheck the Constraints to margin check box, select all four I-bars at the top of the dialog, select the Height check box, and then click the Add 5 constraints button at the bottom of the dialog.
+和Headline标签所做的一模一样:反选Constraints to margin选项,选择4边对齐,勾选Heightg,然后点击Add 5 constraints按钮。
+15. With the lower Subhead label still selected, go to the Size Inspector by clicking the second button from the right in the Inspector toolbar.
+继续选中Subhead标签,打开Size面板。
+Click the Height constraint's Edit button (Figure 16) and change the operator to "greater than or equal to". This allows the height of the button to grow to accommodate multiple lines of text.
+点击Height约束右边的Edit按钮(图16),将operator改为”great than or equal to“。这将使标签的高度自动和文本的行数匹配。
+
+图 16 - 修改Height约束的operator
+16. Now you need to do the same for the Heading label. Click the Heading label in the design surface to select it. In the Size Inspector, click the Height constraint's Edit button and change the operator to "greater than or equal to".
+然后对Heading标签是重复上面的操作。点击Heading标签,在Size面板,点击Height约束的Edit按钮,将operator修改为”greater than or equal to“。
+17. Now you need to change the code in the table view controller to work with the new custom labels.
+然后修改Table View Controller的代码。
+Select DeliveriesViewController.swift in the Project Navigator. In the tableView:cellForRowAtIndexPath: method, change the code that configures the cell to the following:
+选择DeliveriesViewController.swift文件。在tableView:cellForRowAtIndexPath:方法中,将代码修改为:
+
+This code uses the viewWithTag method to get a reference to the new labels using the Tag numbers you set for each. The last line of highlighted code is necessary to get around a "feature" that sometimes prevents labels from wrapping to the next line properly. Setting a label's prefferedMaxLayoutWidth property to the label's current width does the trick.
+这段代码用viewWithTag方法获得指定Tag值的标签。在代码中高亮的部分,最后一行是不能少的,因为标签某些时候会无法正确换行,因此需要将prefferedMaxLayoutWidth属性设置为当前的宽度以解决这个问题。
+18. There is one more change we need to make. Scroll to the top of the DeliveriesViewController.swift file and add the following code to the bottom of the viewDidLoad method:
+还有几个地方需要改。拉到DeliveriesViewController.swift文件顶部,在viewDidLoad方法中加入代码:
+
+Setting the table view's estimatedRowHeight property specifies the approximate height of the cell. Setting the rowHeight property of the table view to UITableViewAutomaticDimension tells iOS that you want it to automatically resize the cells for you. Let's give it a try!
+将表格的estimatedRowHeight属性设为单元格的正确高度。将表格的rowHeight属性设置为UITableViewAutomaticDimenssion,告诉iOS我们需要它自动调整单元格大小。让我们来看看运行效果!
+19. Go to the Simulator, launch the Settings app and set the text size to the largest setting. Next, click Xcode's Run button. When the app appears in the Simulator, the address wraps to the next line (Figure 17)!
+回到模拟器,点开设置程序,将文本大小设为最大。点击Xcode的Run按钮,当程序运行,可以看到email地址已经换行了(图17)!
+
+图 17 - 邮箱地址换行
+
+##Type Changes with Custom Cells
+##字体在自定义单元格上的改变
+Currently, when the Deliveries scene first loads, the labels in the table view take on the font size the user has specified in the Settings app. Previously, when the cell was set to the built-in Subtitle style, if the user changed the font size while the app was running, the font size of the labels changed dynamically. Unfortunately, this is not the case when using a custom cell style. Let's try it.
+
+现在,当Deliveries场景第一次加载时,表格中的标签采用用户在设置程序中已经设好的字体大小显示。显然,当单元格采用内置的Subtitle样式时,如果用户改变了字体大小,则标签上的字体大小也会随之改变。但不幸的是,如果使用的是自定义单元格,这个机制就无效了。我们先来测试一下。
+
+1. Click Xcode's Run button to the run the app in the Simulator.
+点击Run按钮。
+
+2. When the app appears in the Simulator, press the Shift+Command+H keys to go to the Simulator's Home screen.
+当App启动后,按Shift+Command+H键退回到Home屏。
+
+3. Launch the Settings app and navigate to the General > Accessibility > Larger Text screen. Drag the slider to the far left to decrease the text size.
+点开设置程序,进入General->Accessibility->Larger Text界面,将滑条向左拖动,将字体大小改小。
+
+4. Press Shift+Command+H to go back to the Home screen and launch the iDeliverMobileCD app again. As you can see, the label's font size hasn't changed. Go back to Xcode and click the Stop button and we'll address this issue.
+按下Shift+Command+H键,返回Home屏幕,切换到iDeliverMobileCD程序。我们发现,标签文本的字体没有发生丝毫改变。关闭App,我们来解决这个问题。
+
+To get labels (or any other text-based control) in a custom cell to dynamically change font size in response to changing the font size in the Settings app, you must:
+
+要让自定义单元格中的标签(或其他任何文本控件)能够根据设置程序中的字体大小来改变其文本字体,我们必须:
+
+1. Register with the NSNotificationCenter for the UIContentSizeCategoryDidChangeNotification in your view controller's init or viewDidLoad method.
+在viewDidLoad方法中向通知中心注册UIContentSizeCategoryDidChangeNotification通知。
+
+2. In the code that responds to the font change notification, store the label's style setting back into the font property. For example:
+在代码中响应字体改变通知,将标签的样式重新设置正确。例如:
+
+
+3. Unregister for notifications in the view controller's deinit method.
+在ViewController的deinit方法中注销通知。
+
+Let's try this with the Deliveries scene.
+让我们以Deliveries为例进行演示。
+
+1. In the DeliveriesViewController.swift file, add the following code to the bottom of the viewDidLoad method:
+打开DeliveriesViewController.swift文件,在viewDidLoad方法最后加入:
+
+This code asks the Notification Center to call the handleDynamicTypeChange method when the user changes the Dynamic Type setting.
+上述代码让通知中心在用户改变了动态文本设置之后调用handleDynamicTypeChange方法。
+
+2. Add the following handler method below the viewDidLoad method:
+在viewDidLoad方法下面新增方法:
+
+This handler method reloads the table view's data.
+在这个方法中重新加载Table View。
+
+3. Now add the following code to the bottom of the tableView:cellForRowAtIndexPath: method:
+现在在tableView:cellForRowAtIndexPath:方法最后加入代码:
+
+This code resets the label's font style.
+
+ 这段代码重新设置标签的字体风格。
+
+4. Finally, add the following deinit method below the viewDidLoad method:
+最后,在viewDidLoad方法下面增加deinit方法:
+
+
+5. Let's try this code to see how it works at run time. Click Xcode's Run button. When the app appears in the Simulator, you should see the small text from when you previously changed font size. Press the Shift+Command+H keys to go to the Simulator's Home screen.
+让我们测试一下上述代码。点击Run按钮,当App启动后,我们将看到标签文本变成了先前改变的小字体。按下Shift+Command+H键回到Home屏。
+
+6. Launch the Settings app and navigate to the General > Accessibility > Larger Text screen. Drag the slider to the far right to increase the text size.
+打开设置程序,进入General->Accessibility->Larger Text界面,将滑块向右拖到,调大字体。
+
+7. Press Shift+Command+H keys to go to the Simulator's Home screen and launch the iDeliverMobileCD app again. As you can see, the label's font size has increased without having to restart the app!
+按下Shift+Command+H键,回到Home屏,切到iDeliverMobileCD程序。我们将看到,标签文本已经在没有重启App的前提下变大了!
+
+8. Go back to Xcode and click the Stop button.
+回到Xcode,终止程序。
+##What's Bad About This Model
+##这种方法的弊端
+Here are a few things that make this model undesirable:
+这种方法有以下几个弊端:
+
+1. You have to set the font for all controls twice. Once in the Attributes Inspector and a second time in code.
+我们必须每个控件都要设置两次字体。一次是在属性面板,一次是在代码中。
+
+2. You have to create outlets for all text-based controls, even if you don't need them for any other purpose.
+我们必须为每个文本控件都创建一个IBOutlet,哪怕你根本不需要使用这些IBOutlet。
+
+3. You have to add this same code to all view controllers in your app.
+我们必须在每个View Controller中增加同样的代码。
+
+When you come across situations where you must add redundant code in multiple places in your app, it's time to create a universal solution you can reuse in all your projects.
+
+每当我们需要在不同的地方重复加入冗余的代码时,我们就应该考虑创建一种通用的解决方法以在所有项目中重用代码。
+
+I have create a set of class you can add to any project that makes it easy to implement Dynamic Type handing in your app. Before giving it a try, let's get rid of the code you added in the previous section.
+
+我已经创建了几个类,你可以在自己的项目中更容易地实现动态文本。在测试运行之前,先移除我们在前面添加的代码。
+
+1. Delete the following code from the viewDidLoad method:
+从viewDidLoad方法中移除下列代码:
+
+
+2. Delete the following handler method below the viewDidLoad method:
+从viewDidLoad方法下移除该处理方法:
+
+
+3. Delete the following code from the bottom of the tableView:cellForRowAtIndexPath: method:
+从tableView:cellForRowAtIndexPath:方法中移除下列代码:
+
+
+4. Remove the deinit method located below the viewDidLoad method:
+删除位于viewDidLoad下面的deinit方法:
+
+
+##A Better Solution
+##更好的解决方案
+Now let's try a better solution.
+现在来看看更好的解决方案。
+
+1. Right-click the Main.storyboard file in the Project Navigator and select Add Files to iDeliverMobileCD... from the shortcut menu.
+
+ 在项目导航窗口,右键点击Main.storyboard,选择Add Files to iDeliverMobileCD...。
+
+2. In the Add Files dialog, uncheck Copy items if needed.
+
+ 在添加文件对话框,反选Copy items if needed。
+
+3. In the project's folder, select the mmDynamicTypeExtensions.swift file and click Add. We'll take a closer look at this code later, but let's first see how the code works at design time and run time.
+
+ 在项目文件夹,选择mmDynamicTypeExtensions.swift文件,然后点击Add。等一会我们在查看代码,现在先看一下如何在设计时和运行时使用这些代码。
+
+4. Select the Main.storyboard file in the Project Navigator. In the Deliveries scene, select the Heading label at the top of the cell.
+
+ 在项目导航窗口,选中Main.storyboard。在Deliveries场景,选择单元格中位于上方的Heading Label。
+
+5. Go to the Attributes Inspector. Notice there is a new Type Observer attribute (Figure 18).
+
+ 打开属性面板,注意,显示了一个新的Type Observer属性(图18)。
+
+图 18 - Type Observer 属性
+The code you just included in this project added this Type Observer property to the label.
+
+ 刚才添加到项目中的代码为标签添加了一个Type Observer属性。
+
+6. Change the Type Observer setting to On.
+
+ 将Type Observer属性设置为On。
+
+7. Select the Subhead label at the bottom of the cell. In the Attributes Inspector, set the Type Observer attribute to On.
+
+ 选择Subhead标签,在属性面板,将Type Observer属性设置为On。
+
+8. That's it! Let's give it a try. Click Xcode's Run button. When the app appears in the Simulator, you should see the large text from when you previously changed the font size. Press the Shift+Command+H keys to go to the Simulator's Home screen.
+
+ 所有工作完成,让我们来测试一下。点Run按钮,当程序启动后,我们将看到显示了先前我们设置的大字体文本。按下Shift+Command+H键回到Home屏。
+
+9. Launch the Settings app and navigate to the General > Accessibility > Larger Text screen. Drag the slider to the far left to decrease the text size.
+
+打开设置程序,进入General->Accessibility->Larger Text界面。将滑块向左拖动以减小字体。
+
+10. Press Shift+Command+H to go to the Home screen and run the iDeliverMobileCD app again. As you can see, the label's font size has changed!
+
+ 按下Shift+Command+H返回Home屏,切回iDeliverMobileCD程序。你会看到,标签字体大小已然改变!
+
+11. Go back to Xcode and click the Stop button.
+
+返回Xcode,终止程序。
+
+##Dynamic Type Handling
+##动态文本的处理
+Let's take a closer look at the code that makes this work.
+
+让我们来看看代码。
+
+1. Select the mmDynamicTypeExtensions.swift file in the Project Navigator.
+
+ 在项目导航窗口,打开mmDynamicTypeExtension.swift文件。
+
+2. At the top of the file is a protocol that declares a single typeObserver property of type Bool. This is the property you set to On for the table view labels:
+
+ 在文件顶部,是一个协议,该协议仅包含了一个叫做typeObserver的Bool属性。也就是你在标签中设置为On的属性。
+
+
+3. Just below the protocol declaration is an extension of the UILabel class:
+ 在协议声明之后,又定义了一个UILabel的扩展:
+
+This extension adopts the DynamicTypeChangeHandler protocol and implements the typeObserver property. The @IBInspectable attribute is what makes the property appear in the Attributes Inspector. The property setter makes a call to a Dynamic Type Manager's registerControl method.
+
+ 这个扩展声明了对DynamicTypeChangeHandler协议的实现并实现了typeObserver属性。@IBInspectable属性表明这个属性可以显示在属性面板中。这个属性的setter方法调用了动态文本管理器的registerControler方法。
+
+4. Scroll a little further down in the code file and you can see the DynamicTypeManager is declares as a Singleton:
+
+ 向下滚动代码,我们可以看到DynamicTypeManager对象被实现为一个单例对象:
+
+
+
+ The Singleton design pattern restricts the number of instances of a class to one. When an instance is requested, one is created if it doesn't already exist. If an instance exists, it returns a reference to that object.
+
+ 单例模式使得类的实例始终只有一个。当创建一个类的实例时,如果类还未被实例化,则创建新的实例。如果类已经被实例化,则返回现有的实例对象。
+
+ Figure 19 contains a sequence diagram that depicts the architecture I have created for handling Dynamic Type changes.
+
+ 图19是一张序列图,显示了动态文本改变的处理逻辑。
+
+
+图 19 - 动态文本处理的序列图
+
+Here are the key steps:
+这是几个关键步骤:
+
+1. User interface controls register themselves with the Dynamic Type Manager when their typeObserver property is set to true (On in the Attribute Inspector), passing a path to their font property.
+
+ 当typeObserver属性为true时(通过属性面板中),UI控件向动态文本管理器进行注册,将一个 keypath传递给控件的字体属性。
+
+2. When the first control registers itself, an instance of the Dynamic Type Manager is created and registers itself with the NSNotificationCenter for Dynamic Type changes.
+
+ 当第一个控件进行注册时,Dynamic Type Manager实例被创建,并开始向通知中心注册动态文本改变通知。
+
+3. A reference to the control and its font style is stored in an NSMapTable. A map table is a special kind of dictionary that holds weak references to objects, so entries are removed automatically when either the key or value is deallocated. This is a perfect fit for this scenario where we don't want to hold strong references to user interface controls. When the UI controls are released (for example, when the user navigates to a different scene and the view controller is deallocated), the control reference are automatically removed from the NSMapTable (Thanks to the folks at Big Nerd Ranch for this tip!).
+
+创建一个对该控件的引用并将它的字体样式保存到一个NSMapTable中。一个Map Table是字典的一种,保存的是对象的弱引用,因此当key或value被解构时保存的对象自动被移除。这对我们来说再恰当不过了:我们并不想保持对UI控件的强引用。当UI控件释放后(例如,用户导航到另一个View Controller,当前View Contoller被解构),该控件在NSMapTable(感谢[Big Nerd Ranch](https://www.bignerdranch.com/)分享了这个技巧)中的引用将被自动移除。
+
+4. When the user changes the font size in the Settings app, NSNotificationCenter alerts the Dynamic Type Manager.
+
+当用户在设置程序中改变字体大小,通知中心会通知DynamicTypeManager对象。
+
+5. The Dynamic Type Manager iterates through the list of user interface controls in the map table. For each control, it sets the font style and calls the control's sizeToFit method.
+
+DynamicTypeManager对象遍历Map Table中的UI控件,对每个控件,都设置它们的字体样式,并调用sizeToFit方法。
+
+What is there to like about this architecture?
+
+上图这种方式有什么好处?
+
+- It uses extensions rather than subclasses. This allows you to use the out-of-the-box UIKit controls.
+
+ 它使用的是扩展而不是继承。因此我们可以使用“盒子之外的”UIKit组件。
+
+- When you add the mmDynamicTypeExtensions.swift code file to your project, it "just works".
+
+ 你只需要将mmDynamicTypeExtension.swift添加到项目中就可以使用它。
+
+- There is no need to create outlets for UI controls. You simply set a property in the Attributes Inspector.
+
+ 不再需要为UI控件创建IBOutlet。只需要简单地在属性面板中设置一下就好。
+
+- This architecture is loosely coupled. The UI controls provide information about themselves to the Dynamic Type Manager. This means you can register your own custom controls (or new controls that Apple releases in the future) without changing the Dynamic Type Manager.
+
+ 这种方式使用的是松散耦合。UI控件将自己的属性提供给动态文本管理器。这意味着你注册自定义控件(或者苹果未来发布的新控件),而不需要修改动态文本管理器。
+
+- Since you don't need to use this feature for prototype table view cells that are set to one of the default styles, this "opt in" model lets you choose which controls you want to register with the Dynamic Type Manager.
+
+ 不需要在设为默认样式的模板单元格上使用这个特性,你只需要选择将哪个控件注册到动态文本管理器就行了。
+
+####Dynamic Type and Static Text
+####动态文本和静态文本
+Let's check out how Dynamic Type works with static table view cells.
+
+让我们来看一下如何在静态单元格中使用动态文本。
+
+ 1. In the iDeliverMobileDynamicType project, select the Main.storyboard file and scroll over to the Deliveries scene (Figure 20).
+
+ 在iDeliverMobileDynamicType项目中,选中Main.storyboard文件,找到Deliveries场景(图20)。
+
+
+
+ 图 20 - Shipment 场景
+ 2. The table view in this cene contains dynamic protototype cells, just as the Delivieries scene. The main difference is the Shipment scene contains both dynamic and static text. The blue text (Phone, Text, and ID) as well as the Status text is static. This means the text doesn't change as you examine different shipments. The rest of the text is dynamic, changing for each shipment.
+
+ 在这个场景的Table View中,如同Deliveries场景一样包含了动态模板单元格。不同的是Shipment场景中既包含了动态文本也包含了静态文本。蓝色的文本(Phone、Text、和ID)和Status是静态的。也就是说这些文本在不同的发货单中是固定不变的。其他文本则是动态的,每个发货单都不一样。
+
+ 3. To get the labels on this scene to adapt to Dynamic Type, select each label in the design surface, then go to the Attributes Inspector and change the Font to one of the iOS text styles. Here are some recommendations:
+
+ 要让这些标签也使用动态文本,选择每个标签,然后在属性面板中将Font设为任意一种iOS字体风格,比如:
+
+ Name - Headline
+ Address Line 1 - Body
+ Address Line 2 - Subhead
+ Phone labels - Body
+ Text labels - Body
+ Status labels - Body
+ ID labels - Body
+ iPod Touch label - Body
+
+ 4. In the ShipmentViewController.swift file, add the following code to the bottom of the viewDidLoad method:
+
+ 在ShipmentViewController.swift文件中,在viewDidLoad方法最后一行加入代码:
+
+
+
+ Remember, this code tells the table view to use self-sizing cells.
+
+ 记住,这些代码用于告诉Table View使用自适应大小单元格。
+
+ 5. Let's see how this works at run time. Click Xcode's Run button, and when the app appears in the Simulator, select the shipment in the Deliveries scene to navigate to the Shipment scene. You should see the small type that was last specified in the Settings app.
+
+ 现在让我们看看效果。点击Run按钮,当程序启动,在Deliveries窗口选择shipment进入Shipment窗口。我们将看到显示的是我们先前在设置程序中设置的小字体。
+
+ 6. Now let's see if the app responds to Dynamic Type change while the app is running. Go to the Settings app and select the largest font size. Afterward go back to the iDeliverMobileDynamicType app.
+
+ 现在让我们看看在程序运行的情况下App如何处理动态文本的改变。切换到设置程序,选择最大字体。再回到iDeliverMobileDynamicTypeApp。
+
+As shown in Figure 21, all the static text is missing! This is an iOS bug, and unfortunately, it still isn't fixed in the latest version of Xcode 6.2. I'm hoping Apple will fix it, but for now you can get around the issue without resorting to creating custom cells. You just need to add code to the tableView:cellForRowAtIndexPath: method that resets the static text.
+
+如图21所示,所有的静态文本都不见了!这是iOS本身的一个Bug,不幸的是,在Xcode6.2中仍然未得到解决。我希望苹果以后能修正这个Bug,但目前我们不需要自定义单元格就可以解决这个问题。我们只需要在tableView:cellForRowAtIndexPath: 方法中增加一点代码去重置静态文本:
+
+
+
+图 21 -静态文本不见了!
+
+One other problem is that the name text in the first cell is no longer centered. We can also work around this problem by adding code to the same method.
+
+还有一个问题是,第一个单元格不再居中对齐。这个问题也是在同一个方法中增加代码来解决。
+
+In the tableView:cellForRowAtIndexPath: method of the file, add the following highlighted code:
+
+1. 在文件的tableView:cellForRowAtIndexPath: 方法中,添加高亮部分的代码:
+
+
+
+2. Click Xcode's Run button and when the app appears in the Simulator, navigate to the Shipment scene.
+
+ 点击Run按钮,当程序启动,进入Shipment页面。
+
+3. Go to the Settings app and set the font size to the smallest setting. Go back to the iDeliverMobileDynamicType app and you should see the static text is back (Figure 22)! This works because the table view's reloadData method is automatically called when a Dynamic Type change is made.
+
+ 切到设置程序将字体设置为最小。回到iDeliverMobileDynamicType,我们将发现静态文本又回来了(图22)!这是因为当动态文本字体发生改变时,Table View的reloadData方法自动会调用。
+
+
+
+ 图 22 - 静态文本又回来了
+
+##结语
+Last year, my company had a booth at the MacWorld trade show, promoting my iOS app development book series. A visually impaired attendee approached the booth and asked if I was teaching new developers how to create apps that can be used by the visually impaired. Starting with this article, I can finally say yes, and I hope the information presented here helps you to do the same!
+
+去年,我们公司在 MacWorld 展会上有一个展台,展示我的iOS App开发图书系列。一个有弱视的读者来展位上问我,能不能教一下开发者们如何创建适用于弱视患者的App。这导致了本文的产生,我终于可以说Yes了,我希望本文能够让你在面对这个问题的时候能够同样说Yes。
\ No newline at end of file
diff --git "a/issue-19/iOS9 Core Data\346\225\231\345\255\246.md" "b/issue-19/iOS9 Core Data\346\225\231\345\255\246.md"
new file mode 100644
index 0000000..344ca27
--- /dev/null
+++ "b/issue-19/iOS9 Core Data\346\225\231\345\255\246.md"
@@ -0,0 +1,432 @@
+* 原文链接:[Getting Started with Core Data Tutorial](http://www.raywenderlich.com/115695/getting-started-with-core-data-tutorial)
+* 原文作者:[Pietro Rea](http://www.raywenderlich.com/u/pietrorea)
+* [译文出自:开发者前线 www.devtf.cn](www.devtf.cn)
+* 译者:[LastDay](http://lastday.github.io)
+* 校对者:[LastDay](http://lastday.github.io)
+* 状态:完成
+
+
+#Core Data Tutorial教学
+
+
+在这篇文章中我们将学习Core Data的系列教程,你将使用Swift2.0写你的Core Data。你将发现在Xcode中它是很容易上手的,从启动代码导师数据模型编辑器。在教程结束后,你会了解到:
+
+* 使用Xcode的model editor将你想存储在Core Data。
+* 添加新的记录到 Core Data
+* 从Core Data中读取一组数据
+* 在表视图中显示所获取的结果
+
+你也将会了解Core Data背后的数据是什么,以及如何进行交互。OK,现在让我们来构建我的app吧。
+
+##开始
+
+打开你的Xcode新建一个iPhone工程,选择Single View Application template起名为HitList并且选择Use Core Data。
+
+
+
+选中Use Core Data复选框后将会在AppDelegate.swift生成Core Data stack样本代码
+
+Core Data stack由一组对象组成,方便于检索和保存Core Data的数据。有一个对象最为一个整体来管理Care Data的状态和数据模型等等。
+
+这个示例程序的想法很简单。有一个被叫"hit list"的表视图。你可以在这个列表中添加名字,并且最终你将使用Core Data确保数据在各个环节之间。
+
+点击Main.storyboard在Interface Builder.接下来点击Editor,选择Navigation Controller。具体操作如图所示:
+
+
+返回Interface Builder,拖拽一个Table view。
+
+接下来拖拽一个Bar Button Item将它放置到navigation bar。最终,起名为Add。就像这样
+
+
+
+当你每次点击Add的时候,一个包含文本信息字段的弹框将会出现在屏幕上显示。在那里你能够输入默认的名字到进入文本域。
+
+
+如果你想知道原因,你可以不设置表示图的委托,这样就不会触发任何行为。
+
+打开Assistant Editor拖拽table view到 ViewController.swift,在类中插入一个outlet:
+
+
+
+起名为 tableview
+
+```
+@IBOutlet weak var tableView: UITableView!
+
+```
+
+同样将Add拖拽到ViewController.swift,创建一个action 命名为addName:
+
+```
+@IBAction func addName(sender: AnyObject) {
+
+}
+
+```
+
+那么现在你可引用表示图和按钮了。接下来就是建立表示图模型。在ViewContrroller.swift中添加一下代码:
+
+```
+//Insert below the tableView IBOutlet
+var names = [String]()
+
+```
+
+names是一个可变的shtring类型的数组,在tableview中显示。
+
+在viewDidLoad()中实现一下代码:
+
+```
+override func viewDidLoad() {
+ super.viewDidLoad()
+ title = "\"The List\""
+ tableView.registerClass(UITableViewCell.self,
+ forCellReuseIdentifier: "Cell")
+}
+
+```
+
+在这里将建立一个标题,注册UITableViewCell在table view类中。table view将返回正确类型的cell
+
+仍然在ViewController.swift,添加UITableViewDataSource,UITableViewDelegate
+
+```
+//Add UITableViewDataSource to class declaration
+class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate{
+ //这里添加
+ tableView.dataSource = self
+ tableView.delegate = self
+
+
+}
+
+```
+
+
+
+这个时候Xcode会提示ViewCotroller不符合协议.实现data source方法修改这个错误。
+
+```
+// MARK: UITableViewDataSource
+func tableView(tableView: UITableView,
+ numberOfRowsInSection section: Int) -> Int {
+ return names.count
+}
+
+func tableView(tableView: UITableView,
+ cellForRowAtIndexPath
+ indexPath: NSIndexPath) -> UITableViewCell {
+
+ let cell =
+ tableView.dequeueReusableCellWithIdentifier("Cell")
+
+ cell!.textLabel!.text = names[indexPath.row]
+
+ return cell!
+}
+
+```
+
+如果你用过UITableView,这段代码看起来会很相似。第一个方法就是names数量。
+
+第二个方法tableView(_:cellForRowAtIndexPath:)返回对cell对象。
+
+不要现在就运行。首先你需要一个输入names的方法然后在table view中进行显示他们。
+
+实现addName IBAction
+
+```
+//Implement the addName IBAction
+@IBAction func addName(sender: AnyObject) {
+
+ let alert = UIAlertController(title: "New Name",
+ message: "Add a new name",
+ preferredStyle: .Alert)
+
+ let saveAction = UIAlertAction(title: "Save",
+ style: .Default,
+ handler: { (action:UIAlertAction) -> Void in
+
+ let textField = alert.textFields!.first
+ self.names.append(textField!.text!)
+ self.tableView.reloadData()
+ })
+
+ let cancelAction = UIAlertAction(title: "Cancel",
+ style: .Default) { (action: UIAlertAction) -> Void in
+ }
+
+ alert.addTextFieldWithConfigurationHandler {
+ (textField: UITextField) -> Void in
+ }
+
+ alert.addAction(saveAction)
+ alert.addAction(cancelAction)
+
+ presentViewController(alert,
+ animated: true,
+ completion: nil)
+}
+
+```
+每次点击Add按钮的时候,这个方法该方法弹出文本域和两个按钮,保存和取消。
+
+点击保存,将其名称插入到数组中,table view将重新加载数据并显示。
+
+在这里,构建并且第一次运行我们的应用程序。点击顶部的Add按钮,将会插入一个弹框。
+
+
+
+添加4个或者5个左右的数据,就是下面的样子
+
+
+
+你的table view将显示数据,但是并不能实现持久化,什么意思呢?就是说我们现在的数组数据是存放在内存中的,但是如果我们一旦强制退出应用程序或者重新启动你的设备,你的数组数据就会被销毁。
+
+Core Data提供持久化,意思就是他可让数据保持为持久状态,尽管应用重新启动或者是重新运行。
+
+你现在还没有添加任何Core Data。让我们来测试下,切换到Simulator,点击Shift+⌘+H,将会返回home界面。
+
+
+
+看到HitList图标,点击它切换到应用,这些名字一样存在着。这跟我们刚才描述的不一样,为什么呢?
+
+当你点击Home按钮的时候。这个时候操作系统会瞬间冻结所有当前在内存中的一切信息。包括我们的名称数组字符串。同样,当我们的应用切换回去的时候,操作系统会恢复过去的记忆,就像你从来没有离开过一样,意思就是我们的names被恢复了。
+
+多任务模式早在iOS 4中推出。他们创造了iOS用户无缝体验,同时也添加了持续性的概念,这心真的存在吗?
+
+不,这不是真的。如果你完全杀死应用程序或者关闭你的iPhone,你的names数组就会消失。你可以体验下,快速的双击Home。
+
+
+
+在这里,向上拉动你的应程序,在这里就会将程序杀死了。这个时候反回Home,再一次点击你的应用程序,names就会消失。
+
+以上展现的两种方式书有差异的,所以这里看来了解熟悉多任务模式是显而易见的。但是这个在用户的头脑中是没有什么区别的。用户不会在乎通过哪种方式切回到Home,或者切回到应用。
+
+现在的问题就是怎么样让应用无论通过哪种方式返回都能够存在。
+
+现在就到了我们要讲诉的东西了,实现真正的持久性,数据在一个应用中无论怎样都会存在着。
+
+##数据建模
+
+现在你知道如何检测持久性了,让我们开始Core Data的学习。你的目标很简单,就是持久化你输入的名字,重新启动后仍然存在这个应用程序中。
+
+这这里,你已经了解了如何在内存中存储names名称。在这里你将使用Core Data代替那种方法。
+
+第一步就是创建一个managed object model,就是意味着通过Core Data数据将在磁盘上。默认情况下,Core Data使用SQlite数据库作为持久化的存储,所以你可以想象数据模型看成数据库架构。
+
+当你创建了工程的时候我们选择了Use Core Data,Xcode会自动创建一个数据模型文件叫做HitList.xcdatamodeld
+
+
+
+点击HitList.xcdatamodeld打开它,正如你所看到的,Xcode有自己的数据模型编辑器,就像如图所示那样
+
+
+
+这个数据模型有很多功能,让我们创建一个简单的数据实体。
+
+在左下方点击 Add Entity创建一个新的实体,双击我们新创建的实体,并且改名为Person,就像这样:
+
+
+
+你可能很想知道为什么数据模型编辑器使用"实体",而不是简单的定义一个新类。你不久就会看到,Core Data的数据来源于自身的词汇表。以下有一些常见的书语你可能会遇到:
+
+* 在Core Data中一个entity是一个类的定义。举一个典型的例子就是员工和公司的例子。在关系型数据库中,一个实体对应一个表。
+* 一个attribute是连接到特定实体信息的一部分。例如,一个员工可以拥有姓名,职位,工资等attribute。在数据库中attribute(属性)对应表中特定的字段。
+* relationship是多个实体之间的一个连接。
+
+现在你知道了什么是attribute,返回模型编辑并且添加一个attribute到Person中,选择Person,点击+。
+
+建立一个name并且选择类型为String。
+
+
+
+在Core Data包含很多种数据类型,String是其中的一种。
+
+##保存数据到Core Data
+
+在ViewCortroller.swift中 Import Core Data
+
+```
+//Add below "import UIKit"
+import CoreData
+
+```
+
+在Objective-C你可能不得不手动链接框架,但是在Swift中,一个简单的Import语句就就可以让你在你的代码中使用API。
+
+接下来,更换模型。
+
+```
+//将names变味people,并且将people类型改为NSManagedObject类型
+var people = [NSManagedObject]()
+
+
+```
+
+接下来你存储的是Person实体而不是names,所以将数据模型更名为people,并且它现在是NSManagedObject类型而不是简单的String类型。
+
+NSManagedObject被称为在Core Data中的单一对象。你必须使用它创建,修改,保存和删除操作你的持久数据。
+
+就在刚刚你已经改变了视图的模型,你必须也要使用下面的代码替换原来的数据源。
+
+```
+//Replace both UITableViewDataSource methods
+func tableView(tableView: UITableView,
+ numberOfRowsInSection section: Int) -> Int {
+ return people.count
+}
+
+func tableView(tableView: UITableView,
+ cellForRowAtIndexPath
+ indexPath: NSIndexPath) -> UITableViewCell {
+
+ let cell =
+ tableView.dequeueReusableCellWithIdentifier("Cell")
+
+ let person = people[indexPath.row]
+
+ cell!.textLabel!.text =
+ person.valueForKey("name") as? String
+
+ return cell!
+}
+
+```
+
+其实通过比较来看其实最显著的变化在cellForRowAtIndexPath,仔细看看就能够发现其中的变化。
+
+当然,你需要注意的地方还有就是你是如何从NSManagedObject抓去name属性。
+
+```
+cell!.textLabel!.text = person.valueForKey("name") as? String
+
+```
+
+为什么用以上的方式呢?因为NSManagedObject并不知道在你的数据模型中name是如何被定义在你的数据模型中的,因此没有办法直接访问你的name属性。唯一的方法就是用过Core Data提供的key-value来读取,这种方式通常被叫做KVC。
+
+我在这里同样也简单的介绍一下KVC吧。就是说如果你是一个新的iOS开发者可能不了解什么是KVC或者是key-vale编码。KVC就是Cocoa和Cocoa Touch的机制来访问一个对象的而属性间接的使用字符串来识别。在以上的情况下,KVC就像是一本字典。
+
+接下来就是改变我们@IBAction addName方法:
+
+```
+let saveAction = UIAlertAction(title: "Save",
+ style: .Default,
+ handler: { (action:UIAlertAction) -> Void in
+
+ let textField = alert.textFields!.first
+ self.saveName(textField!.text!)
+ self.tableView.reloadData()
+})
+
+```
+
+看到上面我们有新添加了一个saveName方法
+
+```
+func saveName(name: String) {
+ //1
+ let appDelegate =
+ UIApplication.sharedApplication().delegate as! AppDelegate
+
+ let managedContext = appDelegate.managedObjectContext
+
+ //2
+ let entity = NSEntityDescription.entityForName("Person",
+ inManagedObjectContext:managedContext)
+
+ let person = NSManagedObject(entity: entity!,
+ insertIntoManagedObjectContext: managedContext)
+
+ //3
+ person.setValue(name, forKey: "name")
+
+ //4
+ do {
+ try managedContext.save()
+ //5
+ people.append(person)
+ } catch let error as NSError {
+ print("Could not save \(error), \(error.userInfo)")
+ }
+}
+
+```
+
+这里面都是什么呢?我们来分析下
+
+* 在你做svae操作之前,你需要先获取NSManagedObjectContext。你可以认为这是用来managed object context的。想进行保存对象到Core Data中需要两步,首先,你需要插入一个对象到managed object context中。然后提交,将该对象存储到磁盘中。Xcode其实已经产生一个通用的模板,当你选择Use Core Data的时候。这个默认的managed object context存在于application delegate中。要想引用它,你需要获取一个app delegate引用。
+* 创建managed object context并且完成NSManagedObject的初始化,init(entity:insertIntoManagedObjectContext:).你可能会想到NSEntityDescription的所有相关东西
+* 使用NSManagedObject,必须使用你建立的name,必须使用KVC,否则会出现崩溃现象。
+* 你的提交的person被保存在磁盘中,注意save会抛出异常,这就是为什么我们需要使用try do。
+* 接下来就要恭喜你已经成功并且安全的的实现了数据的持久化。仍然是当我们插入后将会重新加载视图
+
+这比使用一个字符串可能复杂的很多,但是并不是很复杂。这里的代码就是获取managed object context和entity,
+
+构建并且运行,添加一些类似下面的名字。
+
+
+
+如果你的数据已经在Core Data中存储,并且实现了数据的持久化,这个时候我们杀死我们的app,然后重现加载我们程序,等一下,发生了什么?table view是空的
+
+
+
+你存续到Core Data中的数据但是重新加载后并没有,仍然是空的,其实数据事实上在那里的等待着,你并没有显示它、
+
+从Core Data中获取
+
+要得到持久化的数据,你必须取出它。在ViewController.swift中天添加一个方法
+
+```
+override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+
+ //1
+ let appDelegate =
+ UIApplication.sharedApplication().delegate as! AppDelegate
+
+ let managedContext = appDelegate.managedObjectContext
+
+ //2
+ let fetchRequest = NSFetchRequest(entityName: "Person")
+
+ //3
+ do {
+ let results =
+ try managedContext.executeFetchRequest(fetchRequest)
+ people = results as! [NSManagedObject]
+ } catch let error as NSError {
+ print("Could not fetch \(error), \(error.userInfo)")
+ }
+}
+
+```
+
+以上代码中做了什么?
+
+* 就像刚才说的,在做一些操作前你需要获取一个managed object context。从delegate中引用managed object context.
+* 根据名字就能看出来,NSFetchRequest就是返回的数据。
+* executefetchrequest()返回满足managed objects读取请求中指定的标准数组。
+
+构建并且重新运行,就能发现你想要的效果了
+
+
+
+现在你就可以随意的测试了。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/issue-19/readme.md b/issue-19/readme.md
new file mode 100644
index 0000000..02e6c10
--- /dev/null
+++ b/issue-19/readme.md
@@ -0,0 +1,5 @@
+add issue-19
+
+
+
+
diff --git "a/issue-19/\345\210\233\345\273\272\350\207\252\346\263\250\345\206\214\347\232\204Swift UI \346\216\247\344\273\266.md" "b/issue-19/\345\210\233\345\273\272\350\207\252\346\263\250\345\206\214\347\232\204Swift UI \346\216\247\344\273\266.md"
new file mode 100644
index 0000000..cca0e4c
--- /dev/null
+++ "b/issue-19/\345\210\233\345\273\272\350\207\252\346\263\250\345\206\214\347\232\204Swift UI \346\216\247\344\273\266.md"
@@ -0,0 +1,151 @@
+> * 原文链接 : [Swift Programming 101: Creating Self-Registering Swift UI Controls](http://www.iphonelife.com/blog/31369/swift-programming-101-creating-self-registering-swift-ui-controls)
+* 原文作者 : [Kevin McNeish](http://www.iphonelife.com/blog/kevin-mcneish)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [kmyhy](https://github.com/kmyhy)
+* 校对者:[LastDay](http://lastday.github.io)
+* 状态:完成
+
+
+对于自定义控件来说,在不破坏原有的消息机制的前提下,如何响应事件通知?在本文中,我将演示一个通知代理类,通过一个简单的例子,我们用该类向已有的iOS UI控件中增加了自己的新功能:为Text View控件增加placeholder文本。
+
+
+##问题:缺失的Placeholder
+
+Placeholder文本是用于在某些控件中提示用户输入指定类型的数据的良好方式。UIKit的UITextField控件的placeholder属性就是用来干这个的。例如,下图中,在Twitter的选项设置中,User Name字段就使用了placeholder文本。通过这个placeholder文本,用户可以知道应当在这里输入一个包含了@符号的Twitter用户名。而在Password字段的placeholder文本则标明该字段是必填的。
+
+
+图 1 - 设置程序中的Placeholder 文本
+
+
+尽管Text Filed有placeholder属性,但它的兄弟控件Text View却没有这个属性。
+
+
+##问题的解决
+
+
+
+ 我准备这样解决这个问题:
+
+
+1. 创建一个UITextView的扩展,添加一个placeholder的计算属性
+2. 在扩展中,当TextView中没有输入任何文字的情况下,显示一个Label,如果输入有文字,则隐藏这个Label。
+
+
+
+就像我在前面的文章中提到的,我更喜欢使用扩展,而不是子类。因为扩展允许我在App中使用“盒子之外”的UIKit控件。我需要做的仅仅是在项目中加入某个类的扩展文件,然后这个类会自动获得新的特性。
+
+##方式1:UITextViewDelegate
+
+
+
+当用户在TextView中输入文本时,iOS有几种通知App的方式。其中一种就是通过UITextViewDelegate协议。在这个扩展中有一个对TextView的引用保存在它自己的delegate属性中。当用户输入或删除字符时,TextView的协议方法会被调用。我们可以在协议方法中加入隐藏和显示placeholder标签的代码。
+
+
+
+这个解决方法的缺点是,Text View太过于依赖delegate属性,因为在一个UIControl中你只能注册一个委托对象。如果你需要向TextView中加入更多的委托对象,你就会比较麻烦。
+
+
+##方式2:NSNotificationCenter
+
+
+
+NSNotificationCenter通过UITextViewTextDidChangeNotification通知来告诉你用户在TextView中输入或删除了某些字符。这是一种更好的选择,因为它不再依赖于delegate属性。缺点是需要向通知中心进行注销。一般,我们在对象的deinit方法中向NSNotificationCenter注销该对象。但是在Swift中,我们无法在扩展中使用deinit方法。那我们怎样才能突破这个限制呢?
+
+##创建一个通知代理
+
+
+我们通过创建一个轻量级的代理对象来突破这个限制,这个代理对象代替TextView来向通知中心进行注册。
+
+
+我已经在我的项目中创建了一个UITextView扩展以及一个代理类,你可以从[这里](http://www.iosappsfornonprogrammers.com/media/blog/NotificationProxyDemo.zip)下载。
+
+
+双击.xcodeproj文件,用Xcode打开这个项目。在项目导航窗口中选中mmTextViewExtensions.swift文件,你可以找到这个通知代理类(如你所见,其中也包含了上一篇文章中的mmDyamicTypeExtensions类)。
+
+
+下面是位于文件头部的通知代理类:
+
+
+
+
+你可以看到,它是UIView子类。这样我们就可以将它当成subview添加到TextView中。addObserverForName:usingBLock:方法通知中心的方法拥有一样的签名。这个方法中只有一行代码,就是将TextView注册到通知中心并将参数传递过去,包括TextView的闭包,这样当通知发生时该闭包被执行。通知中心会返回一个NSObjectProtocol对象,稍后我们会用来进行通知的注销操作。
+
+
+deinit方法也只有一行代码,仅仅是向通知中心进行对象的注销。
+
+
+通知代理类是可重用的。你可以用于任何类型的通知以及你想接收通知的任何类型的对象。
+
+
+然后是UITextView的扩展,在扩展中有一个placeholderLabel属性,如你所想,它引用了一个placeholder标签对象。
+
+
+此外还有一个placeholder的字符串属性。在后面你将看到,所有的奇迹将在运行时发生,当placeholder属性被设置,这个计算属性的代码就会执行。
+
+
+现在打开Main.storyboard文件。如图2所示,故事板中只有一个Scene,在这个Scene的顶部,有一个Text View。
+
+
+图 2 - 主 scene 中包含了一个 text view
+
+
+当你选择Text View,然后打开属性面板,你将看见一个Placeholder属性,它是扩展中增加的属性,这个属性的默认值是“Enter your text here!”。在设计时不会显示这段文本(要在设计时可见,我们需要花费大量的工作,因此这不是本文的主题),但在运行时它会显示。
+
+##The Notification Proxy at Run Time
+##运行时的通知代理
+
+
+图3中的UML序列图显示了所有对象之间发生的消息传递的顺序。
+
+
+
+图 3 - 运行时对象间消息传递的顺序
+
+
+每一步的解释如下:
+
+
+
+
+1. 当运行时,UITextView的placeholder文本被改变时,该扩展的placeholder计算属性中的代码被触发。
+2. 扩展的addPlaceholderLabel方法被调用。
+3. addPlaceholderLabel方法创建一个placeholder标签并设置标签文本。
+4. placeholder标签被添加进UITextView。
+5. 一个通知代理对象被创建。
+6. 扩展调用通知代理的addObserverForName:withBlock:方法,并指定对UITextViewTextDidChangeNotification通知感兴趣。此外闭包代码会在通知发生时执行。
+7. 通知代理对象将UITextView注册到通知中心,同时将通知时间传递给TextView,当通知发生时,UITextView的闭包代码将被执行。
+8. UITextView以subview的方式添加通知代理对象。
+9. 当UITextViewTextDidChangeNotification通知发生,通知中心调用TextView扩展的闭包。
+10. UITextView扩展将placeholder标签的hidden属性设置为true和false,取决于TextView中的文本是否有内容。
+11.当TextView在运行时解构时,placeholder标签和通知代理对象都被解构。
+11. 当通知代理对象解构时,将TextView对象从通知中心中注销。
+
+##Let’s Take it for a Test Run!
+##运行测试
+
+1. In Xcode’s Scheme control, select one of the simulators such as iPhone 6.
+在Xcode的Scheme下拉列表中,选择一个模拟器,例如iPhone6。
+
+2. Click Xcode’s Run button.
+点击Xcode的Run按钮。
+
+3. When the app appears in the Simulator, you can see the placeholder text (Figure 4).
+当App在模拟器中运行后,我们可以看到placeholder文本(图4)。
+
+
+
+图 4 - 运行时的 placeholder 文本
+
+ 4.Now type some code in the text view. As soon as you begin typing, the placeholder label disappears (Figure 5).
+ 现在在TextView中输入任意字符。此时,placeholder将消失(图5)。
+
+
+
+图 5 -placeholder 文字消失了
+
+ 5. If you delete all the characters you typed, the placeholder reappears.
+ 如果将输入的字符删除干净,placeholder文字将重新出现。
+
+##结束语
+
+要解决编程问题有许多方法。最好的方法是先看一下摆在你目前的选择,然后逐个分析其中的利弊。我希望你能看到本文中序列图的好处。它能让你将对象之间在运行时的交互画出来。我在编写代码时也使用了这个方法。你会发现它们能帮助你找出潜在的问题,更能帮你找出编程问题中的新的、更高效的解决方案。
\ No newline at end of file
diff --git "a/issue-19/\345\246\202\344\275\225\345\256\236\347\216\260iOS\345\233\276\344\271\246\345\212\250\347\224\273-\347\254\2541\351\203\250\345\210\206.md" "b/issue-19/\345\246\202\344\275\225\345\256\236\347\216\260iOS\345\233\276\344\271\246\345\212\250\347\224\273-\347\254\2541\351\203\250\345\210\206.md"
new file mode 100644
index 0000000..84228c4
--- /dev/null
+++ "b/issue-19/\345\246\202\344\275\225\345\256\236\347\216\260iOS\345\233\276\344\271\246\345\212\250\347\224\273-\347\254\2541\351\203\250\345\210\206.md"
@@ -0,0 +1,696 @@
+如何实现iOS图书动画:第1部分
+---
+
+> * 原文链接 : [How to Create an iOS Book Open Animation: Part 1](http://www.raywenderlich.com/94565/how-to-create-an-ios-book-open-animation-part-1)
+* 原文作者 : [Vincent Ngo](http://www.raywenderlich.com/u/jomoka)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [kmyhy](https://github.com/kmyhy)
+* 校对者:[LastDay](http://lastday.github.io)
+* 状态:完成
+
+
+
+
+本教程分为2个部分,教你开发一个漂亮的iOS图书打开和翻页动画,就像你在Paper 53中所见到的一样:
+
+
+在第1部分,你将学习到如何定制化Collection View Layout,并通过使用深度和阴影使App看起来更真实。
+
+
+在第2部分,你将学习如何以一种合理的方法在两个不同的控制器之间创建自定义的过渡特效,以及利用手势在两个视图间创建自然的、直观的过渡效果。
+
+
+本教程适用于中级-高级的开发者;你将使用自定义过渡动画和自定义Collection View Layout。如果你从来没有用过Colleciton View,请先参考[其他iOS教程](http://www.raywenderlich.com/tutorials)。
+
+
+> 注意:感谢[Attila Hegdüs](https://twitter.com/hegedus90)创建了本教程中的示例项目。
+
+##开始
+
+从[此处](http://cdn2.raywenderlich.com/wp-content/uploads/2015/05/Starter-Paper1.zip)下载本教程的开始项目;解开zip压缩包,用Xcode打开Paper.xcodeproj。
+
+编译项目,在模拟器中运行App;你将看到如下画面:
+
+
+
+
+这个App的功能已经很完善了,你可以在你的书库中滚动,查看图书,选中某本图书进行浏览。但当你读一本书的时候,为什么它的书页都是并排放置的?通过一些UICollectionView的知识,你可以让这些书页看起来更好一些!
+
+##The Project Structure
+##项目结构
+
+
+关于这个开始项目,有几个重要的地方需要解释:
+
+
+
+Data Models文件夹包含3个文件:
+
+- Books.plist 中包含了几本用于演示的图书信息。每本图书包含一张封面图片,以及一个表示每一页的内容的图片的数组。
+- BookStore.swift实现了单例,在整个App声明周期中只能创建一次对象。BookStore的职责是从Books.plist中加载数据并创建Book类实例。
+- Book.swift用于存放图书相关信息的类,比如图书的封面,每一页的图片,以及页号。
+
+
+
+Books文件夹包含了两个文件:
+
+- BooksViewController.swift是一个UICollectionViewController子类。负责以水平方式显式图书列表。
+- BookCoverCell.swift负责显示图书的封面,这个类被BooksViewController类所引用。
+
+
+
+在Book文件夹中则包括:
+
+- BookViewController.swift也是UICollectionViewController的子类。当用户在BooksViewController中选定的一本书后,它负责显示图书中的书页。
+- BookPageCell.swift被BookViewController用于显示图书中的书页。
+
+
+
+在最后一个文件夹Helper中包含了:
+
+- UIImage+Helpers.swift是UIImage的扩展。该扩展包含了两个实用方法,一个用于让图片呈圆角显示,一个用于将图片缩放到指定大小。
+
+
+这就是整个开始项目的大致介绍——接下来该是我们写点代码的时候了!
+
+##定制化图书界面
+
+首先我们需要在BooksViewController中覆盖Collection View的默认布局方式。但当前的布局是在屏幕上显示3张图书封面的大图。为了美观,我们将这些图片缩减到一定大小,如下图所示:
+
+
+
+
+当我们滑动图片,移动到屏幕中心的图片将被放大,以表示该图书为选中状态。如果继续滑动,该图书的封面又会缩小到一边,表示我们放弃选择该图书。
+
+
+在App\Books文件夹下新建一个文件夹组:Layout。在Layout上点击右键,选择New File...,然后选择iOS\Source\Cocoa Touch Class模板,并点击Next。类名命名为BooksLayout,继承UICollectionViewFlowLayout类,语言设置为Swift。
+
+
+然后需要告诉BooksViewController中的Collection View,适用我们新建的BooksLayout。
+
+
+打开Main.storyboard,展开BooksViewController对象,然后选择Collection View。在属性面板中,设置Layout 属性为 Custom,设置Class属性为BooksLayout,如下图所示:
+
+
+
+
+打开BooksLayout.swift,在BooksLayout类声明之上加入以下代码:
+
+```
+private let PageWidth: CGFloat = 362
+private let PageHeight: CGFloat = 568
+```
+
+
+这个两个常量将用于设置单元格的的大小。
+现在,在类定义内部定义如下初始化方法:
+
+```
+required init(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+
+ scrollDirection = UICollectionViewScrollDirection.Horizontal //1
+ itemSize = CGSizeMake(PageWidth, PageHeight) //2
+ minimumInteritemSpacing = 10 //3
+}
+```
+
+
+上述代码作用如下:
+
+ 1. 设置Collectioin View的滚动方向为水平方向。
+ 2. 设置单元格的大小为PageWidth和PageHeight,即362x568。
+ 3. 设置两个单元格间距10。
+
+
+然后,在init(coder:)方法中加入代码:
+
+```
+override func prepareLayout() {
+ super.prepareLayout()
+
+ //The rate at which we scroll the collection view.
+ //1
+ collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
+
+ //2
+ collectionView?.contentInset = UIEdgeInsets(
+ top: 0,
+ left: collectionView!.bounds.width / 2 - PageWidth / 2,
+ bottom: 0,
+ right: collectionView!.bounds.width / 2 - PageWidth / 2
+ )
+}
+```
+
+
+prepareLayout()方法允许我们在每个单元格的布局信息生效之前可以进行一些计算。
+
+
+对应注释中的编号,以上代码分别说明如下:
+
+
+ 1. 设置当用户手指离开屏幕后,Collection
+ View停止滚动的速度。默认的设置为UIScrollViewDecelerationRateFast,这是一个较快的速度。你可以尝试着设置为Normal 和 Fast,看看二者之间有什么区别。
+ 2. 设置Collection View的contentInset,以使第一本书的封面位于Collection View的中心。
+
+
+现在我们需要处理每一个单元格的布局信息。
+在prepareLayout()方法下面,加入以下代码:
+
+```
+override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
+ //1
+ var array = super.layoutAttributesForElementsInRect(rect) as! [UICollectionViewLayoutAttributes]
+
+ //2
+ for attributes in array {
+ //3
+ var frame = attributes.frame
+ //4
+ var distance = abs(collectionView!.contentOffset.x + collectionView!.contentInset.left - frame.origin.x)
+ //5
+ var scale = 0.7 * min(max(1 - distance / (collectionView!.bounds.width), 0.75), 1)
+ //6
+ attributes.transform = CGAffineTransformMakeScale(scale, scale)
+ }
+
+ return array
+}
+```
+
+
+layoutAttributesForElementsInRect(_:) 方法返回一个UICollectionViewLayoutAttributes对象数组,其中包含了每一个单元格的布局属性。以上代码稍作说明如下:
+
+
+
+ 1. 调用父类的layoutAttributesForElementsInRect方法,已获得默认的单元格布局属性。
+ 2. 遍历数组中的每个单元格布局属性。
+ 3. 从单元格布局属性中读取frame。
+ 4. 计算两本书的封面之间的间距——即两个单元格之间的间距——以及屏幕的中心点。
+ 5. 以0.75~1之间的比率缩放封面,具体的比率取决于前面计算出来的间距。然后为了美观,将所有的封面都缩放70%。
+ 6. 最后,应用仿射变换。
+
+
+
+接下来,在layoutAttributesForElementsInRect(_:)方法后增加如下代码:
+
+```
+override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
+ return true
+}
+```
+
+
+返回true表示每当Collection View的bounds发生改变时都强制重新计算布局属性。Collection View在滚动时会改变它的bounds,因此我们需要重新计算单元格的布局属性。
+
+
+编译运行程序,我们将看到位于中央的封面明显比其他封面要大上一圈:
+
+
+
+
+拖动Colleciton View,查看每本书放大、缩小。但仍然有一点稍显不足,为什么不让书本能够卡到固定的位置呢?
+接下来我们介绍的这个方法就是干这个的。
+
+
+##对齐书本
+
+
+targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:)方法用于计算每本书应该在对齐到哪个位置,它返回一个偏移位置,可用于设置Collection View的contentOffset。如果你不覆盖这个方法,它会返回一个默认的值。
+
+
+在shouldInvalidateLayoutForBoundsChange(_:)方法后添加如下代码:
+
+```
+override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
+ // Snap cells to centre
+ //1
+ var newOffset = CGPoint()
+ //2
+ var layout = collectionView!.collectionViewLayout as! UICollectionViewFlowLayout
+ //3
+ var width = layout.itemSize.width + layout.minimumLineSpacing
+ //4
+ var offset = proposedContentOffset.x + collectionView!.contentInset.left
+
+ //5
+ if velocity.x > 0 {
+ //ceil returns next biggest number
+ offset = width * ceil(offset / width)
+ } else if velocity.x == 0 { //6
+ //rounds the argument
+ offset = width * round(offset / width)
+ } else if velocity.x < 0 { //7
+ //removes decimal part of argument
+ offset = width * floor(offset / width)
+ }
+ //8
+ newOffset.x = offset - collectionView!.contentInset.left
+ newOffset.y = proposedContentOffset.y //y will always be the same...
+ return newOffset
+}
+```
+
+
+这段代码计算当用户手指离开屏幕时,封面应该位于哪个偏移位置:
+
+
+
+ 1. 声明一个CGPoint。
+ 2. 获得Collection View的当前布局。
+ 3. 获得单元格的总宽度。
+ 4. 计算相对于屏幕中央的currentOffset。
+ 5. 如果velocity.x>0,表明用户向右滚动,用offset除以width,得到书的索引,并滚动到相应的位置。
+ 6. 如果velocity.x=0,表明用户是无意识的滚动,原来的选择不会发生改变。
+ 7. 如果velocity.x<0,表明用户向左滚动。
+ 8. 修改newOffset.x,然后返回newOffset。这样就保证书本总是对齐到屏幕的中央。
+
+
+编译运行程序;再次滚动封面,你会注意到滚动动作将变得更整齐了。
+
+
+要完成这个布局,我们还需要使用一种机制,以限制用户只能点击位于中央的封面。目前,不管哪个位置的封面都是可点击的。
+
+
+打开BooksViewController.swift,在注释"//MARK:Helpers"下面加入以下代码:
+
+```
+func selectedCell() -> BookCoverCell? {
+ if let indexPath = collectionView?.indexPathForItemAtPoint(CGPointMake(collectionView!.contentOffset.x + collectionView!.bounds.width / 2, collectionView!.bounds.height / 2)) {
+ if let cell = collectionView?.cellForItemAtIndexPath(indexPath) as? BookCoverCell {
+ return cell
+ }
+ }
+ return nil
+}
+```
+
+selectedCell() will always return the middle cell.
+Next, replace openBook(_:) with the following:
+
+selectedCell()方法返回位于中央的那个单元格。
+替换openBook(_:)方法的代码如下:
+
+```
+func openBook() {
+ let vc = storyboard?.instantiateViewControllerWithIdentifier("BookViewController") as! BookViewController
+ vc.book = selectedCell()?.book
+ // UICollectionView loads it's cells on a background thread, so make sure it's loaded before passing it to the animation handler
+ dispatch_async(dispatch_get_main_queue(), { () -> Void in
+ self.navigationController?.pushViewController(vc, animated: true)
+ return
+ })
+}
+```
+
+
+这里,直接调用新的selectedCell方法,并用它的book属性代替原来的book参数。
+然后,将collectionView(_:didSelectItemAtIndexPath:)方法替换为:
+
+```
+override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
+ openBook()
+}
+```
+
+
+这里,我们简单地删除了原来的打开某个索引处的图书的代码,而直接打开了当前位于屏幕中央的图书。
+编译运行程序,我们将看到每次打开的图书总是位于屏幕中央的那本。
+
+
+BooksLayout的工作就到这里了。后面我们将使这本电子书显得更真实,能够让用户”翻动“书里的每一页!
+
+##翻页布局
+
+最终实现的效果如下:
+
+
+
+Now that looks more like a book! :]
+这看起来就像是一本真正的书! :]
+
+
+在Book文件夹下新建一个Layout文件夹。在Layout文件夹上右键,选择New File...,然后适用iOS\Source\Cocoa Touch Class模板,然后点Next。类名命名为BookLayout,继承于UICollectionViewFlowLayout,语言选择Swift。
+
+
+同前面一样,图书所使用的Collection View需要适用新的布局。打开Main.storyboard,然后选择Book View Controller场景,展开并选中其中的Collection View,然后设置Layout属性为Custom。
+
+
+然后,将Layout属性下的 Class属性设置为BookLayout:
+
+
+
+
+打开BookLayout.swift,在类声明之上加入如下代码:
+
+```
+private let PageWidth: CGFloat = 362
+private let PageHeight: CGFloat = 568
+private var numberOfItems = 0
+```
+
+
+这几个常量将用于设置单元格的大小,以及记录整本书的页数。
+接着,在类声明内部加入代码:
+
+```
+override func prepareLayout() {
+ super.prepareLayout()
+ collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
+ numberOfItems = collectionView!.numberOfItemsInSection(0)
+ collectionView?.pagingEnabled = true
+}
+```
+
+
+这段代码和我们在BooksLayout中所写的差不多,仅有以下几处差别:
+
+
+ 1. 将减速速度设置为UIScrollViewDecelerationRateFast,以加快Scroll View滚动速度变慢的节奏。
+ 2. 记住本书的页数。
+ 3. 启用分页,这样Scroll View滚动时将以其宽度的固定倍数滚动(而不是持续滚动)。
+
+
+仍然在BookLayout.swift中,加入以下代码:
+
+```
+override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
+ return true
+}
+```
+
+
+跟前面一样,返回true,以表示用户每次滚动都会重新计算布局。
+
+
+然后,覆盖collectionViewContentSize() ,以指定Collection View的contentSize:
+
+```
+override func collectionViewContentSize() -> CGSize {
+ return CGSizeMake((CGFloat(numberOfItems / 2)) * collectionView!.bounds.width, collectionView!.bounds.height)
+}
+```
+
+
+这个方法返回了内容区域的整个大小。内容区域的高度总是不变的,但宽度是随着页数变化的——即书的页数除以2倍,再乘以屏幕宽度。除以2是因为书页有两面,内容区域一次显示2页。
+
+
+就如我们在BooksLayout中所做的一样,我们还需要覆盖layoutAttributesForElementsInRect(_:)方法,以便我们能够在单元格上增加翻页效果。
+
+
+在collectionViewContentSize()方法后加入:
+
+```
+override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
+ //1
+ var array: [UICollectionViewLayoutAttributes] = []
+
+ //2
+ for i in 0 ... max(0, numberOfItems - 1) {
+ //3
+ var indexPath = NSIndexPath(forItem: i, inSection: 0)
+ //4
+ var attributes = layoutAttributesForItemAtIndexPath(indexPath)
+ if attributes != nil {
+ //5
+ array += [attributes]
+ }
+ }
+ //6
+ return array
+}
+```
+
+
+不同于在BooksLayout中的将所有计算布局属性的代码放在这个方法里面,我们这次将这个任务放到layoutAttributesForItemAtIndexPath(_:)中进行,因为在图书的实现中,所有单元格都是同时可见的。
+
+
+以上代码解释如下:
+
+
+ 1. 声明一个数组,用于保存所有单元格的布局属性。
+ 2. 遍历所有的书页。
+ 3. 对于CollecitonView中的每个单元格,都创建一个NSIndexPath。
+ 4. 通过每个NSIndexPath来获得单元格的布局属性,在后面,我们会覆盖layoutAttributesForItemAtIndexPath(_:)方法。
+ 5. 把每个单元格布局属性添加到数组。
+ 6. 返回数组。
+
+##Handling the Page Geometry
+##处理页面的几何计算
+
+
+在我们开始实现layoutAttributesForItemAtIndexPath(_:)方法之前,花几分钟好好思考一下布局的问题,它是怎样实现的,如果能够写几个助手方法将会让我们的代码更漂亮和模块化。:]
+
+
+
+
+上图演示了翻页时以书籍为轴旋转的过程。图中书页的”打开度“用-1到1来表示。为什么?你可以想象一下放在桌子上的一本书,书脊所在的位置代表0.0。当你从左向右翻动书页时,书页张开的程度从-1(最左)到1(最右)。因此,我们可以用下列数字表示“翻页”的过程:
+
+0.0 means a page is at a 90 degree angle, perpendicular to the table.
++/- 0.5 means a page is at a 45 degree angle to the table.
++/- 1.0 means a page is parallel to the table.
+
+ 1. 0.0表示一个书页翻成90度,与桌面成直角。
+ 2. +/-0.5表示书页翻至于桌面成45度角。
+ 3. +/-1.0表示书页翻至与桌面平行。
+
+
+注意,因为角度是按照反时针方向增加的,因此角度的符号和对应的打开度是相反的。
+
+
+首先,在layoutAttributesForElementsInRect(_:)方法后加入助手方法:
+
+```
+//MARK: - Attribute Logic Helpers
+
+func getFrame(collectionView: UICollectionView) -> CGRect {
+ var frame = CGRect()
+
+ frame.origin.x = (collectionView.bounds.width / 2) - (PageWidth / 2) + collectionView.contentOffset.x
+ frame.origin.y = (collectionViewContentSize().height - PageHeight) / 2
+ frame.size.width = PageWidth
+ frame.size.height = PageHeight
+
+ return frame
+}
+```
+
+
+对于每一页,我们都可以计算出相对于Collection View中心的frame。getFrame(_:)方法会将每一页的一边对齐到书脊。唯一会变的是Collectoin View的contentOffset在x方向上的改变。
+
+
+然后,在getFrame(_:)方法后添加如下方法:
+
+```
+func getRatio(collectionView: UICollectionView, indexPath: NSIndexPath) -> CGFloat {
+ //1
+ let page = CGFloat(indexPath.item - indexPath.item % 2) * 0.5
+
+ //2
+ var ratio: CGFloat = -0.5 + page - (collectionView.contentOffset.x / collectionView.bounds.width)
+
+ //3
+ if ratio > 0.5 {
+ ratio = 0.5 + 0.1 * (ratio - 0.5)
+
+ } else if ratio < -0.5 {
+ ratio = -0.5 + 0.1 * (ratio + 0.5)
+ }
+
+ return ratio
+}
+```
+
+
+上面的方法计算书页翻开的程度。对每一段有注释的代码分别说明如下:
+
+
+
+ 1. 算出书页的页码——记住,书是双面的。 除以2就是你真正在翻读的那一页。
+ 2. 算出书页的打开度。注意,这个值被我们加了一个权重。
+ 3. 书页的打开度必须限制在-0.5到0.5之间。另外乘以0.1的作用,是为位了在页与页之间增加一条细缝,以表示它们是上下叠放在一起的。
+
+
+
+一旦我们计算出书页的打开度,我们就可以将之转变为旋转的角度。
+在getRation(_:indexPath:)方法后面加入代码:
+
+```
+func getAngle(indexPath: NSIndexPath, ratio: CGFloat) -> CGFloat {
+ // Set rotation
+ var angle: CGFloat = 0
+
+ //1
+ if indexPath.item % 2 == 0 {
+ // The book's spine is on the left of the page
+ angle = (1-ratio) * CGFloat(-M_PI_2)
+ } else {
+ //2
+ // The book's spine is on the right of the page
+ angle = (1 + ratio) * CGFloat(M_PI_2)
+ }
+ //3
+ // Make sure the odd and even page don't have the exact same angle
+ angle += CGFloat(indexPath.row % 2) / 1000
+ //4
+ return angle
+}
+```
+
+
+这个方法中有大量计算,我们一点点拆开来看:
+
+
+
+ 1. 判断该页是否是偶数页。如果是,则该页将翻到书脊的右边。翻到右边的页是反手翻转,同时书脊右边的页其角度必然是负数。注意,我们将打开度定义为-0.5到0.5之间。
+ 2. 如果当前页是奇数,则该页将位于书脊左边,当书页被翻到左边时,它的按正手翻转,书脊左边的页其角度为正数。
+ 3. 每页之间加一个小夹角,使它们彼此分离。
+ 4. 返回旋转角度。
+
+
+得到旋转角度之后,我们可以操纵书页使其旋转。增加如下方法:
+
+```
+func makePerspectiveTransform() -> CATransform3D {
+ var transform = CATransform3DIdentity
+ transform.m34 = 1.0 / -2000
+ return transform
+}
+```
+
+
+修改转换矩阵的m34属性,已达到一定的立体效果。
+
+
+然后应用旋转动画。实现下面的方法:
+
+```
+func getRotation(indexPath: NSIndexPath, ratio: CGFloat) -> CATransform3D {
+ var transform = makePerspectiveTransform()
+ var angle = getAngle(indexPath, ratio: ratio)
+ transform = CATransform3DRotate(transform, angle, 0, 1, 0)
+ return transform
+}
+```
+
+
+在这个方法中,我们用到了刚才创建的两个助手方法去计算旋转的角度,然后通过一个CATransform3D对象让书页在y轴上旋转。
+
+
+所有的助手方法都实现了,我们最终需要配置每个单元格的属性。在layoutAttributesForElementsInRect(_:)方法后加入以下方法:
+
+```
+override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
+ //1
+ var layoutAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
+
+ //2
+ var frame = getFrame(collectionView!)
+ layoutAttributes.frame = frame
+
+ //3
+ var ratio = getRatio(collectionView!, indexPath: indexPath)
+
+ //4
+ if ratio > 0 && indexPath.item % 2 == 1
+ || ratio < 0 && indexPath.item % 2 == 0 {
+ // Make sure the cover is always visible
+ if indexPath.row != 0 {
+ return nil
+ }
+ }
+ //5
+ var rotation = getRotation(indexPath, ratio: min(max(ratio, -1), 1))
+ layoutAttributes.transform3D = rotation
+
+ //6
+ if indexPath.row == 0 {
+ layoutAttributes.zIndex = Int.max
+ }
+
+ return layoutAttributes
+}
+```
+
+
+在Collection View的每个单元格上,都会调用这个方法。这个方法做了如下工作:
+
+
+ 1. 创建一个UICollectionViewLayoutAttributes对象layoutAttributes,供IndexPath所指的单元格使用。
+ 2. 调用我们先前定义的getFrame方法设置layoutAttributes的frame,确保单元格对齐于书脊。
+ 3. 调用先前定义的getRatio方法算出单元格的打开度。
+ 4. 判断当前页是否位于正确的打开度范围之内。如果不,不显示该单元格。为了优化(也是为了符合常理),除了正面向上的书页,我们不应当显示书页的背面——书的封面例外,那个不管什么时候都需要显示。
+ 5. 应用旋转动画,使用前面算出的打开度。
+ 6. 判断是否是第一页,如果是,将它的zIndex放在其他页的上面,否则有可能出现画面闪烁的Bug。
+
+
+编译,运行。打开书,翻动每一页……呃?什么情况?
+
+
+
+书被错误地从中间装订了,而不是从书的侧边装订。
+
+
+
+
+如图中所示,每个书页的锚点默认是x轴和y轴的0.5倍处。现在你知道怎么做了吗?
+
+
+很显然,我们需要修改书页的锚点为它的侧边缘。如果这个书页是位于书的右边,则它的锚点应该是(0,0.5)。如果书页是位于书的左边,测锚点应该是(1,0.5)。
+
+
+打开BookePageCell.swift,添加如下代码:
+
+```
+override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes!) {
+ super.applyLayoutAttributes(layoutAttributes)
+ //1
+ if layoutAttributes.indexPath.item % 2 == 0 {
+ //2
+ layer.anchorPoint = CGPointMake(0, 0.5)
+ isRightPage = true
+ } else { //3
+ //4
+ layer.anchorPoint = CGPointMake(1, 0.5)
+ isRightPage = false
+ }
+ //5
+ self.updateShadowLayer()
+}
+```
+
+
+
+我们重写了applyLayoutAttributes(_:)方法,这个方法用于将BookLoayout创建的布局属性应用到第一个。
+上述代码非常简单:
+
+
+ 1. 检查当前单元格是否是偶数,也就是说书脊是否位于书页的左边。
+ 2. 如果是,将书页的锚点设置为单元格的左边缘,同时isRightPage设置为true。isRightPage变量可用于决定书页的圆角样式应当在那一边应用。
+ 3. 如果是奇数页,书脊应当位于书页的右边。
+ 4. 这只书页的锚点为单元格的右边缘,isRightPage设为false。
+ 5. 最后,设置当前书页的阴影层。
+
+
+编译,运行。翻动书页,这次的效果已经好了许多:
+
+
+
+
+本教程第一部分就到此为止了。你是不是觉得自己干了一件了不起的事情呢——效果做得很棒,不是吗?
+
+##接下来做什么
+
+第一部分的完整代码在此处下载:http://cdn1.raywenderlich.com/wp-content/uploads/2015/05/Part-1-Paper-Completed.zip
+
+
+我们从一个默认的Collection View布局开始,学习了如何定制自己的layout,并用它取得了令人叫绝的效果!用户在用这个App时,会觉得自己真的是在翻一本书。其实,将一个普通的电子阅读App变成一个让用户体验更加真实的带翻页效果的App,只需要做很小的改动。
+
+当然,我们还没有完成这个App。在本教程的第二部分,我们将使App变得更加完善和生动,我们将在打开书和关上书的一瞬间加入自定义动画。
+
+
+在开发App过程中,你也可能曾经有过一些关于布局的“疯狂”想法。如果你对本文有任何问题、意见或想法,请加入到下面的讨论中来!
+
+
+
diff --git "a/issue-19/\345\246\202\344\275\225\345\256\236\347\216\260iOS\345\233\276\344\271\246\345\212\250\347\224\273-\347\254\2542\351\203\250\345\210\206.md" "b/issue-19/\345\246\202\344\275\225\345\256\236\347\216\260iOS\345\233\276\344\271\246\345\212\250\347\224\273-\347\254\2542\351\203\250\345\210\206.md"
new file mode 100644
index 0000000..6f4abbc
--- /dev/null
+++ "b/issue-19/\345\246\202\344\275\225\345\256\236\347\216\260iOS\345\233\276\344\271\246\345\212\250\347\224\273-\347\254\2542\351\203\250\345\210\206.md"
@@ -0,0 +1,819 @@
+> * 原文链接 : [How to Create an iOS Book Open Animation: Part 2](http://www.raywenderlich.com/97690/how-to-create-an-ios-book-open-animation-part-2)
+* 原文作者 : [Vincent Ngo](http://www.raywenderlich.com/u/jomoka)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [kmyhy](https://github.com/kmyhy)
+* 校对者:[LastDay](http://lastday.github.io)
+* 状态:完成
+
+
+
+欢迎回到iOS图书动画系列教程!在第一部分,我们学习了如何创建两个自定义的collection view layout并在图书书页中使用了阴影图层以使我们的App显得更加立体和真实。
+在这一部分,我们将学习如何创建自定义的转场动画并通过捏放手势来打开一本书。
+
+
+
+
+> 注意:感谢[Attila Hegedüs](https://twitter.com/hegedus90)创建了本教程的示例程序。
+
+##开始
+
+本教程以前一部分的内容为基础。如果你还没有看过第一部分,或者想从一个新项目开始,你可以在[这里](http://cdn1.raywenderlich.com/wp-content/uploads/2015/05/Part-1-Paper-Completed.zip)下载上一部分教程中的完整示例程序。
+
+
+
+
+在Xcode中打开项目。现在,你可以选择一本书进行阅读,并从右边滑动进行翻页。这时的转场动画使用的是UINavigationController自带的动画效果。通过本教程的学习,我们将自定义这个动画效果,如下图所示:
+
+
+
+
+这个动画会在“打开书”和“合起书”两个状态之间一一种更加自然的方式平滑过渡,这将更能获得用户的欢心。让我们马上开始吧!
+
+##创建自定义的导航控制器
+
+要创建自定义的push动画和pop动画,我们必须创建自定义导航控制器并实现UINavigationControllerDelegate协议。
+在App文件夹上右击(或ctrl+左键)并点击New File。选择iOS\Source\Cocoa Touch Class模板并将类名设置为CustomNavigationController。让它继承自UINavigationController并将语言设置为Swift。点击Next,Create。
+打开CustomNavigationController.swift,编辑其内容为:
+
+```
+import UIKit
+
+class CustomNavigationController: UINavigationController, UINavigationControllerDelegate {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ //1
+ delegate = self
+ }
+
+ //2
+ func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
+ if operation == .Push {
+ return nil
+ }
+
+ if operation == .Pop {
+ return nil
+ }
+
+ return nil
+ }
+}
+```
+
+
+上述代码分别解释如下:
+
+
+
+1. 在 viewDidLoad 方法中,设置CustomNavigationController的delegate属性为它自己。
+2. navigationController(_:animationControllerForOperation:fromViewController:toViewController:) 方法属于UINavigationControllerDelegate协议。这个方法在两个View Controller之间发生push或pop导航时调用。你可以在这个方法中分别针对push导航和pop导航返回各自的Transition对象。目前我们都返回了nil,这表明我们将使用UINavigationController内置的标准Transition。稍后我们将替换为自己的Transition对象。
+
+
+
+现在我们拥有了自己的Navigation Controller类,接下来在故事板中将默认的UINavigationController替换为我们的CustomNavigationController。
+打开Main.storyboard,在故事板编辑器左侧的对象窗口中选择Navigation Controller对象,打开Identity窗口,在Custom Class下,将Class由UINavigationController修改为CustomNavigationController,如下图所示:
+
+
+
+
+编译运行,什么变化都没有发生。这是因为在委托方法中我们仍然返回了nil,因此使用的仍然是UINavigationController内置的标准Transition。
+
+##创建自定义导航动画
+
+
+
+最有趣的部分来了——创建我们的自定义Transition对象!:]
+要自定义Transition类,我们必须实现UIViewControllerAnimatedTransitioning协议,最主要的是这几个方法:
+
+ - transitionDuration: 必须实现。这个方法返回一个动画时长,使两个动画的播放时间相同(或不同)。
+ - animateTransition: 必须实现。这个方法负责提供参与动画的两个控制器:一个to控制器,一个from控制器。Transiton的大部分工作在这个方法中完成。
+ - animationEnded: 可选的方法。这个方法主要是用来通知你什么时候动画完成了。我们可以在这个方法中进行必要的清理动作。
+
+##创建自定义Transition类
+
+在App文件夹上右击(或ctrl+左键),然后点击New File。选择iOS\Source\Coca Touch Class 模板,将类名设置为BookOpeningTransition,继承NSObject,语言Swift。然后点击Next,Create。
+打开BookOpeningTransition.swift,编辑代码如下:
+
+```
+import UIKit
+
+//1
+class BookOpeningTransition: NSObject, UIViewControllerAnimatedTransitioning {
+
+ // MARK: Stored properties
+ var transforms = [UICollectionViewCell: CATransform3D]() //2
+ var toViewBackgroundColor: UIColor? //3
+ var isPush = true //4
+
+ //5
+ // MARK: UIViewControllerAnimatedTransitioning
+ func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
+ return 1
+ }
+
+ func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
+
+ }
+}
+```
+
+
+
+以上代码对应注释中的编号,分别解释如下:
+
+1. 声明 BookOpeningTransition 类实现UIViewControllerAnimatedTransitioning 协议。
+2. 声明一个 transforms 字典,键存储UICollectionViewCell,值则存储对应的CATransiform3D。这个字典保存在书打开后所有cell的翻页动画。
+3. 指定to视图控制器的背景色,它将让淡出淡入动画看起来更加清楚。
+4. 布尔值isPush 用于标识当前动画是Push动画还是Pop动画。
+5. 增加必须实现的 UIViewControllerAnimatedTransitioning 协议方法,以使编译错误不再出现,稍后我们会实现这些方法。
+
+
+
+定义好需要的变量,接下来就是实现协议方法。
+首先是transitionDuration(_:)方法:
+
+```
+if isPush {
+ return 1
+} else {
+ return 1
+}
+```
+
+
+transitionDuration(_:)方法返回了动画播放时长。这里,无论是Push动画还是Pop动画我们都设置为1秒。通过这个方法我们可以很方便地改变Push动画或Pop动画的时长。
+
+
+
+
+然后,是第二个协议方法——animateTransition——这是最核心的部分!:]我们将这个方法分成两部分来介绍:
+
+1. 实现一个助手方法,用于创建Push动画所需的Transition对象。
+2. 实现一个助手方法,用于创建Pop动画所需的Transition对象。
+
+
+##创建Push动画
+
+
+
+回想你在生活中打开一本书的样子:
+
+
+
+
+
+
+虽然看起来复杂,但我们只需要考虑两个状态,同时让UIView的animateWithDuration方法根据这两个状态进行不同的处理:
+
+1. 状态1,书处于合起状态。
+2. 状态2,书处于打开状态;这就是我们在第一部分教程中实现的部分。
+
+
+首先,在实现animateTransition(:_)协议方法之前,我们来实现几个助手方法。
+仍然在BookOpeningTransition.swift中,编写如下方法:
+
+```
+// MARK: Helper Methods
+func makePerspectiveTransform() -> CATransform3D {
+ var transform = CATransform3DIdentity
+ transform.m34 = 1.0 / -2000
+ return transform
+}
+```
+
+
+这个方法返回了一个Transform对象,并在z轴上增加了一点立体感。在播放动画时,我们将用到这个方法。
+
+
+###状态 1 - 合起书
+
+在makePerspectiveTransform方法后实现如下方法:
+
+func closePageCell(cell : BookPageCell) {
+ // 1
+ var transform = self.makePerspectiveTransform()
+ // 2
+ if cell.layer.anchorPoint.x == 0 {
+ // 3
+ transform = CATransform3DRotate(transform, CGFloat(0), 0, 1, 0)
+ // 4
+ transform = CATransform3DTranslate(transform, -0.7 * cell.layer.bounds.width / 2, 0, 0)
+ // 5
+ transform = CATransform3DScale(transform, 0.7, 0.7, 1)
+ }
+ // 6
+ else {
+ // 7
+ transform = CATransform3DRotate(transform, CGFloat(-M_PI), 0, 1, 0)
+ // 8
+ transform = CATransform3DTranslate(transform, 0.7 * cell.layer.bounds.width / 2, 0, 0)
+ // 9
+ transform = CATransform3DScale(transform, 0.7, 0.7, 1)
+ }
+
+ //10
+ cell.layer.transform = transform
+}
+
+回想一下BookViewController,它是一个CollectionView,代表了书中的一页。我们将每一页和书脊对齐,以y轴为心进行旋转实现翻页效果。首先,书是合起(关闭)的。这个方法将每个cell(即书页)放平并置于封面的下面。
+这是动画运行效果:
+
+
+
+
+
+
+
+以上代码解释如下:
+
+1. 用前面创建的助手方法,生成一个Transform对象。
+2. 判断cell是否是右侧页。
+3. 如果是,设置其角度为0,即放置为水平。
+4. 将它移动到封面下方并居中对齐。
+5. 将它缩放为70%。还记得我们前面将封面也缩放为70%吗?这里是同样的意思。
+6. 如果cell不是右侧页,则就是左侧页。
+7. 设置左侧页的角度为180度。要将它水平放置,我们需要将它翻到书脊的右边。
+8. 将它放到封面下方并居中对齐。
+9. 缩放70%。
+10. 最后,赋给cell的transform属性。
+
+
+在上面的方法后添加如下方法:
+
+```
+func setStartPositionForPush(fromVC: BooksViewController, toVC: BookViewController) {
+ // 1
+ toViewBackgroundColor = fromVC.collectionView?.backgroundColor
+ toVC.collectionView?.backgroundColor = nil
+
+ //2
+ fromVC.selectedCell()?.alpha = 0
+
+ //3
+ for cell in toVC.collectionView!.visibleCells() as! [BookPageCell] {
+ //4
+ transforms[cell] = cell.layer.transform
+ //5
+ closePageCell(cell)
+ cell.updateShadowLayer()
+ //6
+ if let indexPath = toVC.collectionView?.indexPathForCell(cell) {
+ if indexPath.row == 0 {
+ cell.shadowLayer.opacity = 0
+ }
+ }
+ }
+}
+```
+
+:
+
+- fromVC,类型为BooksViewController,用于滚动浏览图书列表。
+
+- toVC,BookViewController类型,让你可以翻阅选定的书。
+
+
+以上代码解释如下:
+
+1. 保存BooksViewController的Cellection View的背景色,然后设置BookViewController的Collection View的背景色为nil。
+2. 隐藏封面。现在toVC将负责处理封面图片的显示。
+3. 遍历书中所有书页。
+4. 保存每一页的当前Transform到transforms字典。
+5. 由于书一开始是合起的,我们将该页转换为合起状态,然后更新阴影图层。
+6. 最后,忽略封面图片的阴影。
+
+###状态 2 - 打开书
+
+现在,状态1的动画完成了,我们可以转移到状态2的处理中来。在这里我们将一本合起的书转换成一本打开的书。在setStartPositionForPush(_:toVC:)方法下添加如下方法:
+
+```
+func setEndPositionForPush(fromVC: BooksViewController, toVC: BookViewController) {
+ //1
+ for cell in fromVC.collectionView!.visibleCells() as! [BookCoverCell] {
+ cell.alpha = 0
+ }
+
+ //2
+ for cell in toVC.collectionView!.visibleCells() as! [BookPageCell] {
+ cell.layer.transform = transforms[cell]!
+ cell.updateShadowLayer(animated: true)
+ }
+}
+```
+
+
+
+
+上述代码解释如下:
+
+1. 隐藏所有书的封面,因为接下来我们要显示所选图书的内容。
+2. 在BookViewController中遍历书中每一页并读取先前保存在transform数组中的的Transform。
+
+
+
+
+在从BooksViewController导航到BookViewController后,我们还需要进行一些清理工作。
+在上面的方法之后加入如下方法:
+```
+func cleanupPush(fromVC: BooksViewController, toVC: BookViewController) {
+ // Add background back to pushed view controller
+ toVC.collectionView?.backgroundColor = toViewBackgroundColor
+}
+```
+
+
+在Push完成时,我们将BookViewController的Collection View的背景色设回原来保存的颜色,隐藏位于它下面的内容。
+
+###Implementing the Book Opening Transition
+###实现打开书的动画
+
+现在我们已经实现了助手方法,接下来要实现Push动画了!
+在空的animateTransition(_:)方法中加入以下代码:
+
+```
+//1
+let container = transitionContext.containerView()
+//2
+if isPush {
+ //3
+ let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! BooksViewController
+ let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! BookViewController
+ //4
+ container.addSubview(toVC.view)
+
+ // Perform transition
+ //5
+ self.setStartPositionForPush(fromVC, toVC: toVC)
+
+ UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.7, options: nil, animations: {
+ //6
+ self.setEndPositionForPush(fromVC, toVC: toVC)
+ }, completion: { finished in
+ //7
+ self.cleanupPush(fromVC, toVC: toVC)
+ //8
+ transitionContext.completeTransition(finished)
+ })
+} else {
+ //POP
+}
+```
+
+
+
+以上代码解释如下:
+
+1. 获取Container View,Container View在两个View Controller发生转场时充当父视图的角色。
+2. 判断当前的转场动作是否是一个Push动作。
+3. 如果是,分别获取fromVC(BooksViewController)和toVC(BookViewController)。
+4. 将toVC(BookViewController)加到Container View。
+5. 设定Push动作的起止点,即toVC和fromVC。
+6. 开始动画。从起始点(书合起的状态)转变到终点(书打开状态)。
+7. 执行清理动作。
+8. 告诉系统,转换完成。
+
+##在Navigation Controller中应用Push动画
+
+
+现在我们已经创建好Push动画,接下来就是将它应用到自定义的Navigation Controller中了。
+打开BooksViewController.swift在类声明中增加属性:
+```
+var transition: BookOpeningTransition?
+```
+
+transition属性用于保存Transition对象,通过它我们可以知道当前动画是Push动画还是Pop动画。
+然后在文件末尾的大括号之后加入一个扩展:
+
+```
+extension BooksViewController {
+func animationControllerForPresentController(vc: UIViewController) -> UIViewControllerAnimatedTransitioning? {
+ // 1
+ var transition = BookOpeningTransition()
+ // 2
+ transition.isPush = true
+ // 3
+ self.transition = transition
+ // 4
+ return transition
+ }
+}
+```
+
+
+通过扩展,我们将一部分代码分离出来。这里,我们将和转换动画有关的方法放到了一起。上面的这个方法创建并返回了一个Transition对象。
+以上代码解释如下:
+
+1. 创建一个新的Transition。
+2. 因为我们是要弹出或者Push一个Controller,所以将isPush设置为true。
+3. 保存当前Transition对象。
+4. 返回Transition对象。
+
+Now open CustomNavigationController.swift and replace the push if statement with the following:
+
+现在打开CustomNavigationController.swift并将Push的if语句替换为:
+
+```
+if operation == .Push {
+ if let vc = fromVC as? BooksViewController {
+ return vc.animationControllerForPresentController(toVC)
+ }
+}
+```
+
+
+上述语句判断当前Push的View Controller是不是一个BooksViewController,如果是,用我们创建的BookOpeningTransition呈现BookViewController。
+编译运行,选择某本书,你将看到书缓缓由合起状态打开:
+
+
+Uh..how come it’s not animating?
+呃...我们的动画效果呢?
+
+
+
+
+书直接从合起状态跳到了打开状态,原因在于我们没有加载cell(书页)!
+
+
+导航控制器从BooksViewController切换到BookViewController,这二者都是UICollecitonViewController。UICollectionViewCell没有在主线程中加载,因此代码一开始的时候以为cell的个数为0——这样当然不会有动画产生。
+
+
+我们需要让Collection View有足够的时间去加载所有的Cell。
+打开BooksViewController.swift将openBook(_:)方法替换为:
+
+```
+func openBook(book: Book?) {
+ let vc = storyboard?.instantiateViewControllerWithIdentifier("BookViewController") as! BookViewController
+ vc.book = selectedCell()?.book
+ //1
+ vc.view.snapshotViewAfterScreenUpdates(true)
+ //2
+ dispatch_async(dispatch_get_main_queue(), { () -> Void in
+ self.navigationController?.pushViewController(vc, animated: true)
+ return
+ })
+}
+```
+
+
+以上代码解释如下:
+
+1. 告诉BookViewController在动画一开始之前截屏。
+2. 将Push BookViewController的动作放到主线程中进行,这样就有时间去加载cell了。
+
+
+编译、运行,这次你将看到正确的Push动画了:
+
+
+
+
+
+这样看起来是不是好多啦?
+现在,关于Push动画的内容就到此结束,接下来,我们开始实现Pop动画。
+
+###实现Pop动画的助手方法
+
+一个View Controller的Pop动作刚好和Push相反。状态1是图书打开的状态,而状态2则变成了书合起的状态:
+
+
+Open up BookOpeningTransition.swift and add the following code:
+
+打开BookOpeningTransition.swift,加入以下方法:
+
+```
+// MARK: Pop methods
+func setStartPositionForPop(fromVC: BookViewController, toVC: BooksViewController) {
+ // Remove background from the pushed view controller
+ toViewBackgroundColor = fromVC.collectionView?.backgroundColor
+ fromVC.collectionView?.backgroundColor = nil
+}
+```
+
+
+setStartPositionForPop(_:toVC)方法仅仅是保存BookViewController的背景色并将BooksViewController的Collection View的背景色删除。注意,你不需要创建任何cell动画,因为书在这个时候是打开状态。
+
+
+接着,在上面的方法后面加入这个方法:
+
+```
+func setEndPositionForPop(fromVC: BookViewController, toVC: BooksViewController) {
+ //1
+ let coverCell = toVC.selectedCell()
+ //2
+ for cell in toVC.collectionView!.visibleCells() as! [BookCoverCell] {
+ if cell != coverCell {
+ cell.alpha = 1
+ }
+ }
+ //3
+ for cell in fromVC.collectionView!.visibleCells() as! [BookPageCell] {
+ closePageCell(cell)
+ }
+}
+```
+
+
+
+
+这个方法创建Pop动画的起止点,即从打开变成合起:
+
+1. 获取选择的书的封面。
+2. 在合起状态,在BooksViewController中遍历私有书的封面,然后对所有对象进行一个渐入效果。
+3. 在BookViewController中遍历当前图书的所有页,将所有cell转变成合起状态。
+
+
+
+现在新建如下方法:
+
+```
+func cleanupPop(fromVC: BookViewController, toVC: BooksViewController) {
+ // Add background back to pushed view controller
+ fromVC.collectionView?.backgroundColor = self.toViewBackgroundColor
+ // Unhide the original book cover
+ toVC.selectedCell()?.alpha = 1
+}
+```
+
+
+
+这个方法在Pop动画完成时执行清理动作:将BooksViewController的Collection View的背景色设回它开始的值并显示封面。
+
+
+
+在animateTransition(_:)方法里面,找到注释有“//POP”的else语句块,添加如下代码:
+
+```
+//1
+let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! BookViewController
+let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! BooksViewController
+
+//2
+container.insertSubview(toVC.view, belowSubview: fromVC.view)
+
+//3
+setStartPositionForPop(fromVC, toVC: toVC)
+UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: {
+ //4
+ self.setEndPositionForPop(fromVC, toVC: toVC)
+}, completion: { finished in
+ //5
+ self.cleanupPop(fromVC, toVC: toVC)
+ //6
+ transitionContext.completeTransition(finished)
+})
+```
+
+
+
+以上代码解释如下:
+
+1. 获取动画中涉及的两个ViewController。现在,fromVC 是BookViewController (打开状态),toVC是BooksViewController(合起状态)。
+2. 向Container View中加入BooksViewController(在BookViewContorller的下方)。
+3. setStartPositionForPop(_:toVC) 方法先保存背景色,再将背景色设为nil。
+4. 执行动画,即从打开状态切换到合起状态。
+5. 动画完成,执行清理动作。将背景色设回原来值,显示封面。
+6. 通知动画完成。
+
+
+##在Navigation Controller中应用Pop动画
+
+
+现在需要创建Pop动画,就如同我们在Push动画所做一样。
+打开BooksViewController.swift,在animationControllerForPresentController(_:)方法后增加如下方法:
+
+```
+func animationControllerForDismissController(vc: UIViewController) -> UIViewControllerAnimatedTransitioning? {
+ var transition = BookOpeningTransition()
+ transition.isPush = false
+ self.transition = transition
+ return transition
+}
+```
+
+这里,我们创建了一个新的BookOpeningTransition对象,但不同的是isPush设置为false。
+打开CustomNavigationController.swift,然后替换Pop部分的if语句为:
+
+```
+if operation == .Pop {
+ if let vc = toVC as? BooksViewController {
+ return vc.animationControllerForDismissController(vc)
+ }
+}
+```
+
+
+上述代码返回一个Transition对象,并执行Pop动画,合起书本。
+编译,运行程序,选择一本书,查看它的打开和合起。如下图所示:
+
+
+##Creating an Interactive Navigation Controller
+##创建互动式的Navigation Controller
+
+
+打开和合起动画搞定了——但我们还能更进一步!我们为什么不用一个更直观的捏放手势来打开和合起书本呢?
+打开BookOpeningTransition.swift,增加如下属性定义:
+
+```
+// MARK: Interaction Controller
+var interactionController: UIPercentDrivenInteractiveTransition?
+```
+
+Next open CustomNavigationController.swift and add the following code:
+然后打开CustomNavigationController.swift,加入下列代码:
+
+
+```
+func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
+ if let animationController = animationController as? BookOpeningTransition {
+ return animationController.interactionController
+ }
+ return nil
+}
+```
+
+
+在这个方法中,我们从BookOpeningTransition对象获得了一个interactionController。这样导航控制器能够跟踪动画进程以便用户可以用捏放手势打开和合起书。
+
+
+打开BooksViewController.swift,在trnasitoin变量下增加如下属性:
+
+```
+//1
+var interactionController: UIPercentDrivenInteractiveTransition?
+//2
+var recognizer: UIGestureRecognizer? {
+ didSet {
+ if let recognizer = recognizer {
+ collectionView?.addGestureRecognizer(recognizer)
+ }
+ }
+}
+```
+
+
+
+这两个属性的作用分别是:
+
+1. interactionController 是一个UIPercentDrivenInteractiveTransition类,它负责管理View Contorller之间转场的自定义动画。interactionController由一个Transition Animator生成,后者是一个实现了UIViewControllerAnimatorTransitioning协议的对象。而我们已经拥有了BookOpeningTransition——这就是一个实现了UIViewControllerAnimatorTransitioning的对象。interactionController能够控制Push动画和Pop动画之间的进度。关于这个类的更多内容,请参考Apple官方文档。
+2. recognizer 是一个UIGestureRecognizer。我们用这个手势识别器实现以捏放手势开合书本。
+
+Now add the following snippet under the transition.isPush = true line of your BooksViewController extension in animationControllerForPresentController(_:):
+
+在BooksViewController扩展的animationControllerForPresentController(_:)方法中,transition.isPush=true一行下面,加入代码:
+
+```
+transition.interactionController = interactionController
+```
+
+
+这句代码让CustomNavigationController知道要用哪个interaction controller。
+在animationControllerForDismissController(_:)方法中transition.isPush=false一行下面加入同样的代码:
+
+```
+transition.interactionController = interactionController
+```
+
+
+在viewDidLoad()方法中增加代码:
+
+```
+recognizer = UIPinchGestureRecognizer(target: self, action: "handlePinch:")
+```
+
+
+
+这里我们初始化了一个UIPinchGestureRecognizer,允许用户在做出捏放手势时调用handlePinch(_:)方法。
+在viewDidLoad()方法下面实现这个方法:
+
+```
+// MARK: Gesture recognizer action
+func handlePinch(recognizer: UIPinchGestureRecognizer) {
+ switch recognizer.state {
+ case .Began:
+ //1
+ interactionController = UIPercentDrivenInteractiveTransition()
+ //2
+ if recognizer.scale >= 1 {
+ //3
+ if recognizer.view == collectionView {
+ //4
+ var book = self.selectedCell()?.book
+ //5
+ self.openBook(book)
+ }
+ //6
+ } else {
+ //7
+ navigationController?.popViewControllerAnimated(true)
+ }
+ case .Changed:
+ //8
+ if transition!.isPush {
+ //9
+ var progress = min(max(abs((recognizer.scale - 1)) / 5, 0), 1)
+ //10
+ interactionController?.updateInteractiveTransition(progress)
+ //11
+ } else {
+ //12
+ var progress = min(max(abs((1 - recognizer.scale)), 0), 1)
+ //13
+ interactionController?.updateInteractiveTransition(progress)
+ }
+ case .Ended:
+ //14
+ interactionController?.finishInteractiveTransition()
+ //15
+ interactionController = nil
+ default:
+ break
+ }
+}
+```
+
+
+对于UIPinchGestureRecognizer,我们要关注这3个状态:开始状态,这让你知道捏放手势何时开始;改变状态,检测捏放手势的变化;结束状态,让你知道捏放手势何时结束。
+handlePinch(_:)方法代码解释如下:
+
+
+**开始状态**
+1. 创建一个UIPercentDrivenInteractiveTransition 对象。
+2. scale取决于捏合点之间的距离,判断scale值是否大于或者等于1。
+3. 如果是,判断相关的View是否是一个Collection View。
+4. 获取正在被捏合的书。
+5. 执行Push BookViewController的动画,显示书本中的书页。
+6. 如果 scale 小于 1…
+7. …执行Pop BookViewController的动画,显示封面
+**改变状态 – 捏合过程中**
+8. 判断当前是否是Push动画。
+9. 如果正在Push一个BookViewConroller,计算捏放手势的进度。该进度必然是0-1之间的数字。我们将原始值除以5以让用户拥有更好的控制感。否则用双指打开的手势打开一本书时,会突然跳到打开状态。
+10. 基于我们计算的进度,更新动画进度。
+11. 如果当前不是Push动画,则它应该是Pop动画。
+12. 当双指捏合合起一本书时,scale值必然是从1慢慢变到0。
+13. 最后, 更新动画进度。
+**结束状态 – 手势终止**
+14. 告诉系统,用户交互式动画完成。
+15.将interaction controller 设置为 nil。
+
+
+最后,我们需要实现“捏合以合起书本”的状态。当然,我们必须将手势识别器传递给BookViewController以便它会Pop。
+打开BookViewController.swift,在book变量声明下增加一个属性:
+
+```
+var recognizer: UIGestureRecognizer? {
+ didSet {
+ if let recognizer = recognizer {
+ collectionView?.addGestureRecognizer(recognizer)
+ }
+ }
+}
+```
+
+当我们将手势识别器传递给BookViewController时,它会被添加到Collection View,因此我们可以跟踪到用户的“关书”手势。
+然后需要在BooksViewController和BookViewController之间传递手势识别器。
+打开BookOpeningTransition.swift。在cleanUpPush(_:toVC)方法中,在设置背景色之后添加如下代码:
+
+```
+// Pass the gesture recognizer
+toVC.recognizer = fromVC.recognizer
+```
+
+
+
+当我们从BooksViewController Push到BookViewController时,将捏放手势传递给BookViewController。这会导致捏放手势自动添加到Collection View中。
+当我们从BookViewController Pop回BooksViewController时,我们必须将捏放手势又传递回去。
+在cleanUpPop(_:toVC)方法中,在我设置背景色之后添加如下代码:
+
+```
+// Pass the gesture recognizer
+toVC.recognizer = fromVC.recognizer
+```
+
+
+编译、运行程序,选择一本书,用捏放手势打开和合起书:
+
+
+
+捏放手势是一种天然就适合用于对书本进行“开关”的手势;它让我们的界面显得更加简单。我们不再需要导航栏的Back按钮——因此我们决定去掉它。
+打开Main.storyboard,选择Custom Navigation View Controller,打开属性面板,在Navigation Controller一栏下面,取消Bar Visibility选项,如下所示:
+
+
+Build and run your app again:
+
+再次编译运行程序:
+
+
+Much cleaner! :]
+
+##接下来做什么
+
+你可以在[这里](http://cdn3.raywenderlich.com/wp-content/uploads/2015/05/Part-2-Paper-Completed_Final.zip)下载到上面所有步骤完成后的最终项目。
+在本教程中,我们学习如何对Collection View进行自定义布局,让App的用户体验更加自然、也更加有趣。我们还创建了自定义动画,使用智能交互让用户以捏放手势开合一本书。这个App在实现了所有基本功能的同时,让程序显得更加的人性化和与众不同。
+
+
+相比较之下,是默认的“淡入/淡出”动画更简单一些。它能节省你一部分开发时间。但是杰出的应用程序都应当有一些自己特有的地方,从而使它们能够脱颖而出。
+要知道,每个人都喜欢记住那些用起来非常有趣的App,在UI上能让人感到兴奋而同时又没有牺牲功能的App。
+
+
+希望你能喜欢本教程,再次感谢[Attila Hegedüs](https://twitter.com/hegedus90)提供了这个教程的示例项目。
+如果对本教程有任何问题,请加入到下面的讨论中来!
\ No newline at end of file
diff --git "a/issue-2/\350\207\252\345\256\232\344\271\211ViewController\345\210\207\346\215\242\346\225\210\346\236\234\344\270\216\345\212\250\347\224\273.md" "b/issue-2/\350\207\252\345\256\232\344\271\211ViewController\345\210\207\346\215\242\346\225\210\346\236\234\344\270\216\345\212\250\347\224\273.md"
index 16f0608..fbea54b 100644
--- "a/issue-2/\350\207\252\345\256\232\344\271\211ViewController\345\210\207\346\215\242\346\225\210\346\236\234\344\270\216\345\212\250\347\224\273.md"
+++ "b/issue-2/\350\207\252\345\256\232\344\271\211ViewController\345\210\207\346\215\242\346\225\210\346\236\234\344\270\216\345\212\250\347\224\273.md"
@@ -40,7 +40,7 @@ class CustomPresentAnimationController: NSObject, UIViewControllerAnimatedTransi
`UIViewControllerAnimatedTransitioning`协议中有两个我们接下来需要添加的方法。将以下方法添加到类中。
```
-func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
+func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 2.5
}
@@ -52,7 +52,7 @@ func animateTransition(transitionContext: UIViewControllerContextTransitioning)
let containerView = transitionContext.containerView()
let bounds = UIScreen.mainScreen().bounds
toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height)
- containerView.addSubview(toViewController.view)
+ containerView?.addSubview(toViewController.view)
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .CurveLinear, animations: {
fromViewController.view.alpha = 0.5
@@ -104,7 +104,7 @@ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
将下列`UIViewControllerTransitioningDelegate`方法添加到类中。这一步会返回我们的自定义animation controller实例。
```
-func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
+func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return customPresentAnimationController
}
```
@@ -146,7 +146,7 @@ class CustomDismissAnimationController: NSObject, UIViewControllerAnimatedTransi
将以下代码添加到类中。
```
-func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
+func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 2
}
@@ -157,8 +157,8 @@ func animateTransition(transitionContext: UIViewControllerContextTransitioning)
let containerView = transitionContext.containerView()
toViewController.view.frame = finalFrameForVC
toViewController.view.alpha = 0.5
- containerView.addSubview(toViewController.view)
- containerView.sendSubviewToBack(toViewController.view)
+ containerView?.addSubview(toViewController.view)
+ containerView?.sendSubviewToBack(toViewController.view)
UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
fromViewController.view.frame = CGRectInset(fromViewController.view.frame, fromViewController.view.frame.size.width / 2, fromViewController.view.frame.size.height / 2)
@@ -183,7 +183,7 @@ let customDismissAnimationController = CustomDismissAnimationController()
添加以下功能至类中。
```
-func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
+func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return customDismissAnimationController
}
```
@@ -208,12 +208,12 @@ func animateTransition(transitionContext: UIViewControllerContextTransitioning)
let containerView = transitionContext.containerView()
toViewController.view.frame = finalFrameForVC
toViewController.view.alpha = 0.5
- containerView.addSubview(toViewController.view)
- containerView.sendSubviewToBack(toViewController.view)
+ containerView?.addSubview(toViewController.view)
+ containerView?.sendSubviewToBack(toViewController.view)
let snapshotView = fromViewController.view.snapshotViewAfterScreenUpdates(false)
snapshotView.frame = fromViewController.view.frame
- containerView.addSubview(snapshotView)
+ containerView?.addSubview(snapshotView)
fromViewController.view.removeFromSuperview()
@@ -255,7 +255,7 @@ class CustomNavigationAnimationController: NSObject, UIViewControllerAnimatedTra
```
var reverse: Bool = false
-func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
+func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 1.5
}
@@ -278,7 +278,7 @@ func animateTransition(transitionContext: UIViewControllerContextTransitioning)
containerView.transform = CGAffineTransformMakeTranslation(direction * containerView.frame.size.width / 2.0, 0)
toView.layer.transform = viewToTransform
- containerView.addSubview(toView)
+ containerView?.addSubview(toView)
UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
containerView.transform = CGAffineTransformMakeTranslation(-direction * containerView.frame.size.width / 2.0, 0)
@@ -286,7 +286,7 @@ func animateTransition(transitionContext: UIViewControllerContextTransitioning)
toView.layer.transform = CATransform3DIdentity
}, completion: {
finished in
- containerView.transform = CGAffineTransformIdentity
+ containerView?.transform = CGAffineTransformIdentity
fromView.layer.transform = CATransform3DIdentity
toView.layer.transform = CATransform3DIdentity
fromView.layer.anchorPoint = CGPointMake(0.5, 0.5)
@@ -330,7 +330,7 @@ navigationController?.delegate = self
然后将以下代码添加到类中。
```
-func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
+func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
customNavigationAnimationController.reverse = operation == .Pop
return customNavigationAnimationController
}
@@ -431,7 +431,7 @@ if operation == .Push {
然后添加以下代码到类中。
```
-func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
+func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return customInteractionController.transitionInProgress ? customInteractionController : nil
}
```
diff --git "a/issue-20/UIStackView\346\225\231\347\250\213\357\274\232\344\272\206\350\247\243Stack View.md" "b/issue-20/UIStackView\346\225\231\347\250\213\357\274\232\344\272\206\350\247\243Stack View.md"
new file mode 100644
index 0000000..05f9144
--- /dev/null
+++ "b/issue-20/UIStackView\346\225\231\347\250\213\357\274\232\344\272\206\350\247\243Stack View.md"
@@ -0,0 +1,519 @@
+> * 原文链接 : [UIStackView Tutorial: Introducing Stack Views](http://www.raywenderlich.com/114552/uistackview-tutorial-introducing-stack-views)
+* 原文作者 : [ Jawwad Ahmad](http://www.raywenderlich.com/u/jawwad)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [kmyhy](https://github.com/kmyhy)
+
+
+> Ray注:恭喜你!为了推广《iOS 9 Feast》一书,这本书的第一部分教程已经为你解锁了。本文来自于《iOS 9 Tutorials》中的一个章节,是该章节的简化版,通过本文,便可对全书内容窥见一斑。祝你阅读愉快!
+
+家家有本难念的经。现在有一个新需求,要你在运行时增加或删除一个视图,这就需要我们重新调整邻近视图的位置。
+
+你会怎么办?也许你会在故事板中新建一些布局约束连接,以便能够安装或卸载其中的一些约束?或者你会使用第三方库来实现?或者看任务复杂程度完全用代码实现?
+
+也许这个在视图附近的视图树中的所有View都不需要在运行时改变,但当你将新视图添加到故事板时,仍然要想方设法为它挤出空间来。
+
+你是否有过这样的时候:因为觉得要解决的布局约束冲突太过棘手,最终不得不清除所有约束并一条条地重新添加约束。
+
+通过对UIStackView的介绍,完成上述任务变得微不足道。Stack View提供了一个对多个视图进行水平或垂直布局的方法。通过对几个属性进行简单设置,比如对齐、分布和间距,可以让我们让其所包含的视图适应于其有效空间。
+
+> 注意:UIStackView教程假设你基本掌握了自动布局。如果你还不熟悉自动布局,请参考[“自动布局教程:第1部分”](http://www.raywenderlich.com/83129/beginning-auto-layout-tutorial-swift-part-1)。
+
+##开始
+
+在本教程,你将创建一个名为Vacation Spots的App。这个App非常简单,仅仅显示了一个能让你原离一切尘嚣的景点列表。
+
+先不要忙着打点行装,因为这个App还有几个问题需要你用Stack View来进行解决,当然,这比你光用自动布局来解决要简单多了。
+
+首先下载本教程的[开始项目](http://cdn5.raywenderlich.com/wp-content/uploads/2015/09/VacationSpots_Starter.zip),然后在iPhone6S 上运行。
+
+
+
+点击London单元格,跳转到伦敦的介绍页面。
+
+一瞥之下,这个页面似乎是OK的,但它其实存在着这几方面的问题。
+
+ 查看视图下方的那排按钮。它们当前的位置是以固定的间距排列的,因此它们无法适应屏幕宽度的变化。要看出问题之所在,你只需按下command+左箭头旋转模拟器屏幕为横屏即可。
+
+
+
+ 点击WEATHER旁边的Hide按钮。它成功地隐藏了文本,但它下面的内容却保持在原来的位置,从而留下了一大块的空白区域。
+
+
+
+ 各版块的顺序也应当调整。如果将"what to see"版块放到"why visit"版块的右边,而不是将"wheather"版块插在两者之间会更好。
+
+ 在横屏模式下,底部的一排按钮太靠下了。如果在横屏模式下,将各版块之间的距离缩小一点会更好。
+
+现在,你已经了解了将要做的工作,是该进入项目中进行工作的时候了。
+
+打开Main.storyboard,找到Spot Info View Controller这个Scene。哇!在你的Stack View上有很多颜色!
+
+
+
+将这些标签和按钮设为不同的背景色,是为了在运行时效果更直观。就是在故事板中,这也有助于看到Stack View属性的改变导致其内部视图的变化。
+
+如果你想在运行App时看见这些颜色,你可以在SpotInfoViewController的viewDidLoad()方法中将下列语句注释。当然现在,还不需要这样做:
+
+```
+// Clear background colors from labels and buttons
+// 清空标签和按钮的背景色
+for view in backgroundColoredViews {
+ view.backgroundColor = UIColor.clearColor()
+}
+```
+
+
+同时,所有已连接Outlet的标签都会有一个提示文本,用于指明它们所连接的Outlet变量名。这将在运行时指明是哪个标签。例如,带有字样的标签将连接到下面这个Outlet变量:
+
+```
+@IBOutlet weak var whyVisitLabel: UILabel!
+```
+
+还要注意另外一点,故事板中Scene的大小并不是默认的启用了Size类时的600x600。
+
+Size类仍然可用,但在初始导航控制器的属性面板中,Simulated Metrics下的Size属性却被设置成了iPhone 4-inch。这仅仅是为了让我们更容易使用故事板一点,Simulated Metrics属性在运行时并没有任何影响——不同设备上视图的大小仍然会自动改变。
+
+
+
+##第一个Stack View
+
+我们首先要做的是底下一排按钮之间的间距。一个Stack View能够将它所含的View以各种方式沿其轴向进行分布,同时也可以将View沿某个方向等距分布。
+
+幸运的是,将已有的View添加到一个新的Stack View中并不是一件多复杂的事情。首先,用Command+左键同时选中Spot Info View Controller底下一排的所有按钮:
+
+
+
+如果Outline视图未打开,先点击故事板画布左下角的Show Document Outline按钮,打开Outline视图:
+
+
+
+在Outline视图中查看是否3个按钮都已经选中:
+
+
+
+如果没有全部选中,你仍然可以用Command+左键在Outline视图中重新选择它们。
+
+一旦选好这3个按钮,找到故事板画布左下角Auto Layout工具栏中新增的Stack按钮并点击它:
+
+
+
+这些按钮将被嵌到一个新的Stack View中:
+
+
+
+3个按钮现在一个挤着一个——你将很快解决这个问题。
+
+虽然Stack View会管理这些按钮的位置,但它自身仍然需要我们为它添加自动布局约束。
+
+当你将View添加到Stack View中时,任何相对于其他视图的约束都被删除。以之前添加的按钮为例,Submit Rating按钮上方有一个相对于Rating标签底部的垂直间距约束:
+
+
+
+现在点击Submit Rating 按钮,可以看到它已经丢失了所有约束:
+
+
+
+另一种检验这些约束已经删除的方法是用Size面板进行查看(⌥⌘5):
+
+
+
+为了给Stack View添加布局约束,首先需要选中它。要在故事板选取一个充满了子视图的Stack View还是比较难的。
+
+一个简单的法子是在Outline视图中选取Stack View:
+
+
+
+另一个技巧是在Stack View 的任意地方按下Shift+右键或者Control+Shift+左键(如果你正在用触控板的话)。这时将弹出一个上下文菜单,列出了位于所点击的地方的View树,你可以在这个菜单中选择Stack View。
+
+现在,我们用Shift+右键的方式选取Stack View:
+
+
+
+然后点击自动布局工具栏中的Pin按钮,添加一个约束:
+
+
+
+首先勾选Constrain to margins。然后在Stack View四周添加下列约束:
+
+```
+Top: 20, Leading: 0, Trailing: 0, Bottom: 0
+```
+
+仔细检查top、leading、trailing、bottom中的数字并确保它们的I型柱都被选中。然后点击Add 4 Constraints:
+
+
+
+现在Stack View的尺寸正确了,但它会拉伸第一个按钮的大小使其填充所有剩余空间:
+
+
+
+决定Stack View如何将它的subview沿轴向分布的属性是它的distribution属性。当前,这个属性的值是Fill,这表示subview会沿轴向完全占据Stack View。因此,Stack View会拉伸其中一个subview使其填充剩余空间,尤其是水平内容优先级最低的那个,如果所有subview优先级相同,则拉伸第一个subview。
+
+当然,你并不希望按钮将整个Stack View都填充——你想让它们等间距分布。
+
+确保Stack View处于选中状态,打开属性面板。将Distribution属性由Fill修改为Equal Spacing:
+
+
+
+编译运行,点击某个单元格,旋转模拟器(⌘→)。你将看到最下一排按钮现在按照等间距排列了!
+
+
+
+如果不使用Stack View,则解决这个问题只能使用空白的View来分隔这些按钮,在按钮之间摆放上一些用于分隔空间的 Spacer View。所有的Spacer View都要添加等宽约束,以及许多额外的约束,才能将这些Spacer View布局正确。
+
+这看起来如下图所示。为了直观起见,这些Spacer View的背景色设置成了浅灰色:
+
+
+
+一旦你不得不在故事板中这样做的时候,就会导致出许多问题,尤其是许多视图都是动态的时候。如果要在运行时添加一个按钮或者隐藏/删除一个按钮时,要想调整这些Spacer View和约束真的是不太容易。
+
+要隐藏Stack View中的视图,你只需要设置该View的Hidden属性为true,剩下的工作Stack View会自己完成。这也是我们解决用户隐藏WEATHER标签下文本的主要思路。在本文后面的内容中,当你将weather标签添加到Stack View之后,我们再来介绍这个问题。
+
+##将各版块转换到Stack View
+
+我们将把SpotInfoViewController中的所有版块都添加到Stack View中。这将有助于我们完成剩下的任务。接下来我们先调整Rating版块。
+
+###Rating版块
+
+就在前面创建的Stack View上方,选取RATING标签,以及旁边的显示为几个星形图标的标签:
+
+
+
+然后点击Stack按钮将它们嵌到一个Stack View中:
+
+
+
+然后点击Pin按钮。勾选Constrain to margins,并添加如下约束:
+
+```
+Top: 20, Leading: 0, Bottom: 20
+```
+
+
+
+打开属性面板,将间距设置为8:
+
+
+
+你可能会看到一个 Misplaced Views的布局约束警告,同时星星标签会显示将会被拉伸到视图之外:
+
+
+
+有时候Xcode会临时提示一些警告,或者显示Stack View的位置不正确,这些警告会在你添加其他约束后消失。你完全可以忽略这些警告。
+
+要解决这个警告,我们可以修改一下Stack View的Frame然后又改回,或者临时修改它的一条布局约束。
+
+让我们试一下。先将Alignment 属性从Fill修改为Top,然后又改回原来的Fill。你将看到这下星星标签显示正常了:
+
+
+
+编译运行,进行测试。
+###解散一个Stack View
+在继续后面的内容之前,先接受一些“急救”训练。有时你发现一些Stack View不再需要了,可能因为过时了,代码进行了重构或者仅仅是突发奇想。
+
+幸运的是,有一种简单的方法可以解散一个Stack View。
+
+首先,选定想解散的Stack View。按下Option键,点击Stack 按钮。这将弹出一个上下文菜单,然后点击Unembed:
+
+
+
+另一种解散的方法是选中Stack View,然后点击Editor\Unemebed菜单。
+
+###垂直的Stack View
+
+接下来,你将创建一个垂直的Stack View。选中WHY VISIT标签及下面的标签:
+
+
+
+Xcode会自动根据这两者的位置推断出这将是一个垂直的Stack View。点击Stack 按钮将二者嵌到一个Stack View:
+
+
+
+下面的那个标签原先有一个right margin of the view的约束,但嵌到Stack View之后被移除了。现在,这个Stack View还没有约束,因此它自动适应了两个标签中的最宽的一个的宽度。
+
+选中Stack View,点击Pin按钮。勾选Constrain to margins,设置Top、Leading、Trainling为0。
+
+然后,点击Bottom右边的下拉按钮,从列表中选择WEATHER(curent distance =20):
+
+
+
+默认,约束是相对于距离最近的对象,对于Bottom约束来说就是距离它15像素的Hide按钮。但我们其实是想让约束相对于WEATHER标签。
+
+最后点击Add 4 Constraints按钮。显示结果如下图所示:
+
+
+
+现在,我们有了第二个Stack View,它的右边对齐于View的右边。但是底下的标签仍然是原来的宽度。你接下来要修改Stack View的alignment属性以解决这个问题。
+
+###Alignment属性
+
+Alignment属性决定了Stack View如何沿它轴向的垂直方向摆放它的subview。对于一个垂直的Stack View,这个属性可以设置为Fill、Leading、Center和Trailing。
+
+对于水平的Stack View,这个属性则稍有不同:
+
+
+
+.Top取代了.Leading,.Bottom取代了.Trailing。此外,水平Stack View还多出了两个属性值:.FirstBaseLine和.LastBaseLine。
+
+这是对于垂直Stack View,将Alignment设置为不同属性值所造成的显示效果:
+
+Fill:
+
+
+
+Leading:
+
+
+
+Center:
+
+
+
+Trailing:
+
+
+
+当你测试完所有Alignment值的布局效果后,将Alignment修改为Fill:
+
+
+
+然后编译运行,测试是否正常。
+
+将Alignment设置为Fill,表示所有View将沿与Stack View轴向垂直的方向进行全占式分布。这会让WHY VISIT标签扩展它的宽度到100%.
+
+如果我们只想让底下的标签将宽度扩展到100%怎么办?
+
+这个问题现在看来还不是多大的问题,因为两个标签在运行时的背景色都是透明的。但对于Weather版块来说就不同了。
+
+我们将用另外一个Stack View来说明这个问题。
+
+##将"What to see"换成Stack View
+
+这个版块和前面一个版块类似,因此我们会简述。
+
+ 1.首先,选中 WHAT TO SEE标签和标签。
+
+ 2.点击Stack按钮。
+
+ 3.点击Pin 按钮。
+
+ 4.勾选Constrain to margins,添加4个约束:
+
+
+ Top: 20, Leading: 0, Trailing: 0, Bottom: 20
+
+ 5.将Stack View的Alignment设置为Fill。
+
+现在你的故事板看起来像这样:
+
+
+
+编译运行,测试是否有任何改变。
+
+现在就只剩下weather版块了。
+
+##将Weather版块转换成Stack View
+
+Weather版块相对复杂一些,因为它多了一个Hide按钮。
+
+一种方法是使用嵌套的Stack View,先将WEATHER标签和Hide按钮嵌到一个水平StackView,再将这个Stack View和标签嵌到一个垂直Stack View。
+
+看起来是这个样子:
+
+
+
+注意,WEATHER标签被拉伸为和Hide按钮一样高了。这并不合适,因为这会导致WEATHER标签和下面的文本之间多出了一些空间。
+
+注意Alignment属性负责Stack View轴向垂直的方向上的布局。所以,我们需要将Alignment属性设置为 Bottom:
+
+
+
+但我们并不想让Hide按钮的高度来决定Stack View的高度。
+
+正确的方法是让Hide 按钮不要和Weather 版块呆在同一个Stack View中,或者任何别的Stack View中。
+
+这样,在顶层View中还会保留一个subview,你将为它添加一个相对于WEATHER标签的约束——WEATHER标签嵌在Stack View里的。也就是说,你要为位于Stack View之外的按钮加一个约束,这个约束是相对于Stack View内的一个标签!
+
+选中WEATHER标签和标签:
+
+
+
+点击 Stack 按钮:
+
+
+
+点击Pin 按钮,勾上Constrain to margins,然后添加如下约束:
+
+```
+Top: 20, Leading: 0, Trailing: 0, Bottom: 20
+```
+
+将Stack View的Alignment设为Fill :
+
+
+
+我们需要在 Hide 按钮左边和WEATHER标签右边加一条约束,这样WEATHER 标签的宽度就不会拉满整个Stack View了。
+
+当然,底下的标签宽度还是需要100%占满的。
+
+我们是通过将WEATHER标签嵌到一个垂直Stack View 来实现的。注意,垂直Stack View的Alignment 属性可以设置为 .Leading,如果将Stack View拉宽,则它里面的View 会保持左对齐。
+
+从Outline视图中选取WEATHER 标签,或者用Control+Shift+左键的方式选取WEATHER 标签:
+
+
+
+然后点击Stack 按钮:
+
+
+
+确保Axis 为 Vertical 的情况下,将Alignment 设置为 Leading:
+
+
+
+好极了!这样外面的Stack View 会将内部的 Stack View 拉伸为完全填充,而内部的 Stack View 则允许标签保持原有宽度!
+
+编译运行。这究竟是为什么?Hide按钮跑到了文字中间去了?
+
+
+
+这是因为WEATHER标签是嵌在 Stack View 中的,任何它和 Hide 按钮之间的约束都被删除了。
+
+从Hide 按钮用右键拖一条新的约束到 WEATHER 标签:
+
+
+
+按下Shift键,同时选择Horizontal Spacing 和 Baseline。然后点击 Add Constraints:
+
+
+
+编译运行。Hide 按钮的位置现在对了,而且当按下Hide 按钮,位于Stack View 中的标签被隐藏后,下面的视图也会被调整——根本不需要我们进行手动调整。
+
+
+
+选择是有的版块都在自己的 Stack View中,我们将在它们外边在套上一个 Stack View,这样最后的两个任务就会变得很轻松。
+
+##顶级 Stack View
+
+在Outline 视图中,用Command+左键选择5个最顶级的 Stack View:
+
+
+
+然后点击 Stack 按钮:
+
+
+
+点击Pin 按钮,勾上 Constrain to margins,将 4 个边的约束都设为0。然后将Spacing 设置为20,Alignment 设为 Fill。现在故事板会是这个样子:
+
+
+
+编译运行:
+
+
+
+噢!这个 Hide 按钮又失去了它 的约束!因为包含 WEATHER 标签的Stack View的外边又套了一层 Stack View。这不是什么大问题,就像之前你做过的那样,再重新为它添加约束就是了。
+
+右键从Hide 按钮拖一条约束到 WEATHER标签,按下 Shift 键,同时选择 Horizontal Spacing 和 Baseline。然后点击 Add Constraints:
+
+
+
+编译运行。Hide按钮的位置正确了。
+
+###重新调整视图位置
+现在,所有的版块都被嵌到一个顶级的 Stack View中了,我们想修改一下 what to see版块的位置,让它位于 weather 版块之后。
+
+从 Outline 视图中选择中间的的 Stack View,然后将它拖到第一、二个 Stack View 之间。
+
+> Note: Keep the pointer slightly to the left of the stack views that
+> you’re dragging it between so that it remains a subview of the outer
+> stack view. The little blue circle should be positioned at the left
+> edge between the two stack views and not at the right edge:
+
+>
+
+
+现在,weather版块是从上到下的第三个版块,由于 Hide 按钮它并不是 Stack View的subview,所以它不会参与移动,它的frame当前是不正确的。
+
+点击 Hide 按钮,选中它:
+
+
+
+然后点击自动布局工具栏中的 Resolve Auto Layout Issues 按钮,选择 Update Frames:
+
+
+
+现在 Hide 按钮将回到正确的位置:
+
+
+
+好,对于你来说,用自动布局调整视图的位置或者重新添加约束都不再是什么问题了,但为什么还是有一种不太完美的感觉呢?
+
+###基于配置的 Size 类
+
+最后还有一个任务没有完成。在横屏模式,垂直空间是比较珍贵的,你想将这些版块之间靠得更近一些。要实现这个,你需要判断当垂直Size类为compact时,将顶层 Stack View的 Spacing属性由 20 改成 10.
+
+选择顶层 Stack View,点击 Spacing 前面的 + 按钮:
+
+
+
+选择 Any Width > Compact Height:
+
+
+
+在新出现的 wAny hC 一栏中,将 Spacing 设为 10:
+
+
+
+编译运行。在竖屏模式下Spacing不会改变。旋转模拟器(⌘←),你会看到各版块之间的间距减少了,现在底部按钮之间的空间也变大了:
+
+
+
+如果你没有添加最外层的 Stack View,你仍然可以使用 Size 类将每个版块之间的垂直间距设置为 10,但这就不是仅仅设置一个地方就能够办到的了。
+
+剩下的时间将会有更加愉快的事情发生,那就是动画!
+
+##动画
+
+现在,在隐藏和显示天气信息时仍然会觉得有一些突兀。你将增加一个动画使这个转换变得更平滑。
+
+Stack View完全支持 UIView 动画。也就是说要以动画方式显示/隐藏它所包含的subview,只需要简单地在一个动画块中切换它的 hidden 属性。
+
+让我们来看看代码怎么实现。打开 SpotInfoViewController.swift,找到
+updateWeatherInfoViews(hideWeatherInfo:animated:)方法。
+
+将方法的最后一行:
+
+```
+weatherInfoLabel.hidden = shouldHideWeatherInfo
+```
+
+替换为:
+
+```
+if animated {
+ UIView.animateWithDuration(0.3) {
+ self.weatherInfoLabel.hidden = shouldHideWeatherInfo
+ }
+} else {
+ weatherInfoLabel.hidden = shouldHideWeatherInfo
+}
+```
+
+编译运行,点击Hide 按钮或 Show 按钮。是不是加入动画之后看起来要好得多呢?
+
+除了对 Stack View 中的视图以动画的方式设置 hidden 属性,你也可以对 Stack View 自身的属性使用 UIView 动画,例如 Alignment 属性、 Distribution 属性、 Spacing 属性和 Axis 属性。
+
+##接下来做什么
+
+你可以从[这里](http://cdn1.raywenderlich.com/wp-content/uploads/2015/09/VacationSpots_Complete.zip)下载完整的示例项目。
+
+在这篇 UIStackView 教程里,你学习了什么是 Stack View,以及它的各个属性,通过这些属性可以对它的 subview 进行布局。Stack View 是高度可配置的,要达到同样的效果往往不止一种实现方式。
+
+最好的学习方式是自己修改各种属性并体验最终效果。不要一个属性一个属性地单独测试,而是要试验各种属性相互搭配后的布局效果。
+
+本教程是"iOS 9 Tutorials"第6章"UIStackView and Auto Layout changes",以及第7章"Intermediate UIStackView"的精简版。如果你想学习更多关于 UIStackView以及其它 iOS9 的新特性,请看这本书!
+
+同时,如果你对本文有任何问题和建议,请加入到下面的讨论中来!
+
+
+
+
diff --git "a/issue-9/Swift2-\346\234\211\345\223\252\344\272\233\346\226\260\347\211\271\346\200\247.md" "b/issue-9/Swift2-\346\234\211\345\223\252\344\272\233\346\226\260\347\211\271\346\200\247.md"
new file mode 100644
index 0000000..110206d
--- /dev/null
+++ "b/issue-9/Swift2-\346\234\211\345\223\252\344\272\233\346\226\260\347\211\271\346\200\247.md"
@@ -0,0 +1,168 @@
+Swift 2 有哪些新特性
+---
+
+> * 原文链接 : [What’s New in Swift 2](http://www.raywenderlich.com/108522/whats-new-in-swift-2)
+* 原文作者 : [Greg Heo](http://www.raywenderlich.com/u/gregheo)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [Sam Lau](https://github.com/samlaudev)
+* 校对者: [Mr.Simple](https://github.com/bboyfeiyu)
+* 状态 : 校正完
+
+在WWDC我们发现Swift团队没有浪费时间在无谓的地方,而是致力于改善Swift 2。
+
+我们将会为你编写和录制很多关于Swift 2的教程,但在此期间我想强调Swift最令人兴奋的改变,为你可以在秋天迁移到Swift 2做准备。
+
+
+
+
+#错误处理
+
+正如Ray在[WWDC 2015 Initial Impressions](http://www.raywenderlich.com/108379/wwdc-2015-initial-impressions)文章中提及,错误处理已经在Swift 2改进了。我们已经迁移到新的系统就像异常处理,而不是**NSError**对象和双指针。
+
+你可能对以下代码比较熟悉:
+
+```
+if drinkWithError(nil) {
+ print("Could not drink beer! :[")
+ return
+}
+```
+
+一般在Cocoa,你传入一个**NSError**对象的引用(一个**inout**参数在Swift),然后方法会赋值给错误变量。但问题是你可以传入一个nil到这里来完全忽略这个错误;或者,你可以传入**NSError**但从不检查它。
+
+Swift 2 为错误检查添加额外保护层。你可以使用**throws**关键字来指定那个函数和方法能够抛出一个错误。然后当你调用某样东西时,可以用**do**, **try**和**catch**多个关键字来捕捉和处理错误。
+
+```
+// 1
+enum DrinkError: ErrorType {
+ case NoBeerRemainingError
+}
+
+// 2
+func drinkWithError() throws {
+ if beer.isAvailable() {
+ // party!
+ } else {
+ // 3
+ throw DrinkError.NoBeerRemainingError
+ }
+}
+
+func tryToDrink() {
+ // 4
+ do {
+ try drinkWithError()
+ } catch {
+ print("Could not drink beer! :[")
+ return
+ }
+}
+
+```
+
+这里有几样东西需要强调的:
+
+1. 为了创建一个错误可以抛出,只是创建一个继承**ErrorType**的**enum**。
+2. 你需要使用**throws**关键字来标志任何函数可以抛出一个错误。
+3. 这里抛出一个错误,它将会在section 4中被捕捉。
+4. 你在一个**do**块中包含任何可以抛出一个错误的代码,而不是其他语言类似的**try**块中。然后,你添加一个**try**关键字到函数被调用的前面,而且这个函数能够抛出一个错误。
+
+新语法是非常简洁和易读。任何API当前使用**NSError**以后都会使用这种错误处理方式。
+
+
+
+
+#绑定
+
+在Swift 1.2,我们失去了金字塔的厄运和能够在一行代码测试多个绑定的optionals:
+
+```
+if let pants = pants, frog = frog {
+ // good stuff here!
+}
+```
+
+这样勉强能够工作,但对于有些人需要缩进那个嵌套着很多optionals才能访问的值的“首选”的代码路径是一个问题。这意味着你需要深入查看缩进主线部分的代码块,而错误条件却在外面。
+
+如果有些方式来检查一些没有值的optionals,然后早点退出。这正是Swift 2提供的guard语句:
+
+```
+guard let pants = pants, frog = frog else {
+ // sorry, no frog pants here :[
+ return
+}
+
+// at this point, frog and pants are both unwrapped and bound!
+```
+
+使用**guard**意味着你可以执行optional binding (或其他操作)和如果条件失败就提供一个代码块在**else**运行。然后,你可以继续执行。在这种情况下,optionals **frog**和**pants**在作用域内被unwrap。
+
+使用**guard**指定某种你希望得到状态而不是检查错误情况之后,使代码更加简洁。
+
+> **注意:** 如果你仍然不明白为什么使用**guard**语句比**if-else**语句更加有用,请查看Swift团队[Eric Cerney‘s post](http://www.raywenderlich.com/u/ecerney)在[Swift guard statement](http://ericcerney.com/swift-guard-statement/)。
+
+
+#协议扩展
+
+面向对象编程?函数式编程?Swift其实还是一种面向协议的编程语言!
+
+在Swift 1,协议就像接口一样可以指定一些属性和方法,然后类,结构体或枚举会遵循它。
+
+现在在Swift 2,你可以扩展协议和给属性和方法添加默认实现。你之前已经可以在类或结构体添加新的方法到**String**或**Array**,但现在你可以添加这些到协议,这让你更加广泛地应用。
+
+```
+extension CustomStringConvertible {
+ var shoutyDescription: String {
+ return "\(self.description.uppercaseString)!!!"
+ }
+}
+
+let greetings = ["Hello", "Hi", "Yo yo yo"]
+
+// prints ["Hello", "Hi", "Yo yo yo"]
+print("\(greetings.description)")
+
+// prints [HELLO, HI, YO YO YO]!!!
+print("\(greetings.shoutyDescription)")
+
+```
+
+注意**Printable**协议现在被命名为**CustomStringConvertible**,而大多数的Foundation对象都遵循__Printable__协议。有了协议扩展之,你可以用自定义功能来扩展系统。相比于向很多类、结构体和枚举添加少量的自定义代码,你可以编写一个通用实现,然后应用到不同的数据类型。
+
+Swift团队已经忙着做这个了 - 如果你在Swift已经使用__map__或__filter__,你可能也认为以方法的方式比全局函数来使用它们更好。多亏了强大的协议扩展,已经有一些新的方法添加到集合类型,例如:__map__,__filter__,__indexOf__和更多!
+
+```
+et numbers = [1, 5, 6, 10, 16, 42, 45]
+
+// Swift 1
+find(filter(map(numbers, { $0 * 2}), { $0 % 3 == 0 }), 90)
+
+// Swift 2
+numbers.map { $0 * 2 }.filter { $0 % 3 == 0 }.indexOf(90) // returns 2
+```
+
+多亏了协议一致性,你的Swift 2代码会变得更加简洁和易读。在Swift 1版本,你需要查看调用函数内部来理解它的工作原理;在Swfit 2版本,函数链会变得清晰。
+
+如果你打算使用面向协议编程 - 请查看[WWDC session on this topic](https://developer.apple.com/videos/wwdc/2015/?id=408)和留意这个网站的教程和文章。
+
+
+#汇总
+
+在WWDC大会中发布很多东西,所以我想强调几样重要的东西:
+
+* __Objective-C 泛型__ - Apple已经开始标注所有的Objective-C代码以便Swift类型能够获取正确类型的optional。使用Objective-C泛型也能正常工作,这样给Swift开发者更好的类型提示。如果你希望出现一些__UITouch__对象或字符串数组,那就会出现你想要的而不是一些__AnyObjects__。
+
+* __重命名 语法__ - __println__已经离开我们一年了;现在它是普通旧的__print__,现在它有第二个参数的默认值设置为__true__来决定是否换行。__do__关键字主要用来错误处理,do-while循环现在是使用__repeat-while__。类似地,有很多协议名都改变了,例如:__Printable__改为__CustomStringConvertible__。
+
+* __Migrator__ - 有很多小的语法改变,你怎样使得你代码变得最新?Swift 1-to-2 migrator会将代码变成最新的标准和改变语法。这个migrator智能到能够更新你的代码使用新的错误处理,和更新块注释到新的格式风格!
+
+* __开源!__ - 对码农有一个重大消息就是在秋天发布Swift 2的时候,Swift将会开源!这意味着不仅可以使用它来iOS开发,更重要的是学习它的源代码。不仅如此,这将是很好的机会来深入源代码,甚至为项目贡献代码,然后在swift编译器提交历史上留下你的名字。
+
+
+#下一步
+
+这只是所有发布特性中的一些简单示例;想了解更多,请查看[WWDC session videos](https://developer.apple.com/videos/wwdc/2015/)和已更新的[Swift Programming Language book](https://itunes.apple.com/us/book/swift-programming-language/id1002622538?mt=11)
+
+如果还有一些人记得在Swift第一个beta版和发布的1.0之间有很多改变,那么将来肯定会有更多地特性出现。我们团队将会持续关注所有的更新,深入挖掘令人兴奋的改变,所以请密切留意教程,书籍和视频。
+
+Swift 2哪部分最令你兴奋?哪部分你想我们第一时间报道?在下面评论让我们知道!
diff --git "a/issue-9/Swift\347\232\204\345\274\202\346\255\245\346\234\272\345\210\266-Future.md" "b/issue-9/Swift\347\232\204\345\274\202\346\255\245\346\234\272\345\210\266-Future.md"
new file mode 100644
index 0000000..9e934da
--- /dev/null
+++ "b/issue-9/Swift\347\232\204\345\274\202\346\255\245\346\234\272\345\210\266-Future.md"
@@ -0,0 +1,247 @@
+# Swift的异步机制-Future
+---
+
+> * 原文链接 : [Back to the Future](https://realm.io/news/swift-summit-javier-soto-futures/)
+* 原文作者 : [Javier Soto](https://twitter.com/Javi)
+* [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [sdq](http://shidanqing.net)
+
+我们在使用Objective-C写异步代码时常常会出现许多问题。它没有很好的错误处理机制,当你发现你需要在你的异步函数中创建新的异步函数它也没有一个很好的可伸缩实现。在这次演讲中,Javier Soto将演示实现一个能够简化异步API的Swift Future类型。并且创建一个Resule类型,我们可以在这个类型上调用map和andThen等操作。对于Swift来说,Futures的未来可能很会基于信号和eactiveCocoa 3。
+
+你可以在[这里](https://github.com/JaviSoto/Talks#swift-summit-2015-back-to-the-futures)看到这次演讲中的代码。
+
+## 更优雅地使用Swift
+
+谢谢各位,很高兴和大家一起来聊一聊Swift,感谢组织者把我们聚集在一起并让我在这里演讲。我的名字叫Javi,是来自Twitter的iOS工程师。
+
+相比于Objective-C,我们在Swift里可以把很多事做得更好。我们已经在之前的演讲中看了不少例子,我想紧接着着他们,和大家谈论一下一种我想引入Swift的设计模式。这种设计模式会使我们的代码更为优雅。
+
+##如今的异步API
+
+今天我不会讲一些很空理论,而是通过实践和大家一起讨论。接下来会看到,在传统情况下我们是如何使用异步API的,并且在使用过程中存在的一些问题,尤其是对错误的处理。然后我会介绍一个简单的Future API,我们会看到它使如何提升我们的异步代码的。
+
+让我们看看如今的问题在哪里?
+
+```swift
+struct User { let avatarURL: NSURL }
+func requestUserInfo(userID: String, completion: (User?, NSError?) -> ())
+func downloadImage(URL: NSURL, completion: (UIImage?, NSError?) -> ())
+
+func loadAvatar(userID: String, completion: (UIImage?, NSError?) -> ()) {
+ requestUserInfo(userID) { user, error in
+ if let user = user {
+ downloadImage(user.avatarURL) { avatar, error in
+ if let avatar = avatar { completion(avatar, nil) }
+ else { completion(nil, error!) }
+ }
+ } else { completion(nil, error!) }
+ }
+}
+```
+
+这是我们参照Objective-C异步API的方法写的Swift代码,其中包含大量的嵌套回调函数。我们可以看到loadAvatar方法,该方法的输入为用户ID,函数体完成对该用户头像的下载。其中,complete block需要传入一个image及一个error,但其实这个方法没有任何逻辑可言。方法本身不知道该如何进一步处理,其内部包含了一些异步方法来完成整个操作,包括采用requestUserInfo方法异步获取用户信息,以及一个下载图片的方法downloadImage。每一次我们都需要检查错误,执行完以后再调用completion block。
+
+你们中的大部分可能已经习惯了这样的代码,但这样的模式非常不容易扩展。即使你没有使用过PHP,你也肯定看到过这样的场景(演讲者展示了一张异步代码层层嵌套的图)。所以让我们避免这样的情况发生吧,我们可以做的更好。此外,这段代码还有一些其它特殊的问题。其中的一个异步API将会查看参数类型。completion block需要传入的一个元祖或者两个数值;它们都是optional型的,因为下载失败的话image会返回nil,而下载成功没有错误的话error会返回nil。
+
+这些问题会让你不爽,但你要知道问题其实一直存在,只是你从没有发现而已。在Objective-C中它们比较隐晦,因为任何类型都可以产生nil值。
+
+如果我们看一下所有可能发生的情况,我们会发现总共有两种:一种是图片下载成功没有error,另一种是图片下载失败发生error。有趣的是其实还存在其余两种可能的情况,但完全没有任何意义。我的app该如何处理即有图片又有错误的状况呢?又或者当既没有图片又没有错误时,会发生什么?
+
+##错误处理的重要性
+
+我常常喜欢说我认为的计算机是不会犯错的完美机器,每当我们遇到了bug,那都是因为攻城狮所希望的与实际要求计算机去做的产生了冲突。如果我们没有一个很好地方式来表达如何能完成我们的设想,那很可能导致我们对计算机下达了错误的命令。
+
+我认为错误处理非常重要,我们需要用合适的方式在应用程序中处理错误。上述这类API会令你在很多情况下用错,举个例子,你可能会首先检查error而不是先查看数值,虽然已经执行成功了,但你仍然会担心产生错误。
+
+##我讨厌NSError
+
+然后是NSError,我非常讨厌NSError,但我不能责怪Apple,因为老实说这的确是Objective-C中针对error最好的做法了。让我们再看一个例子看一下为什么NSError容易产生问题。
+
+```swift
+var error: NSError?
+let string = NSString(contentsOfFile:path encoding:NSUTF8Encoding error:&error)
+
+if string == nil {
+ // Oops:
+ if error.code == NSFileReadNoSuchFileError {
+ // ...
+ }
+ else if ...
+}
+```
+
+这段代码有错误,但是并不是非常明显。我们调用了Foundation API,如果查看文档,会发现我们完全不知道返回的是什么类型的错误,既没有提到领域,也没有提及错误代码。这段代码首先查看错误代码而不是先查看领域,这是没有任何意义的,因为在完全不同的领域可能产生相同的错误代码,非常容易误导开发者。另一种情况是,我们可以传nil给error,告诉Foundation“我不在乎error”,但是我认为鲁棒性强的软件应该认真处理错误。
+
+我们每天都会遇到这样令人抓狂的情况,但我们无法责备开发者。如果我们的工具不能让这一切变得简单、让错误处理更方便,那我们只会变得更懒,我们将会向方法里传nil值,然后忽略这些error。我们不会去了解那些错误是什么,而是让它们显示在控制台然后继续。
+
+##一个提议
+
+这里我有一个提议:我们可以用自己定义的类型来封装我们API中可能产生的错误,而不是使用NSError。我们可以写一个protocol包含所有可能发生的错误类型。
+
+```swift
+protocol ErrorType { }
+
+enum UserInfoErrorDomain: ErrorType {
+ case UserDoesNotExist
+ case UserRequestFailure(reason: String)
+ case NetworkRequestFailure(reason: String)
+}
+
+extension NSError: ErrorType { }
+```
+
+举个例子,如果我们有一个API,我们可以声明一个遵循此protocol的枚举类型,包含了三种清楚的错误情况。这里甚至可以提供原因的信息,之后可以看到我们如何进行使用。如果确实想用NSError,我们也可以将其包含在内。
+
+另一件看似矛盾但是却很酷的事情是,我们甚至可以创建一个NoError类型。
+
+```swift
+enum NoError: ErrorType { }
+
+let error = NoError(?)
+```
+
+
+很奇怪是吧?由于它是空的枚举,我们实际上不能创建这种类型,因为它不存在任何构造函数。虽然没有意义,但是我们知道null类型是可以被构造的。如果API返回的error值是NOError类型,那API就会报错,我们不再需要通过单元测试进行检验,编译器就会替我们发现。
+
+##采用Future建立Result类型
+
+如Brian在之前的演讲里所叙述的那样,这里我们会在Result类型的基础上展开一些工作,Result无论成功或者失败都可以产生一个值。在这页slide里我做了点手脚,因为由于编译器的限制,目前这个代码在Swift里是无法实现的,你必须用class的形式来想办法实现,但我还是先用简单的形式在这里展示以免影响理解。
+
+```swift
+enum Result {
+ case Success(T)
+ case Error(E)
+}
+```
+
+现在让我们来看一下Future,我们如何来实现呢?希望通过研究其实现的过程,我们能够更加理解其价值,这会很有趣!那什么是Future呢?你可能在其它语言或者框架里听到过它的另外一个名字——Promise。它们的概念是完全相同的。
+
+```swift
+struct Future {
+ typealias ResultType = Result
+ typealias Completion = ResultType -> ()
+ typealias AsyncOperation = Completion -> ()
+
+ private let operation: AsyncOperation
+}
+
+```
+
+Future包含了不一样的处理方式,可以对一段时间后才能获取数据先进行抽象的处理,比如网络延迟的情况。或许有人会说 “那是不是像闭包?”或者 “这不就是回调函数!” 不过Future的优势在于,我们可以像已经获取了数据那样进行操作,并且不用关心那些无关的异步操作,使我们的代码变得更简洁。
+
+##实施Future
+
+让我们开始实践吧。我们创建一个struct,类似Result它具有T和E的泛型,因为我们需要使用Result。我定义了一些别名用来帮助我们处理这些类型。我们之前看到过Result,同时需要一个可以触发的completion用于当你向Future要求,“现在给我值吧!”。你将会进到completion block里,它会获取到result并且不会返回任何东西。异步合并是一个闭包,当需要一个函数返回值的时候被调用。这时候completion块被调用,并且告知其函数工作的完成,并且不会返回任何值。Future封装了一些操作处理,我们可以在实例化中直接传入这些操作符。最重要的共有API是start方法,API的使用者可以通过此方法来告诉Future,“Ok,你现在可以去获取数据了,获取完之后交给我”。但是到现在为止用处不大,除了使用Result的方法不同外,它看起来完全和completion block一样。
+
+```swift
+struct Future {
+ init(operation: AsyncOperation) {
+ self.operation = operation
+ }
+
+ func start(completion: Completion) {
+ self.operation() { result in
+ completion(result)
+ }
+ }
+}
+```
+
+###map & andThen
+
+Future真正酷的是在我们实现一些操作的时候。我想要实现map和andThen,让我们先从map开始吧。我们可以用map方法实现的一些功能。比如我们有了用户的ID,然后我们想取得用户头像的链接地址。我们创建一个方法来完成这个功能,另外我们还有一个已经创建出的方法用以下载用户信息并给我们一个基本的用户信息的结构体,也就是URL。如果我们想不用一堆回调来将其中一个转换为另一个,那可以先map,然后通过返回user内部的其它数据进行转换。
+
+```swift
+struct User { let avatarURL: NSURL }
+
+func requestUserInfo(userID: String) -> Future
+
+
+func requestUserAvatarURL(userID: String) -> Future {
+ return requestUserInfo(userID)
+ .map { $0.avatarURL }
+}
+```
+
+这就是函数签名的样子。比较一下它与map的函数签名你会发现很有意思,举个例子,两种类型都是出自swift标准库,Array和Optional。如果我们看一下类型,它们是完全一样的。他们会用函数来将获取的T类型值生成U类型的结果,其中T是容器内部的类型,而U将是新容器的类型。
+
+```swift
+struct Array {
+ func map(f: T -> U) -> [U]
+}
+
+enum Optional {
+ func map(f: T -> U) -> U?
+}
+
+struct Future {
+ func map(f: T -> U) -> Future
+}
+```
+
+##实现map
+
+那么让我们实现它吧。我们需要创建一个新的Future,因为它必须是U型的。我们用下列的方法来验证Future模式的机制。就像我们看到的,方法传给Future一个运算操作。为了转变数值,我们首先需要获得数值,所以我们调用开始方法从而得到结果。该结果可以是成功或失败,因此我们需要根据其情况处理。
+
+```swift
+func map(f: T -> U) -> Future {
+ return Future(operation: { completion in
+ // Retrieve the value from self...
+ self.start { result in
+
+ }
+ }
+}
+```
+
+首先让我们看一下成功的情况。成功的情况下,如同类型签名那样,告诉我们需要通过F函数转化得到想要的数值 - 我们将再次在成功条件下运行它,然后会调用completion block。错误的情况下就更简单了。我们没有value,我们不能进行转换,只需要调用completion block。这就和幻灯片里显示的一样。
+
+在错误情况下发生的短路情况和Brain之前展示的面向铁路对象编程概念基本一样。但是现在我们想要用最初Future给我们的value做一些事,但我们希望获取一个新的value。获取该value不像调用头像URL一样简单。获取该value会导致一些延迟,比如当我们需要另一个网络请求。
+
+##声明andThen
+
+这是行不通的,因为我们需要通过一个函数来返回另一个Future。因此,我们想要再Future API上声明一个Future和一个andThen函数。我们会这样使用,例如:如果我们有一个像刚才看到的请求头像URL,可以在之前的操作后紧接着下载图片,返回头像图片的Future。我们只需要调用andThen函数并传其它的函数。
+
+```swift
+func requestUserAvatarURL(userID: String) -> Future
+
+func downloadImage(URL: NSURL) -> Future
+
+
+func downloadUserAvatar(userID: String) -> Future {
+ return requestUserAvatarURL(userID)
+ .andThen(downloadImage)
+}
+```
+
+这就是签名的样子,类似map,就像我说的重要的区别是,F函数不返回U,它返回的是一个新的Future。实现它非常非常相似。我们重新建立一个Future,初始化并取得value,然后选择Result。在成功的情况下,当我们在value上调用F,我们没有可以返回的value。现在我们有了另一个Future,我们需要启动它,当我们完成得到最后的Result。
+
+在错误的情况下,我们需要对其短路。如果我们没有一个value,就不能转换它,以便继续做更多的操作。我们需要马上调用completion block。这就是整个的实现过程,你会意识到它看起来很像map。事实上,有趣的是你可以在其他情况下实现它,但我选择了单独实现来看看他们会做些什么。
+
+让我们来看看已经完成的工作。我们从嵌套混乱复杂的回调函数以及每次单独调用都会涉及错误处理的情形,转变为通过一个声明方法来告诉我们需要获取头像:我们需要请求用户信息,将它map到头像的URL,然后下载图像。这不是关于代码的能力,它是关于我们在代码中要表达我们的意图的能力。不过其最大的优势在于,代码中不再有错误处理的内容,所有的error都会在运行中自动被识别。
+
+```swift
+func andThen(f: T -> Future) -> Future {
+ return Future(operation: { completion in
+ self.start { firstFutureResult in
+ switch firstFutureResult {
+ case .Success(let value): // ...
+ case .Error(let error): // ...
+ }
+ }
+ }
+}
+
+```
+
+##Future的局限性
+
+我希望你们会和我一样喜欢Future,Future也的确和我想的一样具有实用价值。但是,了解Future的缺陷也同样重要。文中的例子在我们的移动应用中很常见,会存在网络请求,会存在异步请求等等,但是问题是我们不仅仅只处理异步事件;另外,当返回值不只有一个值时Future就会失效,Future只能处理一个值,而当我们处理用户交互的时候,像手势识别,往往会存在多个值的情况。
+
+另一个问题是我们在上文处理的情景并不是用户驱动的,就像交互时你不会告诉app “我现在准备触碰手机了”。 这些行为完全是用户决定的,程序无法预测。
+
+##Signals
+
+好消息是Future正在变得更好。Signals将会解决以上遇到的问题。Signals是一个非常有效的工具。
+
+另一件令人兴奋得事情是我们不必自己去实现它。你可能已经听过了ReactiveCocoa可以用来实现signals并允许你的app使用这些概念。ReactiveCocoa 3是用swift写的,可以在Github上找到。
diff --git "a/issue-9/\345\234\250Swift\345\274\200\345\217\221\344\270\255\351\200\232\350\277\207UINavigationController\346\235\245\350\256\277\351\227\256Sub-Controllers.md" "b/issue-9/\345\234\250Swift\345\274\200\345\217\221\344\270\255\351\200\232\350\277\207UINavigationController\346\235\245\350\256\277\351\227\256Sub-Controllers.md"
new file mode 100644
index 0000000..b8f986b
--- /dev/null
+++ "b/issue-9/\345\234\250Swift\345\274\200\345\217\221\344\270\255\351\200\232\350\277\207UINavigationController\346\235\245\350\256\277\351\227\256Sub-Controllers.md"
@@ -0,0 +1,97 @@
+在Swift开发中通过UINavigationController来访问Sub-Controllers
+===
+
+> * 原文链接 : [Access Sub-Controllers from a UINavigationController in Swift](http://www.andrewcbancroft.com/2015/06/02/access-sub-controllers-from-a-uinavigationcontroller-in-swift/)
+* 原文作者 : [Andrew Bancroft](http://www.andrewcbancroft.com)
+* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn)
+* 译者 : [samw00](http://www.andrewcbancroft.com/2015/06/02/access-sub-controllers-from-a-uinavigationcontroller-in-swift/)
+* 校对者: [mrchenhao](https://github.com/mrchenhao)
+* 状态 : 完成
+
+通过`AppDelegate`或者通过`prepareForSegue(_:sender:)`来访问`UINavigationController`的第一个子元素的访问顺序总是让我有点纳闷。这篇博文中有几段代码可以帮助你和我迅速的弄清楚如何在我们的Swift应用程序开发中和navigation controllers打交道。
+
+##AppDelegate
+
+每一个iOS应用在其启动加载完成后都会显示一个根视图控制器。假设我们要搭建一个以导航控制器(navigation controller)为主的一个app...,也就是说我们这个app的第一(根)视图控制器是一个UINavigationController。在我们的Storyboard中,我们创建了一个简单的场景,给一个视图控制器添加了些UI控件和一些属性,然后我们将这个视图控制器嵌入在一个导航控制器中。
+
+但是如果我想在app启动之后再去设置视图控制器中的某些个属性呢?我们要如何才能做到这点呢?
+
+我一直把我在Storyboard中添加UI要素的第一个场景(first scene)视为“第一视图控制器”。但是,对iOS来说,导航控制器才是实际意义上的第一(根)视图控制器。
+
+当一个app把导航控制器视为其第一(根)视图器时,我们就需要在视图层级结构中“挖”的更深一点才能访问到我们之前所以为的“第一视图控制器“。
+
+###找寻第一视图控制器
+
+下面的代码段展示了如果在`UINavigationController`的视图控制器层级中找到第一个控制器并给其设置一些虚构属性,这一次都发生在AppDelegate中:
+
+```swift
+class AppDelegate: UIResponder, UIApplicationDelegate {
+ var window: UIWindow?
+
+ func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
+ // Override point for customization after application launch.
+
+ let navigationController = window?.rootViewController as! UINavigationController
+ let firstVC = navigationController.viewControllers[0] as! NameofFirstViewController
+ // set whatever properties you might want to set
+ // such as an NSManagedObjectContext reference
+
+ return true
+ }
+
+ // ...
+}
+```
+
+整个流程是这样的:
+
+* 获取window的根视图控制器(在我们的例子中就是导航控制器)
+* 通过导航控制器的视图控制器数组获取其第一个子视图控制器(也就是原先我一直以为的“第一”视图控制器)
+* 设置任何你需要设置的属性
+
+在上面这段代码中,你可能担心我用了隐式拆包这方法来看可选值变量是否有值。通常情况下我也是避免用这种方法的,但在这里我用这种方法的理由是我的这个app的根视图控制器就是一个`UINavigationController`,这点确保了我可以使用隐式拆包这种用法。因为不管如何,只要改变了app的导航模式都会出错。
+
+如果上面的理由没有说服你也别担心 - 你可以把`?`换成`!`然后添加一些`if-let`语句来确保你不会碰到`nil`。举个例子:
+
+```swift
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ // ...
+ if let navigationController = window?.rootViewController as? UINavigationController {
+ if let firstVC = navigationController.viewControllers[0] as? NameOfFirstViewController {
+ firstVC.someproperty = someValue
+ }
+ }
+ // ...
+}
+```
+
+##Prepare for segue
+
+那么`prepareForSegue(_:sender:)`又是怎么一回事?什么时候不得不走这条路呢?
+
+假设,我们有一个应用跳转进一个导航控制器。我们可能需要给下一个视图控制器传值,但是“下一个视图控制器”严格来说其实是一个导航控制器,并不是我们声明属性的那个控制器。
+
+和通过AppDelegate时的情形一样,我们也需要在视图层级中深挖一点来访问第一视图控制器来,这样才能传递我们的数据。下面是一个实现的例子:
+
+```swift
+public class ViewController: UIViewController {
+
+ // ...
+ override public func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ let destinationVC = segue.destinationViewController as! UINavigationController
+ let nextViewController = destinationVC.viewControllers[0] as! SecondViewController
+
+ nextViewController.someProperty = someValue
+ }
+ // ...
+}
+```
+
+在`AppDelegate`例子和`prepareForSegue`的例子中唯一改变的就是我们是从哪里如何获取`UINavigationController`的。在`AppDelegate`例子中,导航控制器是来自window的根视图控制器。在`prepareForSegue`中则来自segue的目标视图控制器。
+
+除此之外,获取导航控制器的第一个子元素的步骤都是一样的。
+
+##总结
+
+如何去访问一个导航控制器的视图控制器层级结构让我觉得有点含糊不清,所以我给我自己写了这篇小博文作为日后的一个参考,但是我希望你也能从这篇博文中受益。
diff --git a/template.md b/template.md
index f67865e..b447635 100644
--- a/template.md
+++ b/template.md
@@ -27,7 +27,4 @@ The idea is simple: clean architecture stands for a group of practices that prod
## 入门指南
大家都知道要写一款精品软件是有难度且很复杂的:不仅要满足特定要求,而且软件还必须具有稳健性,可维护、可测试性强,并且能够灵活适应各种发展与变化。这时候,“清晰架构”就应运而生了,这一架构在开发任何软件应用的时候用起来非常顺手。
-这个思路很简单:简洁架构 意味着产品系统中遵循一系列的习惯原则:
-
-
-
+这个思路很简单:简洁架构 意味着产品系统中遵循一系列的习惯原则:
\ No newline at end of file