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)并打开初始项目,不需要做任何事情,直接编译运行。 + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2015/09/intro-app-search-app-preview.png) + +你会看到一张职员列表。这是一个小型创业公司,只有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。 + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2015/09/intro-app-search-app-screen-3.png) + +现在,编译运行程序,然后选择 Brent Reid。 + +OK,看起来没有什么新奇的事情发生,但在你不知不觉中,Brent 的活动已经被加到搜索索引中了。回到 Home 屏幕(shiftcommandH),通过下拉屏幕或者向右划动屏幕,打开 Spotlight。在搜索栏输入 brent reid 。 + +![](http://cdn1.raywenderlich.com/wp-content/uploads/2015/09/intro-app-search-app-screen-4.png) + +"Brent Reid"显示出来了!如果你没看见,可能需要向下滚动列表。如果你点击这个Brend Reid,它将移动到列表上部,以便下次你可以搜索同一个关键字。 + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2015/09/intro-app-search-whoa-meme.jpg) + +虽然到现在为止结果还蛮不错,但这个搜索结果却是有点索然无味了。 + +除了显示一个名字外,我们还能干什么?现在就让我们彻底进入 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](http://cdn4.raywenderlich.com/wp-content/uploads/2015/06/WatchKit_01.jpg) + +***WatchKit for WatchOS 2*** + +请在下面的评论区说出你自己的想法,当然,你也可以提一些我没有提到的东西啦~ + +## 架构 + +或许在这次的发行版里,对于WatchKit最大的改变就是可以构建本地的专属手表的独立app了。 + +![Wait, did he just say native?](http://cdn2.raywenderlich.com/wp-content/uploads/2015/06/Spitting.png) + +***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*coDmMhq3NgRj0Unl_2EFCg.png) + + +运行后 : + + + +( 如果浏览器不支持视频播放,可以手动拷贝地址到浏览器中观看,地址在 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。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54f8bc03e4b0d007a6fee8a4/1425587211870/?format=1500w) + +在故事版中拖四个按钮到主视图中,名称分别为以下: + +* Create File +* List Directory +* View File Content +* Delete File + +故事版看起来像这样 +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54f8d8bfe4b00d102a265006/1425594560844/?format=1500w) + + +通过按住Ctrl键选择全部按钮,然后点击在底部的第三个按钮选择`Resolve Auto Layout Issues`来添加缺少的约束。 +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54f8d98ce4b070bffa96534e/1425594775771/?format=750w) + +按钮依次放置,即使在设备旋转的时候,选择`Assistant Editor`并让ViewController.swift可见。按住Ctrl将`Create File`拖到`ViewController `类,创建以下动作。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54fb76ade4b03452415dab06/1425766065262/?format=750w) + + +按住Ctrl将`List Directory`拖到`ViewController `类,创建以下动作。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54fb7951e4b0f473c20ec908/1425766739551/listFile-Action.png?format=750w) + + +按住Ctrl将`View File Content`拖到`ViewController `类,创建以下动作。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54fb7989e4b0edea6489289e/1425766794964/?format=750w) + + +按住Ctrl将`Delete File`拖到`ViewController `类,创建以下动作。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54fb7a06e4b0ecb31a5b46ea/1425766920438/?format=750w) + + +在`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`按钮来查看控制台输出。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54fb82f2e4b03edc59f68802/1425769204078/?format=1000w) + + +你可以在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。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/559ce50ee4b0560c42b45099/1436345615076/) + +到Storyboard页面,从Object Library拖两个按钮到主视图上,给这两个按钮设置title,分别为"Take Video" 和 "View Library",Storyboard此时应该如下图所示: + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/559ce595e4b0cebfa4eee243/1436345750826/) + +按住Ctrl键,然后选中这两个按钮,点击在Storyboard右下方的“Resolve Auto Layout Issues”按钮,然后选择“Add Missing Constraints”。如下图所示 : + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/559ce604e4b04c3e89ff0e2e/1436345862940/) + +切换到Assistant Editor界面,并且确保ViewController.swift是可见的。按住Ctrl并从Take Video按钮区域拖出以创建一个Action. + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/559ce655e4b051cc74e74388/1436345942333/) + +同样的,按住按住Ctrl并从View Library按钮区域拖出以创建一个Action。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/559ce67fe4b001bba4e7e8e9/1436345984380/) + +切换到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中了。 + +![TakeVideo-Device.png](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/559d107ee4b0a65ec39328be/1436356736349/TakeVideo-Device.png) + +你可以在[这里](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。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/557809f1e4b0c6cf0d971d80/1433930226089/?format=1500w) + +在这个工程中我们需要开始暂停和停止按钮的图片和一个mp3文件。你可以在这里[下载](http://www.ioscreator.com/tutorials/play-music-avaudioplayer-ios8-swift#)。解压文件并把它们添加到工程,确保是拷贝他们到目标文件夹。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/55782903e4b09779d065bf7f/1433938179851/?format=2500w) + +转到故事板拖两个标签和两个按钮到主视图。依次选择他们并在`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` + +现在故事板看起来是这样的 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/55872ba7e4b087a1e08d485d/1434921897044/?format=2500w) + +选择助理窗口并确保`ViewController.swift`可见。 + +按住`Ctrl`拖动顶部的标签创建下列的插口。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/55872c0ee4b0eb99d4affc23/1434922000928/?format=750w) + +按住`Ctrl`拖动中间的标签创建下列的插口。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/55872c35e4b08fd2d8e19699/1434922041212/?format=750w) + +按住`Ctrl`拖动播放暂停图片创建下列动作。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/55872c6be4b042906e107112/1434922092023/?format=750w) + +按住`Ctrl`拖动播放停止图片创建下列动作。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/55872c89e4b042906e107182/1434922121603/?format=750w) + +在`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,构建并运行程序,按下播放/暂停按钮和停止按钮来控制音乐。 + + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/5587308fe4b087a1e08d5bd3/1434923153194/?format=1500w) + +你可以下载`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。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54d7b6bde4b00ffa20f1df7a/1423423166651/?format=1500w) + +上面的做好之后,打开Storyboard,移除自动生成的ViewController,然后拖拽一个NavigationController到Storyboard里。当刚刚的"初始"ViewController被删除了之后,这个APP就没有“起点”了。选择刚刚拖进来的NavigationController然后进到右边的Attribute Inspector。在 View Controller这一个节点下勾选”Is Initial View Controller“ + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54d7b766e4b08579c6ac7813/1423423335054/?format=750w) + +双击左边 TableViewController 的导航栏,把它的title设为“Numbers”。选择TableViewCell然后到右边的 Attributes Inspector 面板,把 Cell 的 Indentifier 设为"Cell"。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54d7b7dae4b002f56c3e6ca4/1423423451428/?format=750w) + +Storyboard现在看起来应该像下面这样 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54d7b879e4b0ff7e52fca235/1423423610868/?format=2500w) + +因为我们删掉了自动生成的ViewController,所以我们也可以删掉ViewController.swift。添加一个新的swift文件到项目中,选择**iOS->Source->Cocoa Touch Class**。把它命名为TableViewController 然后设置它为UITableViewController的子类。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54d7b8a7e4b0b30d9474144a/1423423656532/?format=1500w) + +点开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方法。编译运行这个项目,然后在搜索框输入搜索关键字,结果就会被展示出来。 + +![](http://static1.squarespace.com/static/52428a0ae4b0c4a5c2a2cede/t/54d7c286e4b00ffa20f21dbc/1423426183530/?format=1000w) + +你可以下载`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希望在我们所有的应用中用统一的方式来写单例。我们其实知道什么才是单例“正确”的书写方式,但是我们无法自证。如果不旁征博引来证明我们是正确的简直就是徒劳。这是我和缺乏信息的互联网/博文圈之间的较量。每个人都知道如果网上没写那就不是真的。这让我非常难过。 + +![](http://static1.squarespace.com/static/5592eb03e4b051859f0b377f/t/55a8b8bfe4b0671daf04772e/1437120704789/?format=500w) + +我的搜索达到了互联网的尽头(Google到了第10页)却啥也没得到。难道真就发表一行单例的证明吗?也许有人做了,就是没找到而已。 + +所以我觉得我自己把所有的初始化构造器的方法都实现一遍,然后通过断点来检查他们。当分析完每个栈帧我所发现的相似点的时候,我终于发现期待已久的东西 - 证据! + +直接上图,对了(还有emoji类哦!): + +![](http://static1.squarespace.com/static/5592eb03e4b051859f0b377f/t/55a8b833e4b044eedeff6ac4/1437120566797/Screen+Shot+2015-07-16+at+4.56.38+PM.png?format=1000w) + +使用全局单例 + +![](http://static1.squarespace.com/static/5592eb03e4b051859f0b377f/t/55a8b860e4b00cca65ee95a2/1437120615798/Using+the+One+Line+Singleton?format=1000w) + +使用一行单例 + +第一张图显示了一个全局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时,编译器都会报错: + +![](http://static1.squarespace.com/static/5592eb03e4b051859f0b377f/t/55ae6ad6e4b035a17a870044/1437493975886/?format=1000w) + +你看!这就是完美的,一行实现单例。 + +## 结论 + +呼应一下[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) +* 状态:完成 + +[![Learn how to make your own +CocoaPod!](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/ice_cream_shop_logo.png)](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*,也是风味模型的一个支持类。 + +[![Storyboard +Annotated](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/storyboard_annotated-427x500.png "Storyboard Annotated")](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/storyboard_annotated.png) + +这个冰淇凌店的老板非常喜欢这个应用程序,但他们又添加了一个新的要求:在他们的应用程序中需要有相同的口味风格选择。 + + +等等,那不是最初的设计,但是,应该没有问题的对于一个NB的开发者来说。 + +[![SuperDev](http://cdn1.raywenderlich.com/wp-content/uploads/2014/11/SuperDev.jpg)](http://cdn1.raywenderlich.com/wp-content/uploads/2014/11/SuperDev.jpg) + + + +你能猜猜这个功能是怎么做的吗?是的,你讲把这个功能集成到你自己的CocoaPod!。 + + 建立你自己的Pod +--------------------------------- + + + +创建一个新的Xcode项目并且选择*iOS\\Framework & Library\\Cocoa +Touch Framework*然后点击下一步。 + +[![Create Cocoa Touch +Framework](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/cocoa_touch_framework-480x284.png "Create 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 +``` + + +你的项目导航器看起来应该是这个样子。 + +[![RWPickFlavor New Pod Files +Hierarchy](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/rwpickflavor_new_pod_files-255x320.png "RWPickFlavor New Pod Files Hierarchy")](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*,就像这样 + +[![Copy Workspace +Files](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/copy_workspace_files.gif)](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*,像这样。 + +[![Background Show In +Finder](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/background_show_in_finder-480x267.png "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* + +[![Update PickFlavorViewController Image View +Image](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/update_pickflavorviewcontroller_imageview-411x320.png "Update PickFlavorViewController Image View Image")](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/update_pickflavorviewcontroller_imageview.png) + + +信不信,最难创造的是你的pod。 + +[![High +Five!](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/high_five_kitten-349x500.jpg)](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展示如下: + + [![Github: New +Repository](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/github_new_repository-480x152.png "Github: New Repository")](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/github_new_repository.png) + + + +进入*RWPickFlavor*仓库,并且选择*Create repository* + + + +Github将创建一个新的仓库在你的账户下。然后你会看到下面的屏幕与一个快速设置,现实您储存的仓库网址。 + +[![Github Quick +Setup](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/github_quick_setup-480x56.png "Github Quick Setup")](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! + +[![Victory +Dance!](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/victory_dance-417x320.png "Victory Dance!")](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”界面。 + +![choose\_flavor](http://cdn4.raywenderlich.com/wp-content/uploads/2015/05/choose_flavor-180x320.png) + + 抽象所有事物 +--------------------------- + + + +如果你像我一样,你可能会想,“哇,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` 为空 + + + +如果你现在运行代码,你将会得到如下的结果: + +![Starter](https://camo.githubusercontent.com/c8da0f4c7cc439957d21dc20efb330245faf2d92/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30362f7374617274696e672d696d6167652e706e67) +![起始](https://camo.githubusercontent.com/c8da0f4c7cc439957d21dc20efb330245faf2d92/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30362f7374617274696e672d696d6167652e706e67) + + + +请注意纵横比固定为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` 属性。 + +![Uniforms](https://camo.githubusercontent.com/91bd7f6d345fa6e57647a466eb3167c09a037c4f/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30352f756e69666f726d732e706e67) + + + +**警告**: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`。 + +![Intervals](https://camo.githubusercontent.com/f13898a25d2805f99f514540477dcb091c3d485c/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30352f696e74657276616c73322e706e67) + + +##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](https://camo.githubusercontent.com/966e51af3a589644eb963c62a96db52a0bae3eb1/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30352f7363726f6c6c56696577322e706e67) + + +标准化过的ScrollView + +![Normalized ScrollView](https://camo.githubusercontent.com/9a902b0499599ba09bb57d7afc1702a1c4b17c7d/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30352f6e6f726d616c697a65645363726f6c6c566965772e706e67) + + + +最后我们将点映射到复平面。为了在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映射到的平复面以及纠正过纵横比的结果。 + +![Complex Plane](https://camo.githubusercontent.com/565c2123454cfacc4ad421a29a36e263ed32e440/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30352f636f6d706c6578506c616e65322e706e67) + + + +为了整合上面所有代码,我们建了一个新的方法叫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为绿色。 + +![Bulb region 2](https://camo.githubusercontent.com/be10da9e8b6a359e8d5a61bc4bfe94931a5c882a/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30362f63617264696f69642d3262756c622e706e67) + +**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); + +![Julia](https://camo.githubusercontent.com/29cf99df9aeb340c50ba8c1d7687489bb56ecefe/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30362f6a756c69612e706e67) + +**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分形。 + +![Bunny](https://camo.githubusercontent.com/0be3f31e3f64d34053ad0627224aebcb37f53e7b/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30352f62756e6e792e706e67) +![兔子](https://camo.githubusercontent.com/0be3f31e3f64d34053ad0627224aebcb37f53e7b/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30352f62756e6e792e706e67) + +![Fractal Bunny](https://camo.githubusercontent.com/cee740f50001ef98b64243995fb580caf2746bb0/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30352f62756e6e794672616374616c2e706e67) + +![分形兔子](https://camo.githubusercontent.com/cee740f50001ef98b64243995fb580caf2746bb0/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30352f62756e6e794672616374616c2e706e67) + +**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的公式。**这个是开放性的挑战。**以下提供了两个例子 + + +**燃烧之船的分形** + +![Burning Ship](https://camo.githubusercontent.com/f91ac892791b93dfc03df9b4237fe3ed56d280f5/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30352f6275726e696e67536869702e706e67) + +**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** + +![Sierpinski Julia](https://camo.githubusercontent.com/44c1795c1996bda8ad6c06e80343c0da10dde8ae/68747470733a2f2f7777772e7765686561727473776966742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031352f30352f7369657270696e736b692d6a756c69612e706e67) + + +**公式** `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层面。 +这个教程带你一起构建一个搜索英国房产信息的应用: + +![这里写图片描述](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/PropertyFinder.png) + +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。模拟器启动并显示一句问候语: + +![这里写图片描述](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/ReactNative-Starter.png) + +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实例。 +![这里写图片描述](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/ViewDebugging.png) + +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 ? + (