├── .DS_Store ├── .gitignore ├── LICENSE ├── README.md ├── git操作流程.md ├── git简单使用教程.md ├── issue-1 ├── CocoaPods指南.md ├── View-Debugging-in-Xcode-6.md ├── iOS开发-可滑动的单元格.md ├── iOS编程101-如何生成圆形和圆角的图像.md └── readme.md ├── issue-10 └── readme.md ├── issue-2 ├── GCD概述、语法以及好的示例.md ├── Swift样式指南2015年4月更新.md └── 自定义ViewController切换效果与动画.md ├── issue-3 ├── Swift的响应式编程.md ├── iOS今日扩展.md ├── iOS后台模式开发指南.md └── readme.md ├── issue-4 ├── Swift和自动引用计数器整理之强、弱和无主引用.md ├── Swift扩展的三个微妙细节.md ├── readme.md ├── 什么是委托代理-Swift开发者指南.md └── 关于AFNetworking安全bug的回复.md ├── issue-5 ├── Core Graphics 教程第三部分(Swift) - Patterns 和 Playgrounds.md ├── Swift-Core-Graphics教程第一部分.md ├── Swift-Core-Graphics教程第二部分-Gradients与Context.md └── readme.md ├── issue-6 ├── iOS8使用iCloud.md ├── iOS项目的持续集成与管理.md └── 通过减少动态分发来提高Swift的执行效率.md ├── issue-7 ├── Core Image入门教程(swift).md ├── UIKit Dynamics 教程 - 起步.md └── readme.md ├── issue-8 ├── UIKit Dynamics和Swift教程:抛掷Views.md ├── WWDC中那些令人感兴趣的事物-iOS9,-Swift2.md └── 使用CAShapeLayer来实现圆形图片加载动画.md ├── issue-9 ├── Swift 2 有哪些新特性.md ├── Swift-EventKit的初学者指南--请求权限.md ├── Swift的异步机制-Future.md └── 在Swift开发中通过UINavigationController来访问Sub-Controllers.md ├── markdown简单教程.md ├── template.md └── 翻译项目协作流程.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlaudev/iOS-tech-frontier/0e21f07cfae753a0aca75389946d4c44ce030bbd/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .media/ 3 | .tmp/ 4 | Guide.localized/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOS开发技术前线 ( ios-tech-frontier ) 2 | 一个定期翻译、发布国内外iOS优质的技术、开源库、软件架构设计、测试等文章的开源项目,让我们的技术跟上国际步伐。 3 | 4 | **翻译QQ群 ( 需审核 ) : 131021281**,项目管理员[Lollypo](https://github.com/Lollypo/)、[Harries Chen](https://github.com/mrchenhao/)、[StormXX](https://github.com/StormXX/),有问题可以通过github或者QQ群联系他们。 5 | 6 | ## 目录 7 | * [文章分类](#category) 8 | * [优秀推荐文章](#recommend) 9 | * [已完成文章列表](#articles) 10 | 11 | ## 参与步骤 12 | 1. 将该项目fork到自己的github; 13 | 2. 在对应期数里面 (比如 issue-2 , 代表第二期) 里面创建一个markdown文件,文件命名格式为: 中文标题名 ( 不要有空格,有空格的地方用"-"连接 ),比如 Android-MVP模式与实践.md; 14 | 3. 将模板[template.md](template.md)中的内容拷贝到markdown文件中,按照模板填写、翻译完内容(文章所需图片请存放到图床上,不要放在仓库中,图片宽度尽量控制在400 px左右),完成翻译后将状态修改为"校对中",提交pull request到本项目; 15 | 4. 管理员校对完成之后便会发布. 16 | 17 | * [git流程简介](git简单使用教程.md) 18 | * [markdown语法简单教程](markdown简单教程.md) 19 | * [git操作流程](git操作流程.md) 20 | 21 | ## 注意事项 22 | 1. 在翻译文章时,保留专有名词; 23 | 2. 在忠于原文的基础上,进行适当的意译,以适应国人阅读习惯; 24 | 3. 当翻译完成后,通读一遍,检查语句是否通顺,有无错别字等问题; 25 | 4. 保持文章的整洁性。 26 | 27 | [参与翻译、校对的流程](https://github.com/bboyfeiyu/android-tech-frontier/blob/master/%E7%BF%BB%E8%AF%91%E9%A1%B9%E7%9B%AE%E5%8D%8F%E4%BD%9C%E6%B5%81%E7%A8%8B.md) 28 | 29 | 30 | 31 | 32 | ## 文章分类 33 | | 来源 | 介绍 | 34 | |----------|-------------| 35 | | [Ray Wenderlich] | iOS社区,经常更新优秀文章[Ray Wenderlich](http://www.raywenderlich.com/) | 36 | | [App Coda] | iOS社区,经常更新优秀文章[App Coda](http://www.appcoda.com/) | 37 | | [Medium] | 类似于Reddit的社区,这是其中的iOS板块[Medium](https://medium.com/ios-os-x-development/) | 38 | | [Others] | 其他来源的优秀文章 | 39 | 40 | 41 | ## 推荐文章 42 | 如果您有好的文章推荐我们翻译,请在 在[bboyfeiyu/iOS-tech-frontier](https://github.com/bboyfeiyu/iOS-tech-frontier/issues/) 下提一个issue,[Issue 模板](https://github.com/bboyfeiyu/iOS-tech-frontier/issues/1 43 | ): 44 | 45 | * 推荐理由: `这是一篇关于XXXXXXX的优秀文章` 46 | * 原文链接: 写明原始链接 47 | * 标签: `推荐`, 来源,如`raywenderlich`,`appcoda`等 48 | * milestone: 设置为当月,如`2015/04` 49 | 50 | 51 | ## 翻译任务跟踪和管理 52 | 53 | 有想要翻译文章的朋友请从issue列表中选择一个翻译任务,然后在issue中添加一个评论,将自己的github账户添加到评论中( 比如 : @Mr.Simple认领该翻译任务 ),管理员会将该issue的标签设置为翻译中。翻译者及时更新翻译状态,状态通过标签跟踪: 54 | 55 | * 待认领 56 | * 翻译中 57 | * 翻译完成 58 | * 校对中 59 | * 校对完成 60 | * 已发布 61 | 62 | 译者翻译完成之后想本项目发布pull request,校对完成之后会合并您的提交,并且将对应的issue关闭。 63 | 64 | 65 | 66 | ## 已完成列表 67 | 68 | ### 第八期 (2015.6.12) 69 | | 文章标题 | 译者 | 70 | |----------------------|------------------------| 71 | | [UIKit Dynamics和Swift教程:抛掷Views](issue-8/UIKit Dynamics和Swift教程:抛掷Views.md) | [samw00](https://github.com/samw00) | 72 | | [使用CAShapeLayer来实现圆形图片加载动画](issue-8/使用CAShapeLayer来实现圆形图片加载动画.md) | [Sam Lau](https://github.com/samlaudev) | 73 | | [WWDC中那些令人感兴趣的事物-iOS9,-Swift2](issue-8/WWDC中那些令人感兴趣的事物-iOS9,-Swift2.md) | [Lollypo](https://github.com/Lollypo) | 74 | 75 | ### 第七期 (2015.6.8) 76 | | 文章标题 | 译者 | 77 | |----------------------|------------------------| 78 | | [Core Image入门教程(swift)](issue-7/Core Image入门教程(swift).md) | [Sam Lau](https://github.com/samlaudev) | 79 | | [UIKit Dynamics 教程 - 起步](issue-7/UIKit Dynamics 教程 - 起步.md) | [Lollypo](https://github.com/Lollypo) | 80 | 81 | ### 第六期 (2015.6.1) 82 | | 文章标题 | 译者 | 83 | |----------------------|------------------------| 84 | | [iOS8使用iCloud](issue-6/iOS8使用iCloud.md) | [Harries Chen](https://github.com/mrchenhao) | 85 | | [iOS项目的持续集成与管理](issue-6/iOS项目的持续集成与管理.md) | [Sam Lau](https://github.com/samlaudev) | 86 | | [通过减少动态分发来提高Swift的执行效率](issue-6/通过减少动态分发来提高Swift的执行效率.md) | [samw00](https://github.com/samw00/) | 87 | 88 | ### 第五期 (2015.5.25) 89 | | 文章标题 | 译者 | 90 | |----------------------|------------------------| 91 | | [Core Graphics教程第一部分(Swift) - 起步](issue-5/Swift-Core-Graphics教程第一部分.md) | [sdq](https://github.com/sdq/) 、 [StormXX](https://github.com/StormXX/) | 92 | | [Graphics教程第二部分-Gradients与Context](issue-5/Swift-Core-Graphics教程第二部分-Gradients与Context.md) | [HarriesChen](https://github.com/mrchenhao) | 93 | | [Core Graphics 教程第三部分(Swift) - Patterns 和 Playgrounds](issue-5/Core Graphics 教程第三部分(Swift) - Patterns 和 Playgrounds.md) | [MrLoong](https://github.com/MrLoong) | 94 | 95 | 96 | ### 第四期 (2015.5.15) 97 | | 文章标题 | 译者 | 98 | |----------------------|------------------------| 99 | | [关于AFNetworking安全bug的回复](issue-4/关于AFNetworking安全bug的回复.md) | [Lollypo](https://github.com/Lollypo/) | 100 | | [Swift和自动引用计数器整理之强、弱和无主引用](issue-4/Swift和自动引用计数器整理之强、弱和无主引用.md) | [samw00](https://github.com/samw00/) | 101 | | [Swift扩展的三个微妙细节](issue-4/Swift扩展的三个微妙细节.md) | [samw00](https://github.com/samw00/) | 102 | | [什么是委托代理?-Swift开发者指南](issue-4/什么是委托代理-Swift开发者指南.md) | [sdq](https://github.com/sdq/) | 103 | 104 | 105 | ### 第三期 (2015.5.8) 106 | | 文章标题 | 译者 | 107 | |----------------------|------------------------| 108 | | [Swift的响应式编程](issue-3/Swift的响应式编程.md) | [Mr.Simple](https://github.com/bboyfeiyu/) | 109 | | [iOS后台模式开发指南](issue-3/iOS后台模式开发指南.md) | [MollyMmm](https://github.com/MollyMmm/) | 110 | | [iOS今日扩展](issue-3/iOS今日扩展.md) | [Harries Chen](https://github.com/mrchenhao/) | 111 | 112 | 113 | 114 | ### 第二期 (2015.5.1) 115 | | 文章标题 | 译者 | 116 | |----------------------|------------------------| 117 | | [使用Swift自定义ViewController切换效果与动画](issue-2/自定义ViewController切换效果与动画.md) | [Shopie](https:www.devtd.cn) | 118 | | [Swift样式指南2015年4月更新](issue-2/Swift样式指南2015年4月更新.md) | [liulinxu](https://github.com/liulinxu) | 119 | | [GCD概述、语法以及好的示例](issue-2/GCD概述、语法以及好的示例.md) | [starmier](https://github.com/starmier/) | 120 | 121 | 122 | ### 第一期 (2015.4.24) 123 | | 文章标题 | 译者 | 124 | |----------------------|------------------------| 125 | | [iOS编程101-如何生成圆形和圆角的图像](issue-1/iOS编程101-如何生成圆形和圆角的图像.md) | [7heaven](https://github.com/7heaven) | 126 | | [CocoaPods指南](issue-1/CocoaPods指南.md) | [Lollypo](https://github.com/Lollypo) | 127 | | [iOS开发-可滑动的单元格](issue-1/iOS开发-可滑动的单元格.md) | [Harries Chen](https://github.com/mrchenhao) | 128 | | [View Debugging in Xcode 6 ](issue-1/View-Debugging-in-Xcode-6.md) | [Mr.Simple](https://github.com/bboyfeiyu) | 129 | -------------------------------------------------------------------------------- /git操作流程.md: -------------------------------------------------------------------------------- 1 | # git参与写作流程 2 | 3 | ## 一、fork一份到你的个人账户下 4 | 在主仓库的右上角点击fork,将主仓库fork一份到你的个人名下。此时你就有了一份主仓库的拷贝,但是这份拷贝并不会自动与主仓库进行同步。 5 | 6 | ## 二、完成翻译任务 7 | 把你fork的仓库clone到你的本机中,然后完成相应的翻译任务。完成翻译之后先提交到本地,也就是在执行了git commit之后,此时你需要先从主仓库pull最新的数据。如果你还没有通过git remote add 添加主仓库的地址,并且将仓库命名为tech,那么你需要如下命令手动添加(以下所有的命令都是在你本地的仓库目录下)。 8 | 9 | 针对Android翻译项目 : 10 | 11 | ``` 12 | git remote add tech git@github.com:bboyfeiyu/android-tech-frontier.git 13 | ``` 14 | 15 | 针对iOS翻译项目 : 16 | 17 | ``` 18 | git remote add tech git@github.com:bboyfeiyu/iOS-tech-frontier.git 19 | ``` 20 | 21 | ## 三、从主仓库pull master分之的最新数据 22 | 添加了主仓库之后(只需添加一次),你就可以通过如下命令从主仓库更新最新数据: 23 | 24 | ``` 25 | git pull tech master 26 | ``` 27 | 通过这个命令之后你就和主仓库的数据进行了同步,此时可能会发生冲突,冲突的原因是多个人同时修改了一个文件的同一块地方,导致git没法自动合并。可以阅读[这篇文章](http://www.cnblogs.com/sinojelly/archive/2011/08/07/2130172.html)解决冲突,解决冲突之后就可以提交到你的个人仓库。依次执行下面三个命令 : 28 | 29 | ``` 30 | git push add . 31 | ``` 32 | 33 | ``` 34 | git push commit -m "添加了xxx文章" 35 | ``` 36 | 37 | ``` 38 | git push orign master 39 | ``` 40 | 41 | ## 向主仓库发起pull request 42 | 将最新的数据提交到你的个人仓库之后就可以向主仓库发起pull request。此时由于你上一步已经同步了主仓库的最新数据,因此就可以自动被合并。发出pull request之后等待管理处理即可,此后注意查看邮箱状态,校对人员的校对信息会发到你的个人邮箱中。 43 | 44 | ## 校对完成,管理合并你的pull request,翻译完成 45 | 46 | -------------------------------------------------------------------------------- /git简单使用教程.md: -------------------------------------------------------------------------------- 1 | #git简单教程 (适用于参与开发技术前线) 2 | 3 | ## 1、git的安装与配置 4 | [Git详解之一:Git起步](http://blog.jobbole.com/25775/) 5 | 6 | ## 2、参与开发技术前线的项目 7 | 8 | 1. 首先到[iOS-tech-frontier](https://github.com/bboyfeiyu/iOS-tech-frontier),将该仓库fork(右上角)到你的个人账户下,因此你首先需要注册一个github账户; 9 | 2. 用命令行进入到你的某个目录下,然后输入如下命令 : `git clone https://github.com/这里替换为你的用户名/iOS-tech-frontier`将你fork的那份克隆下来; 10 | 3. 进入到iOS-tech-frontier目录中,在对应期数的文件夹中(例如,issue-2代表第二期)创建你的文章文件,例如:swift编程指南.md,注意文件名不要有空格; 11 | 4. 按照规范翻译文章,并且保留原文,每段英文下面跟一段译文; 12 | 5. 完成翻译之后,通过如下命令提交你的工作。首先提交到本地,`git add .`,然后`git commit -m "我翻译了xxx"`,最好通过`git push origin master`将你的翻译内容提交到github上; 13 | 6. 等待提交结束之后,你的提交也只是提交到了你个人的项目中,此时你需要向主仓库发一个pull request (简称 pr ) 请求,可参考[Fork + Pull模式](http://www.worldhello.net/gotgithub/04-work-with-others/010-fork-and-pull.html); 14 | 7. 发布pr之后管理员会安排人员进行文章校对,有问题的地方校对人员会在pr下进行评论,翻译人员确认问题之后修改问题即可; 15 | 8. 校对并且修改完之后翻译人员更新pr,管理员确认没有问题之后会合并该pr,整个翻译流程就此结束啦! 16 | 17 | 如果在这个过程中有冲突,翻译人员需要先解决冲突,可以参考[Git下的冲突解决](http://www.cnblogs.com/sinojelly/archive/2011/08/07/2130172.html)。 18 | 19 | ## 3、更详细的资料 20 | 21 | [git - 简易指南](http://www.bootcss.com/p/git-guide/) 22 | [pro git中文版](http://pan.baidu.com/s/1o6Hsets) -------------------------------------------------------------------------------- /issue-1/View-Debugging-in-Xcode-6.md: -------------------------------------------------------------------------------- 1 | # View Debugging in Xcode 6 2 | 3 | # Xcode 6中的View调试 4 | 5 | >* 原文链接 : [view-debugging-in-xcode-6](http://www.raywenderlich.com/98356/view-debugging-in-xcode-6) 6 | * 原文作者 : [Scott Berrevoets](http://www.raywenderlich.com/98356/view-debugging-in-xcode-6) 7 | * [译文出自 : 开发技术前线 www.devtf.cn](www.devtf.cn) 8 | * 译者 : [Mr.Simple](https://github.com/bboyfeiyu) 9 | * 校对者: [这里校对者的github用户名]() 10 | * 状态 : 校对中 11 | 12 | 我们在开发app的时候经常会遇到一些在代码中难以发现的view中的bug或者自动布局约束。 13 | 14 | 这时候,掌握view调试的技巧就显得很重要—但是在Xcode6出现之前,调试view从未如此简单。 15 | 16 | 我们现在可以直接在Xcode中直观地看到view的整个层级,而不是在console中打印frames或者试图在脑海里凭空描绘布局。 17 | 18 | 本教程会带大家一览各种选择,并自行决定选择适合自己的。那么,大家准备好写一些代码了吗?告诉大家一个好消息,其实不必写代码。相反,我们只需要观看开源库中的view层级来更好地了解它是怎么写出来的—我们甚至可以连代码都不用看。 19 | 20 | ![pic-1](http://cdn1.raywenderlich.com/wp-content/uploads/2015/04/app-view.jpg) 21 | 22 | *在Xcode中调试view从未如此简单* 23 | 24 | 25 | ##入门指南 26 | 27 | 本教程将使用到的开源库是由Jesse Squires编写的JSQMessagesViewController。大家应该觉得这个库的UI看起来很熟悉吧,因为Jesse特意把它设计得像Messages应用。 28 | 29 | 首先我们要跳转到[GitHub project page](https://github.com/jessesquires/JSQMessagesViewController)页面,下载源码并解压到相应路径中。 30 | 31 | >注:这个库使用CocoaPods来管理其对其他库的依赖性。如果大家不熟悉CocoaPods的工作原理,我建议大家在进行下一步之前先点击此处CocoaPods tutorial了解一下。 32 | 33 | 下一步,找到终端中刚才解压的项目路径然后运行`pod install`来安装所需的依赖。然后打开`JSQMessages.xcworkspace` 并在`iPhone 5s simulator`上运行这个app。(大家也可以使用其他的simulator,但是本教程是基于4英寸显示屏的尺寸,所以选择同样的尺寸比较容易推进。) 34 | 35 | >注:实时View调试仅在iOS 8上运行app是有效。本工具即使在Xcode 6环境下,在iOS 7上仍无法使用。 36 | 37 | 点击`Push via storyboard`,然后我们就来到了与乔布斯和库克的短信对话线程。(哈哈,看到这里大家心里可能有点发毛,甚至怀疑自己是不是在做梦。额,这不是他俩本人好吗!)以下就是大家看到的view。 38 | 39 | ![pic-2](http://cdn4.raywenderlich.com/wp-content/uploads/2015/04/Screen-Shot-2015-04-07-at-12.03.28-PM.png) 40 | 41 | *Steve Wozniak也来凑热闹啦!* 42 | 43 | 返回Xcode后点击调试条上的`Debug View Hierarchy`按钮。或者,直接转到`Debug\View Debugging\Capture View Hierarchy`。 44 | 45 | ![pic-3](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-1.png) 46 | 47 | 此时Xcode正在阻断app并将主动权交给调试程序,正如我们按了调试条上的暂停键把app给暂停了。 48 | 49 | 另外,Xcode使用画布代替了代码编辑器,并在画布上绘制app主窗口的完整view层级,包括用来表明每个view的边界的细线部分(我们称之为线框)。 50 | 51 | 52 | ![pic-4](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-2.png) 53 | 54 | 我们知道,当我们向view层级中添加一个subview时,我们其实是在向现有的view堆栈上添加一个层次。因为大部分的view不会重叠,当我们运行app时所有的view看起来只是一个大层次的一部分而已。此刻我们看到的屏幕与此非常相似,只不过多了几条线而已。 55 | 56 | 那这几条线有什么用呢?此刻我们看到的是一个从上往下看的view堆栈视觉图,想象一下,如果我们能想象出各层次在堆栈中的位置的话会怎么样呢?点击并拖拽画布,我们就可以与View层级的3D视觉效果模型进行互动,而不再是平面的2D效果。 57 | 58 | ![pic-5](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-3.gif) 59 | 60 | 我们可以从侧面、顶部、底部、从各个角度甚至从后部观察整个层级。 61 | 62 | ![pic-6](http://cdn5.raywenderlich.com/wp-content/uploads/2015/04/views.png) 63 | 64 | >注:大家的画布呈现的效果和本教程的效果可能会有些差异。为了确保进度统一,请大家按住`cmd+6`跳转到调试导航器。 65 | > 66 | 67 | 在边框底部我们可以看到左侧有两个按钮。如图所示,同时取消选定两个按钮。如果未取消选定,画布可能就会隐藏了某些view。 68 | 69 | ![pic-7](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/filter-image1.png) 70 | 71 | 72 | ##探索View层级 73 | 74 | 用透视法观察这个3D模型的最自然和最通用的方法是从左侧来看,如下图所示(稍后我会在本教程讲到原因)。 75 | 76 | ![pic-8](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-4.png) 77 | 78 | 如果你想查看这个app是如何构建的,这个图把view的层级用透视的感觉展现了出来。。但是,堆栈的“底部”(左侧)似乎存在许多空白的view。这是为什么呢? 79 | 80 | 点击最左侧的view(也就是最后面那个)后,我们可以看到Xcode会将其显示高亮来标示我们选择的地方。我们还可以看到Jump Bar(就在画布的上方)把UIWindow显示为最后一项--最后一项会反映当前选择的view及其类的类型。 81 | 82 | ![pic-9](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-5.png) 83 | 84 | 由于这款app只用了一个窗口,我们大可以假设Jump Bar开始处的`UIWindow`就是app的主窗口,换句话说,是`AppDelegate`的`Window`属性。 85 | 86 | 好了,这样我们就知道如何找到window了,但是我们似乎并不需要查看它。下一个view又该怎么弄呢?在画布上点击窗口右侧的view(也就是顶部)并重新观察Jump Bar。`UILayoutContainerView`对吧?这特么连一个公共类都算不上! 87 | 88 | ![pic-10](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/iw21s.jpg) 89 | 90 | 那一步之后,view层级是这样的: 91 | 92 | 1. `UINavigationTransitionView`:是指导航控制器过渡的container view 93 | 94 | 2. `UIViewControllerWrapperView`:是指含有view controller的`view`属性的wrapper view 95 | 3. `UIView`:是指一个view controller的一个最高层view(与view controller的`view`属性相同) 96 | 4. `JSQMessagesCollectionView`:是指该项目用来显示所有短信息的collection view 97 | 98 | ##F聚焦 View的Interest 99 | 100 | 在调试这一独特的view层级时,前四个view(从窗口处开始)只是视觉噪声,没有实际意义;它们的存在可能会干扰大家去了解这个view层级中的其他信息。如果大家能够拨开迷雾,过滤掉这些视觉噪声,那才是极好的... 101 | 102 | 大家还可以:观察画布右下角的双向滑块。默认情况下两个滑块分别分布在最左和最右。 103 | 104 | ![pic-11](http://cdn2.raywenderlich.com/wp-content/uploads/2015/04/Screen-Shot-2015-04-07-at-12.24.14-PM.png) 105 | 106 | 将左边的滑块稍微向右移动一点点,我们可以看到代表app窗口的线框从画布上消失了。再向右移动一点,我们可以发现,`UINavigationTransitionView`也消失了。 107 | 108 | 把左边的滑块尽量拖拽至隐藏`JSQMessagesCollectionView`的所有父视图,那么画布应该如下图所示: 109 | 110 | ![pic-12](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-6.png) 111 | 112 | 我们看右边的导航条就没有那么分散,但它其实是被展示在了collection view的顶部,我们难以看到底部发生了什么变化。还好,我们可以把它隐藏起来。 113 | 114 | 由于我们现在关注的是包含了许多组成导航条的小视图的屏幕区域,所以在这里我把导航条放大,以便大家看清楚我到底怎么操作。 115 | 116 | 使用画布下方靠中间部分的三个缩放控制按钮: 117 | 118 | ![pic-13](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/Screen-Shot-2015-03-15-at-5.03.26-PM.png) 119 | 120 | 大家可能已经猜到了,+按钮代表放大,—按钮代表缩小,=代表将画面重置到正常尺寸。放大以后我们可以更清楚地观看导航条。 121 | 122 | ![pic-14](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-7.png) 123 | 124 | 125 | >注:如果大家用的是触摸屏,收放手势则代表画面的缩放。如果由于放大倍数太高导致画面不能全屏显示完整,我们可以上下左右移动触摸屏以查看未显示的部分。我们还可以使用鼠标滚轮来操作。 126 | 127 | 放大之后的toolbar很容易看到细节部分,但是view之间会有些重叠,所以要区分哪个view是哪个并不容易。 128 | 129 | 要解决这个问题,我们需要使用画布左下角的间距滑块。滑块越往右滑,Xcode中不同view之间的间距越大。 130 | 131 | ![pic-15](http://cdn3.raywenderlich.com/wp-content/uploads/2015/04/Screen-Shot-2015-04-07-at-12.29.20-PM.png) 132 | 133 | 在这一特例中,我们需要将滑块尽量往右移动,避免view与toolbar重叠。要想达到理想的效果,我们可能会需要在画布上单击并拖曳鼠标慢慢实现。 134 | 135 | ![pic-16](http://cdn4.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-8.png) 136 | 137 | 现在我们就完美地操作了3D模型,从而轻而易举地将导航条隐藏起来。 138 | 139 | 将右边滑动条的右滑块慢慢往左拖曳,直到`UINavigationBar`处。记住,我们可以使用Jump Bar来选择最上层从而判定每个view的类。我们会看到先消失的是导航条目,然后是包含它们的按钮,接下来是几个私有view,最后是导航条。 140 | 141 | ![pic-17](http://cdn3.raywenderlich.com/wp-content/uploads/2015/04/HideNavBar.png) 142 | 143 | *看,导航条不见了吧!* 144 | 145 | >注:如果我们把画布翻转以下让顶层处在左边,然后再观察这个3Dview层级,滑动条的左滑块移动右边堆栈底部的view。同样地,右滑块会移动左边的view。 146 | 147 | >把滑动条从左移到右,从而使view由右向左消失(反之亦然)是反直观的,这也是为什么我们在观看顶层在右侧的模型时觉得这是最自然的角度。 148 | 149 | 很遗憾,隐藏导航条(root view为`_UIBackdropView`)的同时,view会导致屏幕底部的toolbar的item内容消失。我们调节缩放尺寸或者将画布往下移看看是什么情况。 150 | 151 | 由于toolbar item是屏幕的一个重要部分,所以我想大家有必要看看,所以我只隐藏了到`_UIBackdropView`(不含)的这一部分的view。操作完成后导航条堆栈应该是下图的样子。 152 | 153 | ![pic-18](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-9.png) 154 | 155 | ##更多View选项 156 | 157 | 不相关的view被隐藏之后,我们得重新看一遍屏幕。我们可以把模型拖曳到原位,但是有时候确实很难百分百回到原位。别急,我们还留了一招。 158 | 159 | 看到缩放按钮左边的那四个按钮了吗?左起第三个是`ResetViewing Area`按钮。它的作用是撤销屏幕旋转并显示view层级的正面构造,就像simulator或真机上的一样。 160 | 161 | ![pic-19](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/Screen-Shot-2015-03-15-at-5.03.26-PM-copy.png) 162 | 163 | 此时画布看起来应如下图所示: 164 | 165 | ![pic-20](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/head-on-view-with-wireframes.png) 166 | 167 | 大家可能注意到了,我们在调试器里看到的东西并不完全是app实际运行是我们所看到的。 168 | 169 | 首先,每个单独的view仍然有线框;通过这些线框我们看到透明的或者无内容的view,但是如果我们不看细节的话,它们会变得非常模糊。 170 | 171 | 我们可以通过`View Mode button`(`Reset Viewing Area`按钮右边那个)将其关闭。当我们点击view Mode按钮时,我们可以决定要不要显示view的线框和/或其内容。 172 | 173 | ![pic-21](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/wireframes.gif) 174 | 175 | 一个只有线框的view在定位时非常有用,view的外形并不那么重要。而当我们想调试view的外观时,view的内容就非常有用了。 176 | 177 | 为了减少在这种情况下线框引起的杂乱(尤其是导航条和工具条附近),我们要把view Mode切换到`Contents`来去除所有的线框,只留下app的核心部分。 178 | 179 | ![pic-22](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/no-wireframes.png) 180 | 181 | 下一步,当前的view缺少了几样东西。运行app时,我们可以看到文本泡泡上方显示发送者姓名的标签或短信的时间戳,以及最后一个文本泡泡显示的金门大桥图像。但调试器却没有显示这些标签和图像! 182 | 183 | 要想解决这个问题,首先请大家看到画布中间那行按钮的第一个按钮。它是一个显示或隐藏省略view的触发器。这些省略的view都已经将它们的`clipsToBounds`属性设置成了YES。 184 | 185 | ![pic-23](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/clip-to-bounds-button.png) 186 | 187 | 这恰恰就是那些标签里面所含的内容,大概是因为姓名或日期太长,不能延伸到标签范围以外吧。图像也是同样的原理,使用圆角半径剪裁来达到圆角的效果。点击触发器我们就会看到Xcode中显示了view。 188 | 189 | ![pic-24](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-11.png) 190 | 191 | >注:大家可能会看到新的可视条目的线框。如果真有的话,可以使用我们之前用过的View Mode按钮将线框先打开后关闭,问题应该就解决了。 192 | 193 | 这下大家看到了吧:我们在Xcode里复刻了一个近乎完美的view层级。 194 | 195 | ![pic-25](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/ragecomic1.png) 196 | 197 | *也不难吧?* 198 | 199 | 200 | ##查看View 201 | 202 | 既然重要的部分都搞定了,那我们来看看这些不同的view的布局吧。 203 | 204 | 大家已经知道,collection view可以把所有view集合起来。但是大家想想,如果我们能把各个起作用的元素作一个概观,那岂不是更棒?我要告诉大家的是,我们当然可以实现! 205 | 206 | 按住`cmd + 6`转到调试导航器。由于这是一个普通的调试会话,所以调试导航器会提供关于当前会话的上下文信息。对于view调试来说,这意味着Xcode会为所有的窗口提供一个包含所有视图的树形View。树形view展开之后如下图: 207 | 208 | ![pic-26](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-12.png) 209 | 210 | >注:我们可以看到调试导航器的底部有一些控制树形view显示何种条目的选项。Apple的文件材料显示,左侧的按钮会把私有元素从view实现系统中过滤出来,不过这个按钮在Xcode 6.2环境下似乎不管用。 211 | 212 | >右侧按钮则把那些将属性设置为YES的view隐藏起来,搜索栏只显示符合搜索条件的view和constrait。 213 | 214 | >本教程将两个按钮都取消了选择且不使用搜索过滤。 215 | 216 | 从这里开始我们可以开始稍微往深一点探索了。将最后一个`JSQMessagesCollectionViewCellOutgoing`展开,下面只有一个子视图: `UIView`。 217 | 218 | 如果大家以前接触过collection view,大家就会知道这其实是讲得通的。因为任何一个`UICollectionViewCell`都会有一个包含单元内容的`contentView`属性。 219 | 220 | 点击(但不要展开)调试导航器的`UIView`之后我们可以看到,此时Xcode已经在画布中将其显示高亮,这样大家可以很容易找到它在哪儿。 221 | 222 | ![pic-27](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-13.png) 223 | 224 | 想要真正明白iOS怎样定位那个单元,我们需要按住`cmd + option + 4`打开Size Inspector。顶部显示的是view的边界、定位以及锚定点。 225 | 226 | ![pic-28](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-14.png) 227 | 228 | 但是,真正有意思的是该view中应用的Auto Layout约束列表。大家可以立刻知道单元内容的宽为312点,高为170点,并且位于其父视图的中心。内含的单元也是312*170,所以大家明白了吧,content view占了整个单元的空间。 229 | 230 | 下方是灰色的约束,表明这些约束决定了view及其子视图之间的关系。 231 | 232 | 要想了解某个特定约束的细节,首先我们要展开树形view中的那个view,然后是约束项。大家看到的是跟Size导航器中列出的相同的约束。 233 | 234 | ![pic-29](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-15.png) 235 | 236 | 点击第一个约束(对我个人来说这是一个self.midX约束)并按住`cmd + option + 3`切换到Object inspector。此时可以看到约束的item,multiplier,constant 和优先级。这非常像我们在在Interface Builder里面编辑一个约束时看到的summary。 237 | 238 | ![pic-30](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-16.png) 239 | 240 | 除了尺寸大小和约束信息以外,我们还可以看到与object inspector中特定view显示有关的其他细节。返回至调试导航器,展开树形结构上的`UIView`之后我们看到它包含了三个`JSQMessageLabel`,两个`UIView`。选择第一个`JSQMessageLabel`(与时间戳一起的那个),然后打开Object inspector。 241 | 242 | ![pic-31](http://cdn3.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-17.png) 243 | 244 | 第一部分显示的是object的类名称和存储地址,第二部分显示的是从属于其外表的object的各公共属性的值。 245 | 246 | 这里我们可以看到标签的文本颜色为不含alpha的0.67灰,字体大小为12pt。 247 | 248 | 其他类中也包含了一些有用的信息,比如界面是如何可视化的。回到调试导航器,展开单元根`UIView`下的第二个UIView,可以看到一个`UIImageView`。 249 | 250 | ![pic-32](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-18A.png) 251 | 252 | 选择树形结构中的image view后查看Object inspector。 253 | 254 | ![pic-33](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-18B.png) 255 | 256 | 此时我们看到,view显示了用户的头像--在这种情况下,指的是作者的姓名首字母大写,JSQ。我们既可以查看正常图像,也可以查看带标记的图像以及颜色偏暗的图像,带标记高亮图像,这些图像可以反映用户何时点击单元。 257 | 258 | 单元根view下的另外两个`JSQMessageLabel`目前没有文本,但是它们拟用于显示收件箱新消息的发送人姓名以及短信发送失败时提示错误信息。 259 | 260 | 以上就是在Xcode中调试view的步骤,够简单吧?想要继续运行app,我们只需要点击调试条上的Continue按钮,或者跳转到**`Debug\Continue`**,就和我们在普通调试中的操作一样。 261 | 262 | ##实时修改 263 | 264 | 到目前为止,大家都了解了在Xcode中调试view的基本方法,那么现在我们就来练练手:我们在教程中探索过了collection view,现在通过调试器将其垂直滚动条指示器标红。 265 | 266 | 下面提出两点建议: 267 | 268 | 1. view调试和其他普通程序调试一样,我们可以在控制台使用expr或者其他命令,但是要记住,在恢复执行前这些改变不会显示出来。关于其他命令的更多信息,大家可以参考这个教程[debugging apps in Xcode](http://www.raywenderlich.com/28289/debugging-ios-apps-in-xcode-4-5)。 269 | 270 | 2. 由于Objective-C的指针其实就是存储地址,所以我们发送一个object时实际上就是发送一个存储地址。这在调试器中是一样的道理,所以像这样的命令会打印出存储地址中的object的描述。 271 | 272 | 273 | 故障前先做几次尝试总是没错的,请看以下解决方法! 274 | 275 | 内部解决方法:解决方法 显示/隐藏 276 | 277 | 首先将view mode设置为“Contents” 278 | 279 | ![pic-34](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-19.png) 280 | 281 | 在调试导航器中展开树形view中的collection view查看其子视图。 282 | 283 | ![pic-35](http://cdn1.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-20.png) 284 | 285 | collection view里最后两个view就是的两个例子。它们分别是水平滚动条和垂直滚动条的指示器。点击第二个后我们可以看到垂直指示器在画布上被显示为高亮。 286 | 287 | ![pic-36](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-21.png) 288 | 289 | 将Object inspector的image view存储地址复制下来。 290 | 291 | ![pic-37](http://cdn2.raywenderlich.com/wp-content/uploads/2015/03/Screenshot-22.png) 292 | 293 | 此处滚动条指示器的存储地址是`0x7fde6c484640`。给滚动条指示器上色所需要的步骤是发送一个`setBackgroundColor`信息到该地址的object。以下是具体操作: 294 | 295 | ```expr (void)[0x7fde6c484640 setBackgroundColor:[UIColor redColor]]``` 296 | 297 | 继续运行app,大家可以注意到当我们滚动collection view里的滚动条时,滚动条指示器变红。 298 | 299 | 恭喜大家掌握了在Xcode 6中调试view的基本知识! 300 | 301 | ##老式调试方法 302 | 303 | 实时view调试使得在Xcode 6中调试view变得更简单,但这并不意味着我们之前偏好的老式技巧一无是处。其实,iOS8为view调试技能包中引入了一个很受欢迎的附加物:`_printHierarchy`。 304 | 305 | >注:大家已经掌握了在Xcode 6中调试view的基本知识,所以如果大家愿意,尽可以把这些可选部分省略掉。但是,大家如果对一些好用的老式技巧感兴趣的话,接着往下看! 306 | 307 | ###打印View Controller Hierarchy 308 | 309 | `_printHierarchy`是`UIViewController`的一个私有方法,我们可以用它来打印控制台的**`view controller`**层级。建立之后运行,选择**`view controller`**然后点击调试条上的暂停按钮。 310 | 311 | 现在在控制台输入这些信息,然后按return。 312 | 313 | ```po [[[[UIApplication sharedApplication] keyWindow] rootViewController] _printHierarchy]``` 314 | 315 | 我们得到的结果应该与以下结果类似: 316 | 317 | ```, state: appeared, view: 318 | | , state: disappeared, view: not in the window 319 | | , state: appeared, view: ``` 320 | 321 | 这表明,`UINavigationController`的第一个view controller是`TableViewController`--也就是我们选择如何push controller时的那个。第二个view controller是`DemoMessagesViewController`,也就是我们正在调试的view controller。 322 | 323 | 也许我举的这个特例并未让大家感到欣喜,但是如果大家的导航控制器中同时包含几个子视图控制器,并且modal view controller 中的popover包含一个tab bar controller(我其实也觉得我app里面的某些UI不是很好...),那么这对理解view controller层级的运作原理非常有帮助。 324 | 325 | ###打印View层级 326 | 327 | 如果大家的空间想象力不是很强并且倾向于直观地观看view层级的文本概述,大家可以使用`UIView`上传统的私有的`recursiveDescription` 。这里打印出的view层级与上述view controller层级非常相似。 328 | 329 | 打开**`Views\JSQMessagesCollectionViewCellOutgoing.m`**并在`awakeFromNib`中添加一个断点。 330 | 331 | ![pic-34](http://cdn5.raywenderlich.com/wp-content/uploads/2015/03/breakpoint.png) 332 | 333 | 建立并运行,然后选择**`Push via Storyboard`**。`JSQMessagesCollectionViewCellOutgoing`加载的同时调试器会分裂。然后输入以下内容至控制台: 334 | 335 | ```po [self.contentView recursiveDescription]``` 336 | 337 | 这一步会打印出`JSQMessagesCollectionViewCellOutgoing`的`contentView`的层级,如下所示: 338 | 339 | ```; layer = > 340 | | > 341 | | > 342 | | > 343 | | | > - (null) 344 | | > 345 | | | > - (null) 346 | | >``` 347 | 348 | 这看起来非常基础,但是我们想要在 iOS 8之前的系统中调试view层级的话它还是非常有用的。 349 | 350 | ###使用debugQuickLookObject 351 | 352 | 最后,Xcode 5.1引入了一个叫做**`Debug Quick Look`**的特性。这个特性在我们调试中并且知晓了代码中特定点的object的外形时非常有用。 353 | 354 | 我们的自定义类可以执行`debugQuickLookObject`方法并且返回Xcode中可视觉上显示的一切信息。然后,当我们在进行调试并且需要查看object时,我们可以使用quick look,Xcode会将该object视觉再现。 355 | 356 | 比如,`NSURL`执行 `debugQuickLookObject`时会返回一个`UIWebView`,这个`UIWebView`中含有URL,所以我们可以看到URL背后所包含的内容。 357 | 358 | 更多关于使用Quick Look的调试信息,请参阅[文件材料](https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/CustomClassDisplay_in_QuickLook/Introduction/Introduction.html#//apple_ref/doc/uid/TP40014001-CH1-SW1)。 359 | 360 | ##未来趋势在何方? 361 | 362 | 以上就是实时View调试的全部内容。这个工具简单实用,并且能够节省我们手动筛选view层级并试图理解其绘制视图的原理和地址的时间。 363 | 364 | 如果大家想找一个比Xcode更先进更全面的工具,我建议大家看看Reveal。这是一个付费app,但是它的功能比Xcode的view调试更加强大。大家可以[点击此处](http://www.raywenderlich.com/99594/view-debugging-with-xcode-and-reveal-tech-talk-video)查看我们关于这方面的技术讲座。 365 | 366 | 希望大家喜欢本教程并且通过本教程的学习在调试UI时更得心应手。如果大家有任何建议或问题,欢迎在下方的论坛讨论区发言! 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | -------------------------------------------------------------------------------- /issue-1/iOS开发-可滑动的单元格.md: -------------------------------------------------------------------------------- 1 | `iOS开发-可滑动的单元格` 2 | --- 3 | 4 | >* 原文链接 : [Swipeable table view cells in iOS apps](https://medium.com/ios-os-x-development/swipeable-table-view-cells-in-ios-apps-472da0af1935) 5 | * 译者 : [Harries Chen](https://github.com/mrchenhao) 6 | * 校对者: [Mr.Simple](https://github.com/mrchenhao) 7 | 8 | 我在过去的一个月里致力于开发我的创业项目,其中有一个功能就是我想让用户通过左右滑动表格来显示更多的选项(就像系统自带的邮件程序现实更多和删除一样,在这篇文章中我将讨论一些关于这方面的方法,以及如何动手实现它) 9 | 10 | 首先,让我们来看一下现有的解决方法。 11 | 12 | * UITableViewRowAction (new in iOS 8) 13 | 14 | 这是最好的方法如果你只是希望加一些从左滑到右才会出现的按钮。 15 | 16 | * [SWTableViewCell](https://github.com/CEWendel/SWTableViewCell) 17 | 18 | 这是一个功能很全的按钮库,支持左滑和右滑,同时也可以在iOS8以前的版本中使用 19 | 20 | * [MGSwipeTableCell](https://github.com/MortimerGoro/MGSwipeTableCell) 21 | 22 | 如果你想要实现表格的左右滑动功能,你可以从上面的解决方按照找到你需要的。但是为什么我们还要自己来实现呢?就我而言,我需要MGSwipeTableCell的一些功能,但是有的功能它对我并没有用,如果你想要自己来实现它,或者仅仅只是想要知道它的原理,那么接下去看。 23 | 24 | 25 | ##原理剖析 26 | 27 | 制作一个可滑动的UITableViewCell其实是一件非常简单的事情,它的要点就是在contentView里面包含了UIScrollView,在UIScrollView里面有两个UIView,一个用于放置按钮,一个用于放置单元格的内容,难点在于如何配置滑动页面,下面是一张图片来展示UIScrollView里的主要内容。 28 | 29 | ![这里写图片描述](http://img.blog.csdn.net/20150420235653232) 30 | 31 | * frame 32 | 33 | 视图的大小,(也就是实际在屏幕上显示的面积),在上图中,外面的高度看起来会比UIScrollView的高度高很多,实际上在代码中他们的高度是一样的,只是为了更好的展示。 34 | 35 | * contentSize 36 | 37 | 在可滚动的视图中,显示的大小和常常与实际内容的大小不同,当实际内容比显示的内容要大的时候,就可以进行滚动,以便让用户看到所有的内容。这个属性代表了在scrollview实际内容的宽和高。在这个例子中,我们实际上会保持它和视图大小相同,这要归功于一个叫做contentInset属性 38 | 39 | * contentInset 40 | 41 | 这个属性将使我们的扩大内容区域之外的可滚动范围。通常,当contentSize属性被设置为与frame相同的(或更小)的尺寸作为视图本身,视图并不会产生滚动的效果。然而,contentInset将延长使内容产生滚动,让你可以滚动至内容边界的区域。这有什么用吗?例如,它可以用在显示上拉刷新的加载画面之中。在这个例子中,我们将使用它来腾出空间已显示额外的按钮。 42 | 43 | * contentOffset 44 | 45 | 这个值代表当前滚动视图的滚动量,当用户滚动的时候,这个值将会被改变,你也可以通过程序来改变它的值,我们随后将会通过改变这个值来控制按钮的出现。 46 | 47 | Implementing the cell, step by step 48 | This section will demonstrate how to actually make the cell display buttons when swiping on it. The code examples will be simplified to focus on the concept. At the end I will provide a full implementation. 49 | 50 | First, let’s subclass UITableViewCell to begin implementing our swipeable table cell. 51 | 52 | The easiest way to subclass classes is to go to File > New > File… (⌘N). Pick UITableViewCell as the class to subclass. 53 | 54 | ##一步步实现 55 | 56 | 本节将展示如何通过滑动来使单元格显示按钮。代码将被简化以便更好的理解这一概念。最后我将提供[完整的代码](https://github.com/blixt/SwipeableTableViewCell)。 57 | 58 | 第一步,子类化UITableViewCell 59 | 60 | ![这里写图片描述](http://img.blog.csdn.net/20150420235749907) 61 | 62 | 63 | 子类化最简单的方法就是文件>新建>文件...(⌘N)。选择的UITableViewCell作为类的子类。 64 | 65 | 因为我们希望实现对滚动控制,所以我们需要指定我们的类实现UIScrollViewDelegate协议。方法是后面加上尖括号,在里面写上需要实现的协议 66 | 67 | ``` 68 | @interface SwipeableTableViewCell:UITableViewCell 69 | 70 | ``` 71 | 72 | 接下来,我们将在单元格初始化的时候配置滚动视图。选择你想要的初始化方式,可以是`initWithCoder:`也可以是 `initWithStyle:reuseIdentifier:` 。我建议创建一个common方法来配置单元格,以便从不同的初始化放来调用。 73 | 74 | ``` 75 | // 创建一个可以横向滚动的滚动视图 76 | UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.bounds]; 77 | scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 78 | scrollView.contentSize = self.bounds.size; 79 | scrollView.contentInset = UIEdgeInsetsMake(0, 160, 0, 0); 80 | scrollView.delegate = self; 81 | scrollView.scrollsToTop = NO; 82 | scrollView.showsHorizontalScrollIndicator = NO; 83 | scrollView.showsVerticalScrollIndicator = NO; 84 | [self.contentView addSubview:scrollView]; 85 | self.scrollView = scrollView; 86 | 87 | ``` 88 | 89 | 当我们我们创建滚动视图后,我们需要做一些事情。首先,我们设置它的大小的。这意味着在父视图大小发生改变的时候应该怎样改变。我们希望滚动视图填充整个单元格当它运行时改变大小的时候(这很可能发生,因为在iPhone 5,6和6 plus都具有不同的屏幕宽度)。然后我们设置与滚动视图相同大小的内容,在左边插入与我们所需要添加的按钮宽度相同的视图。关闭scrollsToTop事件,因为我们不关心这个状态栏是否被点击(这个功能通常用于滚动视图顶部)。这将让我们的表视图利用这特性,而不是打破这一行为。同时我们也关掉水平和垂直滚动栏,这样,当我们滚动的时候半透明的黑条不会出现。 90 | 91 | ``` 92 | // 设置内容大小 93 | UIView *contentView = [[UIView alloc] initWithFrame:scrollView.bounds]; 94 | contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 95 | contentView.backgroundColor = [UIColor whiteColor]; 96 | [scrollView addSubview:contentView]; 97 | self.scrollViewContentView = contentView; 98 | 99 | UILabel *label = [[UILabel alloc] initWithFrame:CGRectInset(contentView.bounds, 10, 0)]; 100 | label.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 101 | [self.scrollViewContentView addSubview:label]; 102 | self.scrollViewLabel = label; 103 | 104 | ``` 105 | 106 | 不必惊讶于上述代码,我们只是增加了必要的部分呈现的表格单元格的主要区域 107 | 108 | ``` 109 | // 创建按钮的容器. 110 | UIView *buttonsView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 160, self.bounds.size.height)]; 111 | buttonsView.autoresizingMask = UIViewAutoresizingFlexibleHeight; 112 | self.buttonsView = buttonsView; 113 | [self.scrollView insertSubview:buttonsView atIndex:0]; 114 | 115 | // 创建两个按钮并放入容器. 116 | UIButton *yesButton = [UIButton buttonWithType:UIButtonTypeCustom]; 117 | yesButton.autoresizingMask = UIViewAutoresizingFlexibleHeight; 118 | yesButton.backgroundColor = [UIColor greenColor]; 119 | yesButton.frame = CGRectMake(0, 0, 80, self.bounds.size.height); 120 | [yesButton setTitle:@"Yes" forState:UIControlStateNormal]; 121 | [yesButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 122 | [self.buttonsView addSubview:yesButton]; 123 | 124 | UIButton *noButton = [UIButton buttonWithType:UIButtonTypeCustom]; 125 | noButton.autoresizingMask = UIViewAutoresizingFlexibleHeight; 126 | noButton.backgroundColor = [UIColor redColor]; 127 | noButton.frame = CGRectMake(80, 0, 80, self.bounds.size.height); 128 | [noButton setTitle:@"No" forState:UIControlStateNormal]; 129 | [noButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 130 | [self.buttonsView addSubview:noButton]; 131 | ``` 132 | 133 | 这将创建一个包含按钮的视图。这里需要注意的是,视图宽度应该是所有的按钮宽度的总和,它的需要插入在滚动视图之前(为了让显示在下面)。然后,我们只需要添加我们想要的按钮。为了使代码尽可能的简洁,我没有添加任何按钮的点击事件处理代码。 134 | 135 | 如果你此时运行程序,它可以滑动从而让按钮出现,但是如果你再iPhone6 或者更大的屏幕上运行的时候,将会遇到一个问题,我们稍后会解决它。另外一个需要解决的问题就是当我们在滑到一半就停止的时候会出现按钮显示一半的情况。我们先来修复它。 136 | 137 | ``` 138 | - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { 139 | if (scrollView.contentOffset.x < -scrollView.contentInset.left) { 140 | targetContentOffset->x = -scrollView.contentInset.left; 141 | } else { 142 | *targetContentOffset = CGPointZero; 143 | } 144 | } 145 | 146 | ``` 147 | 148 | 当你的类得到用户停止滑动的通知(通过UIScrollViewDelegate协议)你需要将滚动视图滚动到按钮的最左端(如果滑动量超过按钮)或者重新隐藏按钮(如果滑动量不够) 149 | 150 | 到了这里我们完成了一些基本的工作。接下来我们来处理一些细节上的问题。继续往下看 151 | 152 | 153 | 接下来最大的问题就是就是来适应更大的屏幕,(例如iPhone6)我们可以通过一个简单的方法来修复这个问题(如果你有更好的方法,务必告诉我)。 154 | 155 | ``` 156 | - (void)layoutSubviews { 157 | [super layoutSubviews]; 158 | // This is necessary to ensure that the content size scales with the view. 159 | self.scrollView.contentSize = self.contentView.bounds.size; 160 | self.scrollView.contentOffset = CGPointZero; 161 | } 162 | 163 | 164 | ``` 165 | 166 | 这段代码的作用就是在单元格的面积发生变化的时候,修复内容大小和偏移量 167 | 168 | 这会避免图形出现问题。接下来,我们需要阻止滚动视图向另外一个方向滚动,(因为没有任何按钮来显示)。这非常简单: 169 | 170 | ``` 171 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView { 172 | if (scrollView.contentOffset.x > 0) { 173 | scrollView.contentOffset = CGPointZero; 174 | } 175 | } 176 | ``` 177 | 178 | 如果用户向另外一个方向滑动,将偏移量设置为0来阻止它。 179 | 180 | 最后,你可能想让按钮安静的呆在原地,而不是与单元格一起移来移去。要做到这一点,我们将简单地移动按钮容器视图来抵消滚动偏移量。我们可以做到这一点,通过改变scrollViewDidScroll:上述方法如下: 181 | 182 | ``` 183 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView { 184 | if (scrollView.contentOffset.x > 0) { 185 | scrollView.contentOffset = CGPointZero; 186 | } 187 | 188 | self.buttonsView.frame = CGRectMake(scrollView.contentOffset.x, 0, 160, self.buttonsView.frame.size.height); 189 | } 190 | 191 | ``` 192 | 193 | ##还可以做些什么 194 | 现在,我们已经制作了最基本功能,还有很多地方仍然可以提高。比如说: 195 | 196 | * 滚动表视图时自动隐藏按钮 197 | * 在滑动一个单元格的时候自动隐藏其它的单元格按钮 198 | * 允许在单元的两侧的按钮 199 | * 启用在外部创建按钮(例如,在视图控制器) 200 | * 动态计算按钮的大小 201 | * 改进开启和关闭的动画效果 202 | 203 | 204 | 最后,我实现上面的功能和所提供的代码在GitHub上:[GitHub](https://github.com/blixt/SwipeableTableViewCell) 205 | 感谢您的阅读!如果有任何问题。最简单的方式就是在我Twitter下留言,我得名字叫做@blixt。 206 | 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /issue-1/iOS编程101-如何生成圆形和圆角的图像.md: -------------------------------------------------------------------------------- 1 | #iOS编程101:如何生成圆形和圆角的图像 2 | --- 3 | 4 | >* 原文链接:[iOS programming 101:How To Create Circular Profile Picture and Rounded Corner Image](http://www.appcoda.com/ios-programming-circular-image-calayer/) 5 | * 原文作者:[Simon ng](https://github.com/simonng) 6 | * 译文出自:[开发技术前线 www.devtf.cn](http://www.devtf.cn) 7 | * 译者 : [7heaven](https://github.com/7heaven) 8 | * 校对者:[Mr.Simple](https://github.com/bboyfeiyu) 9 | * 状态:完成 10 | 11 | 12 | iOS7带来的一个比较大的变化是圆形图像的使用比方形图像更频繁。在系统默认的应用,比如通讯录和电话中都能找到圆形图像。在这个文章中,我们一起来研究一下如何使用CALayer类来创建圆形或者圆角的图像。 13 | 14 | 可能你之前并没有听过CALayer类。但是在你构建你的应用的时候肯定用到了它。UIKit里面的每一种view(例如:UIView, UIImageView)都是建立在CALayer的实例基础上的(layer对象)。layer对象被设计用来管理view的后备储存和处理view相关的动画。layer对象提供一系列的标签来控制view的可视化内容: 15 | 16 | * 背景颜色 17 | * 边缘以及边缘宽度 18 | * 阴影颜色,阴影宽度等 19 | * 透明度 20 | * 圆角半径 21 | 22 | 23 | 下面我们就使用圆角半径这个标签来挥之圆角和圆形的图像。 24 | 25 | ![art1](http://www.appcoda.com/wp-content/uploads/2014/04/circular-image-featured.jpg) 26 | 27 | 28 | 要明白CALayer是如何工作的,最好的办法就是实践。我们来创建一个简单的包含圆形图片的个人资料view. 29 | 30 | ##demo项目 31 | 32 | 首先,我们从下载[这个项目](https://www.dropbox.com/s/l7tfno7d89ip9em/ProfileDemoTemplate.zip)开始。项目中已经预搭建了profile view 但是你编译运行app的时候会发现图像还是方形的。整个demo项目非常简单。唯一需要注意的是ProfileViewController是和Storyboard中的view相关联的。 33 | 34 | 并且profile图像(UIImageView)已经链接到ProfileViewController.h里的profileImageView属性上。 35 | 36 | ![art2](http://www.appcoda.com/wp-content/uploads/2014/04/square-image-profile.jpg) 37 | 38 | ##创建圆形图像 39 | 40 | 接下来我们来研究一下如何通过边缘半径来把普通图像转换成圆形图像。 41 | 42 | 打开ProfileViewController.m,在viewDidLoad:方法中添加以下代码 43 | 44 | ``` 45 | self.profileImageView.layer.cornerRadius = self.profileImageView.frame.size.width / 2; 46 | self.profileImageView.clipsToBounds = YES; 47 | ``` 48 | 49 | 每个view都绑定了一个layer属性。上面的代码中第一行便是设置这个layer对象(即CALayer类的实例)的边缘半径属性。要把方形图像变成圆形图像,边缘半径必须设成图像宽度的一半。举个例子,如果方形图像的宽度是100像素,那半径就是50像素。第二步则是要把clipsToBounds属性设为YES来使layer正常工作。 50 | 51 | 现在你再重新编译并运行app的话,你会发现图像已经变成圆形了。 52 | 53 | ![art3](http://www.appcoda.com/wp-content/uploads/2014/04/circular-image-profile.jpg) 54 | 55 | 简单吧?只需要两行代码,方形图像就变成圆形图像了。完全不需要ps。 56 | 57 | ##添加边缘 58 | 59 | 接下来,我们给图像添加个边缘,让它看起来更完美。和上面一样,仅需两行代码便能实现这个效果。在viewDidLoad:方法的设置边缘半径的代码下添加以下两行代码: 60 | 61 | ``` 62 | self.profileImageView.layer.borderWidth = 3.0f; 63 | self.profileImageView.layer.borderColor = [UIColor whiteColor].CGColor; 64 | ``` 65 | 66 | 上面的代码非常简单明了。我们只是设置了边缘的宽度还有边缘颜色。现在编译运行app后你会看到有一圈白色包围在图像边缘上。 67 | 68 | ##创建圆角图像 69 | 70 | 相同的方法也可以应用到圆角图像上。技巧是把边缘半径改成其他值。比如10, 71 | 72 | ``` 73 | self.profileImageView.layer.cornerRadius = 10.0f; 74 | ``` 75 | 76 | 现在图像就会变成圆角矩形图像了。 77 | 78 | ![art4](http://www.appcoda.com/wp-content/uploads/2014/04/square-corner-image-profile.jpg) 79 | 80 | 完整的项目可以从[这边](https://www.dropbox.com/s/8dova2z23z42ppp/ProfileDemo.zip)下载。 81 | -------------------------------------------------------------------------------- /issue-1/readme.md: -------------------------------------------------------------------------------- 1 | # 开发技术前线 iOS 第1期 周报 2 | 3 | ## 二、iOS 4 | ### 2.1 技术文章 5 | | 文章标题 | 译者 | 6 | |----------------------|------------------------| 7 | | [iOS编程101-如何生成圆形和圆角的图像](https://github.com/bboyfeiyu/iOS-tech-frontier/blob/master/issue-1/iOS编程101-如何生成圆形和圆角的图像.md) | [7heaven](https://github.com/7heaven) | 8 | | [CocoaPods指南](https://github.com/bboyfeiyu/iOS-tech-frontier/blob/master/issue-1/CocoaPods指南.md) | [Lollypo](https://github.com/Lollypo) | 9 | | [iOS开发-可滑动的单元格](https://github.com/bboyfeiyu/iOS-tech-frontier/blob/master/issue-1/iOS开发-可滑动的单元格.md) | [Harries Chen](https://github.com/mrchenhao) | 10 | | [View Debugging in Xcode 6 ](https://github.com/bboyfeiyu/iOS-tech-frontier/blob/master/issue-1/View Debugging in Xcode 6.md) | [Mr.Simple](https://github.com/bboyfeiyu) | 11 | 12 | 13 | ### 2.2 开源库 14 | 15 | * [一个 AutoLayout 下自动计算 UITableViewCell 高度的扩展](https://github.com/forkingdog/UITableView-FDTemplateLayoutCell) 16 | 17 | * [下拉刷新组件](https://github.com/CoderMJLee/MJRefresh) 18 | 19 | -------------------------------------------------------------------------------- /issue-10/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlaudev/iOS-tech-frontier/0e21f07cfae753a0aca75389946d4c44ce030bbd/issue-10/readme.md -------------------------------------------------------------------------------- /issue-2/GCD概述、语法以及好的示例.md: -------------------------------------------------------------------------------- 1 | GCD概述、语法以及好的示例 2 | --- 3 | 4 | >* 原文链接 : [Grand Central Dispatch (GCD): Summary, Syntax & Best Practices](http://amattn.com/p/grand_central_dispatch_gcd_summary_syntax_best_practices.html) 5 | * 原文作者 : [amattn](http://amattn.com/p/grand_central_dispatch_gcd_summary_syntax_best_practices.html) 6 | * [译文出自 : 开发技术前线 www.devtf.cn](www.devtf.cn) 7 | * 译者 : [starmier](https://github.com/starmier/) 8 | 9 | ##队列 10 | 11 | 苹果官方最初对GCD(Grand Center Dispatch)的描述是这样的: 12 | 13 | 1. 线程是复杂的 14 | 1. 通过GCD可以使得线程变得简单有趣 15 | 16 | 这两种看法都是正确的,当然还有一些其他的观点: 17 | 18 | .GCD 并不是一个线程库或者包装线程 19 | .GCD 可以使用线程,但是开发人员并不能直接控制它 20 | .GCD 是一个通过FIFO队列方式实现的并发库 21 | .GCD 只是libdispath的一种通称;#include 22 | 23 | ##提交block到队列 24 | 25 | GCD的主要机制是通过提交block块到队列,或者响应被弹出队列的事件。有不同的方式可以创建block,也有不同类型的队列,他们中的一些非常有趣。最终,调度执行任务或者执行任务来响应事件。 26 | 27 | 并发方面需要开发者手动处理。线程管理会自动调谐系统负载。通常的并发的危险是:所有UI更新必须在主线程中完成,可以搜索文件/谷歌来确认NS或UI是否是线程安全的。 28 | 29 | 这篇文章主要着重“创建block块队列”,但是开发者需要意识到libdispatch底层的机制: 30 | 31 | 32 | -调度组 //协调队列中不同的线程组 33 | 34 | -信号量 //传统的计数信号量 35 | 36 | -队列屏障 //给定并发队列中的的同步任务 37 | 38 | -调度源 //处理低级别事件的事件 39 | 40 | -调度 I/O //基于文件说明符的操作 41 | 42 | -调度数据缓冲区 //内存的数据缓冲区 43 | 44 | 45 | ##创建或者获取队列 46 | 47 | 48 | 这是值得重复强调的问题:使用GCD的主要机制是提交任务到队列。 49 | 50 | 理解队列的最好方法是要认识到在底层实现中,队列只有两种类型:串行队列和并行队列 51 | 52 | 串行队列是单行线的,但是不受约束。如果你添加一堆任务到每个串行队列,通过一个线程就可以同时启动这些任务。没有承诺的意思是,串行队列会切换到不同任务的线程。串行队列总是等待一个任务完成再执行下一个。即任务按照FIFO的形式执行。你可以通过dispatch_queue_create创建你需要的串行队列。 53 | 54 | 55 | 主队列是一种特殊的串行队列。不像其他串行队列,没有承诺的,在某一个时间他们只“约会”许多线程中的一个,主队列“嫁给”了主线程,用来执行所有任务。主队列中的任务要在runloop中优雅的运行,这样小的行动不要阻塞UI和其他重要的部分。像所有的串行队列、任务以先进先出的顺序完成。你通过dispatch_get_main_queue可以得到它。 56 | 57 | 58 | 如果串行队列是一夫一妻制,那么并发队列是滥交。他们将任务提交给任何可用的线程甚至根据系统负载来创建新的线程。同时他们在不同的线程执行不同的任务。任务提交全局队列中的任务是线程安全的和少副作用是非常重要的。任务提交执行的顺序是先进先出,但完成的顺序是没有保证的。 59 | 60 | 61 | 在Mac OS X 10.6和iOS 4中,只有三个,内置(全局)并发队列,你不能获取他们,你只能用dispatch_get_global_queue获取它们。Mac OS 10.7和iOS 5,您可以创建他们,通过方法dispatch_queue_create(“label”,DISPATCH_QUEUE_CONCURRENT)。你不能为你自己创建的并发队列设置优先级。在实践中,往往更多的使用设定了优先级的全局并发队列而不是自己创建。 62 | 63 |    64 | 用于创建或获取队列的方法进行了总结: 65 |    66 | 67 | dispatch_queue_create // create a serial or concurrent queue 68 | dispatch_get_main_queue // get the one and only main queue 69 | dispatch_get_global_queue // get one of the global concurrent queues 70 | dispatch_get_current_queue // DEPRECATED 71 | dispatch_queue_get_label // get the label of a given queue 72 | dispatch_get_current_queue快速指南:这个方法已经被弃用,它也并不总是适用每一个案例。如果你的实现需要这个,那么你应该重构实现它。最常见的用例是“运行一些block在正在运行的任何队列上”。重构设计应该传递一个有明确的目标的队列块作为参数或参量,而不是试图依靠运行时确定提交哪些队列。 73 | 74 | ##添加任务到队列 75 | 76 | 如果你有创建你自己的队列,你可以添加任务到队列中。 77 | 78 | 79 | 主要实现机制如下: 80 |    81 | // Asynchronous functions 82 | dispatch_async 83 | dispatch_after 84 | dispatch_apply 85 | // Synchronous functions 86 | dispatch_once 87 | dispatch_sync 88 | dispatch_async 将提交一个任务队列并立即返回。 89 | dispatch_after 方法执行会立即返回,但有些延迟,直到指定的时间内完成了任务提交。 90 | dispatch_apply也立即返回并多次提交任务。 91 | dispatch_sync将提交一个任务队列,当任务完成时返回。 92 | dispatch_once将提交一个任务一次,并且在应用程序生命周期中只提交一次,当块任务完成时返回。 93 | 94 | 在实践中,我发现自己使用dispatch_async 、dispatch_after和dispatch_once最多。 95 | 96 |    97 | 示例代码: 98 |    99 | 100 | // add ui_update_block to the main queue 101 | dispatch_async(dispatch_get_main_queue(), ui_update_block); 102 | 103 | // add check_for_updates_block to some_queue in 2 seconds 104 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), some_queue, check_for_updates_block); 105 | // add work_unit_block to some_queue i times. 106 | dispatch_apply(i, some_queue, work_unit_block); 107 | // perform the only_once_block once and only once. 108 | static dispatch_once_t onceToken = 0; // It is important this is static! 109 | // wait for completion 110 | dispatch_once(&onceToken, only_once_block); 111 | 112 | // add blocking_block to background_queue & wait for completion 113 | dispatch_sync(background_queue, blocking_block); 114 | 115 | 116 | ##队列内存管理 117 | 118 | GCD第一次出现在Mac OS X10.6和iOS 4系统中。当时,GCD对象(队列、信号量、队列屏障等)和CFObjects一样,在使用过程中,需要开发者调用dispatch_release和dispatch_retain进行管理。 119 | 120 | 在Mac OS X 10.8和iOS 6中,GCD对象是由自动引用计数进行管理,而手动引用计数则被明确禁止。 121 | 122 |    123 | 此外,对于ARC有以下几点说明: 124 |    125 | 1. 如果你在block块中使用的是GCD对象,可能会造成循环引用。使用__weak或显式地破坏对象(通过dispatch_source_cancel等机制)可以很好的解决这个问题。Xcode 4.6中的静态分析器捕获不到这一点。例子: 126 |    127 | 128 | // Create a GCD object: 129 | dispatch_queue_t someQueue = dispatch_queue_create("someQueue", nil); 130 | // put a block on the queue, the queue retains the block. 131 | dispatch_async(someQueue, ^{ 132 | // capture the GCD object inside the block, 133 | // the block retains the queue and BAM! retain cycle! 134 | const char *label = dispatch_queue_get_label(someQueue); 135 | NSLog(@"%s", label); 136 | }); 137 | 138 | 139 | // You can use the typical __weak dance to workaround: 140 | __weak dispatch_queue_t weakQueue = someQueue; 141 | dispatch_async(someQueue, ^{ 142 | __strong dispatch_queue_t strongQueue = weakQueue; 143 | const char *label = dispatch_queue_get_label(strongQueue); 144 | NSLog(@"%s", label); 145 | }); 146 | 147 | 148 | 2. 最后,说明淹没珍宝的巨人dispatch_data_create_map。使用GCD方法dispatch_data_create_map和dispatch_data_apply创建内部对象时,需要额外的小心。如果父GCD对象被释放,那么内部对象会被变成野指针,将会有不好的事情发生。__block关键字或objc_precise_lifetime修饰父dispatch_data_t对象,可以帮助保持父对象的生命周期。 149 | 150 | 151 | // dispatch_data_create_map returns a new GCD data object. 152 | // However, since we are not using it, the object is immediately 153 | // destroyed by ARC and our buffer is now a dangling pounter! 154 | dispatch_data_create_map(data, &danglingBuffer, &bufferLen); 155 | 156 | // By stashing the results in a __strong var, our buffer 157 | // is no longer dangerous. 158 | __strong dispatch_data_t newData = dispatch_data_create_map(data, &okBuffer, &bufferLen); 159 | 160 | 161 | ##队列实例 162 | 队列,和其他强大的工具一样,如果使用不当会导致严重的问题。现实世界中使用需要一些纪律。 163 | 这里有一些常规准则: 164 | 165 |    166 | * 使用主队列时,应该限制请求主线程,并且要避开耗时操作,以防止UI卡顿。 167 | * 创建的每一个串行队列应该有一个目的。 168 | * 创建的每一个串行队列应该根据目标被合理的命名/标记。 169 | * 并发队列中执行的任务必须是线程安全的。 170 | 171 | 172 | 上面的描述的第二条应该得到进一步研究。因为队列是轻量级的,你可以创建很多很多。最好是有许多专门的串行队列,而不是填充许多不连续的任务到一个或两个“超级”串行/并行队列。 173 | 174 | dispatch 实践的典型用法: 175 | 176 | //used for importing into Core Data so we don't block the UI 177 | dispatch_queue_create("com.yourcompany.CoreDataBackgroundQueue", NULL); 178 | 179 | //used to prevent concurrent access to Somefile 180 | dispatch_queue_create("com.yourcompany.SomeFile.ReadWriteQueue", NULL); 181 | 182 | //used to perform long calculations in the the background 183 | dispatch_queue_create("com.yourcompany.Component.BigLongCalculationQueue", NULL); 184 | 185 | 186 | 实际中,我们通常嵌套使用队列: 187 | 188 | dispatch_queue_t background_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, NULL); 189 | dispatch_async(background_queue, ^{ 190 | // do some stuff that takes a long time here... 191 | 192 | // follow up with some stuff on the main queue 193 | dispatch_async(dispatch_get_main_queue(), ^{ 194 | // Typically updating the UI on the main thread. 195 | }); 196 | }); 197 | 198 | 这时,我们开启了一个在后台执行的任务队列。当任务完成后,我们在主线程中触发UI更新操作。 199 | 200 | 另外,需要强调的是,这个dispatch方法(dispatch_get_global_queue),不可过度使用。它会影响代码的可读性和可维护性,被认为是比较尖锐的代码。(何为尖锐?就是小心、谨慎地使用,否则出了问题,你根本找不着原因。我的一个项目里就是因为使用了至少3个global_queue,出现了crash,后来,我全改为了自定义dispatch_queue_t。当然,使用环境不一样,也不一定会出现我这样的问题。) 201 | 202 | ##深入学习 203 | 如果你对GCD每个方面(调度组、信号量、队列屏障等)都特别感兴趣,请邮件我,我会分享出来更多内容。 204 | 205 | 同时,应用通常的知识点、查阅网络文档与通过Xcode或是在WWDC中讨论GCD和块都是一样的 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /issue-2/Swift样式指南2015年4月更新.md: -------------------------------------------------------------------------------- 1 | Swift 样式指南:2015年4月更新 2 | --- 3 | 4 | >* 原文链接 : [Swift Style Guide: April 2015 Update](http://www.raywenderlich.com/102357/swift-style-guide-april-2015-update) 5 | * 译者 : [liulinxu](https://github.com/liulinxu) 6 | * 校对者: [Lollypo](https://github.com/Lollypo) 7 | 8 | 这是令IOS开发者兴奋的一周,因为Swift1.2测试版随Xcode6.3发布.。开发人员可以在应用商店中下载Xcode6.3。Swift 1.2带来更快的编译,更简洁的语法,一种改善过的If let语句等等。如果你还不清楚 9 | 这些变化,看看我们的文章中描述的Swift 1.2的新特性。 10 | Our goal is to stay on top of changes to Swift and iOS – we are in the process of updating our books and tutorials to Swift 1.2. 11 | 我们的目标是持续关心Swift和IOS的变化——我们正在为Swift1.2更新我们的图书和教程。 12 | 13 | raywenderlich.com Swift样式指南就是我们已经更新的一个文档。我们已经为了Swift1.2做了一些更新,和一些其他的更新基于团队成员和读者提出的请求和问题。 14 | 就让我们一探究竟,看看有什么变化! 15 | 16 | >注意:我们的样式指南不同于其他的样式指南;我们是一个教程网站,这意味着我们的工作重点是为了我们网页和印刷的书籍提供更短,更可读的代码。许多决定都着眼于节约空间进行打印,易读性,和写作教程。 17 | 18 | 19 | ##多个条件绑定。 20 | 21 | swift 1.2 现在允许你在一行代码中绑定多个条件。我们的样式指南推荐使用新的风格而不是旧的风格,特别在多个if let语句中。 22 | 23 | 当你需要测试并且需要绑定几个条件时,这种新的语法可以让你避免金字塔式代码。 24 | 25 | 同样注意的是你也可以在if let语句中包含多个布尔条件,这样会使你的代码变得更加整洁。 26 | 27 | ##枚举 和 case 28 | 29 | 经过大量的讨论后,我们正式使用在我们的教程中的样式是:用大写驼峰命名法命名枚举值。 30 | 31 | 这里有一点前后矛盾,因为枚举值在枚举范围中就像类的常量,并且感觉他们应该用小写驼峰命名法命名。然而,样式指南的使用符合我们一般的做法,在现有的Cocoa中的枚举和swift 本身都是使用以大写字母开头命名枚举值,例如optional类型其实就是一个枚举的实现,它默认的值为Some和None,同样也是大写开头的。 32 | 33 | ##尾随闭包 34 | 35 | 尾随闭包是必须的为了保持这个闭包表达式被它的函数调用,而无需将它们包装在参数括号中。 36 | 37 | 在以前的指南中,我们基本上都可以使用尾随闭包,但是,这里有一个特殊情况,就是当有2个闭包参数时,再使用尾随闭包就不是个明智的选择。 这种情况你就必须使用正常语法保持所有闭包表达式“在同一水平线上”并且命名参数。 38 | 39 | 还有另外一种方法就是一个使用命名一个尾随,这样看起来很怪,我们不建议使用。 40 | 41 | ##函数和方法的命名 42 | 43 | 和Objective-C一样,一个完整的、明确的函数包含方法名和参数名。当指向教程中的一个函数。就像你在Xcode方法跳转栏中看的一样,Swift 遵循了和Object-C一样的格式。 44 | 45 | 以前,我们定义当上下文清楚的情况下,只写一个光秃秃的函数名是OK的。这样就允许我们写viewDidAppear,而不是完整的函数名viewDidAppear(_:)。 46 | 47 | 经过一番讨论,我们觉得这是最好的方法用来区分某个东西是函数还是变量。新的引导规则是要求一致涵盖整个函数标签。这就意味着像viewDidLoad()代表的就是没有入参的格式,而tableView(_:cellForRowAtIndexPath:) 就表示有入参的格式。 48 | 49 | ##标记 50 | 51 | 在过去的样式更新指南中,我们推出了协议扩展的规则。这个规则保持着协议与协议方法的一致性,使得添加和寻找相关的代码更容易。 52 | 53 | 作为这个规则的补充规则,我们希望为了更好的提供导航添加MARK注解。 54 | 55 | 对于共同的分组方法扩展是必须的,并且MARK标签可以帮助你更容易的找到源文件。 56 | 57 | ## 影子变量(Shadowing) 58 | 59 | 说到optional绑定,在这次样式指南中有一样东西没有改变,那就是我们使用相同的名称为optional类型和unwrapped optional类型: 60 | 61 | 这样的好处就是这命名保持简短并且我们可以防止一些事情就像maybeView和unwrappedWidget、optionalTableView。一旦你熟悉了Swift和可选类型,这代码就变得很有意思。这里的不足之处就是这代码对于初学者来说看起来怪怪的并且很难懂。 62 | 63 | 这“ issue thread on variable shadowing”的问题一直存在。并且我们开放来解决这个问题,如果你是特别令人信服的。“Especially convincing”当然包含提供一个Apple Watch Edition版本。 64 | 65 | ## 后续发展 66 | 67 | 法国作家Jean Cocteau说过,“样式是一种用简单的方式去表达复杂事情的方式”。这个就是是程序设计的真真应该关心的——复杂的算法和应用程序逻辑,通过代码用一种容易理解的方式表达。我希望我们的网站的编码风格有助于你理解那些有时复杂的问题,当我们试图教你扩展你在Swift开发方面的知识。 68 | 69 | Swift 1.3 或者2.0 或者以后的版本肯定都会在这方面多做工作,并且我们会继续适应为您提供一个一致的和可读的风格。随着Swift的发展,为什么不一起来参与呢?如果你认为需要在Swift语言上做一些改变,通过GitHub中我们的Swift样式指南知识库,来加入我们的讨论。 70 | 71 | 如果你已经给出了评论,提出一个新的问题,或者发送一个拉代码的情况 - 谢谢!期待反馈的到来。如果你有一个一般性的问题或意见,请加入下面的论坛讨论! -------------------------------------------------------------------------------- /issue-3/Swift的响应式编程.md: -------------------------------------------------------------------------------- 1 | # Swift的响应式编程 2 | --- 3 | 4 | > * 原文链接 : [Reactive Swift](https://medium.com/swift-programming/reactive-swift-3b6050375534) 5 | * 原文作者 : [Agnes Vasarhelyi](https://medium.com/@vasarhelyia) 6 | * [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn) 7 | * 译者 : [Mr.Simple](https://github.com/bboyefiyu) 8 | * 校对者: [Lollypo](https://github.com/Lollypo) 9 | * 状态 : 完成 10 | 11 | 让我们首先回到Apple刚推出Objective-C的继任者-Swift的时候,那真是一个非比寻常的时刻。 12 | Siri还没有开启地狱之门,Prezi还没有支持订阅,那时的朝鲜也还没有hack任何人的email。一种新语言的出现让我个人非常兴奋,尤其是这是一种类型安全的脚本语言。虽然Swift还在快速的发展中,但是我们不必担心它是否已经稳定。当那一刻到来之时,我应该已经知道如何交付整洁的、可测试的代码。还要有非常灵活、流畅的UI?如果你对Objective-C 和 MVC之外的东西感兴趣,那么就请继续读下去吧。 13 | 14 | ## MVC 与 MVVM 15 | 16 | 让我们从零开始,当我们设计一个应用时,你可能会先考虑应用的架构。Cocoa框架以[Model-View-Controller](https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html) (也称为 MVC)为基础架构,它的结构如下图所示。 17 | 18 | ![](https://d262ilb51hltx0.cloudfront.net/max/1600/0*pEvMKHBK9HeAgrYC.) 19 | 20 | 虽然,你可能发现这个架构并没有让你的设计更有效,从上图中我们可以看到Controller角色的职责太过复杂。如果你有在iOS或者Mac上使用MVC的经验,我打赌你已经领略到了View Controller承担了过多的职责。你使用MVC的经验越多,你就越会想找到另一个方法来解决掉MVC存在的这些问题。当使用MVC处理网络请求时,你会发现这会使得Controller变得极为臃肿,它不仅要处理获取数据的请求,同时也要负责将这些数据渲染到UI上。更蛋疼的是你有尝试过对一个ViewController进行单元测试吗?不堪回首啊!这涉及到View、业务逻辑的测试。随着View的复杂度的增长,它变得越来越难以维护。这就是为什么人们不对MVC进行测试的原因。 21 | 22 | [Model-View-ViewModel](http://www.objc.io/issue-13/mvvm.html) (也叫MVVM)对于你的应用来说是一个更好的选择。这是微软发布的一个针对事件驱动实现的、非常强大的架构模式。正如你看到的,与MVC不同的是这里的View持有ViewModel角色。 23 | 24 | ![](https://d262ilb51hltx0.cloudfront.net/max/1600/0*SUaY1aSgIcvDys9j.) 25 | 26 | 当然,这些都依赖于你想如何实现这个模式,不管如何,最终MVVM模式都会给你带来更低的复杂性、更好的可测试性。同时能达到这些效果吗?怎么可能?反正我相信了!它把所有的逻辑移到ViewModel角色中,以此来减少View Controller的职责。( 译者注: 关于MVC、MVP、MVVM的区别请参考[这篇文章](http://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html)。) 27 | 28 | ![](http://img.blog.csdn.net/20150507120115027) 29 | 30 | 闪开,让哥用一个项目来演示MVVM模式的使用,这是一个用来控制酒窖气温的半自动iOS客户端项目。如何你想了解更多关于这个项目的细节,可以参考[这篇文章](http://blog.risingstack.com/brewfactory-full-stack-homebrew-with-iot/)。这个项目的关键在于我们能够通过这个App看到酒窖中各个时间段的温度。 31 | 32 | 让我们回到MVVM与酒窖的温度上。为了能够读取到温度值,我们需要在我们的服务器上使用WebSocket,使得我们基于MVC架构的应用能够读取它,并且将它显示到iPhone上。首先我们需要建立一个Brew 模型类,这个类中有一个温度字段( temp ). 33 | 34 | ```swift 35 | class Brew { 36 | var temp = 0.0 37 | } 38 | ``` 39 | 40 | 我们还需要建立一个将酒窖温度显示到UI上的ViewController。 41 | 42 | ```swift 43 | class BrewViewController: UIViewController { 44 | @IBOutlet weak var tempLabel: UILabel! 45 | } 46 | ``` 47 | 48 | 通过MVC的方式,我们需要在ViewController中实现网络请求逻辑、更新UI的操作。 49 | 50 | ```swift 51 | socket.on("temperature_changed", callback: {(AnyObject data) -> Void in 52 | self.brew.temp = data[0] as Float 53 | dispatch_async(dispatch_get_main_queue(), { 54 | self.updateTempLabel() 55 | }) 56 | }) 57 | ``` 58 | 59 | 更新UI的函数 : 60 | 61 | ```swift 62 | func updateTempLabel() { 63 | self.brewLabel.text = NSString(format:"%.2f ˚C", self.brew.temp) 64 | } 65 | ``` 66 | 67 | 此时,我们成功的把这些代码都放到一个ViewController中了,非常好不是吗?必须不是呐! 68 | 69 | 70 | 想象一下这种场景,当你需要先验证用户的身份、绘制一个实时的图表或者处理出入的字段时你如何实现?MVC架构的完整例子在[这里](https://gist.github.com/vasarhelyia/70b2c3fae2bbe38c4a7a)。但是现在,先让我们来看看基于MVVM架构的实现: 71 | 72 | ```swift 73 | var brew = Brew() 74 | dynamic var temp: Float = 0.0 { 75 | didSet { 76 | self.brew.temp = temp 77 | } 78 | } 79 | ``` 80 | 81 | 上面就是存储在ViewModel中的变量,虽然它们看起来有点陌生,但是我也不知道怎么才能使它们看起来不那么奇怪。不管它啦,我们先来看Socket: 82 | 83 | ```swift 84 | socket.on("temperature_changed", callback: {(AnyObject data) -> Void in 85 | self.temp = data[0] as Float 86 | }) 87 | ``` 88 | 89 | 这样,网络请求的逻辑就被移到了ViewModel中。但是我们如何修改UI呢?[KVO](http://nshipster.com/key-value-observing/)是我们的好朋友,我们就用它来实现,这里是viewDidLoad中的一个代码片段 : 90 | 91 | ```swift 92 | self.brewViewModel.addObserver(self, forKeyPath: "temp", options: .New, context: &brewContext) 93 | ``` 94 | 95 | 我们需要观察酒窖中的温度,因此,我为View Model添加了一个单独的字段。你可能会问,为什么我们要关心Brew模型是否在View Model中?我要说的是,Brew模型可能会有不同的字段,而你可能需要将它序列化,通过网络传输它,或者其他的一些什么操作。因此,我们需要它的状态。 96 | 97 | ```swift 98 | override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer) { 99 | if context == &brewContext { 100 | self.updateTempLabel((change[NSKeyValueChangeNewKey]) as Float) 101 | } else { 102 | super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context) 103 | } 104 | } 105 | ``` 106 | 107 | 哇噢,这代码太多了?KVO实现产生了很多的样板代码,但是它还是比MVC实现更为轻量级,尤其是当我们考虑添加越来越多的功能到我们的应用中时,这些逻辑就会被移到View Model中。[这里](https://gist.github.com/vasarhelyia/e4fa64096517ae966d01)是MVVM实现的完整代码段。 108 | 109 | 总之,MVVM实现好了很多,但是我们还有这还能再优化的感觉。现在,我们知道了展示可变数据模型到MVVM的View上的关键是数据绑定,既然这样,那还有什么比响应式编程适合这种情况? 110 | 111 | ## 响应式编程 112 | “Reactive programming is programming with asynchronous data streams.” 113 | “响应式编程就是以异步数据流的形式进行编程。” 114 | 115 | 如果你对这个概念还不熟悉,我建议你阅读一下[这篇文章](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) (中文版在这里 : [那些年我们错过的响应式编程](http://www.devtf.cn/?p=174) ),这是一篇非常好的介绍。在响应式编程的世界里,任何东西都是流( stream )。下面这幅图就展示了将一个按钮点击或者触摸事件转换为流。 116 | 117 | ![](https://d262ilb51hltx0.cloudfront.net/max/1600/0*JyZB_1WzJRNIY7o-.) 118 | 119 | 120 | 如果运用响应式编程到我们的项目中,那么情况应该是这样的: 将socket数据看成是温度的流,这个流被UILabel监听着。转换它们的流程也就是上图的函数式编程所展示的那样,map、merge、filter等其他的操作,这个过程非常赞!让我们来看看这些概念如何跟Cocoa一起配合。 121 | 122 | ## ReactiveCocoa 123 | 124 | 当我们计划在Cocoa上使用响应式编程时,最好的选择就是使用[ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa)。ReactiveCocoa是一个受到[函数式编程](http://en.wikipedia.org/wiki/Functional_reactive_programming)启发的数据绑定框架。 125 | 126 | 它通过信号来进行操作,这些信号被定义为push-driven的流。这意味着,一个信号代表了一个在未来会交付数据或者任意结果到它的观察者的异步工作流。除了数据模型变化会产生一个信号之外,ReactiveCocoa也提供了一些内置的UIKit/AppKit绑定,例如rac_textSignal和其他用用的东西。 127 | 128 | 你可能略微地不确定什么时候才会计划启动你的下一个Swift项目,你可能也会想在这个项目中实现响应式编程。无需这样,github上的开发人员会帮助你实现这些功能。他们正在计划发布名为Great Swiftening的ReactiveCocoa 3.0版本,当然你也可以使用2.4.x版本,因为它们也足够稳定。 129 | 130 | Swift在集合类型上也内置了一些函数式操作,它们能很方便的执行某些操作,我们无需对此进行重复地工作。 131 | 132 | 让我们看看使用ReactiveCocoa作为MVVM模式的数据绑定系统时我们如何创建一个ReactiveViewModel。看如下代码,我们的View Controller看起来是不是更优雅了? 133 | 134 | ```swift 135 | self.brewViewModel.tempChangedSignal.map { 136 | (temp: AnyObject!) -> AnyObject! in 137 | return NSString(format:"%.2f ˚C", temp as Float) 138 | } ~> RAC(self.tempLabel, "text") 139 | } 140 | ``` 141 | 142 | 由于C式的复杂的宏在Swift中不可用,因此用于Objective-C的RAC宏被替换掉了。感谢Yusef Napora提供了一个直截了当的[解决方法](http://napora.org/a-swift-reaction/),还有一种解决方法是在ColinEberhardt的这个[Sample App](https://github.com/ColinEberhardt/ReactiveSwiftFlickrSearch)中。 143 | 144 | 现在我们回到酒窖气温检测项目中,我们已经准备好了运用响应式编程!我们看看剩下的代码: 145 | 146 | ```swift 147 | socket.on("temperature_changed", callback: 148 | {(AnyObject data) -> Void in 149 | tempChangedSignal.sendNext(data[0]).deliverOn(RACScheduler.mainThreadScheduler()) 150 | }) 151 | ``` 152 | 153 | 在这里,我们仅仅创建了一个温度数据信号,并且确保UILabel的更新会被执行在主线程中。我对于ReactiveCocoa简直没有抵抗力了!现在我们来执行http请求、处理当试图在服务端初始化一个Brew对象时引发的错误,代码如下 : 154 | 155 | ```swift 156 | NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue(), completionHandler: { 157 | (response:NSURLResponse!, data: NSData!, error: NSError!) -> Void in 158 | if error == nil { 159 | subscriber.sendNext(JSON(data)) 160 | subscriber.sendCompleted() 161 | } else { 162 | subscriber.sendError(error) 163 | } 164 | }) 165 | ``` 166 | 167 | 如果成功我们将接收到的Json数据发送给订阅者,否则调用sendError函数将错误信息发送给订阅者。非常方便,不是吗? 168 | 169 | ```swift 170 | syncSignal.subscribeError({ (error: NSError!) -> Void in 171 | UIAlertView(title: "Error creating brew", message: error.localizedDescription, delegate: nil, cancelButtonTitle: "OK").show() 172 | }) 173 | ``` 174 | 175 | 我们仅仅是需要在ViewController中实现错误处理,然后等待着响应式编程的魔法降临到我们身上,例如 丢失连接。 176 | 177 | ## 总结 178 | 179 | 酒窖温度监测项目是一个真实的示例,起初我用Objective-C中的MVC实现.这种实现非常简单,却少了份惊喜.后来,Swift出现了,我理所当然地喜欢上这样的创新.因此我决定重写整个App.当我与繁重的控制器以及UI刷新机制斗争了许久之后,这些东西我不是很精通,所以我决定尝试响应式编程.由于我早已知道这与MVVM能够兼容,所以就决定尝试一下.这次经历清除了我一半代码.于是,经过ReactiveCocoa转换的App的价值感觉就像是剩下的代码的价值的两倍.从那时候起,做一些调整(比如根据一些数据改变按钮的状态)仅仅需要编写一行用于发送启用信号到按钮命令的代码,这几乎没有成本.想想使用原来的方式将花费多少时间呐! 180 | 181 | 有很多其他的因素可能使你想尝试ReactiveCocoa和Swift。如果你想对如何酿造啤酒感兴趣,或者只是对这些代码感兴趣,那么你可以这[这里(BrewMobile)](https://github.com/brewfactory/BrewMobile)查看到所有的代码。如果你有更好的技术使得这个App变得更好,请让我知道!当然,也欢迎给我们贡献代码。 182 | 183 | 在结束之前,分享一下我关于BrewMobile的未来的一些思考,我正在打算将React Native引入到BrewMobile。如果搞定了我会再次分享我的经验。 184 | 185 | ## 其他资料 186 | 187 | ### Swift 188 | * [http://www.objc.io/issue-16/power-of-swift.html](http://www.objc.io/issue-16/power-of-swift.html) 189 | * [https://designcode.io/swift-design](https://designcode.io/swift-design) 190 | * [http://blog.scottlogic.com/2014/07/24/mvvm-reactivecocoa-swift.html](http://blog.scottlogic.com/2014/07/24/mvvm-reactivecocoa-swift.html) 191 | * [http://www.objc.io/books/](http://www.objc.io/books/) 192 | 193 | ### MVVM模式 194 | [http://www.objc.io/issue-13/mvvm.html](http://www.objc.io/issue-13/mvvm.html) 195 | [http://www.teehanlax.com/blog/model-view-viewmodel-for-ios/](http://www.teehanlax.com/blog/model-view-viewmodel-for-ios/) 196 | 197 | ### 响应式编程 198 | * [https://gist.github.com/staltz/868e7e9bc2a7b8c1f754](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) 199 | * [http://blog.risingstack.com/functional-reactive-programming-with-the-power-of-nodejs-streams/](http://blog.risingstack.com/functional-reactive-programming-with-the-power-of-nodejs-streams/) 200 | * [http://rxmarbles.com](http://rxmarbles.com) 201 | 202 | ### ReactiveCocoa 203 | * [https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/DesignGuidelines.md](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/DesignGuidelines.md) 204 | * [http://nshipster.com/reactivecocoa/](http://nshipster.com/reactivecocoa/) 205 | 206 | ### Ghostbusters 207 | * [https://itunes.apple.com/us/movie/ghostbusters/id532285804](https://itunes.apple.com/us/movie/ghostbusters/id532285804) 208 | -------------------------------------------------------------------------------- /issue-3/iOS今日扩展.md: -------------------------------------------------------------------------------- 1 | # iOS8 今日扩展 2 | --- 3 | 4 | >* 原文链接 : [iOS 8: Creating a Today Widget](http://code.tutsplus.com/tutorials/ios-8-creating-a-today-widget--cms-22379) 5 | * 译者 : [Harries Chen](https://github.com/mrchenhao) 6 | * 校对者: [Mr.Simple](https://github.com/bboyfeiyu) 7 | * 状态 : 校对中 8 | 9 | 创建扩展是iOS8升级后的一个重要的特性,在本次介绍中,我将通过创建一个自定义的今日组件在通知中心来讲解。首先我们短暂的来了解一下有关扩展来明白组件这个重要的概念。 10 | 11 | #1.扩展是什么? 12 | 13 | 扩展是一个特殊的程序。但是它并不属于一个完整的APP,它需要有一个容器APP(containing app)来进行发布。容器可以是一个已经存在的APP,也可以创建一个新的。一个容器可以有一个或一个以上的扩展。扩展不能独立的进行发布,它需要有一个容器。 14 | 15 | 扩展被它所属的宿主程序(host app)所加载和控制。举个例子,它可以像Safari一样有分享扩张,或者像通知中心的今日摘要和其他组件一样。系统的每一个支持扩展的地方叫做扩展插入点。 16 | 17 | 为了创建扩展,你需要添加一个target到容器(cotaining app)的工程文件之中。Xcode提供的模板已经包括扩展接入点所需要的框架, 18 | 19 | #2.今日扩展接入点 20 | 21 | 扩展提供了今日扩展接入点,就是所谓的组件,可以提供简单快捷的访问信息。组件关联到通知中心。设计一个简单易用的的组件是非常重要的,因为太多的交互可能会导致用户的操作困难。要注意千万不要使用键盘。 22 | 23 | 我们都希望组件能够有好看的界面和及时更新,保证它稳定的工作尤为重要,你需要让你的组件又快又节省资源。避免影响这个用户体验,系统将终止组件的运行如果组件占用了太多内存。组件需要的是简单,专注于显示他们需要显示的内容。 24 | 25 | 理论说够了,让我们来创建一个自己的今日组件,我们用它来显示磁盘的用量,通过一个进度条来提示用户。在这个过程中,我们也会涉及其他的扩展。 26 | 27 | #3.实现步骤 28 | 29 | ##步骤1:创建项目 30 | 31 | 如果你想要创建一个扩展作为一个已经存在的app,打开Xcode工程,然后到步骤2。如果你像我一样从零开始,你需要先创建一个容器程序(containing app)。 32 | 33 | 打开Xcode在File目录选择`New > Project...` 我们使用`objective-C`作为开发语言,选择 `Single View Application` 模板。 34 | 35 | ![这里写图片描述](http://img.blog.csdn.net/20150505211111854) 36 | 37 | ##步骤2:添加一个target 38 | 39 | 打开 `File` 目录,选择 `New > Target....` 在 `Application Extension` 的类别中选择 `Today Extension` 模板。 40 | 41 | ![这里写图片描述](http://img.blog.csdn.net/20150505211012076) 42 | 43 | 你会注意到target被添加到我们当前的工程,扩展将会被嵌入到容器程序。然后扩展会有一个标识符类似容器程序 `com.tutsplus.Today.Used-Space`. 44 | 45 | ![这里写图片描述](http://img.blog.csdn.net/20150505211128844) 46 | 47 | 点击 `Next`, 给组件取个名字,例如`Used Space`, 点击 `Finish` 创建一个`target..`,Xcode将为你创建一个新的scheme,然后询问你是否激活,点击`Activate`继续。 48 | 49 | ![这里写图片描述](http://img.blog.csdn.net/20150505211405506) 50 | 51 | Xcode将为组件创建一个名叫`Space Used`的分组 然后在其中生成一些文件,一个`UIViewController`和一个`storyboard`,组件就只是一个`UIViewController`和一个`storyboard`,如果你打开视图控制器的头文件,你会发现它就是一个普通的`UIViewController`的子类。 52 | 53 | 如果你选择扩展的target,打开 `Build Phases`并展开`Link Binary With Libraries`,你会发现它添加了 `Notification Centre`得框架。 54 | 55 | ![这里写图片描述](http://img.blog.csdn.net/20150505211245628) 56 | 57 | 58 | #4.用户界面 59 | 60 | 我们现在来创建一个基本的用户界面,首先需要确定组件的大小,有两种方式来告诉系统我们所需的大小。第一是是采用自动布局(Auto Layout),第二是使用`viewcontroller`的`preferredContentSize`属性。 61 | 62 | 自动布局的概念在组件上依然是可用的,不仅需要注意iPhone的各种宽度(以及iPad和未来的设备)也需要注意它可能会在横屏模式下现实。如果界面是用的是自动布局来约束,对开发者来说是非常方便的。调节组件的高度可以通过`setPreferredContentSize`来实现。 63 | 64 | ##步骤1:添加元素 65 | 66 | 在Xcode编辑器中打开 `MainInterface.storyboard`. 你会发现已经有一个HelloewWorld的label已经在视图中了,选中并删除它,因为我们不会用到,添加一个新的label并且让它靠右对齐像图中那样。 67 | 68 | ![这里写图片描述](http://img.blog.csdn.net/20150505211321445) 69 | 70 | 在 `Attributes Inspector` 中设置颜色为白色, 字体对齐方式为靠右对齐, 文本占 50.0%。 71 | 72 | ![这里写图片描述](http://img.blog.csdn.net/20150505211554018) 73 | 74 | 在编辑器中选择 `Size to Fit Content` 来让label自动调节大小。 75 | 76 | 接着添加一个`UIProgressView`对象在label的左边位置,如图所示。 77 | 78 | ![这里写图片描述](http://img.blog.csdn.net/20150505211623066) 79 | 80 | 选中进度条,在`Attributes Inspector`改变 `Progress Tint `为白色, 改变`Track Tint` 为深褐色.这将会使得进度条更加明显,接下来添加一些约束。 81 | 82 | ##步骤2:添加约束 83 | 84 | 选中label添加如图所示的上下约束。不要勾选`Constrain to margins` 的选项。 85 | 86 | ![这里写图片描述](http://img.blog.csdn.net/20150505211458134) 87 | 88 | 89 | 选择进度条添加如图所示的上左右的约束,同时不要忘记反选` Constrain to margins`。 90 | 91 | ![这里写图片描述](http://img.blog.csdn.net/20150505211735276) 92 | 93 | 因为我们改变了约束,所以我们会有一个小问题需要解决,现在的进度条大小并不能正确的反应约束后的进度条大小,选择进度条, 点击 `Resolve Auto Layout Issues` 按钮,选择 `Update Frames` 选择 `Selected Views`.这将会根据约束来更新进度条的大小。 94 | 95 | ![这里写图片描述](http://img.blog.csdn.net/20150505230954758) 96 | 97 | ##步骤3:构建与运行 98 | 99 | 100 | 是时候来看一下我们的组件了,选`择Used Space schem`e,选择`Prodect`目录中得`Run`或者直接按下 `Command-R`,下拉显示通知中心,然后点击下方的`edit`按钮,你的今日组件将会出现在可添加的地方,点击它左边的添加按钮。 101 | 102 | ![这里写图片描述](http://img.blog.csdn.net/20150505231028449) 103 | 104 | 这就是我们的扩展的样子。 105 | 106 | ![这里写图片描述](http://img.blog.csdn.net/20150505231126645) 107 | 108 | 看起来不错,但是为什么在下面有那么多空隙呢?为什么操作系统没有遵循进度条的约束呢? 109 | 110 | 这个问题都是操作系统的标准间距导致的,我们接下来将会改变它,请注意,无论如何,保证进度条的左边距与名字对齐的。 111 | 112 | 如果你选择你的设备或者在其它设备运行,你会发现组件会调整大小,这得益于我们使用了自动布局( Auto Layout)。 113 | 114 | ##步骤4:修正按钮边距 115 | 116 | 打开 `TodayViewController.m`. 可以看到控制器实现了`NCWidgetProviding`协议. 这意味着我们需要实现`widgetMarginInsetsForProposedMarginInsets:`方法来返回一个自定义的边距(`UIEdgeInsets`结构),修改方法如下。 117 | 118 | 再次运行程序查看结果,发现组件的下边距变小了,你可以定制这些边距达到任意你想要的效果。 119 | 120 | ![这里写图片描述](http://img.blog.csdn.net/20150505231004344) 121 | 122 | ##步骤5:连接Outlets 123 | 124 | 最后,我们来添加两个Outlets,打开`storyboard`,切换到双编辑器模式,确保现实`TodayViewController.m`。 125 | 126 | 按住`Control`并拖动label到`viewcontroller`的接口部分来创建label的接口,命名为`percentLabel`,重复这个过程为`UIProgressView`创建`barView`。 127 | 128 | ![这里写图片描述](http://img.blog.csdn.net/20150505231035076) 129 | 130 | # 5.显示真实数据 131 | 132 | 我们将使用 `NSFileManageer`类来计算设备的可用空间,那么我们改如何更新组件的数据呢? 133 | 134 | 这就就是`NCWidgetProviding`协议的另外一个方法。操作系统将调用`widgetPerformUpdateWithCompletionHandler: `方法当组件被载入的时候或者从后台进入的时候。即使组件不可见,系统还是有可能载入和更新它并保存快照,快照将在组件下次出现的时候显示,这是因为在组件显示之前会有一小段的时间。 135 | 136 | 通过处理完成句柄来判断是否需要调用新的内容或数据更新。block有一个参数NCUpdateResult来描述是否我们要更新内容。如果不是的话,操作系统会知道没有必要保存一个新的快照。 137 | 138 | ##步骤1:定义属性 139 | 140 | 我们需要先定义一些属性来保存剩余、已使用和总得空间,把这些属性添加到TodayViewController.m。 141 | 142 | ##步骤2:实现updateSizes 143 | 144 | 创建一个辅助方法`updateSizes`来拉取必要的数据和计算设备的空间使用率。 145 | 146 | ``` 147 | - (void)updateSizes 148 | { 149 | // Retrieve the attributes from NSFileManager 150 | NSDictionary *dict = [[NSFileManager defaultManager] 151 | attributesOfFileSystemForPath:NSHomeDirectory() 152 | error:nil]; 153 | 154 | // Set the values 155 | self.fileSystemSize = [[dict valueForKey:NSFileSystemSize] 156 | unsignedLongLongValue]; 157 | self.freeSize = [[dict valueForKey:NSFileSystemFreeSize] 158 | unsignedLongLongValue]; 159 | self.usedSize = self.fileSystemSize - self.freeSize; 160 | } 161 | ``` 162 | 163 | 164 | ##步骤3:存储 165 | 166 | 我们可以方便的使用`NSUserDefaults`来存储数据。组件的生命周期很短,所以我们需要缓存这个值,我们可以给界面设置一个初始的值,然后再计算真实的值。 167 | 168 | 这对我们是否要更新组件的快照是非常有帮助的,让我们来创建一个快捷方法来访问`NSUserdefaults`。 169 | 170 | 注意我们使用了一个宏定义`RATE_KEY`,别忘了把它加入`TodayViewController.m`。 171 | 172 | ##步骤4:更新界面 173 | 174 | 因为我们的组件是一个视图控制器,`viewDidLoad`方法很适合用来更新用户界面,我们创建一个帮助方法`updateInterface`来更新界面。 175 | 176 | ##步骤5:调用完成句柄 177 | 178 | 可用空间的变化导致跟新的过于频繁,为了判断我们是否真的需要刷新界面,我们计算已使用量并且设置了一个更新的下限0.1%,而不是一改变就刷新界面。修改 179 | `widgetPerformUpdateWithCompletionHandler: `如下: 180 | 181 | ``` 182 | - (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler 183 | { 184 | 185 | [self updateSizes]; 186 | 187 | double newRate = (double)self.usedSize / (double)self.fileSystemSize; 188 | 189 | if (newRate - self.usedRate < 0.0001) { 190 | completionHandler(NCUpdateResultNoData); 191 | } else { 192 | [self setUsedRate:newRate]; 193 | [self updateInterface]; 194 | completionHandler(NCUpdateResultNewData); 195 | } 196 | } 197 | ``` 198 | 199 | 在我们每次重新计算之后,如果与上次有明显的变化,则保存值并更新界面,告诉操作系统。如果没有明显的变化,我们就不需要新的快照。如果有错误发生将会在`NCUpdateResultFailed `来报告错误的发生,但是在这个例子中没有出现错误。 200 | 201 | ##步骤6:构建与运行 202 | 203 | 再一次运行你的程序,它将正确的显示您已经使用了多少空间。 204 | 205 | #6 总结 206 | 207 | 我们来回顾一下组件的生命周期,当我们打开今日面板,系统可能会显示上次的快照知道准备完成。界面会被加载,组件将取出缓存在NSUserDefaults的来更新用户界面。 208 | 209 | 接着`widgetPerformUpdateWithCompletionHandler:`会被调用,重新计算真实的值,如果缓存的值和真实值相近,不会发生任何事情,如果存在不同,那么新的值会被缓存界面会被刷新。 210 | 211 | 212 | 在后台的时候,组件可能会被系统调用,如果返回`NCUpdateResultNewData`,新的快照就会被创建为了下次的出现。 213 | 214 | #7添加更多得信息和动画 215 | 216 | 虽然我们已经完成了显示使用空间的功能,它会有一个精确的数字。为了避免复杂的用户界面,使交互更加友好。如果点击百分比标签,组件将额外显示一个新的标签。这也是一个很好的机会去学习如何在组件中使用动画。 217 | 218 | ##步骤1:改变用户界面 219 | 220 | 打开`MainInterface.storyboard`选择label,在`In the Attributes Inspector`,在`View`选项下,找到`User Interaction Enabled`选项并启用它。 221 | 222 | 接下来,我们需要去除label的底部约束。label视图的底部的距离会改变,这意味着约束将成为无效。 223 | 224 | 选择label,在`Size Inspector`展开'Size',选择底部的空间约束,并点击删除。你也可以手动选择视图中的约束引导并删除它。label现在只有顶部的约束,如下图所示。 225 | 226 | ![这里写图片描述](http://img.blog.csdn.net/20150505231328699) 227 | 228 | 通过点击画面上方的三个图标的第一个选择视图控制器。在`Size Inspector`的`Size`中,将高度设置为`106`。 229 | 230 | ![这里写图片描述](http://img.blog.csdn.net/20150505231355775) 231 | 232 | 添加一个新的label,,在`Attributes Inspector`设置其颜色为白色。此外,设置行数`3`,高度`61`,宽度`200`。这应该足以容纳三条信息。让它保存靠左和靠下对齐。 233 | 234 | ![这里写图片描述](http://img.blog.csdn.net/20150505231418568) 235 | 236 | 最后一步是打开助理编辑和创建名为detailslabel的outlet。 237 | 238 | ##步骤2:安装 239 | 240 | 241 | 控件将只会在一瞬间扩大。我们可以在`NSUserDefaults `保存一个布尔变量来记住上一次的状态,但是,为了简单,每次组件加载它时将关闭。当点击百分比label的时候,额外的信息将会出现。 242 | 243 | 让我们首先定义两个宏在`TodayViewController.m `来帮助我们设置大小。 244 | 在viewDidLoad,添加两行代码来设置控件的初始高度,让label透明。我们将在百分比label被点击的时候使详细label出现。 245 | 246 | 请注意,我们设置控件的宽度为0,因为宽度将由操作系统决定。 247 | 248 | ##更新详情标签 249 | 250 | 在详情label中,我们使用NSByteCountFormatter来展示可用、已使用和总的值。添加以下代码的视图控制器。 251 | 252 | ``` 253 | -(void)updateDetailsLabel 254 | { 255 | NSByteCountFormatter *formatter = 256 | [[NSByteCountFormatter alloc] init]; 257 | [formatter setCountStyle:NSByteCountFormatterCountStyleFile]; 258 | 259 | self.detailsLabel.text = 260 | [NSString stringWithFormat: 261 | @"Used:\t%@\nFree:\t%@\nTotal:\t%@", 262 | [formatter stringFromByteCount:self.usedSize], 263 | [formatter stringFromByteCount:self.freeSize], 264 | [formatter stringFromByteCount:self.fileSystemSize]]; 265 | } 266 | 267 | ``` 268 | 269 | ##步骤4:检测点击事件 270 | 271 | 为了检测点击事件,我们在试图中重写了`touchesBegan:withEvent:`方法。想法很简单,当检测到触摸事件,展开详情标签并更新。请注意,插件的大小的是在`callingsetPreferredContentSize`更新的。 272 | 273 | ##步骤5:添加动画 274 | 275 | 虽然这个组件运行的不错,我们还是可以通过在收展详情标签使用渐变效果来提升用户体验,这个可以通过实现`viewWillTransitionToSize:withTransitionCoordinator:`来完成。这个方法会在组件的高度变化的时候被调用。因为这是通过渐变协调对象来完成,所以我们可以添加一些额外的动画。 276 | 277 | 你可以看到的,我们改变了标签的alpha值,但是你可以添加任何类型的动画来增强的用户体验。 278 | 279 | ##步骤6:构建与运行 280 | 281 | 我们再一次运行程序。通过点击百分比label来现实详情label。 282 | 283 | ![这里写图片描述](http://img.blog.csdn.net/20150505231256241) 284 | 285 | #结论 286 | 287 | 这些逻辑似乎过于复杂对于这样一个简单的任务,但是你将熟悉创建今日扩展的的全过程。记住这些原则在设计和开发你的插件的时候。记得要保持它的简单和明了,同时也别忘了性能。 288 | 289 | 缓存在一些快速操作也许不需要,但你要做耗时的处理它就变得尤为重要。用你的视图控制器的知识来检查它是否能够在各种屏幕尺寸下工作。建议你避免滚动视图或复杂的触摸识别。 290 | 291 | 虽然扩展将有一个单独的容器,正如我们前面看到的,它可以使应用程序和包含扩展之间的数据共享。你也可以使用`NSExtensionContext `的`OpenURL:completionhandler:`通过一个自定义的URL scheme来快速启动你的程序从组件。如果你需要分享你的延伸,创建一个框架来使用您的应用程序和扩展。 292 | 293 | 我希望这里介绍的知识是有用的,帮助创建你的下一个伟大的今天扩展。 -------------------------------------------------------------------------------- /issue-3/readme.md: -------------------------------------------------------------------------------- 1 | # 第三期 -------------------------------------------------------------------------------- /issue-4/Swift和自动引用计数器整理之强、弱和无主引用.md: -------------------------------------------------------------------------------- 1 | Swift和自动引用计数(ARC)整理之强,弱和无主引用 2 | --- 3 | 4 | > * 原文链接 : [Strong, Weak, and Unowned - Sorting out ARC and Swift](http://www.andrewcbancroft.com/2015/05/08/strong-weak-and-unowned-sorting-out-arc-and-swift/) 5 | * 原文作者 : [Andrew Bancroft](http://www.andrewcbancroft.com/) 6 | * 译文出自 : [开发技术前线 www.devtf.cn](www.devtf.cn) 7 | * 译者 : [samw00](https://github.com/samw00/) 8 | * 校对者: [MrSimple](https://github.com/bboyfeiyu/) 9 | * 状态 : 完成 10 | 11 | 我敢打赌有相当一部分的Swfit开发者纠结于`strong`(强引用),`weak`(弱引用)和`unowned`(无主引用)这几种不同类型的引用方式对他们的代码在运行时所造成的影响。如果我的日子依赖于自动引用计数(ARC),我想我是不会费神来尝试解释其中的奥妙的。 12 | 13 | 在我每次声明变量或是常量时,我都不是特别确定到底应该用哪种类型的引用,为了让自己不再纠结,我最终决定把[苹果官方关于ARC的文档](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html)拿出来拜读,试着去理解和消化里面的每一句话。这篇文章尝试着把我脑里关于Swift和自动引用计数(ARC)的整理分享给大家。 14 | 15 | 这篇文章稍微有点长所以我想:“为何不先把总结直接抛出来让小伙伴们先睹为快呢?然后如果实好奇再继续看我是如何得出这总结的”。所以下面献上我的总结! 16 | 17 | ##总结 18 | 19 | * 在你开始担心应该是用`strong`,`weak`还是`unowned`之前,先问自己一个问题:“我是在和引用类型打交道吗?”。如果是在和结构体或是枚举类型打交道,你就压根不用管是用`weak`还是`unowned`来修饰这些常量或变量,因为ARC就没做这些类型的内存管理。 20 | * `strong`强引用适用于父对象指向子对象,但反过来不能用强引用,听说过父亲拥有儿子的,可没听说过儿子拥有父亲的。事实上,在大多数情况下,`strong`都是默认引用类型。 21 | * 当两个实例是_optionally_关联在一起的,确保其中一个实例是用`weak`弱引用指向另一个实例。 22 | * 两个实例A和B,如果实例A必须在实例B存在的前提下才能存在,那么实例A必须用`unowned`无主引用指向实例B。也就是说,有强制依赖性的那个实例必须对另一个实例持有无主引用。 23 | 24 | 想具体了解我是如何得出这些结论的,就要看接下来的详细解说了,你可以直接跳到你感兴趣的部分也可以顺序读下去。 25 | 26 | ##ARC和内存管理 27 | 对很多人来说,一提到“内存管理”就云里雾里的。 28 | 29 | 内存管理的本质之所以被蒙上各种神奇的面纱,是因为大多数我们熟悉的编程语言都把这一块给提炼了出去。大多数时候,我们只要简单的敲我们的代码而不用管我们的实例对象占用了内存的几个字节以及当我们不需要这些对象的时候,它们是如何被清除的。总之程序就是能正常运行。 30 | 31 | 自动引用计数(ARC)就是对内存管理的一种提炼。这是苹果通过清除内存中不用的类实例来释放内存空间所采用的一种方法。 32 | 33 | ARC只对类起作用(所以没结构体和枚举什么事),因为自动引用计数只对**引用类型**有效。结构体和枚举属于值类型,所以ARC不会对值类型的实例做任何内存管理。 34 | 35 | 引起我们关于引用类型关键字讨论的问题是这:“ARC是如何知道某个实例已经“用完”它所占据的内存空间呢?”。 36 | 37 | ##strong 强引用 38 | 在ARC的世界里,`strong`,`weak`和`unowned`就是修饰引用本质的关键字。 39 | 40 | 所有的引用在没有特殊说明之前都是`strong`强引用。大多数情况下,这就是对的。在Swift中,当你声明一个变量或是常量时,默认就是`strong`强引用,所以不用在前面添加`strong`。 41 | 42 | 那么,一个引用是强是弱和ARC的内存管理有什么关系吗? 43 | 44 | 在ARC中,只有当指向一个类实例的**所有**`strong`强引用都被断开了,那么这个类实例才会被清除,内存才会被释放。那么问题来了,`strong`强引用在什么情况下才会断开呢? 45 | 46 | `strong`强引用在下列情况下会被断开... 47 | 48 | * 当指向某一实例的变量被设为`nil` 49 | * 当持有一个子类实例引用的父类变量被设为`nil`,这样会同时断开父类和子类之间的相互引用 50 | * 当变量或常量离开了自己的作用域,比如在`if/else`或者`for`循环中初始化了某些变量,当程序执行完这些控制流语句时,相关引用就断开了,同时ARC会释放内存。 51 | 52 | ARC中内存管理的所有事情都取决于到底有**几个**`strong`强引用指向实例。当指向一个实例的`strong`强引用为零的时候,那么就是这个实例被清除,所占据内存空间被释放的时候。 53 | 54 | 那么`weak`弱引用和`unowned`无主引用又是怎么一回事? 55 | 56 | ##weak弱引用和unowned无主引用 57 | 当我们谈到类实例与类实例之间的关系时,就应该要想到`weak`弱引用和`unowned`无主引用。 58 | 59 | ###实例之间的关系 60 | 类与类之间的关系是面对对象编程的一个核心。不管你对类之间的关系有什么规划,总之这些关系是存在的,而且这些关系又对ARC如何做内存管理有影响。 61 | 62 | 那么类与类之间到底有哪些关系类型呢?哪些关系类型需要我们用到`weak`弱引用,哪些需要`unowned`无主引用呢? 63 | 64 | ####层级类关系类型 Hierarchical Relationships 65 | 在Swift中,能经常看到这些层级关系的身影。经常是某一个类的实例对一个或多个子类实例拥有`strong`强引用,而这些子类实例又对自己的一个或多个子类实例持有`strong`强引用。在这种情况下,`strong`强引用方向都一致:从父类指向子类。 66 | 67 | 在这种层级结构中,用`strong`强引用是没问题的。但是,如果某一个子类要反向持有一个父类的引用怎么办呢?如果没考虑好,在ARC中就会遇到麻烦。 68 | 69 | ####可选,相互依赖关系类型 Optional, mutually dependent relationships 70 | 在这一部分,我的目的是讲明`weak`弱引用所扮演的角色。 71 | 72 | 当类实例互相依赖时,它们之间需要相互持有引用。但有时候,依赖性不是强制的,在这种情况下,就允许某一个实例在不持有其他实例引用的情况下也能够存在。 73 | 74 | 如果用`database cardinality`来打个比方,我们可以说双方的关系是0:1。 75 | 76 | 如果我们面前有个例子那是最好不过的了 - 在这过去的一周,Oklahoma州的一个动物园被龙卷风袭击了,有几头珍奇异兽逃脱了,这事给了我灵感! 77 | 78 | 在动物园里,一个`Animal`生活在一个`Exhibit`(展区)里。有时`Exhibits`可能是空的,可能正在被清理,或者原本生活在这里的`Animal`生病了或被转移了,又或者一个龙卷风来袭了,等等... 79 | 80 | 同样的,一个`Animal`可能不住在`Exhibit`中。它可能被临时放在某个地方等`Exhibit`被打扫干净后再回去,或者是在接受治疗也可能是被转移去另一个动物园,也有可能是趁龙卷风来袭逃走了... 81 | 82 | 这动物园的例子可以用代码这样来表示: 83 | 84 | ``` 85 | class Animal { 86 | let name: String 87 | let species: String 88 | init(name: String, species: String) { 89 | self.name = name 90 | self.species = species 91 | } 92 | 93 | var exhibit: Exhibit? // 注意这是个可选类型变量 94 | } 95 | 96 | class Exhibit { 97 | let title: String 98 | init(title: String) { self.title = title} 99 | 100 | var animal: Animal? // 注意这也是个可选类型 101 | } 102 | ``` 103 | 现在有一个`Animal`生活在一个`Exhibit`里。如果我们给这两者建立某种联系,让它们互相拥有对方的信息这样可能会更好。所以我们先实例化一个`Animal`和一个`Exhibit`,然后紧接着把实例赋给对方中相对应的属性。 104 | 105 | 现在我们考虑龙卷风袭击了动物园,然后`Animal`逃离了它的`Exhibit`这事。假设,这个`Animal`没有被找回(实际上他们已经找到那些逃走的动物),再假设这只逃跑的`Animal`正在Oklahoma的平原上自由的奔跑。而动物园,却不得不关闭它的`Exhibit`。在代码要体现这的话,我们只要把`Animal`的实例设为nil,同时把`Exhibit`的实例也设为nil。 106 | 107 | ####内存泄漏 108 | 刚刚给出的这个动物园例子正好说明了在你的应用中造成内存泄漏是可能的。这如何说起? 109 | 110 | 这一切都和ARC有关,只有当指向某个类实例的引用计数为**零**时,ARC才会释放内存。 111 | 112 | 在我们的`Animal` - `Exhibit`例子中,两个实例互相拥有指向对方的引用。当我们把`Animal`设为nil时,`Exhibit`实例仍然会通过自己的`animal`属性持有对`Animal`的引用。 113 | 114 | 反过来说,既然`Animal`实例仍然存在,那么它也通过自己的`exhibit`属性持有一个指向`Exhibit`的引用。所以当我们把`Exhibit`设为nil时,`Animal`还是会指向它不放手。 115 | 116 | 那么现在我们就处在一个尴尬的境地。 `Animal`和`Exhibit`就这么一直互相持有对方的引用(所以谁的内存空间也不会被释放)而除此之外再没其他东西再指向它们了,那么就没办法再访问这两个实例了,它们就永远存在于内存中,内存就这么泄漏了。 117 | 118 | 但是解决的办法总是有的!在这种“可选,相互依赖的关系”中,`weak`弱引用将起着至关重要的作用。 119 | 120 | ####打破强引用循环 121 | 刚刚提到的尴尬场面就是“强引用循环”。 122 | 123 | 好在我们有方法破除这类循环从而避免导致内存泄漏。 124 | 125 | 在互相依赖的关系中,就像上文提到的`Animal`和`Exhibit`这种,只要将其中的一个引用从`strong`强引用转为`weak`弱引用就能破除强引用循环。 126 | 127 | 至于哪个类持有`weak`弱引用不是重点,只要有其中一个类持有。 128 | 129 | ``` 130 | class Animal { 131 | let name: String 132 | let species: String 133 | init(name: String, species: String) { 134 | self.name = name 135 | self.species = species 136 | } 137 | 138 | var exhibit: Exhibit? // 用强引用指向exhibit 139 | } 140 | 141 | class Exhibit { 142 | let title: String 143 | init(title: String) {self.title = title} 144 | 145 | weak var animal: Animal? // 这里用到_weak_弱引用来指向animal 146 | } 147 | ``` 148 | 让`Exhibit`中的`animal`属性拥有一个对`Animal`实例的`weak`弱引用会消除形成强引用循环的可能性。 149 | 150 | 那么`unowned`无主引用又是怎么一回事? 151 | 152 | ####强制,单向依赖关系 153 | 这一部分,我的目标是讲明`unowned`无主引用所扮演的角色。 154 | 155 | 还有最后一类关系类型是直接涉及到ARC这个主题。在有些情况下,两个类实例是相互有联系的,但是其中一个实例必须要在另一个实例存在的前提下才能存在。 156 | 157 | 让我们继续刚才动物的那个例子。假设我们的动物园向`Visitors`游客发行`AnnualPasses`年票。一个`Visitor`游客可以不用`AnnualPass`年票就能进动物园,就光他自己存在就足够欣赏各种展厅了。但是,一个`AnnualPass`年票,却必须需要有一个`Visitor`来持有才有存在的意义。 158 | 159 | 在代码中,我们这样来写: 160 | 161 | ``` 162 | class Visitor { 163 | let name: String 164 | var annualPass: AnnualPass? 165 | init(name: String) { 166 | self.name = name 167 | } 168 | } 169 | 170 | class AnnualPass { 171 | let number: UInt64 172 | unowned let passholder: Visitor // 注意这里我们用了_unowned_无主引用 173 | init(number: UInt64, passholder: Visitor) { 174 | self.name = number 175 | self.passholder = passholder 176 | } 177 | } 178 | ``` 179 | 注意下面几点: 180 | 181 | 1. 游客`Visitor`对`AnnualPass`年票实例有一个可选的`strong`强引用 182 | 2. 年票`AnnualPass`对`Visitor`实例持有一个不可选的`unowned`无主引用 183 | 184 | 这样做的目的在于,当`Visitor`变量被设为nil时,`strong`强引用计数能为零,从而也能够避免因强引用循环而导致的内存泄漏。 185 | 186 | ##总结 187 | 我知道我在一开始就写了总结,但为了文章的完整性,我在下面再总结一次... 188 | 189 | * 在你开始担心应该是用`strong`,`weak`还是`unowned`之前,先问自己一个问题:“我是在和引用类型打交道吗?”。如果是在和结构体或是枚举类型打交道,你就压根不用管是用`weak`还是`unowned`来修饰这些常量或变量,因为ARC就没做这些类型的内存管理。 190 | * `strong`强引用适用于父对象指向子对象,但反过来不能用强引用,听说过父亲拥有儿子的,可没听说过儿子拥有父亲的。事实上,在大多数情况下,`strong`都是默认引用类型。 191 | * 当两个实例是_optionally_关联在一起的,确保其中一个实例是用`weak`弱引用指向另一个实例。 192 | * 两个实例A和B,如果实例A必须在实例B存在的前提下才能存在,那么实例A必须用`unowned`无主引用指向实例B。也就是说,有强制依赖性的那个实例必须对另一个实例持有无主引用。 193 | -------------------------------------------------------------------------------- /issue-4/Swift扩展的三个微妙细节.md: -------------------------------------------------------------------------------- 1 | Swift扩展的三个微妙细节 2 | --- 3 | 4 | > * 原文链接 : [3 Nuances of Swift Extensions](http://www.andrewcbancroft.com/2015/04/22/3-nuances-of-swift-extensions/) 5 | * 原文作者 : [Andrew Bancroft](http://www.andrewcbancroft.com/) 6 | * 译文出自 : [开发技术前线 www.devtf.cn](www.devtf.cn) 7 | * 译者 : [samw00](https://github.com/samw00/) 8 | * 校对者: 9 | * 状态 : 校对中 10 | 11 | 每当我初次翻看某文档时,我都走马观花似的快速阅过,还一边点着头一边喃喃自语说:“好!懂了,就这么回事!”,可是过后当我真正要运用到这些我以为已经理解了的知识点时,却发现实际情况和我想的往往不一样,每当这时我就懵了,心想:“哇哦...怎么回事?这和我想的完全不一样啊!文档里有说这事吗?”。 12 | 13 | 最近的几次讨论促使我扪心自问是否真正的理解了Swift中的扩展。我阅读过关于扩展的文档,并且我“认为”我自己对这块内容已经是理解的相当透彻了。可是这几次讨论,加上自己私下通过敲代码的验证,让我发现了我原先不曾注意到几个微妙的细节。 14 | 15 | **更新**:这篇文章刚一发表,Swift社区就出手襄助并帮助我弄明白了我最根本的纠结点在哪。为此,我写了另一篇文章“[阐明Swift访问控制](http://www.andrewcbancroft.com/2015/04/24/clarifying-swift-access-control-hint-swift-isnt-c-sharp/)”进一步说明我之前的误解。为了避免犯我曾今犯过的错误,我建议大家去读一读。 16 | 17 | ## 三个关于扩展的微妙细节 18 | 对下面列出的三个细节的思考严重挑战了我之前对Swift扩展的理解: 19 | 20 | 1. Swift扩展对它所扩展类型的**visibility**。比如,扩展能访问被`private`所修饰的内容吗? 21 | 2. 定义扩展的位置是否对扩展的**visibility**有影响。比如我这有一个类型我想写个扩展,把扩展写在同一个源文件里和把扩展写在另一个文件里有什么区别吗? 22 | 3. 扩展里“成员”的默认访问修饰符以及是否给他们添加修饰对这个扩展作为一个类型的公共接口的影响。 23 | 24 | 在我开始之前,假设我有一个公共结构体`Person`。这个结构体有一些私有属性,`name`,`gender`,和`age`。用一个枚举把`Gender`封装了一下。这个结构体看起来如下: 25 | 26 | ``` 27 | public struct Person { 28 | private var name: String 29 | private var gender: Gender 30 | private var age: Int 31 | 32 | public init(name: String, gender: Gender, age: Int) { 33 | self.name = name 34 | self.gender = gender 35 | self.age = age 36 | } 37 | 38 | public func howOldArdYou() -> String { 39 | return formattedAge() 40 | } 41 | 42 | // 私有方法,用于下面分析扩展的`visibility`... 43 | private func formattedAge() -> String { 44 | switch self.gender { 45 | case .Male: 46 | return "I'm \(self.age)." 47 | case .Female: 48 | return "Not telling." 49 | } 50 | } 51 | 52 | public enum Gender { 53 | case Male 54 | case Female 55 | } 56 | } 57 | ``` 58 | 现在,就让我们给`Person`写个扩展,通过实践来弄清楚刚刚提到的三个小细节... 59 | 60 | ##扩展对类型的访问能力 61 | 当我提出第一个细节时,关于扩展对被扩展类型的访问能力时,我问了一个问题:“扩展能访问到被`private`修饰的内容吗?”。答案一开始出乎我的预料:能...扩展能访问到。 62 | 63 | 然而,这里就要考虑到第二个细节所涉及的问题,那就是:在**哪里**定义这个扩展是绝对有影响的。 64 | 65 | ###定义在同一个文件里 66 | 如果扩展和类型是在写同一个源文件里,则扩展**能**访问到在类型中被`priavte`所修饰的内容。 67 | 68 | 举个栗子,在Person.swift里定义一个`Person`的扩展就会允许这个扩展访问被`private`修饰的变量和方法 69 | 70 | ``` 71 | extension Person { 72 | func getAge() -> Int { 73 | return age // 尽管age是 --private--, 但编译成功 74 | } 75 | 76 | func getFormattedAge() -> String { 77 | return formattedAge() // 尽管 formattedAge是 --private--,但编译成功 78 | } 79 | } 80 | ``` 81 | “这谁知道?!什么??为啥?”,我当时就没想明白... 82 | 83 | 至于为什么把扩展写在同一个源文件里头会这样,我自己的推理是其实可以在写类型的时候,就把扩展的implementation当作类型的一部分给写了,这样的最终效果是一样的。 84 | 85 | 我在我要“扩展”的类型的**源文件**里,所以无论是我把要新添加的功能当作这个类型的扩展写下来,或是就在这个类型里面定义我原本打算写在扩展里的功能是没有区别的,都是一样的效果。 86 | 87 | 所以,站在编译器的角度来看,编译器可能会说:“好吧,我看到这里写了一个扩展,但是真没这个必要,因为扩展和类型都在同一个源文件里...,所以开发者完全可以把扩展里的这些代码直接写在类型里面...,所以他/她能够访问到被`private`修饰的代码段。” 88 | 89 | **更新:**我上面的写的推理恰恰说明了我压根就没搞明白Swift访问控制机制。所以我建议大家读一读我后来写的“[阐明Swfit控制机制](http://www.andrewcbancroft.com/2015/04/24/clarifying-swift-access-control-hint-swift-isnt-c-sharp/)”这篇文章,里面有更多的细节。 90 | 91 | ###定义在不同文件里 92 | 把扩展写在另一个文件里,则扩展无法访问类型中那些被`private`修饰的内容了。 93 | 94 | 按照上文我自己推理的逻辑来反过来想,定义在不同文件中就访问不了私有属性对我来说也是说的通的。 95 | 96 | 大多数情况下,你都会给那些你**没有**源代码的类型扩展,在这种情况下,扩展就只能访问那些被`public`修饰的内容了。 97 | ![pic1](http://www.andrewcbancroft.com/wp-content/uploads/2015/04/PersonExtensions_swift.png) 98 | 99 | ##默认情况下的扩展访问控制 100 | 对最后一个细节的验证也让我更深的体会。[苹果官方文档](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AccessControl.html#//apple_ref/doc/uid/TP40014097-CH41-ID25)说了,但是直到我动手验证了一番,我才算领会到了默认访问控制修饰符给扩展所造成的微妙的影响。 101 | 102 | ###没有明确声明访问修饰赋时的默认访问 103 | 简单的说,当你声明一个扩展但没有特别明确指明访问修饰符时(默认情况下),这个扩展的默认访问等级取决于被扩展的那个类型的访问等级。 104 | * 如果类型是`public`或者是`internal`,那么扩展的implementation的“成员”就默认为`internal`。这里让我没想到是,除非你特别声明,那么给`public`类型的扩展的成员变量在默认情况下也是`internal`。 105 | * 如果类型是`private`,那么默认情况下扩展的implementation中的“成员”也是`private` 106 | 107 | 下面就是在我们不明确的声明添加什么访问修饰的前提下,来看扩展会是一个什么反应(为了能访问私有属性变量和方法,我在Person.swift里定义了这个扩展): 108 | 109 | ``` 110 | public struct Person { 111 | // ... 112 | 113 | // ... 114 | } 115 | 116 | extension Person { 117 | func getAge() -> Int { 118 | return age 119 | } 120 | 121 | func getFormattedAge() -> String { 122 | return formattedAge() 123 | } 124 | } 125 | ``` 126 | 像上面这段代码用默认的访问修饰符时就会允许在同一个模块中的实例访问扩展里的API。但是,如果被扩展的类型的实例是在另一个模块(比如在测试模块),则无法访问扩展中任何新增的公共API。 127 | ####同一模块 128 | ![pic2](http://www.andrewcbancroft.com/wp-content/uploads/2015/04/SameModule.png) 129 | ####不同模块(测试) 130 | ![pic3](http://www.andrewcbancroft.com/wp-content/uploads/2015/04/DifferentModule.png) 131 | 因为某些原因,我一直都以为如果给一个是`public`的类型添加扩展,那么扩展里的成员也理应是`public`。我不知道为什么我会这么想,但是幸好我的验证把这点捋清楚了。 132 | 133 | ###正常声明扩展,但给扩展的implementation添加public修饰 134 | 给扩展的implementation的成员添加了`public`访问控制修饰,那么不管是在同一模块还是不同模块(test target)都能访问这些成员。 135 | 136 | 只要成员被`public`修饰,那么在同一个源文件里声明扩展还是在另一个文件里声明扩展已经无所谓了...但是,正如前文所讲,只有在同一个源文件中声明的扩展才能够访问那些被`private`修饰的类型成员变量。 137 | ####在不同(左)和同一个(右)源文件中声明的扩展 138 | ![pic4](http://www.andrewcbancroft.com/wp-content/uploads/2015/04/public_extension_members.png) 139 | ####在不同模块中也能访问公共的扩展成员变量 140 | ![pic5](http://www.andrewcbancroft.com/wp-content/uploads/2015/04/public_member_visibility.png) 141 | 这里请注意,在我写`extension Person {...}`时,我没有给这个扩展添加任何的修饰,我只是给这个扩展的成员添加了`public`。即便如此,新添加的方法仍然可以在不同的模块中被访问到。 142 | 143 | 也就是说,没有必要写`public extension Person {...}`。因为`Person`已经是`public`了,所以基于`Person`的扩展也就很自然的延用了类型本身的访问等级。 144 | 145 | ##总结 146 | 对我来说,这篇文章所提到的三个关于Swift扩展的细节已足以让我敲敲代码去验证一番了。我希望这里所作的分析能够为那些尝试理解Swfit扩展的朋友扫清一些障碍。 147 | -------------------------------------------------------------------------------- /issue-4/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlaudev/iOS-tech-frontier/0e21f07cfae753a0aca75389946d4c44ce030bbd/issue-4/readme.md -------------------------------------------------------------------------------- /issue-4/什么是委托代理-Swift开发者指南.md: -------------------------------------------------------------------------------- 1 | 什么是委托代理?- Swift开发者指南 2 | --- 3 | 4 | >* 原文链接 : [What is Delegation? – A Swift Developer’s Guide](http://www.andrewcbancroft.com/2015/03/26/what-is-delegation-a-swift-developers-guide/) 5 | * 原文作者 : [andrew](http://www.andrewcbancroft.com/) 6 | * [译文出自 : 开发技术前线 www.devtf.cn](www.devtf.cn) 7 | * 译者 : [sdq](https://github.com/sdq/) 8 | * 校对者: 9 | * 状态 : 完成 10 | 11 | 12 | ## 什么叫代理模式 13 | 我非常不喜欢看到“代理是一种XXX的设计模式”这种类型的说法,因为对大多数新手程序员来说,有大量接触过“设计模式”,用这样的定义去解释是不合适的。 14 | 15 | 由于Swift降低了iOS开发的门槛,很多初次尝试这个平台的开发者同时也可能是程序开发的新手。在这里我尝试用我觉得更便于理解的方式来解释什么叫代理: 16 | 17 | ###设计模式 18 | 19 | 首先,让我们来说一下“设计模式”。 20 | 21 | **设计**指的是架构设计,它让人联想到一种管理事物的策略,它是一种通过各个不同单元的合作并最终有效完成任务的方法。 22 | 23 | **模式**指的是对于一些主要的,司空见惯的,可以预测的行为方式经过提炼之后得到的具有可重复性使用的行事模型。“模式”往往给人一种印象,即这种方法在长久时间的检验下已经具有了普适性,易懂性和常用性的特征。如果用“适者生存”的法则来比喻,那些在真实世界不能承受时间锤炼的方式终会消逝,而那些坚强地留存下来的方式就形成了我们所谓的“模式”。 24 | 25 | 在软件开发领域,**设计模式**是一套长期检验后公认的易见、重用性高、能产生实际效能的经验总结,常用于软件代码的架构设计,策略规划和结构组织。 26 | 27 | ###代理 28 | 29 | 现在让我们来看看代理(Delegate),我想到了三个解释: 30 | 31 | * 动词,“to delegate”,意思是“交出控制权”。 32 | * 名词,“a delegate”,意思是“代理人”。 33 | * 改造的名词,“a delegator“,意思是“委托人”。 34 | 35 | 在现实世界中,代理这个词同时囊括了关系与责任。委托人(delegator名词)将控制权与责任委托(delegate动词)给另一个人,也就是代理人(delegate)。 36 | 37 | 那么我们怎么将其映射到软件中去呢?事实上,它能很好地运用到代码中。 38 | 39 | 相对于人,在软件中我们处理的对象是类的实例(或者可以是其它数据结构,比如structs,但我想让它保持简单,用“类”来概括这个想法)。代理在软件中是这样发生的,你会遇见这样的情况,一个类(委托者类)会将控制权与责任(这里指的是行为逻辑)委托给另一个类,这样的行为被称作代理。 40 | 41 | ## 代理是如何被使用的 42 | 43 | 没错,代理是一种设计模式。代理虽然是存在于其它各个平台的设计模式,但只有在Apple代理被如此深度地使用着,在iOS的API中代理几乎无所不在。这是一种将一个类的责任转移给另一个类的设计模式,因此它创造了各个类之间责任与关注点的分离。但是究竟是哪种责任与关注点?代理在实际中如何使用的呢? 44 | 45 | 46 | ### 通信 47 | 48 | 在实际应用中,代理通常用来作为一个类与另一个类之间通信的方式。观察Apple官方API对代理的处理方式会观察到线索,比如以下UITableViewDelegate的例子: 49 | 50 | * tableView(_:willSelectRowAtIndexPath:) 51 | * tableView(_:didSelectRowAtIndexPath:) 52 | * tableView(_:didHighlightRowAtIndexPath:) 53 | 54 | 或者是UIScrollViewDelegate的例子: 55 | 56 | * scrollViewDidScroll(_:) 57 | * scrollViewWillBeginDragging(_:) 58 | * scrollViewWillEndDragging(_:withVelocity:targetContentOffset:) 59 | * scrollViewDidEndDragging(_:willDecelerate:) 60 | 61 | 根据我对Apple官方API的观察,代理倾向于使用在某些事件即将发生/已发生时一个实例与另一个实例之间的通信。tableview与scrollview将实现生命周期中某些响应行为的机会委托给其它类,称之为代理。 62 | 63 | 同样值得注意的是代理倾向于使用的通信范围,代理适用于一个实例仅需要发消息给一个单一的接收者(代理)这样的场景,然而NSNotificationCenter则适合于从一个实例对象将信息进行广播。 64 | 65 | ### 自定义 66 | 67 | 代理模式的另一种常见用法,是授权于代理实例,使其可以自定义实例内部的某些特定状态。再提一下,从Apple官方的一些API中可以看到一些这类使用场景,让我们首先看一下UITableViewDelegate: 68 | 69 | * tableView(_:heightForRowAtIndexPath:) 70 | * tableView(_:editActionsForRowAtIndexPath:) 71 | * tableView(_:indentationLevelForRowAtIndexPath:) 72 | * tableView(_:shouldHighlightRowAtIndexPath:) 73 | 74 | 这些自定义的点都是UITableView允许其代理自行处理的。有一些方法非常的重要,在没有从其代理收到信息前,tableview可能无法显示。tableview将这些点的实现方法的责任转移给了它的代理,这样做可以让控制更为灵活,便于自定义。 75 | 76 | 77 | ## 总结 78 | 79 | 代理在iOS中十分重要,理解它到底是什么成为关键点。这篇文章中,我们分解了名词“设计模式”和“代理”来解释为什么这些词被用来描述这样的策略。最后,我们看了代理模式是如何在实际中应用的,观察了两个代理的常用方式:类与类之间的通信以及自定义。 80 | 81 | -------------------------------------------------------------------------------- /issue-4/关于AFNetworking安全bug的回复.md: -------------------------------------------------------------------------------- 1 | 关于AFNetworking 安全bug的回复 2 | --- 3 | 4 | > * 原文链接 : [A response to recent concerns about security vulnerabilities in AFNetworking](https://gist.github.com/AlamofireSoftwareFoundation/f784f18f949b95ab733a?utm_campaign=iOS_Dev_Weekly_Issue_196&utm_medium=email&utm_source=iOS%2BDev%2BWeekly) 5 | * 原文作者: [AlamofireSoftwareFoundation](https://github.com/AlamofireSoftwareFoundation) 6 | * 译文出自: [开发技术前线http://www.devtf.cn/](http://www.devtf.cn/) 7 | * 译者 : [Lollypo](https://github.com/Lollypo) 8 | * 校对者: [Lollypo](https://github.com/Lollypo) 9 | * 状态 : 完成 10 | 11 | 上周大量的出版物流出这么一个故事,大约有1000多个应用程序由于AFNetworking的一个SSL Bug而易受侵害。这些文章中对于该问题包含了一些不正确的误导性称述。 12 | 13 | 正是因为如此,我们发表了这篇文章来回应并澄清这些不正确的描述。 14 | 15 | 16 | 17 | ### 背景信息 18 | 19 | 对于那些不熟系AFNetworking的童鞋,我们准备了一些与该故事有关联的详细信息: 20 | 21 | - [AFNetworking](https://github.com/AFNetworking/AFNetworking)是一个开源的第三方库,其基于苹果的基础平台上为开发者提供了便利的工具。 22 | - AFNetworking 其中的一个组件名为`AFSecurityPolicy`,它负责依据应用程序配置的策略处理身份验证。这其中包括了[X.509证书 ](http://en.wikipedia.org/wiki/X.509)(当通过HTTPS连接时服务器传回的)的校验。 23 | - [证书绑定](http://security.stackexchange.com/a/29990) 是通过强制执行证书服务器发送证书与客户端的凭证来改进标准TLS评分的安全技术。从[版本1.2.0](https://github.com/AFNetworking/AFNetworking/tree/1.2.0)开始,AFNetworking提供证书绑定功能。 24 | - [中间人攻击](http://en.wikipedia.org/wiki/Man-in-the-middle_attack)是一个攻击服务,通过将其本身嵌入到服务器与客户端之间的这种方式,这样两者都认为它们仍然是在直接通信。 25 | - 这样的攻击会通过不可信的WiFi热点代理客户端与服务器之间的请求。如果没有正确的验证响应,攻击者可以拦截通讯信息,这样可能会暴露用户凭证或其他敏感信息。 26 | - AFNetworking文档 [强烈建议应用程序间通过HTTPS以及使用证书/公钥绑定来通信,以减轻MitM攻击](http://cocoadocs.org/docsets/AFNetworking/2.5.3/Classes/AFSecurityPolicy.html).。 27 | 28 | 29 | 30 | ### 时间线 31 | 32 | 考虑到有些朋友对此还不太了解,我们提供了与此相关的突发事件的时间表: 33 | 34 | - 在2015年2月12日,[AFNetworking 2.5.1](https://github.com/AFNetworking/AFNetworking/tree/2.5.1)发布。此版本合并了一个补丁来修正当安全机制由`SSLPinningMode` 设为`AFSSLPinningModeNone`时的[校验凭证](https://github.com/AFNetworking/AFNetworking/blob/2.5.1/AFNetworking/AFSecurityPolicy.m#L257-L259)。默认情况下,证书服务器不会在 35 | 授权改变时做合法性验证,除非客户端配置不同的行为,比如启用SSL绑定。 36 | 37 | - 在2015年3月12日,[从这个GitHub的Issue]( https://github.com/AFNetworking/AFNetworking/issues/2590) 中我们第一次意识到这种变化的行为的影响。 38 | 39 | - 在2015年3月26日,Minded Security Research机构的Simone Bovi 和 Mauro Gentile 发表了一篇[博客](http://blog.mindedsecurity.com/2015/03/ssl-mitm-attack-in-afnetworking-251-do.html?m=1)详述在AFNetworking 2.5.1中潜在的MitM漏洞。 40 | 41 | - 同样在2015年3月26日, [AFNetworking 2.5.2](https://github.com/AFNetworking/AFNetworking/tree/2.5.2)发布。这个版本[恢复到之前的证书校验机制](https://github.com/AFNetworking/AFNetworking/blob/2.5.2/AFNetworking/AFSecurityPolicy.m#L257-L265)以及如果将机制`validatesDomainName`设为`YES`,那么就设置安全机制`SSLPinningMode` 为`AFSSLPinningModeNone`。 42 | 43 | - 在2015年4月20日,[AFNetworking 2.5.3](https://github.com/AFNetworking/AFNetworking/tree/2.5.3)发布。该版本做的附加改变是默认将所有机制的`validatesDomainName`设为`YES `。 44 | 45 | - 在2015年4月21日,[一个Issue在Github上被打开](https://github.com/AFNetworking/AFNetworking/issues/2673),该Issue请求增加关于AFNetworking安全特性的文档。我们遵循了这个建议并且正在积极的检查我们的参考文档。 46 | 47 | - 同样在2015年4月21日,来自SourceDNA的Nate Lawson发布了一篇[博客](http://sourcedna.com/blog/20150420/afnetworking-vulnerability.html) 宣传说在应用商店中识别iOS应用程序的工具就是使用了AFNetworking 2.5.1的。一些记者,包括Ars Technica的的Dan Goodin,发表了一篇[文章](http://arstechnica.com/security/2015/04/1500-ios-apps-have-https-crippling-bug-is-one-of-them-on-your-device/)引用该博客及其作者。但是所有出版物都未向任何一个AFNetworking维护者请求回应。 48 | 49 | - 在2015年4月24日,SourceDNA继续发表了一篇[博客](http://sourcedna.com/blog/20150424/afnetworking-strikes-back.html)指称更多的应用程序变得易受侵害。Ars Technica的的Dan Goodin同样也跟着发表了一篇[文章](http://arstechnica.com/security/2015/04/critical-https-bug-may-open-25000-ios-apps-to-eavesdropping-attacks/)做了同样的事.同样的,没有任何一个出版物向任何一个AFNetworking维护者请求回应。 50 | 51 | 52 | 53 | ### AFNetworking用户可操纵的信息 54 | 55 | 如果你是AFNetworking用户,那么这里是你需要知道的一些可操纵的信息: 56 | 57 | #### 如果你的应用程序通过HTTPS通讯但是尚未启用SSL 绑定,这可能会被报道中的中间人攻击 58 | 59 | 来自AFSecurityPolicy的文档 60 | 61 | > 添加SSL证书绑定有助于避免你的应用程序遭受中间人攻击或其他安全漏洞。处理敏感的客户数据或财务信息的应用程序我们强烈建议启用SSL绑定并使用 HTTPS 连接通信。 62 | 63 | 按照以下这些建议操作的任何应用程序在任何时刻应该都不会暴露上述漏洞。 64 | 65 | ####如果你的应用程序启用了SSL绑定且通过HTTPS通信,它就不会像报道中说的那样易受 MitM攻击 66 | 67 | 大量使用 AFNetworking的应用程序遵循了建议的步骤启用了SSL证书或公共密钥绑定。这些应用程序不会像报道中说的那样易受 MitM攻击。 68 | 69 | ####如果你正在使用早先的AFNetworking版本, 我们强烈建议你升级到版本2.5.3 70 | 71 | AFNetworking 2.5.1 以及 2.5.2 不适合应用程序产品,尤其是它们不经过额外配置就无法提供TLS评分。 72 | 73 | AFNetworking 2.5.3默认为安全的行为,即使未使用SSL绑定也支持域名校验。 74 | 75 | ####如果你正在使用NSURLConnection / NSURLSession而不是AFNetworking,你仍然需要审查你的身份校验的实现 76 | 77 | 苹果内置的NSURLConnection/NSURLSession以及安全框架API提供的凭证校验的安全实现。然而,就像任何API,应用程序只是在使用这些API的时候是安全的。 78 | 79 | 决定是否使用AFNetworking并不能保证你的应用程序免受攻击,例如MitM。这完全却取决于该应用程序如何使用这些可用的API。最终,这些开发人员还要在生产环境中测试应用程序的健壮性和网络安全。 80 | 81 | #### 如果你想报告一个漏洞,请发邮件给[security@alamofire.org](security@alamofire.org) 82 | 83 | 我们会尽快的作出回应。 84 | 85 | #### 如果你想要贡献自己的一份力量来让AFNetworking变得更改,请打开一个Issue或者Pull Request 86 | 87 | AFNetworking是开源的,这意味着任何人都有机会作出贡献,使它变得更好。[Issues](https://github.com/AFNetworking/AFNetworking/issues/new) 或者[Pull Requests](https://github.com/AFNetworking/AFNetworking/issues/new) 都行。 88 | 89 | 90 | 91 | ### 关于负责的安全研究和新闻声明 92 | 93 | 安全研究人员在实现面向用户的软件的安全性方面扮演者一个重要的角色。如果安全研究人员与开发者同心协力,遵守协定作出[负责任的漏洞报告](http://en.wikipedia.org/wiki/Responsible_disclosure),那么就可以快速的解决应用程序的脆弱性问题同时将现有用户的风险降到最低。 94 | 95 | 我们是这样子做的,然而,对于某些安全研究人员以及出版商决定由他们自身报道AFNetworking的事情我们感到非常失望。信息安全是所有人都需要了解的一个重要话题,无论是安全研究人员或是新闻工作者都有机会让读者了解这些实际情况。很不幸的是,大多数时候,这些报道都是通过恐惧而不是客观现实来吸引流量的。 96 | 97 | 目前尚未由明确的方法来证明由多少应用程序遭受了这种行为;猜测安全问题的严重性来计算当它们发生的时候会造成多么大的破坏以及反应的比例数。同样,基于提供少量的工具给商家或他们的用户来推测出脆弱性的应用程序。 98 | 99 | 事实是,一直以来编写安全的软件都是一个困难的挑战。这样做需要跨越多个学科的工程师的合作。它是极为重要的任务,那个人最好是理性的、 负责任的。 100 | 101 | 作为软件维护者,由许多东西我们可以做得更好的,并且正在积极采取步骤来改善我们的组织和流程。我们期待着与信息安全社会的成员密切合作,从现在开始负责任地找出并解决任何漏洞。 102 | 103 | 104 | 105 | ### 一项关于负责任的开放源代码维护声明 106 | 107 | 我们对那些正在使用AFNetworking的所有开发者以及iOS社区表示最诚挚的歉意。 108 | 109 | 作为一个著名的开源项目维护者,提供满足高标准的选择让用户选择作为应用程序的依赖是我们的责任。我们未能尽快的发布更新版本的回应。我们未能有效地传达给你的重要的安全信息。以上,我们真的很抱歉,我们愿意承担全部责任。 110 | 111 | 在未来的几周,我们将推出了重构的的 AFNetworking 和及其相关的项目,以确保软件质量和通信的一致性一同向前发展。对于用户而言这意味着更加频繁的版本发布和更多的透明度和反馈处理issues和pull requests的过程。我们很兴奋地想知道对于AFNetworking 项目和它的用户,这将意味着什么。 -------------------------------------------------------------------------------- /issue-5/Core Graphics 教程第三部分(Swift) - Patterns 和 Playgrounds.md: -------------------------------------------------------------------------------- 1 | Core Graphics 教程第三部分(Swift) - Patterns 和 Playgrounds 2 | --- 3 | >* 原文链接:[Core Graphics Tutorial Part 3: Patterns and Playgrounds](http://www.raywenderlich.com/90695/modern-core-graphics-with-swift-part-3) 4 | * 原文作者:[ Caroline Begbie ](http://www.raywenderlich.com/u/caroline) 5 | * 译文出自:[开发者前线](www.devtf.cn) 6 | * 译者:[MrLoong](https://github.com/MrLoong) 7 | * 校对者:[samw00](https://github.com/samw00) 8 | 9 | 欢迎回到Core Graphics 教学系列的第三也是最后一部分!Flo,你的water drinking tracking app将使用Core Graphics 进行最终的改进。 10 | 11 | 在[第一部分](http://www.raywenderlich.com/90690/modern-core-graphics-with-swift-part-1),你通过UIKit画了三个自定义形状的控件,[在第二部分](http://www.raywenderlich.com/90693/modern-core-graphics-with-swift-part-2)你创建了一个视图用来显示用户在过去一周的喝水量,并且研究了transforming the context transformation matrix (CTM) 12 | 13 | 在这第三也是最后一部分,你将使**Flo**升级到它的最终形态,特别的是你将: 14 | 15 | * 创建一个重复的背景图案 16 | * 为成功每天喝掉8杯水的用户从头到尾画一枚奖章 17 | 18 | 如果你还没有这个项目的源代码,从本系列的第二部分下载[**Flo工程**](http://cdn1.raywenderlich.com/wp-content/uploads/2015/02/Flo-Part2-6.3.zip) 19 | 20 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/12/FinalApp-192x320.png) 21 | 22 | ##Background Repeating Pattern 背景重复模式 23 | 你在这部分的任务是使用UIKit的方式创建这个背景图案: 24 | 25 | ![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/12/3-FinalBackground-300x500.png) 26 | 27 | ~~~ 28 | Note: If you need to optimize for speed, then work through 29 | Core Graphics Tutorial: Patterns which demonstrates a basic 30 | way to create patterns with Objective-C and Core Graphics. 31 | For most purposes, like when the background is only drawn 32 | once, UIKit’s easier wrapper methods should be acceptable. 33 | ~~~ 34 | ~~~ 35 | 注意:如果你需要对速度进行优化,可以去看[Core Graphics Tutorial:Patters](http://www.raywenderlich.com/33496/core-graphics-tutorial-patterns)里面演示一个使用Object-c和Core Graphics的基本方法去创建一个格局.在大多数情况下当背景仅被描绘一次,使用UIKit封装好的简易方法应该是可以接受的。 36 | ~~~ 37 | 38 | 点击**File\New\File…**并且选择IOS **iOS\Source\Cocoa Touch Class** 模版创建一个类名为**BackgroundView**的UIView。点击下一步然后创建 39 | 40 | 进入**Main.storyboard**,在 **Identity Inspector**选择ViewController主界面,改变为**BackgroundView** 41 | 42 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/12/3-BackgroundViewStoryboard3-480x187.png) 43 | 44 | 点击**Assistant Editor**,让**BackgroundView.swift**和**Main.storyboard**并排显示。 45 | 46 | 替换**BackgroundView.swift**的代码: 47 | 48 | ``` 49 | import UIKit 50 | 51 | @IBDesignable 52 | 53 | class BackgroundView: UIView { 54 | 55 | //1 56 | @IBInspectable var lightColor: UIColor = UIColor.orangeColor() 57 | @IBInspectable var darkColor: UIColor = UIColor.yellowColor() 58 | @IBInspectable var patternSize:CGFloat = 200 59 | 60 | override func drawRect(rect: CGRect) { 61 | //2 62 | let context = UIGraphicsGetCurrentContext() 63 | 64 | //3 65 | CGContextSetFillColorWithColor(context, darkColor.CGColor) 66 | 67 | //4 68 | CGContextFillRect(context, rect) 69 | } 70 | } 71 | ``` 72 | 73 | 现在你的storyboard背景应该是黄色的,更多的细节参考上面的代码: 74 | 75 | 1. **lightColor**和**darkColor**拥有**@IBInspectable**属性,所以更容易去配置背景颜色。你使用橙色和黄色作为临时色,这样你就能直到到底在发生什么事。**patternSize**控件控制着重复形式的大小。它的初始值设置为large,所以更容易看清发生了什么。 76 | 2. **UIGraphicsGetCurrentContext()**让你了解视图上下文同样也是**drawRect(_:)**将会绘画的区域。 77 | 3. 使用Core Graphics的方法**CGContextSetFillColorWithColor()** 去设置当前上下文的填充色。注意当你使用Core Graphics时,需要用到**darkColor**的一个属性**CGColor**。 78 | 4. **CGContextFillRect()**使用当前填充色填充当前上下文的整个背景,而不是建立一个矩形路径。 79 | 80 | 你现在需要使用**UIBezierPath()**画这三个橙色的三角形。图中的点对应代码中的数字。 81 | 82 | ![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/12/3-GridPattern.png) 83 | 84 | 仍然在**BackgroundView.swift**中操作,在 **drawRect(_:)**末尾添加如下代码: 85 | 86 | ``` 87 | let drawSize = CGSize(width: patternSize, height: patternSize) 88 | 89 | //insert code here 90 | 91 | 92 | let trianglePath = UIBezierPath() 93 | //1 94 | trianglePath.moveToPoint(CGPoint(x:drawSize.width/2, 95 | y:0)) 96 | //2 97 | trianglePath.addLineToPoint(CGPoint(x:0, 98 | y:drawSize.height/2)) 99 | //3 100 | trianglePath.addLineToPoint(CGPoint(x:drawSize.width, 101 | y:drawSize.height/2)) 102 | 103 | //4 104 | trianglePath.moveToPoint(CGPoint(x: 0, 105 | y: drawSize.height/2)) 106 | //5 107 | trianglePath.addLineToPoint(CGPoint(x: drawSize.width/2, 108 | y: drawSize.height)) 109 | //6 110 | trianglePath.addLineToPoint(CGPoint(x: 0, 111 | y: drawSize.height)) 112 | 113 | //7 114 | trianglePath.moveToPoint(CGPoint(x: drawSize.width, 115 | y: drawSize.height/2)) 116 | //8 117 | trianglePath.addLineToPoint(CGPoint(x:drawSize.width/2, 118 | y:drawSize.height)) 119 | //9 120 | trianglePath.addLineToPoint(CGPoint(x: drawSize.width, 121 | y: drawSize.height)) 122 | 123 | lightColor.setFill() 124 | trianglePath.fill() 125 | 126 | ``` 127 | 128 | 注意你是如何使用一个路径画三个三角形的,**moveToPoint(_:)**就像在纸上抬起你的笔从一个地方绘制和移动到另一个地方。 129 | 130 | 你的storyboard现在应该有一个橙色和黄的的图像在你背景的左上方。 131 | 132 | 到目前为止,你都是直接在一个视图的上下文中直接绘画。为了能重复这个图案,你需要在上下文外创建一个图像,然后用这个图像作为上下文中的背景图案。 133 | 134 | 找到下面这段代码,在drawRect(_:)的顶部,但是在下面这段代码之后: 135 | 136 | ``` 137 | et drawSize = CGSize(width: patternSize, height: patternSize) 138 | ``` 139 | 140 | 然后插入下面的代码 141 | 142 | ``` 143 | UIGraphicsBeginImageContextWithOptions(drawSize, true, 0.0) 144 | let drawingContext = UIGraphicsGetCurrentContext() 145 | 146 | //set the fill color for the new context 147 | darkColor.setFill() 148 | CGContextFillRect(drawingContext, 149 | CGRectMake(0, 0, drawSize.width, drawSize.height)) 150 | ``` 151 | 152 | 嘿!那些橘黄色三角形从面板中消失了,它们去哪了? 153 | 154 | **UIGraphicsBeginImageContextWithOptions()**创建了一个新的上下文,并且将其设置为当前的绘图上下文,所以你在这个新的上下文中作画。刚才方法的参数为: 155 | 156 | * context的大小 157 | * 背景是否透明— 如果你需要透明的, 这里设置为false. 158 | * context scale的大小. 如果要适应视网膜屏幕, 这里应该设置为2, 如果要适应iPhone 6 Plus, 应该为3.0. 然而这里使用0.0将自动适配屏幕 159 | 160 | 然后你用 **UIGraphicsGetCurrentContext()**得到这个新的上下文的引用。 161 | 162 | 然后你将这个新的context设置为黄色。你可以通过设这context的属性opacity为false让原来的背景色显示,但是画一个不透明的context比画一个透明的要快得多,凭这一点就应该选opaque。 163 | 164 | 在**drawRect(_:)**的末尾添加下面的代码: 165 | 166 | ``` 167 | let image = UIGraphicsGetImageFromCurrentImageContext() 168 | UIGraphicsEndImageContext() 169 | 170 | ``` 171 | 172 | 在当前的context提取一个UIImage。当你使用**UIGraphicsEndImageContext()**结束当前context时,描绘context还原为视图context,所以任何未来在**drawRect(_:)** 中的的绘图都会反应在viwe中。 173 | 174 | 为了使图像作为一个可重复的图案,在drawRect(_:)末尾添加代码: 175 | 176 | ``` 177 | UIColor(patternImage: image).setFill() 178 | CGContextFillRect(context, rect) 179 | ``` 180 | 181 | 这会让图片当作UIColor中的一个新的颜色来用,而不是一个纯色。 182 | 183 | 编译并且运行app,你现在的app应该有一个相当亮眼的背景: 184 | 185 | ![](http://cdn1.raywenderlich.com/wp-content/uploads/2014/12/3-BoldBackground2.png) 186 | 187 | 去Main.storyboard,选择background view,并且在Attributes Inspector改变IBInspectable属性的值为: 188 | 189 | * Light Color: RGB(255, 255, 242) 190 | * Dark Color: RGB(223, 255, 247) 191 | * Pattern Size: 30 192 | 193 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/12/3-BackgroundColors2-473x320.png) 194 | 195 | 尝试去画一些不同的背景图案,看看除了三角形,你能不能把背景图案变成polka dot。 196 | 197 | 当然,你可以使用自己的非矢量图作为背景图案 198 | 199 | ##Drawing Images 绘制图像 200 | 201 | 在本教程的最后阶段,你将为喝够水的人送上一个奖牌,当计数器纪录的数量达到8杯的时候这个奖牌将出现。 202 | 203 | ![](http://cdn1.raywenderlich.com/wp-content/uploads/2014/12/3-MedalFinal.png) 204 | 205 | 我知道这肯定比不上博物馆里的一件艺术品,但是请你知道,如果你改进它,我并不会生气,甚至你可以通过画一个奖杯而不是奖牌来升华一下。 206 | 207 | 你将在**Swift Playground**中绘制这个奖牌,而不是用**@IBDesignable**,然后把代码复制到**UIImageView**的子类中。虽然与storyboards互动通长是很有用的,但他们有局限性。他们仅能描绘简单的代码,当你创建一个复杂的设计时,storyboards通长很耗时间。 208 | 209 | 在这种情况下,只有当用户喝满八杯水的时候才需要画一个图像。如果用户从来没有到达到过目标,那就根本不需要绘制奖牌。 210 | 211 | 一旦绘制,他也不需要使用drawRect(_:)和setNeedsDisplay().进行重绘。 212 | 213 | 是时候作画了。首先点击Standard Editor按钮,让Xcode返回单一界面编辑模式。 214 | 215 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/12/3-StandardEditor.png) 216 | 217 | 点击**File\New\File…**并且选择IOS Playground模版。点击 Next 命名playground为MedalDrawing然后点击Create。 218 | 219 | 把playground中的代码替换为: 220 | 221 | ``` 222 | import UIKit 223 | 224 | let size = CGSize(width: 120, height: 200) 225 | 226 | UIGraphicsBeginImageContextWithOptions(size, false, 0.0) 227 | let context = UIGraphicsGetCurrentContext() 228 | 229 | //This code must always be at the end of the playground 230 | let image = UIGraphicsGetImageFromCurrentImageContext() 231 | UIGraphicsEndImageContext() 232 | ``` 233 | 234 | 这将创建一个绘制上下文,就像你之前刚给重复背景图片做的是一回事。 235 | 236 | 注意最后两行。你总是需要它们在playground底部,这样你就可以在playground中预览图片。 237 | 238 | 下一步,在灰色的结果栏中单击代码右边的+按钮: 239 | 240 | ![](http://cdn2.raywenderlich.com/wp-content/uploads/2015/02/ShowFinishedImage.gif) 241 | 242 | 这将在代码的底部预览你的图像。图像将根据你代码的每次更新而更新。 243 | 244 | 在你作画之前,最好能在你的脑海里先有一张绘制草图,看看我在构造这篇tutorial时所构想的“杰作”: 245 | 246 | ![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/12/3-Sketch.png) 247 | 248 | 下面是绘制奖牌的顺序: 249 | 250 | 1. 后丝带(红色) 251 | 2. 奖章(金色渐变) 252 | 3. 扣环(暗金色) 253 | 4. 前丝带(蓝色) 254 | 5. 数字1(暗金色) 255 | 256 | 记住不要变动playground的最后两行(在你最后提取背景图像的地方),并且在那两行代码前添加描绘代码: 257 | 258 | 首先设置你要的非标准颜色。 259 | 260 | ``` 261 | //Gold colors 262 | let darkGoldColor = UIColor(red: 0.6, green: 0.5, blue: 0.15, alpha: 1.0) 263 | let midGoldColor = UIColor(red: 0.86, green: 0.73, blue: 0.3, alpha: 1.0) 264 | let lightGoldColor = UIColor(red: 1.0, green: 0.98, blue: 0.9, alpha: 1.0) 265 | ``` 266 | 267 | 这些现在应该看起来非常熟悉了。注意当你声明颜色的时候,颜色会出现在palyground的右边。 268 | 269 | 下面的代码将绘制奖章的红色丝带: 270 | 271 | ``` 272 | //Lower Ribbon 273 | var lowerRibbonPath = UIBezierPath() 274 | lowerRibbonPath.moveToPoint(CGPointMake(0, 0)) 275 | lowerRibbonPath.addLineToPoint(CGPointMake(40,0)) 276 | lowerRibbonPath.addLineToPoint(CGPointMake(78, 70)) 277 | lowerRibbonPath.addLineToPoint(CGPointMake(38, 70)) 278 | lowerRibbonPath.closePath() 279 | UIColor.redColor().setFill() 280 | lowerRibbonPath.fill() 281 | ``` 282 | 283 | 没有什么新东西,就是创建一个路径并且填充它。你应该看见红色的路径出现在右侧。 284 | 285 | 添加描绘扣环的代码: 286 | 287 | ``` 288 | //Clasp 289 | 290 | var claspPath = UIBezierPath(roundedRect: 291 | CGRectMake(36, 62, 43, 20), 292 | cornerRadius: 5) 293 | claspPath.lineWidth = 5 294 | darkGoldColor.setStroke() 295 | claspPath.stroke() 296 | ``` 297 | 298 | 这里你将通过**UIBezierPath(roundedRect:)**中的**cornerRadius**这个参数来设置圆角。 299 | 扣环应该在右侧显示了。 300 | 301 | 添加绘制奖章的代码: 302 | 303 | ``` 304 | //Medallion 305 | 306 | var medallionPath = UIBezierPath(ovalInRect: 307 | CGRect(origin: CGPointMake(8, 72), 308 | size: CGSizeMake(100, 100))) 309 | //CGContextSaveGState(context) 310 | //medallionPath.addClip() 311 | let gradient = CGGradientCreateWithColors( 312 | CGColorSpaceCreateDeviceRGB(), 313 | [darkGoldColor.CGColor, 314 | midGoldColor.CGColor, 315 | lightGoldColor.CGColor], 316 | [0, 0.51, 1]) 317 | CGContextDrawLinearGradient(context, 318 | gradient, 319 | CGPointMake(40, 40), 320 | CGPointMake(40,162), 321 | 0) 322 | //CGContextRestoreGState(context) 323 | ``` 324 | 注意注释部分,这些都是暂时显示颜色渐变是如何写的: 325 | 326 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/12/3-SquareGradient.png) 327 | 328 | 为了使颜色从左上角至右下角渐变,改变渐变最终点的x坐标,修改**CGContextDrawLinearGradient()**中的代码: 329 | 330 | ``` 331 | CGContextDrawLinearGradient(context, 332 | gradient, 333 | CGPointMake(40, 40), 334 | CGPointMake(100,160), 335 | 0) 336 | ``` 337 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/12/3-SkewedGradient.png) 338 | 339 | 现在取消奖章那部分代码中的三行注释从而创建了一个剪切路径把渐变图案约束在奖章的圈圈内。 340 | 341 | 就像你在第二部分绘制图形是一样,你在添加剪切路径前保存了绘画上下文,在渐变画好自后你有将上下文恢复了回去,所以上下文不再是被剪切的状态了。 342 | 343 | ![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/12/3-ClippedGradient.png) 344 | 345 | 用奖牌的圆圈路径来绘制奖牌上的实线,但绘制前要改变比例。你只将改变应用到一个路径上而不是转变整个context。 346 | 347 | 在绘制奖章的代码下添加: 348 | 349 | ``` 350 | //Create a transform 351 | //Scale it, and translate it right and down 352 | var transform = CGAffineTransformMakeScale(0.8, 0.8) 353 | transform = CGAffineTransformTranslate(transform, 15, 30) 354 | 355 | medallionPath.lineWidth = 2.0 356 | 357 | //apply the transform to the path 358 | medallionPath.applyTransform(transform) 359 | medallionPath.stroke() 360 | ``` 361 | ![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/12/3-MedalOutline.png) 362 | 363 | 这将路径的尺寸缩小至原始尺寸的80%,并转换路径使其人在渐变视图中居中。 364 | 365 | 之后添加前丝带的绘制代码: 366 | 367 | ``` 368 | //Upper Ribbon 369 | 370 | var upperRibbonPath = UIBezierPath() 371 | upperRibbonPath.moveToPoint(CGPointMake(68, 0)) 372 | upperRibbonPath.addLineToPoint(CGPointMake(108, 0)) 373 | upperRibbonPath.addLineToPoint(CGPointMake(78, 70)) 374 | upperRibbonPath.addLineToPoint(CGPointMake(38, 70)) 375 | upperRibbonPath.closePath() 376 | 377 | UIColor.blueColor().setFill() 378 | upperRibbonPath.fill() 379 | ``` 380 | 381 | 这和你之前给后丝带添加的代码非常相似,创建一个bezier路径并且填充。 382 | 383 | ![](http://cdn4.raywenderlich.com/wp-content/uploads/2014/12/3-UpperRibbon.png) 384 | 385 | 最后一步是在奖章上画数字1,代码如下: 386 | 387 | ``` 388 | //Number One 389 | 390 | //Must be NSString to be able to use drawInRect() 391 | let numberOne = "1" 392 | let numberOneRect = CGRectMake(47, 100, 50, 50) 393 | let font = UIFont(name: "Academy Engraved LET", size: 60) 394 | let textStyle = NSMutableParagraphStyle.defaultParagraphStyle() 395 | let numberOneAttributes = [ 396 | NSFontAttributeName: font!, 397 | NSForegroundColorAttributeName: darkGoldColor] 398 | numberOne.drawInRect(numberOneRect, 399 | withAttributes:numberOneAttributes) 400 | ``` 401 | 402 | 在这里你定义了一个字符串并设置了字体属性,然后使用**drawInRect(_:).**在context进行了重绘 403 | 404 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/12/3-NumberOne.png) 405 | 406 | 看上去不错 407 | 408 | 你已经很接近成功了,但它看起来有一些二维 - 如果加上一些阴影会更好。 409 | 410 | ##Shadows 阴影 411 | 412 | 创建一个阴影需要三个要素:颜色,偏移(阴影的距离和方向)和模糊 413 | 414 | 在 playground 顶部,位于自定义颜色之后但在//Lower Ribbon之前,插入实现阴影的代码: 415 | 416 | ``` 417 | //Add Shadow 418 | let shadow:UIColor = UIColor.blackColor().colorWithAlphaComponent(0.80) 419 | let shadowOffset = CGSizeMake(2.0, 2.0) 420 | let shadowBlurRadius: CGFloat = 5 421 | 422 | CGContextSetShadowWithColor(context, 423 | shadowOffset, 424 | shadowBlurRadius, 425 | shadow.CGColor) 426 | ``` 427 | 428 | Okay,这能给我们一个阴影,但结果并不是我们所想象的那样。为什么呢? 429 | 430 | ![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/12/3-MessyShadows.png) 431 | 432 | 当你在上下文中绘制一个对象时,该代码会给所有的对象都创建阴影。 433 | 434 | ![](http://cdn4.raywenderlich.com/wp-content/uploads/2014/12/3-IndividualShadows.png) 435 | 436 | 啊!你的奖章由五个对象所组成。难怪看起来有点模糊。幸运的是这改起来比较容易。就是简单的把所有的对象通过一个透明的layer组起来,然后你只需要对整个对象组绘制一个阴影就可以。 437 | 438 | ![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/12/3-GroupedShadow.png) 439 | 440 | 在阴影代码后添加组合对象的代码。以这段开头: 441 | 442 | ``` 443 | CGContextBeginTransparencyLayer(context, nil) 444 | ``` 445 | 446 | 当你开启一组,你也需要结束它。所以在playground末端添加这一段代码,但仍然在截取最后的图像之前: 447 | 448 | ``` 449 | CGContextEndTransparencyLayer(context) 450 | ``` 451 | 452 | 现在你将有一个完整的奖牌图像,整洁的阴影。 453 | 454 | ![](http://cdn1.raywenderlich.com/wp-content/uploads/2014/12/3-MedalFinal.png) 455 | 456 | ##Image View Using Core Graphics Image创建一个新的图像视图 457 | 458 | 给这个图像视图创建一个新的文件。 459 | 460 | 单击**File\New\File…** ,并且选择 **Cocoa Touch Class** 模版,单击Next,并且将类命名为**MedalView**,父类为**UIImageView**,然后点击Next,点击Creat 461 | 462 | 进入**Main.storyboard**,把**UIImageView** 添加为**Counter View**的一个子视图。然后选择UIImageView,在**Identity Inspector**把类改为 **MedalView**。 463 | 464 | ![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/12/3-MedalViewClass.png) 465 | 466 | 在**Size Inspector**给视图设置坐标为x=76,y=147,宽度为80,高度为80: 467 | 468 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/12/3-MedalViewCoordinates.png) 469 | 470 | 在**Attributes Inspector**中改变**Image Mode**为**Aspect Fit**,这样图像可以自动调整大小适应视图。 471 | 472 | ![](http://cdn4.raywenderlich.com/wp-content/uploads/2014/12/3-MedalAspectFit.png) 473 | 474 | 去medalview.swift并添加一个方法用来创建奖章: 475 | 476 | ``` 477 | func createMedalImage() -> UIImage { 478 | println("creating Medal Image") 479 | 480 | } 481 | ``` 482 | 483 | 这会打印一句话,这样你就能直到图像什么时候被创建。 484 | 485 | 进入**MedalDrawing** playground,复制整个代码除了开始的 import UIKit。 486 | 487 | 返回**MedalView.swift**把playground中的代码黏贴到**createMedalImage()**中去。 488 | 489 | 在**createMedalImage()**尾部添加: 490 | 491 | ``` 492 | return image 493 | ``` 494 | 495 | 应该会编译错误。 496 | 在类的顶部添加一个属性来持有奖牌图像: 497 | 498 | ``` 499 | lazy var medalImage:UIImage = self.createMedalImage() 500 | ``` 501 | 502 | 添加一个方法去显示奖牌: 503 | 504 | ``` 505 | func showMedal(show:Bool) { 506 | if show { 507 | image = medalImage 508 | } else { 509 | image = nil 510 | } 511 | } 512 | ``` 513 | 514 | 在**ViewController.swift**类顶部添加一个外部应用: 515 | 516 | ``` 517 | @IBOutlet weak var medalView: MedalView! 518 | ``` 519 | 520 | 去Main.storyboard将new MedalView和这个外部引用相连。 521 | 522 | 返回**ViewController.swift**并且添加这个方法: 523 | 524 | ``` 525 | func checkTotal() { 526 | if counterView.counter >= 8 { 527 | medalView.showMedal(true) 528 | } else { 529 | medalView.showMedal(false) 530 | } 531 | } 532 | ``` 533 | 534 | 如果你今天喝了足够的水,这个奖牌将显示。 535 | 536 | 在**viewDidLoad()** 和 **btnPushButton(_:)**这两个方法的底部调用: 537 | 538 | ``` 539 | checkTotal() 540 | ``` 541 | 542 | 编译并运行应用,它应该像这样: 543 | 544 | ![](http://cdn2.raywenderlich.com/wp-content/uploads/2014/12/3-CompletedApp.png) 545 | 546 | 在调试控制台中,只有当计数器达到8时,你才会看到创建奖章的那句log,这是因为medalImage使用了一个延迟声明lazy declaration。 547 | 548 | ##Where to Go From Here? 何去何从 549 | 550 | 你已经在这部系列教程中走了很长的路。你应该已经掌握了Core Graphics的基本知识,创建图案和渐变还有转换context,最为重要的是,你学会了如何在app中融汇地使用它们。 551 | 552 | 在这[Flo right here](http://cdn1.raywenderlich.com/wp-content/uploads/2015/02/Flo-Part3-6.31.zip)下载完整的版本。这个版本还包括一些额外地数据和辐射渐变让按钮在被按下的时候能给用户一个更好的UI体验。 553 | 554 | 我希望在制作Flo时你乐在其中,并且你能只用Core Graphics和UIKit做一些惊叹漂亮的UI!如果你有任何问题,意见,或者你想讨论如何绘制一个奖杯,而不是一个奖章,请加入论坛讨论下。 555 | -------------------------------------------------------------------------------- /issue-5/readme.md: -------------------------------------------------------------------------------- 1 | # 第五期 -------------------------------------------------------------------------------- /issue-6/iOS项目的持续集成与管理.md: -------------------------------------------------------------------------------- 1 | iOS项目的持续集成与管理 2 | --- 3 | 4 | > * 原文链接 : [Continuous Integration & Deployment for iOS Projects](https://medium.com/ribot-labs/continuous-integration-deployment-for-ios-projects-7358b72ca2e9) 5 | * 原文作者 : [Matt Oakes](https://medium.com/@matto1990) 6 | * [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn) 7 | * 译者 : [Sam Lau](https://github.com/samlaudev) 8 | * 校对者: [Mr.Simple](https://github.com/bboyfeiyu) 9 | * 状态 : 校正完 10 | 11 | 当实现新功能时,如果忽略可维护性而引入[技术债务](http://en.wikipedia.org/wiki/Technical_debt),那将会需要延迟解决它或导致增加维护成本。 12 | 13 | 最近我们已经思考通过哪些方式来提高代码的质量: 14 | 15 | * 当代码的质量下降时,通过设置一些工具来马上提醒开发者 16 | * 文档化一些编码规范和思考在过去的几个项目中如何避免维护性差的问题 17 | 18 | **我将会简单地概括我们需要设置什么才能自动监控代码质量.** 19 | 20 | #基础 21 | 我们选择一个持续集成工具[Jenkins](https://jenkins-ci.org/),让它运行在一台放在我们工作室的Mac Mini。其实我不怎么喜欢Jenkins,但到目前为止,它是最稳定和最适合的工具来完成这些工作。 22 | 23 | 我们已经通过[Homebrew](https://brew.sh/)和[rbenv](https://github.com/sstephenson/rbenv)来分别安装Jenkins和Ruby,而rbenv能够为我们提供一个最新和稳定的[Ruby Gems](https://rubygems.org/)环境。有个Homebrew和Ruby Gems两个包管理工具之后,我们就几乎能够安装所有我们需要的工具,但很少会破坏与原有OS X系统更新提供的Ruby。 24 | 25 | #单元测试 26 | 我们使用[Specta](https://github.com/specta/specta)和[Expecta](https://github.com/specta/expecta)来测试我们的iOS项目。 27 | 28 | Specta让我们采用行为驱动开发(BDD)风格的语法来编写测试,相比于*XCTest*的语法,它更加易读。它还有一个强大的分组测试功能,在测试之前或之后运行一些代码块,这样的话,能够极大地减少重复代码。 29 | 30 | Expecta是一个匹配器框架,我们可以在测试中使用它来创建断言。它的语法非常强大,与此同时,它比内建的*XCAssert*套件更加易读。例如: 31 | 32 | ``` 33 | expect(@"foo").to.equal(@"foo"); 34 | expect(foo).notTo.equal(1); 35 | expect([bar isBar]).to.equal(YES); 36 | expect(baz).to.equal(3.14159); 37 | ``` 38 | 我们在开发时,通过XCode来运行测试;而使用通过Homebrew来安装的Jenkins时,会借助[XCTool](https://github.com/facebook/xctool)。XCTool是一个可代替的选择来*xcodebuild*,它能让你通过命令行的方式来非常轻松地运行测试套件和生成JUnit风格的测试报告。 39 | 40 | ``` 41 | $ xctool -workspace Project.xcworkspace -scheme Project -reporter junit:junit-report.xml test 42 | 43 | ``` 44 | 45 | 这些测试报告会发布在Jenkins上,而Jenkins会使用[JUnit Plugin](https://wiki.jenkins-ci.org/display/JENKINS/JUnit+Plugin)来根据时间的推移提供单元测试结果的图表,同时会向我们显示我们的测试是否稳定。 46 | 47 |
48 | 49 | 50 |
51 | 52 | #Pull Request测试 53 | 我们想我们的测试尽可能运行以至于如果我们破坏什么东西,我们就会马上知道。我们在[feature branches](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow/)做些修改,然后提交一个pull request到Github,那么代码就会被另一个开发者审查。只要被打开,我们就能运行所有的测试来确保没有任何东西被破坏。 54 | 55 | 当新的pull requst是开放状态时,为了管理这些,我们安装[Github Pull Request plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitHub+pull+request+builder+plugin)来将信息从Github发送到Jenkins。如果有任何测试失败,它将会显示在Github,然后我们就不将代码合并,直到代码被修复为止。 56 | 57 | 58 | #代码覆盖率 59 | 我们也会用[Gcovr](http://gcovr.com/)工具来生成代码覆盖率报告,Gcovr的安装方式也是Homebrew。你需要针对main target的debug congfiguration改变两个构建设置来配置项目。将*Generate Test Coverage Files*和*Instrument Program Flow*都设置为*YES*。 60 | 61 |
62 | 63 | 64 |
65 | 66 | 当我们运行单元测试来生成代码覆盖率报告时,我们需要将*OBJROOT=./build*添加到XCTool命令行的尾部。 67 | 68 | ``` 69 | $ gcovr -r . — object-directory build/Project.build/Debug-iphonesimulator/Project.build/Objects-normal/x86_64 — exclude ‘.*Tests.*’ — xml > coverage.xml 70 | 71 | ``` 72 | Gcovr输出的代码覆盖率报告也会被插件[Cobertura Jenkins plugin](https://wiki.jenkins-ci.org/display/JENKINS/Cobertura+Plugin)发布,这个插件会提供一种可视化的方式来根据时间的推移来显示代码覆盖率。 73 | 74 | 现在我们不仅可以看到测试是否通过,还可以看到代码的测试覆盖范围。 75 | 76 | #静态分析 77 | 在工具集中,其中一个强大并能够保持高质量的代码的工具就是静态分析工具。这些工具会扫描你的代码,然后生成一个报告,这个报告会告诉你破坏代码风格规则的代码位置。举几个规则的例子: 78 | 79 | * 未使用的变量或参数 80 | * 长变量名,方法名或代码行 81 | * 覆盖一个方法,但没有在这个方法调用*super* 82 | * 方法太长或方法过于复杂 83 | * 还更多的规格... 84 | 85 | 我们使用[OCLint](http://oclint.org/)静态分析工具,这个工具能够支持C,C++和Objective-C语言。OCLint通过结合XCTool使用来生成**json-compilation-database** reporter 86 | ,从而提供[great integration](http://docs.oclint.org/en/dev/guide/xctool.html)特性。我们首先添加另一个reporter到我们的XCTool命令行,然后将那个report传递到OCLint来执行静态分析。 87 | 88 | ``` 89 | $ xctool -workspace Project.xcworkspace -scheme Project -reporter junit:junit-report.xml -reporter json-compilation-database:compile_commands.json test 90 | $ oclint-json-compilation-database -e Pods -report-type pmd -o oclint-pmd.xml 91 | 92 | ``` 93 | 这个report以[PMD](http://pmd.sourceforge.net/)的方式来生成,然后使用[PMD Plugin](https://wiki.jenkins-ci.org/display/JENKINS/PMD+Plugin)被发布到Jenkins。有了这些插件之后,你也可以在测试失败之前,设置每个警告的优先级(底,中,高)中一些限制。最初,我们设置这些限制为低,那么只要我们引入代码,就会被提醒,从而提高代码质量。 94 | 95 |
96 | 97 | 98 |
99 | 100 | 101 | #自动部署 102 | 最后一个问题不是如何提高代码质量,而是如何节省时间。开发者通常都会将编译好的代码通过[Crashlytics](https://www.crashlytics.com/)发送到设计师来设计审查,或在sprint结束演示时发给用户。发送一个已经编译好的app通常花一个开发者的10分钟左右时间,但它需要他们来切换任务和干扰他们的心流。 103 | 104 | 最近我们已经配置一个在夜晚构建系统,它会在早上自动发送一个新版本的app给每个人。 105 | 106 | 为了做到这样,我们使用[fastlane](http://fastlane.tools/)。fastlane是一个定义**lanes**的一些操作来执行的强大工具集。现在我们有三个已经定义好的lanes,一个是用来发布给ribot开发者,一个是用来发布给在ribot的每个人,最后一个是发布给用户。 107 | 108 | 109 | ``` 110 | before_all do |lane| 111 | cert 112 | sigh 113 | end 114 | desc “Deploy a new build to ribot iOS developers over crashlytics” 115 | lane :dev do 116 | ipa 117 | crashlytics({ groups: ‘ribot-developers’ }) 118 | end 119 | desc “Deploy a new build to people at ribot over crashlytics” 120 | lane :internal do 121 | ensure_git_status_clean 122 | append_build_time 123 | ipa 124 | crashlytics({ groups: ‘ribot’ }) 125 | reset_git_repo 126 | end 127 | desc “Deploy a new build to everyone over crashlytics” 128 | lane :external do 129 | ensure_git_status_clean 130 | increment_build_number 131 | ipa 132 | crashlytics({ groups: [‘ribot’, ‘client’] }) 133 | commit_version_bump 134 | add_git_tag 135 | push_to_git_remote 136 | end 137 | after_all do |lane| 138 | clean_build_artifacts 139 | end 140 | 141 | ``` 142 | 143 | 通过使用**fastlane**工具(通过Ruby Gems来安装)来运行一个lane。 144 | 145 | ``` 146 | fastlane internal 147 | 148 | ``` 149 | 在开始使用所有的*lanes*之前,我们应该自动确保我们有一个有效的signing certificate和最新的provisioning profile。所有我们的配置都放在一个*.env*文件,它让我们有些默认配置,但当我们运行*fastlane*根据需要来覆盖它们。 150 | 151 | 在将来,我们会通过使用*deliver*操作来自动化app store提交过程。 152 | 153 | 154 | #最后总结 155 | 到目前为止,我们已经尝试这些过程,并在工程中呈现出好的结果。我们期望看到只要适当地使用这些工具,就能提高代码的质量,这些报告将会让我们随着时间推移来量化代码质量。我们期待在下一个工程中适当地使用这些工具会发生什么。 -------------------------------------------------------------------------------- /issue-6/通过减少动态分发来提高Swift的执行效率.md: -------------------------------------------------------------------------------- 1 | 通过减少动态分发来提高Swift的执行效率 2 | --- 3 | 4 | > 5 | * 原文链接 : [Increasing Performance by Reducing Dynamic Dispatch](https://developer.apple.com/swift/blog/?id=27) 6 | * 原文作者 : [Apple官方博文](https://developer.apple.com/swift) 7 | * 译文出自 : [开发技术前线 www.devtf.cn](www.devtf.cn) 8 | * 译者 : [samw00](https://github.com/samw00/) 9 | 10 | 11 | 和其他很多编程语言一样,Swift允许一个类重写其父类中声明的方法和属性。这意味着程序在运行时需要先确定要调用的方法和访问的属性是属于哪个类的,确定之后,则会执行间接调用(indirect call)或间接访问(indirect access)。这个运行机制称为**动态分发**(dynamic dispatch)。这个机制能够提高语言的表现力,但其付出的代价是在每次间接调用或间接访问时,会产生一定量的运行时开销(runtime overhead)。在执行敏感代码时,最好能避免产生这样的开销。这篇博客将罗列三种通过消除这动态分发机制来提高程序执行效率的方式:`final`,`private`和全模块优化(Whole Module Optimization)。 12 | 13 | Consider the following example: 14 | 15 | 先来看看下面这段代码 16 | 17 | ``` 18 | class ParticleModel { 19 | var point = ( 0.0, 0.0 ) 20 | var velocity = 100.0 21 | 22 | func updatePoint(newPoint: (Double, Double), newVelocity: Double) { 23 | point = newPoint 24 | velocity = newVelocity 25 | } 26 | 27 | func update(newP: (Double, Double), newV: Double) { 28 | updatePoint(newP, newVelocity: newV) 29 | } 30 | } 31 | 32 | var p = ParticalModel() 33 | for i in stride(from: 0.0, through: 360, by: 1.0) { 34 | p.update((i * sin(i), i), newV: i * 1000) 35 | } 36 | ``` 37 | 38 | 正如上面代码所写的那样,编译器会通过动态分发机制来: 39 | 40 | 1. 调用`p`的`update()`方法。 41 | 2. 调用`p`的`updatePoint`方法。 42 | 3. 获取`p`中`point`这个属性元组。 43 | 4. 获取`p`中的`velocity`属性。 44 | 45 | 46 | 这可能和你当初看到代码时所期望的不一样。因为某个`PracticalModel`的子类可能会通过一个计算过的属性来重写`point`或`velocity`属性或者重写`updatePoint()`或`update()`中的实现方法,所以通过一个动态分发的机制去调用是有必要的。 47 | 48 | 在Swift中,动态分发调用既是在一个方法表中找寻要调用的函数,然后执行间接调用,这样的执行效率会低于直接调用。除此之外,间接调用也阻止编译器各种优化,从而使得间接调用的成本更高。在执行某些关键代码时,并不需要动态分发来提高执行效率的时候,你可以使用一些技术来对动态分发调用进行一些限制和约束。 49 | 50 | ##当你确定某个声明不会被重写时,用final关键字修饰 51 | 52 | 53 | 加上关键字`final`就意味着类,方法或者属性的声明不能被重写。这就能让编译器安全的省略动态分发的间接调用。比方说在下面的代码中,属性`point`和`velocity`会通过加载对象已存的属性来被直接访问,`updatePoint()`也会被直接调用。但是`update()`这个方法还是会通过动态分发来间接调用,这样能允许子类自定义`update()`中的实现。 54 | 55 | ``` 56 | class ParticlModel { 57 | final var point = (x: 0.0, y: 0.0) 58 | final var velocity = 100.0 59 | 60 | final func updatePoint(newPoint: (Double, Doulbe), newVelocity: Double) { 61 | point = newPoint 62 | velocity = newVelocity 63 | } 64 | 65 | func update(newP: (Double, Double), newV: Double) { 66 | updatePoint(newP, newVelocity: newV) 67 | } 68 | } 69 | ``` 70 | 71 | 也可以给整个类加一个`final`关键字来修饰。这样就禁止了给当前类添加任何子类也意味着类里所有的方法和属性都是`final`。 72 | 73 | ``` 74 | final class ParticleModel { 75 | var point = (x: 0.0, y:0.0) 76 | var velocity = 100.0 77 | // ... 78 | } 79 | ``` 80 | 81 | ##用private关键字来暗示在同一文件中的声明为final 82 | 83 | 给声明添加关键字`private`会把该声明限制在当前文件可见。这允许编译器去寻找所有潜在能被重写的声明。任何没有标明能被重写的声明,编译器都会自动推断为`final`并去除访问该属性或方法时的间接调用。 84 | 85 | 假设在当前文件中,没有类会重写`ParticleModel`,编译器会把所有`priavte`修饰的声明的动态分发调用替换成直接调用。 86 | 87 | ``` 88 | class ParticleModel { 89 | private var point = (x:0.0, y:0.0) 90 | private var velocity = 100.0 91 | 92 | private fun updatePoint(newPoint: (Double, Double), newVelocity: Double) { 93 | point = newPoint 94 | velocity = newVelocity 95 | } 96 | 97 | func update(newP: (Double, Double), newV: Double) { 98 | updatePoint(newP, newVelocity: newV) 99 | } 100 | } 101 | ``` 102 | 103 | 就像上面这个例子,属性`point`和`velocity`被直接访问,`updatePoint()`也是被直接调用。同样的,`update()`仍会被间接调用,因为没有添加`private`修饰。 104 | 105 | 和关键字`final`一样,同样可以用`private`来修饰一个类,这会使类成为一个私有类,类中的方法和属性也同样为私有`private`。 106 | 107 | ``` 108 | private class ParticleModel { 109 | var point = (x:0.0, y:0.0) 110 | var velocity = 100.0 111 | // ... 112 | } 113 | ``` 114 | 115 | ##使用全模块优化将internal声明推断为final 116 | 117 | 118 | 带有`internal`访问的声明(如果没有特别声明,此为默认)只在当前模块中可见。通常来讲Swift会分开编译同一模块中的不同文件,所以编译器是无法确定某个`internal`声明是否在另一个文件被重写。但是,如果开启了全模块优化,则所有的模块都会在同一时被一起编译。这就能让编译器来对整个模块进行处理,如果某个`internal`的声明没有可见重写,则推断为`final`。 119 | 120 | 现在我们回过头去看之前的代码段,这次添加一些额外的`public`关键字。 121 | 122 | ``` 123 | public class ParticleModel { 124 | var point = (x:0.0, y:0.0) 125 | var velocity = 100.0 126 | 127 | func updatePoint(newPoint: (Double, Double), newVelocity: Double) { 128 | point = newPoint 129 | velocity = newVelocity 130 | } 131 | 132 | public func update(newP: (Double, Double), newV: Double) { 133 | updatePoint(newP, newVelocity: newV) 134 | } 135 | } 136 | 137 | var p = ParticleModel() 138 | for i in stride(from: 0.0, through: times, by: 1.0) { 139 | p.update((i * sin(i), i), newV: i * 1000) 140 | } 141 | ``` 142 | 143 | 当用全局模块优化来编译这段代码时,编译器能够推断`point`,`velocity`属性和`updatePoint()`方法为`final`。相反的,因为`update()`方法被public修饰,则不能推断为`final`。 144 | -------------------------------------------------------------------------------- /issue-7/Core Image入门教程(swift).md: -------------------------------------------------------------------------------- 1 | Core Image入门教程(swift) 2 | --- 3 | 4 | > * 原文链接 : [Core Image Tutorial: Getting Started](http://www.raywenderlich.com/76285/beginning-core-image-swift) 5 | * 原文作者 : [Nick Lockwood](http://www.raywenderlich.com/u/nicklockwood) 6 | * 译文出自 :[开发技术前线 www.devtf.cn](http://www.devtf.cn) 7 | * 译者 : [Sam Lau](https://github.com/samlaudev) 8 | * 校对者: [Harries Chen](https://github.com/mrchenhao) 9 | * 状态 : 校正完 10 | 11 | Core Image是一个强大的框架,它能够让你轻松地对图像进行过滤。你能够通过修改图像的饱和度、色调或曝光率来获取各种特效。你也可以使用CPU或GPU更快地来处理图像数据,快到能够实时处理视频帧数据(video frame)。 12 | 13 | Core Image过滤器能够以链式的方式结合将多个特效应用到一个图像或视频帧数据。多个过滤器能够组合成单个过滤器应用在一个图像。通过每次与每个过滤器比较,这使它非常有效地处理图像。 14 | 15 | 在这个教程中,你将会亲手实践如何使用Core Image.通过使用几个不同的过滤器,你会看到实时使用各种炫酷的特效是多么容易。 16 | 17 | #基础 18 | 19 | 在你开始之前,让我们先讨论在Core Image框架中几个很重要的类: 20 | 21 | * **CIContext**. 所有处理core image的工作都在CIContext完成。这个与Core Graphics或OpenGL context有几分相似。 22 | * **CIImage**. 这个类保存图像数据,它能够从UIImage,图像文件或像素数据等方式来创建。 23 | * **CIFilter**. CIFilter类有一个字典(dictionary)来定义特定属性的过滤器,比如常用过滤器有饱和度,颜色翻转,裁剪等属性,还有很多没列出来。 24 | 25 | ###CoreImageFun 26 | 打开Xcode并使用**iOS \ Application \ Single View Application**模板来创建一个新工程。输入**CoreImageFun**作为Product Name,选择iPhone为Devices option和确定使用**swift**作为编程语言。 27 | 28 | 下载[教程的资源](http://cdn5.raywenderlich.com/downloads/CIResources.zip),并添加图片**image.png**到工程 29 | 30 | 下一步,打开**Main.storyboard**你文件,拖拽一个image view到已存在的view,image view作为view的子视图。在Attributes Inspector中,设置image view的content mode属性为Aspect Fit,这样它就不会拉伸图片。 31 | 32 | 下一步,确定Document Outline(在Interface Builder里canvas左边)是可见的 - 你可以从menu中**Editor \ Show Document Outline**来启用。 33 | 34 | 按着control键从image view拖动到它的superview三次来添加三个约束(constraint): 35 | 36 | 1. 添加一个Top Space to Layout Guide的约束,如果有必要的话,使用Size Inspector来设置约束的constant为0 37 | 2. 添加一个Center Horizontally in Container的约束(同样也需要设置constant为0) 38 | 3. 添加一个Equal Width约束 39 | 40 | 最后,为了约束image view的高度,按着control键从image view拖动到本身,然后添加一个Aspect Ratio约束,使用Size Inspector来设置它的multiplier的值为8:5对应宽高比和constant factor为0。最后,选择**Editor \ Resolve Auto Layout Issues \ All Views in View Controller \ Update Frames**,那么Interface Builder会根据这些约束来更新布局 41 | 42 | 下一步,打开Assistant Editor,然后确保它显示**ViewController.swift**。按着control键从UIImageView拖动到刚刚打开的**ViewController**类的大括号。命名outlet为**imageView**,然后点击connect。 43 | 44 | 编译和运行这个项目来确保目前进展顺利 - 你应该看到一个空屏。初始化设置完成,现在进入Core Image的世界。 45 | 46 | #基本图片过滤器 47 | 48 | 你将会通过使用**CIFilter**应用到图像和显示到屏幕来入门。每次你想将CIFilter应用到一个图像,都需要做四件事: 49 | 50 | 1. **创建一个CIImage对象**。CIImage有几个初始化方法,其中包括:CIImage(contentsOfURL:), CIImage(data:), CIImage(CGImage:), CIImage(bitmapData:bytesPerRow:size:format:colorSpace:)等,大多数你都会使用CIImage(contentsOfURL:)方法。 51 | 2. **创建一个CIContext对象**。一个CIContext是基于CPU或CPU,在初始化它时比较耗资源,所以需要复用它而不是多次创建。当你输出CIImage对象时,你会经常需要CIContext对象。 52 | 3. **创建CIFilter对象**。当你创建一个filter时,你需要配置多个属性来决定你使用的filter。 53 | 4. **获取filter的输出**。filter会输出一个CIImage类型的image给你 - 你可以使用CIContext将它转换为UIImage类型image。 54 | 55 | 让我们看看它是如何工作。添加以下代码到**ViewController.swift**的viewDidLoad():方法 56 | 57 | ``` 58 | // 1 59 | let fileURL = NSBundle.mainBundle().URLForResource("image", withExtension: "png") 60 | 61 | // 2 62 | let beginImage = CIImage(contentsOfURL: fileURL) 63 | 64 | // 3 65 | let filter = CIFilter(name: "CISepiaTone") 66 | filter.setValue(beginImage, forKey: kCIInputImageKey) 67 | filter.setValue(0.5, forKey: kCIInputIntensityKey) 68 | 69 | // 4 70 | let newImage = UIImage(CIImage: filter.outputImage) 71 | self.imageView.image = newImage 72 | ``` 73 | 74 | 让我们逐段分析以上代码: 75 | 76 | 1. 这行代码创建一个NSURL对象,它保存image文件的路径 77 | 2. 通过CIImage(contentsOfURL:)构造器来创建CIImage对象 78 | 3. 创建CIFilter对象。CIFilter构造器将name作为参数,然后用dictionary来指定filter的键值对。每个filter都拥有唯一的键和多个有效值。**CISepiaTone** filter只接收两个值**kCIInputImageKey**(CIImage)和**kCIInputIntensityKey**(在0到1之间的一个浮点数)。这里你设置它的值为0.5。如果没有值提供的话,大多数filters都有默认值。但CIImage没有默认值,它必须要提供。 79 | 4. 通过使用filter的**outputImage**属性能够轻松地获取CIImage。一旦你有个输出的CIImage,你需要将它转换为UIImage。使用**UIImage(CIImage:)**构造器来创建UIImage。如果你已经将CIImage转换为UIImage,你就能将image显示到image view 80 | 81 | ![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/07/CI-Sepia-Crop.jpg) 82 | 83 | 84 | #把它放进Context 85 | 86 | 在你向前看之前,有个优化建议你应该需要知道。 87 | 88 | 之前我曾提及过,你需要有个**CIContext**来使用CIFilter,但以上例子都没有提及过这个对象。结果变成了由**UIImage(CIImage:)** 构造器来为你处理所有的工作。UIImage它创建一个**CIContext**,然后用它来执行所有过滤图像的操作,这让你更加容易地使用Core Image的API。 89 | 90 | 但这种方式有一个主要缺点就是每次使用时都创建一个新的**CIContext**对象。**CIContext**对象是为了复用来提高性能。如果你想用一个slider来更新filter的值,是以每次创建一个新的CIContext来实现更新filter的方式,那么运行速度将会很慢。 91 | 92 | 让我们以恰当的方式来实现。在**viewDidLoad()**里删除步骤4的代码,然后用一下代码来代替: 93 | 94 | ``` 95 | // 1 96 | let context = CIContext(options:nil) 97 | 98 | // 2 99 | let cgimg = context.createCGImage(filter.outputImage, fromRect: filter.outputImage.extent()) 100 | 101 | // 3 102 | let newImage = UIImage(CGImage: cgimg) 103 | self.imageView.image = newImage 104 | ``` 105 | 106 | 我们再一次逐段分析代码: 107 | 108 | 1. 你设置CIContext对象,然后用它来画一个CGImage。**CIContext(options:)**构造器以NSDictionary为参数,它指定一些选项,比如:颜色格式,或者context是否运行在CPU或GPU。对于这个app来说,默认值是可以的,所以你可以传递nil这个值进去。 109 | 2. context调用**createCGImage(outputImage:fromRect:)**方法在给定CIImage参数并返回一个新的CGImage示例。 110 | 3. 你根据刚刚获取的CIImage,使用**UIImage(CGImage:)**构造器来创建一个UIImage对象。注意,当我们使用CIImage对象之后,没有必要显式地释放它,虽然在Objective-C需要这样做。但在Swift中,ARC会自动释放Core Foundation的对象。 111 | 112 | 编译和运行,确保项目进展顺利 113 | 114 | 在这个例子中,自己创建CIContext与不创建没什么不同。但下个部分中,你会看到当你动态地修改filter时,为什么CIContext对性能影响很大。 115 | 116 | 117 | #修改Filter值 118 | 119 | 目前还不错,但这只是使用Core Image filters的入门方式。让我们添加一个slider,然后设置它以便你能够实时调整filter的设置 120 | 121 | 打开**Main.storyboard**,选取一个slider,将它拖放到image view的上面,并水平对齐。选中view,然后点击**Editor \ Resolve Auto Layout Issues \ Selected Views \ Reset to Suggested Constraints**,添加需要的宽约束。 122 | 123 | 确保Assistant Editor可见和显示**ViewController.swift**,然后按着control键从slider拖动到之前添加的@IBOutlet下面,设置name为**amountSlider**,然后点击**Connect**。 124 | 125 | 当你还选中slider时,也让我们连接slider到一个action method。再一次按着control键从slider拖动到ViewController类的}上面。设置Connection为**Action**,name为**amountSliderValueChanged**,确保Event设置为**Value Changed**,然后点击**Connect**。 126 | 127 | 每次slider改变时,你需要根据不同的值来重新创建image filter。然而,你不想重复整个耗时且没效率的过程。你需要在你的类改变几样东西,那么你就要在viewDidLoad方法中创建一些对象并保存。 128 | 129 | 最重要的一件事就是当你需要使用CIContext时,你只需复用它。如果你每次都重新创建它,你的程序将会变得很慢。另一件事就是保存CIFilter和CIImage,CIImage主要保存原始的图像。每次输出都会产生新的CIImage,但你刚开始使用的图像都会保持不变。 130 | 131 | 你需要添加几个实例变量来完成这个任务。添加以下三个属性到你的ViewController类: 132 | 133 | ``` 134 | var context: CIContext! 135 | var filter: CIFilter! 136 | var beginImage: CIImage! 137 | 138 | ``` 139 | 140 | 请注意,你已经用!语法来声明那些值为implicitly-unwrapped optionals,因为直到**viewDidLoad**才初始化它们。你也可以用?,但采用那种方式是为了当你使用那几个实例变量时,防止optionals为nil。implicitly-unwrapped语法由于不用到处使用!标识来访问变量,它让代码更加易读。 141 | 142 | 在**viewDidLoad**改变代码,那么它使用这些属性而不是使用新的局部变量,代码如下: 143 | 144 | ``` 145 | beginImage = CIImage(contentsOfURL: fileURL) 146 | 147 | filter = CIFilter(name: "CISepiaTone") 148 | filter.setValue(beginImage, forKey: kCIInputImageKey) 149 | filter.setValue(0.5, forKey: kCIInputIntensityKey) 150 | 151 | let outputImage = filter.outputImage 152 | 153 | context = CIContext(options:nil) 154 | let cgimg = context.createCGImage(outputImage, fromRect: outputImage.extent()) 155 | 156 | ``` 157 | 158 | 现在你将会实现changeValue方法。你在这个方法需要做的是修改CIFilter dictionary的inputIntentsity键对应的值。 159 | 160 | 一旦你修改这个值,你需要重复这几个步骤: 161 | 162 | * 从CIFilter获取输出的CIImage 163 | * 将CIImage转换为CGImage 164 | * 将CGImage转换为UIImage,然后将它显示在image view 165 | 166 | 用以下代码代替amountSliderValueChanged(sender:)方法: 167 | 168 | ``` 169 | @IBAction func amountSliderValueChanged(sender: UISlider) { 170 | 171 | let sliderValue = sender.value 172 | 173 | filter.setValue(sliderValue, forKey: kCIInputIntensityKey) 174 | let outputImage = filter.outputImage 175 | 176 | let cgimg = context.createCGImage(outputImage, fromRect: outputImage.extent()) 177 | 178 | let newImage = UIImage(CGImage: cgimg) 179 | self.imageView.image = newImage 180 | } 181 | 182 | ``` 183 | 184 | 你会注意到,你已经将方法定义中参数类型从**AnyObject**转换为**UISlider**。你只用这个方法来从**UISlider**获取值,以便你改变值。如果你不管它,默认是**AnyObject**,你需要将它转换为**UISlider**,否则下一行代码就会抛出错误。 185 | 186 | 你可以从slider获取**浮点数**的值。你的slider默认设置为0.5,最小值为0,最大值为1。通过slider设置CIFilter是多么方便。 187 | 188 | CIFilter有多个方法允许你在dictionary根据不同键来设置多个值。而这里,你刚设置**inputIntensity**键对应的值,而这个值是从slider获取。Swift自动将CGFloat值转换为NSNumber对象,来符合**setValue(value:forKey:)**方法的使用。 189 | 190 | 剩下的代码看起来很熟悉,因为它与**viewDidLoad**方法的逻辑一样。你将会多次使用这段代码。从现在起,你会用**amountSliderValueChanged(sender:)**方法来将CIFilter输出的图像渲染到ImageView。 191 | 192 | 编译和运行,你可以修改slider值来实时地改变图像。 193 | 194 | 195 | #从相册获取图片 196 | 197 | 现在你可以改变filter的值,事情也开始变得有趣。但是,如果你不喜欢这张花朵的图片。你可以设置**UIImagePickerController**来从相册选取图片放进你的app来使用。 198 | 199 | 你需要创建一个button来跳转到相册视图,所以打开**Main.storyboard**,拖动一个button到scene的右底部,并改变按钮文字为"Photo Album"。像之前一样,使用Auto Layout来Reset to Suggested Constraints。button应该在slider的右下边。 200 | 201 | 确保Assistant Editor是可见和显示**ViewController.swift**,然后按着control键从button拖动到}的上面。设置Connection为**Action**,name为**loadPhoto**,确保Event设置为**Touch Up Inside**,最后点击**Connect**。 202 | 203 | loadPhoto方法实现如下: 204 | 205 | ``` 206 | @IBAction func loadPhoto(sender : AnyObject) { 207 | let pickerC = UIImagePickerController() 208 | pickerC.delegate = self 209 | self.presentViewController(pickerC, animated: true, completion: nil) 210 | } 211 | 212 | ``` 213 | 214 | 第一行代码主要是创建一个**UIImagePickerController**对象。然后设置image picker的delegate为self(ViewController) 215 | 216 | 你会这里得到一个警告。你需要声明**ViewController**遵循**UIImagePickerControllerDelegate**和**UINavigationControllerDelegate**协议。 217 | 218 | 仍在ViewController.swift文件,在文件顶部改变类的定义,代码如下: 219 | 220 | ``` 221 | class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate { 222 | ``` 223 | 224 | 而方法实现如下: 225 | 226 | ``` 227 | func imagePickerController(picker: UIImagePickerController!, didFinishPickingMediaWithInfo info: NSDictionary!) { 228 | self.dismissViewControllerAnimated(true, completion: nil); 229 | println(info); 230 | } 231 | 232 | ``` 233 | 234 | **UIImagePickerControllerDelegate**方法还没完成 - 它只是一个占位符来打印被选择图片的信息。注意,不管你怎样实现UIImagePickerControllerDelegate这个方法,你必须在实现中显式地dimiss UIImagePickerController。如果你不这样做的话,你就会永远地盯着image picker。 235 | 236 | 编译和运行这个app,点击button。它会跳转到相册任你选择图片。 237 | 238 | > 如果你在模拟器运行app,你可以不会有图片。在模拟器(或在没有摄像头的设备),你可以使用Safari来保存图片到相册。打开Safari,查找一张图片,点着并长按,你就会有一个选项来保存图片。下一次你运行你app,它将会出现在你图片库。 239 | 240 | 在控制台中,你选择完一张图片之后,就看到类似以下的打印信息: 241 | 242 | ``` 243 | { 244 | UIImagePickerControllerMediaType = "public.image"; 245 | UIImagePickerControllerOriginalImage = " size {1165, 770} orientation 0 scale 1.000000"; 246 | UIImagePickerControllerReferenceURL = "assets-library://asset/asset.PNG?id=DCFE1435-2C01-4820-9182-40A69B48EA67&ext=PNG"; 247 | } 248 | 249 | ``` 250 | 251 | 注意,它有一个dictionary入口,对应就是用户选择的“原始图片”。这个就是你想拽取和过滤的东西。 252 | 253 | 现在你已经有方法选取图片了,你怎样使用它作为你的**beginImage**呢? 254 | 255 | 很简答,只需将delegate方法修改成以下代码: 256 | 257 | ``` 258 | func imagePickerController(picker: UIImagePickerController!, didFinishPickingMediaWithInfo info: NSDictionary!) { 259 | self.dismissViewControllerAnimated(true, completion: nil); 260 | 261 | let gotImage = info[UIImagePickerControllerOriginalImage] as UIImage 262 | 263 | beginImage = CIImage(image:gotImage) 264 | filter.setValue(beginImage, forKey: kCIInputImageKey) 265 | self.amountSliderValueChanged(amountSlider) 266 | } 267 | 268 | ``` 269 | 270 | 你需要从你选择的图片来创建一个新的**CIImage**。你可以通过在dictionary的**UIImagePickerControllerOriginalImage**键获取值,从而获取**UIImage**的表示。注意,最好就是用UIImagePickerControllerOriginalImage这个常量,而不是硬编码的字符串,因为Apple可能在将来会改变这个键的名字。 271 | 272 | 你需要通过**CIImage(image:)**构造器将image转换为**CIImage**对象。然后在filter dictionary设置键,那么就可以创建新的CIImage。 273 | 274 | 最后一行代码看起来有点奇怪。还记得我说过,怎样运行**changeValue**方法,最新的值被设置在filter,然后更新image view。 275 | 276 | 好,你需要再做一次,所以你只是调用**changeValue**方法。即使slider的值还没改变,你仍然可以使用那个方法的代码来完成工作。你可以分解那段代码到自己的方法(如果你想做得更加复杂来避免混淆),但这种情况下,你的目的是为了复用amountSliderValueChanged方法。传递**amountSlider**作为sender以致它有正确的值使用。 277 | 278 | 编译和运行,你能从相册中更新任何图片。 279 | 280 | 如果你创建完美的深褐色图片,你怎样才能保存它。你可以截图,但最恰当的方式就是保存已过滤的图片到相册。 281 | 282 | 283 | #保存到相册 284 | 285 | 为了保存到相册,你需要使用**AssetsLibrary** framework。在**ViewController.swift**文件顶部添加以下导入语句: 286 | 287 | ``` 288 | import AssetsLibrary 289 | ``` 290 | 291 | 有一件你需要知道的事就是当你保存图片到相册,它需要花费几秒时间,即使在你关闭app之后仍在继续处理图片。 292 | 293 | 由于当你切换到另一个app的时候,GPU就停止处理,这将变成一个问题。如果图片还没完成保存,当你迟点找到它的时候,它将不在那里。 294 | 295 | 有一个解决方案就是使用基于CPU的**CIContext**来渲染。默认选择就是使用GPU,因为它处理速度更快,但你不想为了添加保存功能而降低过滤性能。而现在,你会创建另一个**CIContext**来保存image。注意,软件渲染将不会在模拟器正常工作。 296 | 297 | 添加一个新button到你的app,这个button会让你保存修改后的图片。打开**Main.storyboard**,添加一个新的button,修改button标题为“Save to Album”。将button放在slider的左边,然后添加相应的约束。 298 | 299 | 然后像上一次一样,连接button到一个新方法**savePhoto(sender:)**,方法实现如下: 300 | 301 | ``` 302 | @IBAction func savePhoto(sender: AnyObject) { 303 | // 1 304 | let imageToSave = filter.outputImage 305 | 306 | // 2 307 | let softwareContext = CIContext(options:[kCIContextUseSoftwareRenderer: true]) 308 | 309 | // 3 310 | let cgimg = softwareContext.createCGImage(imageToSave, fromRect:imageToSave.extent()) 311 | 312 | // 4 313 | let library = ALAssetsLibrary() 314 | library.writeImageToSavedPhotosAlbum(cgimg, 315 | metadata:imageToSave.properties(), 316 | completionBlock:nil) 317 | } 318 | 319 | ``` 320 | 321 | 在这段代码块中: 322 | 323 | 1. 从filter获取CIImage的输出。 324 | 2. 创建一个新的,基于软件的,使用CPU渲染器的CIContext对象。 325 | 3. 生成CGImage。 326 | 4. 保存CIImage到photo library。 327 | 328 | 编译和运行app(记住要运行在实际设备,因为你已经使用软件渲染),现在你可以永久地保存完美的图像到你的photo library。 329 | 330 | 331 | #Image元数据是什么? 332 | 333 | 让我们讨论一下关于image元数据。用手机拍照出来的图片有很多关联它的数据,例如GPS坐标,图片格式,和方向。 334 | 335 | 方向是一样特殊的东西你需要保存。加载UIImage变成CIImage,渲染成CGImage,然后转换回一个UIImage这个过程是为了从image抓取元数据。为了保存方向,你需要保存它然后传递它作为UIImage构造器参数。 336 | 337 | 添加一个新属性到**ViewController**类定义: 338 | 339 | ``` 340 | var orientation: UIImageOrientation = .Up 341 | ``` 342 | 343 | 下一步,添加下面代码到**imagePickerController(picker:didFinishPickingMediaWithInfo:)**方法里,设置**beginImage**代码行的上面: 344 | 345 | ``` 346 | orientation = gotImage.imageOrientation 347 | 348 | ``` 349 | 350 | 这样就会保存原始image方向的属性。 351 | 352 | 最后,修改在**amountSliderValueChanged**方法里,那段创建UIImage来设置imageView对象的代码: 353 | 354 | ``` 355 | let newImage = UIImage(CGImage: cgimg, scale:1, orientation:orientation) 356 | 357 | ``` 358 | 359 | 现在,如果你拍一张照片而不是默认方向,它将会默认保存。 360 | 361 | 362 | #还有哪些Filters是可用的? 363 | 364 | CIFilter API在Mac OS有超过160个,在iOS 8有126个关于filters的API。 365 | 366 | 为了查找有哪些filters在给定的设备是可用的,你可以使用CIFilter的**filterNamesInCategory(kCICategoryBuiltIn)**方法。这个方法会返回一个关于filter名字的数组。 367 | 368 | 除此之外,每个filter有一个**attributes()**方法返回一个dictionary,dictionary包含关于filter的信息。这些信息包括filter的名字,分类,filter接受哪些输入,默认输入,和这些输入接受哪些值。 369 | 370 | 让我们将所有东西放在一个类的方法,这个方法记录当前所有可用的filters信息。添加这个方法到ViewController类的定义: 371 | 372 | ``` 373 | func logAllFilters() { 374 | let properties = CIFilter.filterNamesInCategory(kCICategoryBuiltIn) 375 | println(properties) 376 | 377 | for filterName: AnyObject in properties { 378 | let fltr = CIFilter(name:filterName as String) 379 | println(fltr.attributes()) 380 | } 381 | } 382 | 383 | ``` 384 | 385 | 这个方法仅仅通过**filterNamesInCategory()**方法来获取filters的数组。首先,它打印名字的列表。然后,遍历列表获取每个名字,根据名字实例化filter和打印它的属性dictionary。 386 | 387 | 在**viewDidLoad()**方法最后调用这个方法: 388 | 389 | ``` 390 | self.logAllFilters() 391 | ``` 392 | 393 | 你会看到很多filters在控制台被列出: 394 | 395 | ``` 396 | [CIAttributeFilterDisplayName: Color Monochrome, inputColor: { 397 | CIAttributeClass = CIColor; 398 | CIAttributeDefault = "(0.6 0.45 0.3 1)"; 399 | CIAttributeType = CIAttributeTypeColor; 400 | }, inputImage: { 401 | CIAttributeClass = CIImage; 402 | CIAttributeType = CIAttributeTypeImage; 403 | }, CIAttributeFilterCategories: ( 404 | CICategoryColorEffect, 405 | CICategoryVideo, 406 | CICategoryInterlaced, 407 | CICategoryNonSquarePixels, 408 | CICategoryStillImage, 409 | CICategoryBuiltIn 410 | ), inputIntensity: { 411 | CIAttributeClass = NSNumber; 412 | CIAttributeDefault = 1; 413 | CIAttributeIdentity = 0; 414 | CIAttributeSliderMax = 1; 415 | CIAttributeSliderMin = 0; 416 | CIAttributeType = CIAttributeTypeScalar; 417 | }, CIAttributeFilterName: CIColorMonochrome] 418 | ``` 419 | 420 | 哇,有很多的filters!它会给你一些灵感在你自己的app中尝试其他filters。 421 | 422 | 423 | #更加复杂的Filter链 424 | 425 | 现在我们已经知道在iOS平台有哪些可用的filter,是时候创建更加复杂的filter链了。为了做到这样,你会创建一个专门的方法来处理CIImage。它会接受一个CIImage作为参数,过滤它,让它看起来像一张老旧的图片,然后返回一个已经修改的CIImage。 426 | 427 | 添加以下方法到ViewController: 428 | 429 | ``` 430 | func oldPhoto(img: CIImage, withAmount intensity: Float) -> CIImage { 431 | // 1 432 | let sepia = CIFilter(name:"CISepiaTone") 433 | sepia.setValue(img, forKey:kCIInputImageKey) 434 | sepia.setValue(intensity, forKey:"inputIntensity") 435 | 436 | // 2 437 | let random = CIFilter(name:"CIRandomGenerator") 438 | 439 | // 3 440 | let lighten = CIFilter(name:"CIColorControls") 441 | lighten.setValue(random.outputImage, forKey:kCIInputImageKey) 442 | lighten.setValue(1 - intensity, forKey:"inputBrightness") 443 | lighten.setValue(0, forKey:"inputSaturation") 444 | 445 | // 4 446 | let croppedImage = lighten.outputImage.imageByCroppingToRect(beginImage.extent()) 447 | 448 | // 5 449 | let composite = CIFilter(name:"CIHardLightBlendMode") 450 | composite.setValue(sepia.outputImage, forKey:kCIInputImageKey) 451 | composite.setValue(croppedImage, forKey:kCIInputBackgroundImageKey) 452 | 453 | // 6 454 | let vignette = CIFilter(name:"CIVignette") 455 | vignette.setValue(composite.outputImage, forKey:kCIInputImageKey) 456 | vignette.setValue(intensity * 2, forKey:"inputIntensity") 457 | vignette.setValue(intensity * 30, forKey:"inputRadius") 458 | 459 | // 7 460 | return vignette.outputImage 461 | } 462 | ``` 463 | 464 | 这里会逐段讲解: 465 | 466 | 1. 以相同的方式设置深褐色的filter。你在这个方法传递一个浮点数来设置深褐色效果的强度。这个值是由slider提供。 467 | 468 | 2. 设置一个创建随机噪音模式的filter,filter效果如下: 469 | 470 | ![](http://cdn4.raywenderlich.com/wp-content/uploads/2012/09/CIRandomGenerator.gif) 471 | 472 | 它好像没有接受任何参数。你会使用这个噪音模式来添加纹理到你的最终“老旧相片”的外观。 473 | 474 | 3. 修改随机噪音生成器的输出。你想改变的它的灰度和减轻它一点以至于效果没有那么炫酷。你会注意到,输入image的键被设置到随机filter的outputImage属性。这是一种传递一个filter的输出作为下个输入的方便方式。 475 | 476 | 4. **imageByCroppingToRect()**接受被裁剪的rect作为参数,然后返回CIImage。在这种情况下,你需要裁剪CIRandomGenerator filter的输出,因为它无限地碎片化。在某种情况下,如果你不裁剪它,你会得到一个错误信息,说filters有‘an infinite extent’。CIImages实际上是不包含image数据,它们创建它时将它描述成一个'recipe'。直到你在CIContext调用一个方法时数据才实际被处理。 477 | 478 | 5. 将深褐色filter的输出和CIRandomGenerator filter的输出结合起来。这个filter执行像在photoshop层‘Hard Light’设置完全一样的操作。大多数在photoshop(但不是全部)的filter选项都可以通过使用Core Image来实现。 479 | 480 | 6. 在这个合成的输出中,运行一个vignette filter,它会使你的图片边缘变暗。你使用从slider获取的值来设置半径和效果的强度。 481 | 482 | 7. 最后,返回最后filter的输出。 483 | 484 | 关于filter chain讲到这里。你现在应该有一个如何形成复杂filter chain的想法。通过组合多个Core Image filters成各种各样的chains,你就能实现无穷多种的特效。 485 | 486 | 下一件事就是在**amountSliderValueChanged()**方法实现。改变这两行代码: 487 | 488 | ``` 489 | filter.setValue(sliderValue, forKey: "inputIntensity") 490 | let outputImage = filter.outputImage 491 | ``` 492 | 493 | 成这行代码: 494 | 495 | ``` 496 | let outputImage = self.oldPhoto(beginImage, withAmount: sliderValue) 497 | ``` 498 | 499 | 这只是用新的,更复杂的filter方法来代替之前深褐色效果。你可以传递slder的值给强度,和你使用在viewDidLoad方法中被设置的beginImage作为输入的CIImage。编译和运行,你应该得到一个更加完善的旧图片效果,这种效果会是深褐色,有一点噪音和一点渐晕。 500 | 501 | 这种噪音效果会是更加微妙,但我会留给你自行试验。现在你完全有能力随意操纵Core Image,尽管去尝试吧! 502 | 503 | 504 | #下一步 505 | 506 | 这个是[示例工程](http://cdn4.raywenderlich.com/wp-content/uploads/2014/07/CoreImageFun.zip)包含这个教程所有的代码。 507 | 508 | 它包含了如何使用Core Image filters的基本方法。这是一个很容易上手的技术,所以你应该很快就能使用它来应用到一些优雅的filters。 509 | 510 | 记住,这个示例app在一开始的时候打印出所有可用的filters列表到控制台。为什么不尝试使用这些filters呢?你可以在[Core Image filter reference documentation](https://developer.apple.com/library/mac/documentation/GraphicsImaging/Reference/CoreImageFilterReference/Reference/reference.html)查看更多关于filters的详细信息。 511 | 512 | 如果你在这个教程中有任何问题或评论,请加入这个论坛来讨论。 -------------------------------------------------------------------------------- /issue-7/UIKit Dynamics 教程 - 起步.md: -------------------------------------------------------------------------------- 1 | ## UIKit Dynamics 教程: 起步 2 | 3 | > - 原文链接 : [UIKit Dynamics Tutorial: Getting Started](http://www.raywenderlich.com/76147/uikit-dynamics-tutorial-swift) 4 | - 原文作者: [James Frost](http://www.raywenderlich.com/u/frosty) 5 | - 译文出自: [开发技术前线http://www.devtf.cn/](http://www.devtf.cn/) 6 | - 译者 : [Lollypo](https://github.com/Lollypo) 7 | - 校对者: [Harries Chen](https://github.com/mrchenhao) 8 | - 状态 : 完成 9 | 10 | **更新备注**:本教程是由[Colin Eberhardt](http://www.raywenderlich.com/u/ColinEberhardt)编写的[ iOS 7 by Tutorials](http://www.raywenderlich.com/?page_id=48020)其中某章节的精简版本.由James Frost更新到iOS8并用Swift重写,在Xcode 6 beta 7版本上测试通过。 11 | 12 | iOS的设计目标鼓励你去创建数字界面模拟触摸,手势,改变方向等,就好像它们真实的物理对象而远非像素的集合。最终给予用户更深层级的交互而不是肤浅的拟真设计。 13 | 14 | 这听起来感觉挺难的,因为让数字界面看起来是真实的远比让它感觉是真实的简单。不过好在你有一些漂亮的新工具: 15 | 16 | - **UIKit Dynamics** 是集成到UIKit中的物理引擎。这允许你创建一个感觉像是真实的界面,通过添加一些行为,例如重力,锚连接 (弹簧)以及力. 你可以定义你希望你的界面需要的物理特征, Dynamics会帮你关心其他的细枝末节。 17 | - **Motion Effects** 允许你创建类似于iOS 7 主屏幕中酷酷的视差效果. 基本上你可以利用手机加速器提供的数据来创建界面来模拟手机方向发生改变的反应。 18 | 19 | 当一起使用的时候,由 Dynamics 与 Motion形成用户体验的工具,让你的界面给用户生活带来非同一般的体验。当观察到他们的操作是通过一种自然、动态的方式,你的用户将会与你的app进行更深层次的连接。 20 | 21 | > **注意**: 由于本教程编写的时候iOS 8 还处于测试中,因此[没有提供iOS8的截图](http://www.raywenderlich.com/?p=74138). 所有的截图都是iOS7环境下的,不过看起来与iOS 8相差不大. 22 | 23 | 24 | 25 | ### Getting started 起步 26 | 27 | UIKit Dynamics是非常有趣的;最好的学习方法就是脚踏实地先学一些小例子。 28 | 29 | 打开Xcode,依次选择**File / New / Project …** 然后选择 **iOS Application / Single View Application** 最后命名你的项目为**DynamicsDemo**。项目创建完成之后,打开 **ViewController.swift** ,然后在`viewDidLoad`方法中添加如下代码: 30 | 31 | ``` swift 32 | let square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100)) 33 | square.backgroundColor = UIColor.grayColor() 34 | view.addSubview(square) 35 | ``` 36 | 37 | 以上代码仅仅是在界面上添加一个方形的`UIView`。 38 | 39 | 运行你的App,你就可以在模拟器中看到如下的正方形: 40 | 41 | ![](http://cdn1.raywenderlich.com/wp-content/uploads/2013/09/LonelySquare.png) 42 | 43 | 如果你在真机中运行此app,可以尝试倾斜、颠倒甚至是摇晃它.你会发现这样做没什么效果.这是因为一切是按照设计来工作的.当你添加一个View到界面上时,你希望它一直固定在设定好的坐标上,除非是你给界面添加一些动态的效果. 44 | 45 | 46 | 47 | ### Adding gravity 添加重力 48 | 49 | 还是在 **ViewController.swift**中,在`viewDidLoad`之上加上下面这些属性: 50 | 51 | ``` swift 52 | var animator: UIDynamicAnimator! 53 | var gravity: UIGravityBehavior! 54 | ``` 55 | 56 | 这些属性是表示自动拆包可选类型(在类型后由!表示).这些属性必须是可选类型的,因为我们在`init`方法中不会初始化它们.同时也是因为在你初始化之后它们不可能为空.这样通过!就可以避免每一次都手动拆包. 57 | 58 | 在`viewDidLoad`方法的最后加上这段代码: 59 | 60 | ``` swift 61 | animator = UIDynamicAnimator(referenceView: view) 62 | gravity = UIGravityBehavior(items: [square]) 63 | animator.addBehavior(gravity) 64 | ``` 65 | 66 | 我会在一会儿之后解释这个.现在,运行你的程序.你会看到你的方块慢慢的开始加速向下运行,直到掉落在屏幕的底部. 67 | 68 | ![](http://cdn1.raywenderlich.com/wp-content/uploads/2013/09/FallingSquare.png) 69 | 70 | 刚刚添加的代码中,有两个Dynamics的类在起作用: 71 | 72 | - **UIDynamicAnimator** 是UIKit物理引擎.这个类跟踪你添加到Dynamics中的各种行为,例如重力,并且提供了整体的上下文.当你创建一个动画实例时, 你需要传递一个该动画用来定义坐标系统的View. 73 | - **UIGravityBehavior** 模拟重力行为以及对一个或者多个物体施力,允许你模拟物理行为.当你创建一个行为的实例时, 你需要将此行为关联到一系列的物体上 — 通常来说是View. 这样你就可以选择哪些物体受到该行为的影响,在这个案例中物体受重力影响. 74 | 75 | 大多数的行为有一些配置属性;例如,重力行为允许你改变它的角度和大小.尝试修改这些属性让你的对象降落,侧移,或者是不同加速度的对角线. 76 | 77 | 注意:稍微说一下,在物理世界中,重力(g)表示米每秒平方,约等于9.8m/s2.使用牛顿第二定律,运用下列公式可以计算出在重力的影响下物体将会下滑多远. 78 | 79 | **distance = 0.5 × g × time2** 80 | 81 | 在UIKit Dynamics中,该公式是一样的不过单位却是有所不同.你使用的是每千像素平方秒而不是米.使用牛顿第二定律,基于你所提供的重力组件,仍然可以计算出在任意时间点你的View的地点. 82 | 83 | 但是你真的需要全部了解么?并不是这样;你真正需要知道的是**g**越大就表示物体下降的越快,这在数学的角度来看并不难理解. 84 | 85 | 86 | 87 | ### Setting boundaries 设定边界 88 | 89 | 虽然你看不到它,这个方块甚至会继续下落后即便它已经消失在屏幕的底部.为了让它在屏幕的范围内,你需要定义一个边界. 90 | 91 | 在**ViewController.swift**中添加另外一个属性: 92 | 93 | ``` swift 94 | var collision: UICollisionBehavior! 95 | ``` 96 | 97 | 在`viewDidLoad`方法的最后加上这几行: 98 | 99 | ``` swift 100 | collision = UICollisionBehavior(items: [square]) 101 | collision.translatesReferenceBoundsIntoBoundary = true 102 | animator.addBehavior(collision) 103 | ``` 104 | 105 | 以上代码创建了一个碰撞行为,其中定义了若干边界与该物体交互. 106 | 107 | 以上代码将`translatesReferenceBoundsIntoBoundary` 属性设为`true`,而不是显示的定义边界坐标. 这表示被关联的View的坐标被用于`UIDynamicAnimator`. 108 | 109 | 运行程序,你会看到方块碰撞屏幕的底部,然后反弹一下,停了下来,就像这样: 110 | 111 | ![](http://cdn1.raywenderlich.com/wp-content/uploads/2013/09/SquareAtRest.png) 112 | 113 | 这是一些令人印象深刻的行为,特别是考虑到你此时只添加了这一点代码. 114 | 115 | 116 | 117 | ### Handling collisions 处理碰撞 118 | 119 | 接下来,你将添加一个不可移动的障碍物来与下落的方块进行碰撞交互. 120 | 121 | 将下面的代码添加到`viewDidLoad`方法中添加方块View的后面: 122 | 123 | ``` swift 124 | let barrier = UIView(frame: CGRect(x: 0, y: 300, width: 130, height: 20)) 125 | barrier.backgroundColor = UIColor.redColor() 126 | view.addSubview(barrier) 127 | ``` 128 | 129 | 运行你的程序;你会看到一个红色的"障碍物"在屏幕的中间穿过.然而事实是障碍物并未对下落的方块产生影响: 130 | 131 | ![](http://cdn4.raywenderlich.com/wp-content/uploads/2013/09/BadBarrier.png) 132 | 133 | 这并不是你想要的效果,但这却是一个重要的提醒:Dynamics为只对已关联的行为的View产生影响. 134 | 135 | 可以看一下这张图: 136 | 137 | ![](http://cdn1.raywenderlich.com/wp-content/uploads/2013/09/DynamicClasses.png) 138 | 139 | **UIDynamicAnimator**与一个提供了坐标系统的View相关联.然后你在与此关联的物体上添加了若干个行为对它们施力.大多数行为可以与多个物体关联,同样的一个物体也可以与若干个行为关联.上面的图显示当前程序中有关联的行为. 140 | 141 | 无论是你现在代码中的行为还是障碍物都是不被知道的,所以从引擎系统角度看,障碍物是不存在的. 142 | 143 | 144 | 145 | ### Making objects respond to collisions 让物体对碰撞做出反应 146 | 147 | 为了使方块与障碍物相碰撞,找到初始化碰撞行为的那行,然后替换成下面的: 148 | 149 | ``` swift 150 | collision = UICollisionBehavior(items: [square, barrier]) 151 | ``` 152 | 153 | 碰撞对象需要知道应该与之几乎的每一个View;因此添加障碍物到物体列表中,这样就允许碰撞对象与之发生作用. 154 | 155 | 运行你的程序;这两个对象产生碰撞行为,如下面的截图所示: 156 | 157 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2013/09/GoodBarrier.png) 158 | 159 | 这个碰撞的行为围绕与之关联的每个物体,形成了一个"边界".将他们从对象改变成更坚固的物体. 160 | 161 | 更新之前的图示,现在可以看到现在碰撞行为与View相关联了: 162 | 163 | ![](http://cdn2.raywenderlich.com/wp-content/uploads/2013/09/DynamicClasses2.png) 164 | 165 | 然后,两个物体的相互作用还是有些不太对劲.障碍物应该是不可移动的,但是当两个物体碰撞时,你当前的配置的障碍物向着屏幕的底部移动了. 166 | 167 | 更奇怪的是,障碍物居然在屏幕底部弹跳,而且不像方块那样停止下来 -这样是因为重力行为没有与障碍物交互.这同样也解释了为什么障碍物直到与方块碰撞才移动. 168 | 169 | 看起来你需要一个不一样的解决问题的办法.由于障碍物是不可移动的,没有任何必须要让运动引擎直到它的存在,但这样如何检测碰撞呢? 170 | 171 | 172 | 173 | ### Invisible boundaries and collisions 可见的边界与碰撞 174 | 175 | 恢复碰撞行为的初始化到原来的状态,让它只知道方块: 176 | 177 | ``` swift 178 | collision = UICollisionBehavior(items: [square]) 179 | ``` 180 | 181 | 此行之后,添加以下内容: 182 | 183 | ``` swift 184 | // add a boundary that has the same frame as the barrier 185 | collision.addBoundaryWithIdentifier("barrier", forPath: UIBezierPath(rect: barrier.frame)) 186 | ``` 187 | 188 | 上面的代码添加了一个具有同样结构的障碍物View作为可见的边界.这个红色的障碍物保持着对用户可见,但对Dynamics引擎不可见,同时,边界对于Dynamics引擎可见,对于用户却不可见.当方块落下时,看起来似乎是与障碍物交互的,然而实际上却是撞到了不可移动的边界. 189 | 190 | 运行你的程序看看这个动作,如下图: 191 | 192 | ![](http://cdn1.raywenderlich.com/wp-content/uploads/2013/09/BestBarrier.png) 193 | 194 | 现在方块在边界处反弹,旋转了一下,然后继续继续朝屏幕底部落下. 195 | 196 | 现在的UIKit Dynamics的能力变得相当清楚:你可以用少量代码完成效果.在这之中有很多细节;接下来的部分我将向你展示一些Dynamics在你的应用程序中与对象的交互细节。 197 | 198 | 199 | 200 | ### Behind the scenes of collisions 碰撞背后的知识 201 | 202 | 任何Dynamic行为都有一个action属性,用来提供一个block,在动画的每一步都执行:添加以下代码到`viewDidLoad`: 203 | 204 | ``` swift 205 | collision.action = { 206 | println("\(NSStringFromCGAffineTransform(square.transform)) \(NSStringFromCGPoint(square.center))") 207 | } 208 | ``` 209 | 210 | 以上代码的日志记录了下落方块的center与transform属性.运行程序,然后你就会在Xcode的输出窗口看到这些输出. 211 | 212 | 从开始到400毫秒这段时间,你应该是看到类似下面的日志信息: 213 | 214 | ``` swift 215 | [1, 0, 0, 1, 0, 0], {150, 236} 216 | [1, 0, 0, 1, 0, 0], {150, 243} 217 | [1, 0, 0, 1, 0, 0], {150, 250} 218 | ``` 219 | 220 | 这里你可以看到运动引擎一直在改变方块的center — 也就是动画执行中的每一步的frame值. 221 | 222 | 只要方块撞到障碍物,就开始旋转,这就会生成类似以下的日志信息: 223 | 224 | ``` swift 225 | [0.99797821, 0.063557133, -0.063557133, 0.99797821, 0, 0] {152, 247} 226 | [0.99192101, 0.12685727, -0.12685727, 0.99192101, 0, 0] {154, 244} 227 | [0.97873402, 0.20513339, -0.20513339, 0.97873402, 0, 0] {157, 241} 228 | ``` 229 | 230 | 这里你可以看到Dynamics使用的是transform与基于底层物理模型定位视图偏移的组合. 231 | 232 | 虽然对于Dynamics的这些属性的兴趣可能不大,不过重要的是要知道,他们正在发挥作用.因此,如果你以编程方式更改frame或transform的属性,你可以假设这些值将被覆盖.这意味着当对象被Dynamics控制时,你就不能使用transform缩放对象了. 233 | 234 | 该方法为Dynamics行为签名,选择物体而不是View.应用动力学行为的唯一需要就是让对象实现 `UIDynamicItem` 协议,这样: 235 | 236 | ``` swift 237 | protocol UIDynamicItem : NSObjectProtocol { 238 | var center: CGPoint { get set } 239 | var bounds: CGRect { get } 240 | var transform: CGAffineTransform { get set } 241 | } 242 | ``` 243 | 244 | `UIDynamicItem`协议允许Dynamics读写center和transform属性,允许其基于其内部计算来移动物体.它对bounds也有读取的权限,用来确定物体的大小。这允许它来创建碰撞边界的四周的物体以及计算当前物体的质量. 245 | 246 | 这个协议意味着引擎不与UIView耦合,此外还有一个UIKit类不是视图但仍然遵循这个协议:`UICollectionViewLayoutAttributes`.这允许引擎在集合视图内执行动画. 247 | 248 | 249 | 250 | ### Collision notifications 碰撞提醒 251 | 252 | 到目前为止,你已经添加了一些视图和行为然后让Dynamics控制.在这一节你会看看如何在物体碰撞时接收通知. 253 | 254 | 还是在**ViewController.swift**文件中,修改类的声明来遵循 `UICollisionBehaviorDelegate`协议: 255 | 256 | ``` 257 | class ViewController: UIViewController, UICollisionBehaviorDelegate { 258 | ``` 259 | 260 | 在 `viewDidLoad`方法中,将视图控制器设置为刚刚初始化的碰撞代理对象,如下: 261 | 262 | ``` 263 | collision.collisionDelegate = self 264 | ``` 265 | 266 | 接下来,在类中实现碰撞行为代理中的一个方法: 267 | 268 | ```swift 269 | func collisionBehavior(behavior: UICollisionBehavior!, beganContactForItem item: UIDynamicItem!, withBoundaryIdentifier identifier: NSCopying!, atPoint p: CGPoint) { 270 | 271 | println("Boundary contact occurred - \(identifier)") 272 | 273 | } 274 | ``` 275 | 276 | 该代理方法会在发生碰撞时调用.这会在控制台输出日志信息.为了避免太多的日志信息搞乱了控制台的消息,请随意删除上一节添加的`collision.action`日志. 277 | 278 | 运行程序,物体发生交互,然后控制台会输出这些: 279 | 280 | ``` 281 | Boundary contact occurred - barrier 282 | Boundary contact occurred - barrier 283 | Boundary contact occurred - nil 284 | Boundary contact occurred - nil 285 | Boundary contact occurred - nil 286 | Boundary contact occurred - nil 287 | ``` 288 | 289 | 从上面的日志消息中可以看到,方块与标识的边界发生两次碰撞;这是您前面添加的不可见的边界。(null)标识符是指外部引用视图边界. 290 | 291 | 这些日志消息可以引人入胜(很严肃的说!),不过需要提供一个视觉指示项来表示反弹. 292 | 293 | 在第二次输出日志的行后,添加以下代码: 294 | 295 | ```swift 296 | let collidingView = item as UIView 297 | collidingView.backgroundColor = UIColor.yellowColor() 298 | UIView.animateWithDuration(0.3) { 299 | collidingView.backgroundColor = UIColor.grayColor() 300 | } 301 | ``` 302 | 303 | 上面的代码将碰撞物体的的背景颜色更改为黄色,然后再消褪到灰色. 304 | 305 | 运行程序然后查看这个效果: 306 | 307 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2013/09/YellowCollision.png) 308 | 309 | 方块每次碰撞到边界都会变化成黄色. 310 | 311 | 到目前为止UIKit Dynamics 通过计算它们的bounds自动为你的物体设置物理属性(如质量和弹性).接下来你将看到如何自己使用`UIDynamicItemBehavior`类来控制这些物理属性. 312 | 313 | 314 | 315 | ### Configuring item properties 设置属性 316 | 317 | 在`viewDidLoad`方法中,在方法的末尾加上以下代码: 318 | 319 | ``` 320 | let itemBehaviour = UIDynamicItemBehavior(items: [square]) 321 | itemBehaviour.elasticity = 0.6 322 | animator.addBehavior(itemBehaviour) 323 | ``` 324 | 325 | 以上代码创建了一个物体行为,并与方块关联上,接着将其添加到animator中. elasticity属性控制物体的弹力;其值为1.0时表示碰撞完全反弹;也就是说,碰撞不消耗能量或者减缓速度.你为方块设置的elasticity为0.6,这意味着你的方块每次碰撞时都会减缓速度. 326 | 327 | 运行你的程序,然后你会发现方块现在的行为很有弹性,如下所示: 328 | 329 | ![](http://cdn5.raywenderlich.com/wp-content/uploads/2013/09/PrettyBounce.png) 330 | 331 | > 注意:如果你想知道我是如何产生上图的方块轨迹图的,这实际上很简单!我只是添加了一个block到每个行为中的action属性中,该block每三秒执行一次,然后使用当前方块的center和transform属性在视图中添加一个新的方块.下面就是我的解决方案. 332 | > 解决方案: 333 | ``` 334 | var updateCount = 0 335 | collision.action = { 336 | if (updateCount % 3 == 0) { 337 | let outline = UIView(frame: square.bounds) 338 | outline.transform = square.transform 339 | outline.center = square.center 340 | outline.alpha = 0.5 341 | outline.backgroundColor = UIColor.clearColor() 342 | outline.layer.borderColor = square.layer.presentationLayer().backgroundColor 343 | outline.layer.borderWidth = 1.0 344 | self.view.addSubview(outline) 345 | } 346 | ++updateCount 347 | } 348 | ``` 349 | 350 | 在上面的代码你只改变了物体的弹力;然而,物体的行为类有许多可以控制的属性,如下: 351 | 352 | - `elasticity` – 弹力,决定碰撞时的弹力,例如,当物体发生碰撞行为时的弹力或者韧性. 353 | - `friction` – 摩擦力,决定物体滑动时的抵抗力. 354 | - `density` – 密度,如果加上大小,就能表示物体的质量.质量越大,物体越难加速或减速. 355 | - `resistance` – 阻力,任何线性运动的抵抗力.与摩擦力不同的是,摩擦力只对滑动有效. 356 | - `angularResistance` – 旋转的阻力. 357 | - `allowsRotation` – 这个有点意思,因为这不表示真实物理世界中的属性. 将这个属性设置为NO可以让物体不管任何发生力量的旋转都无效. 358 | 359 | 360 | 361 | ### Adding behaviors dynamically 动态添加行为 362 | 363 | 在当前情况下,你的程序为系统设置全部的行为,然后让Dynamics处理系统中的物理事件直到是全部的物体都静止下来.接下来,你将看到如何动态的添加与删除行为. 364 | 365 | 打开**ViewController.swift**,然后在`viewDidLoad`方法中添加以下属性: 366 | 367 | ``` 368 | var firstContact = false 369 | ``` 370 | 371 | 添加以下代码到碰撞的代理方法`collisionBehavior(behavior:beganContactForItem:withBoundaryIdentifier:atPoint:)`的最后 372 | 373 | ``` 374 | if (!firstContact) { 375 | firstContact = true 376 | 377 | let square = UIView(frame: CGRect(x: 30, y: 0, width: 100, height: 100)) 378 | square.backgroundColor = UIColor.grayColor() 379 | view.addSubview(square) 380 | 381 | collision.addItem(square) 382 | gravity.addItem(square) 383 | 384 | let attach = UIAttachmentBehavior(item: collidingView, attachedToItem:square) 385 | animator.addBehavior(attach) 386 | } 387 | ``` 388 | 389 | 以上代码检测初次接触时的障碍物和方块,然后创建第二个方块,并为其添加碰撞与重力行为.此外,你创建一个锚链接行为来为一对对象创建虚拟的弹簧效果. 390 | 391 | 运行你的程序;当最初的方块碰撞到障碍物时,你将会看到一个新的方块出现,就像下面这样: 392 | 393 | ![](http://cdn5.raywenderlich.com/wp-content/uploads/2013/09/Attachment.png) 394 | 395 | 尽管这样看起来两个方块之间似乎是连接着的,不过你在屏幕上是无法看到线条或者弹簧之类的. 396 | 397 | 398 | 399 | ### User Interaction 用户交互 400 | 401 | 正如刚刚所看到的那样,你可以动态的添加或是删除你定义的物理系统中正在运动的行为.在最后一节,你将添加另一种类型的Dynamics行为-`UISnapBehavior`,当用户点击屏幕时,`UISnapBehavior`会生成一个弹簧动画跳转到指定位置. 402 | 403 | 删除你在上一节添加的代码:`firstContact`属性与`collisionBehavior()`中的if语句.这样当界面中只有一个方块时,就很容易看出来`UISnapBehavior`的作用. 404 | 405 | 在`viewDidLoad`中加上这两个属性: 406 | 407 | ``` 408 | var square: UIView! 409 | var snap: UISnapBehavior! 410 | ``` 411 | 412 | 这记录了方块视图,这样你就可以在控制器的任何地方访问到它.接下来你就会用到`snap`对象. 413 | 414 | 在`viewDidLoad`方法中,从声明方块的地方移除`let`关键字,这样就可以使用新的属性来替代局部变量: 415 | 416 | ``` 417 | square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100)) 418 | ``` 419 | 420 | 最后,实现`touchesEnded`方法,每当用户点击屏幕的时候就创建新的触摸行为: 421 | 422 | ``` 423 | override func touchesEnded(touches: NSSet, withEvent event: UIEvent) { 424 | if (snap != nil) { 425 | animator.removeBehavior(snap) 426 | } 427 | 428 | let touch = touches.anyObject() as UITouch 429 | snap = UISnapBehavior(item: square, snapToPoint: touch.locationInView(view)) 430 | animator.addBehavior(snap) 431 | } 432 | ``` 433 | 434 | 这段代码相当简单.首先,它会检查当前是否已经存在触摸行为并且移除.然后创建一个新的触摸行为来检测用户是否触摸方块,最后添加到animator中. 435 | 436 | 运行你的程序.尝试点击一下;无论你点击哪个地方,方块都应该会放大. 437 | 438 | 439 | 440 | ### Where To Go From Here? 何去何从 441 | 442 | 此时,你应该对于Dynamics的核心概念有了一定的理解.你可以下载本教程最后[DynamicsDemo项目](http://cdn5.raywenderlich.com/wp-content/uploads/2014/07/DynamicsDemo-Final-7.zip) 进行进一步的研究。 443 | 444 | UIKit Dynamics 给你的应用带来物理引擎的能力.通过精妙的的反弹和弹簧和重力效果,给你的应用程序注入新的血液,让用户如同身临其境. 445 | 446 | 如果你想要更深入的学习UIKit Dynamics,请查阅我们编写的[iOS 7 By Tutorials](http://www.raywenderlich.com/?page_id=48020). 这本书让你的知识更进一步,向你展示如何在真实场景中使用UIKit Dynamics: 447 | 448 | ![](http://cdn5.raywenderlich.com/wp-content/uploads/2013/09/SandwichFlowDynamics.png) 449 | 450 | 用户可以把菜谱上一瞥,当他们将菜谱放回时,它会掉回菜谱堆中,或者靠近屏幕顶部.最终给予应用程序一个真实的物理效果。 451 | 452 | 我希望你能喜欢这个UIKit Dynamics的教程,我们觉得这是非常炫酷的,期待你在程序中创造性的使用他们.如果你有任何的问题或意见,请在一下进行讨论! -------------------------------------------------------------------------------- /issue-7/readme.md: -------------------------------------------------------------------------------- 1 | # 第7期 -------------------------------------------------------------------------------- /issue-8/UIKit Dynamics和Swift教程:抛掷Views.md: -------------------------------------------------------------------------------- 1 | UIKit Dynamics 和 Swift 教程:抛掷视图 (Tossing Views) 2 | --- 3 | 4 | > * 原文链接 : [UIKit Dynamics and Swift Tutorial:Tossing Views](http://www.raywenderlich.com/94719/uikit-dynamics-swift-tutorial-tossing-views) 5 | * 原文作者 : [Ray Wenderlich](http://www.raywenderlich.com) 6 | * 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn) 7 | * 译者 : [samw00](https://github.com/samw00) 8 | * 校对者: [mrchenchao](https://github.com/mrchenchao) 9 | * 状态 : 完成 10 | 11 | 在这篇UIKit Dynamics教程中,你将学会如何通过添加手势,用一种很自然的方式将视图抛掷出屏幕的显示区域。如下图所示: 12 | 13 | ![效果预览图](http://cdn1.raywenderlich.com/wp-content/uploads/2014/07/UIKitDynamics3.gif) 14 | 15 | 你可能已经在[Tweetbot](https://itunes.apple.com/us/app/tweetbot-3-for-twitter-iphone/id722294701)这个流行的app里看到这一个技巧被大量的运用。 16 | 17 | 这篇教程特别适合中级水平的开发人员,因为内容包括如何实现一些特别棒的特效,比如用原生的UIKit框架实现旋转(rotation)和飞离(fly-away)等动画效果。 18 | 19 | 如果你刚刚接触UIKit dynamics, 也别紧张 - 因为这篇教程会一步一步引导你如何去做。 20 | 21 | 废话不多说,让我们直切主题吧! 22 | 23 | #让我们开始吧 24 | 25 | > 注意: 这一节是为那些想从头开始写这个项目的开发员所准备的。有经验的开发者可以直接跳过这一节去下一节"UIDynamicAnimator and UIAttachmentBehavior", 在那我们已为你准备好了这个教程的启动项目。 26 | 27 | 运行你的Xcode,选择`File\New\Project...`, 选择`iOS\Application\Single View Application template`然后点击`Next`。将项目命名为DynamicToss,确保语言用的是Swfit然后将设备设为iPhone. 28 | 29 | 接下来在左边点选项目名称并在Xcode顶部位置点击**General**。在**Deployment Info/Device Orientation**中不要勾选**Landscape Left**和**Landscape Right**,因为你的app只有竖屏这一种显示模式。 30 | 31 | ![](http://cdn4.raywenderlich.com/wp-content/uploads/2015/02/dynamictoss-orientation.png) 32 | 33 | 然后,下载你这个项目需要的[图片](http://cdn2.raywenderlich.com/wp-content/uploads/2014/05/flying_goldfish.zip),照片提供者:[gameartguppy.com](http://www.vickiwenderlich.com/)。 34 | 35 | ![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/05/goldfish_feature.jpg) 36 | 37 | 将图片解压后添加到你项目中Images asset目录里。 38 | 39 | 接着,点击**Main.storyboard**然后再右边的工具栏中选择**File Inspector**。在本片教程中你不需要用到auto layout和size classes,所以勾掉**Use Auto Layout**选项。这会自动勾掉size classes。 在弹出的对话框中,在Keep size class data for:这一选项选iPhone,然后点击**Disable Size Classes**。 40 | 41 | 在storyboard中,在默认视图控制器中添加一个image view,然后将图片设为**goldfish_feature**。在image view的尺寸观察器中设置下列数值:X=33, y=137, Width=254, Height=172。紧接着在autoresizing mask中消掉弹性高度,宽度,右边距和底边距。这意味着只有顶边距和左边距是“被点选“的。 42 | 43 | 你的屏幕看起来应该像这样: 44 | 45 | ![](http://cdn1.raywenderlich.com/wp-content/uploads/2015/02/dynamictoss-imageview.png) 46 | 47 | 然后,在视图控制器中拖入两个view用以跟踪你的手势,并设置为下列数值 48 | 49 | * View 1: (X=156, Y=219, Width=8, Height=8, Background=red) 50 | * View 2: (X=80, Y=420, Width=8, Height=8. Background=blue) 51 | 52 | 当你完成之后,你的视图看起来应该是这样的: 53 | 54 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2015/02/dynamictoss-views.png) 55 | 56 | 基本上那就是你在storyboard中要做的所有准备,接下来就是代码了! 57 | 58 | ##Touch Handling 触摸操作 59 | 60 | 打开**ViewController.swift**并在类中加入下列属性: 61 | 62 | ```swift 63 | @IBOutlet weak var imageView: UIImageView! 64 | @IBOutlet weak var redSquare: UIView! 65 | @IBOutlet weak var blueSquare: UIView! 66 | 67 | private var originalBounds = CGRect.zeroRect 68 | private var originalCenter = CGPoint.zeroPoint 69 | 70 | private var animator: UIDynamicAnimator! 71 | private var attachmentBehavior: UIAttachmentBehavior! 72 | private var pushBehavior: UIPushBehavior! 73 | private var itemBehavior: UIDynamicItemBehavior! 74 | ``` 75 | 76 | 你将在等一会连接这些外部引用。其他的属性用来控制image view的运行轨迹和在触摸事件中跟踪image view的状态。 77 | 78 | 打开**Main.storyboard**然后右键(或者按住control)点击**View Controller**。在外部引用列表中的**blueSquare**右边有一个空心的小圈圈,点选后不要松开鼠标左键,然后拖拽到blue square的视图上来再松手。这会讲属性和视图对象绑定起来。 79 | 80 | 用同样的方式绑定red square和属性**imageView**。现在三个view属性应该绑定好了,如下图所示: 81 | 82 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/05/finallinking.png) 83 | 84 | 红色和蓝色的正方形代表着UIKit dynamics物理引擎用来时图像运动起来的点。 85 | 86 | 蓝色的正方形就代表着你触摸开始的那个点,也就是当你手指第一次触碰到屏幕的那个点。红色的正方形会跟踪你手指滑动的轨迹。 87 | 88 | 待会你将对dynamics做一些配置,使得当你在移动那个点的时候,image view也会相应的进行物理运动。 89 | 90 | 最后你还有一件事要做 - 给view添加一个手势识别。打开**ViewController.swift**然后在文件中加入下列方法: 91 | 92 | ```swift 93 | @IBAction func handleAttachmentGesture(sender:UIPanGestureRecognizer) { 94 | 95 | let location = sender.locationInView(self.view) 96 | let boxLocation = sender.locationInView(self.imageView) 97 | 98 | switch sender.state { 99 | case .Began: 100 | println("Your touch start position is \(location)") 101 | println("Start location in image is \(boxLocation)") 102 | 103 | case .Ended: 104 | println("Your touch end position is \(location)") 105 | println("End location in image is \(boxLocation)") 106 | 107 | default: 108 | break 109 | } 110 | } 111 | ``` 112 | 113 | 你还要添加一个拖拽手势识别,也就是panning,然后当panning触发时,调用这个方法。现阶段,这个方法就是简单的现实你的手指在两个坐标系(view, image view)中的位置而已。 114 | 115 | 打开**Main.storyboard**,往视图上拖拽一个**Pan Gesture Recognizer**。然后按住control点住**Pan Gesture Recognizer**拖到你的**View Controller**,然后连接**handleAttachmentGesture:**。 116 | 117 | 现在编译运行。在屏幕上滑动或者拖拽,你应该能在控制台上看到: 118 | 119 | ``` 120 | Your touch start position is (125.0,227.0) 121 | Start location in image is (92.0,90.0) 122 | Your touch end position is (195.5,374.0) 123 | End location in image is (162.5,237.0) 124 | ``` 125 | 126 | 棒极了!你已经将一切都准备好了-接下来就是添加动态特效了。 127 | 128 | #UIDynamicAnimator 和 UIAttachmentBehavior 129 | 130 | > 注意:如果你略过了之前的内容,先下载[项目](http://cdn4.raywenderlich.com/wp-content/uploads/2015/02/DynamicToss-Starter.zip)然后继续下面的内容。 131 | 132 | 你要做的第一件事就是当你拖拽时,让你的image view也跟着动起来。你会用到UIKit Dynamics类中一个叫UIAttachmentBehavior的类。 133 | 134 | 打开**ViewController.swift**,然后在**viewDidLoad()**里面,将下边的代码添加到**super.viewDidLoad()**下面。 135 | 136 | ```swift 137 | animator = UIDynamicAnimator(referenceView: view) 138 | originalBounds = imageView.bounds 139 | originalCenter = imageView.center 140 | ``` 141 | 142 | 上面的代码设立了一个UIDynamicAnimator,这是UIKit的一个引擎,就是为实现物理动画而准备的。这个引擎所参照的坐标系就是你提供的这个视图控制器的view。 143 | 144 | 通过给animator添加各种行为(behaviors),你可以实现attaching views, pushing views, 让视图受重力影响等等。 145 | 146 | 让我们先从UIAttachmentBehavior开始,当你做了一个拖拽手势时让image view跟随你的指尖移动。 147 | 148 | 为了实现这效果,在**handleAttachmentGesture(sender:)**中添加如下代码,在**case .Began**中的两个**println**语句之后: 149 | 150 | ```swift 151 | // 1 152 | animator.removeAllBehaviors() 153 | 154 | // 2 155 | let centerOffset = UIOffset(horizontal: boxLocation.x - imageView.bounds.midX, 156 | vertical: boxLocation.y - imageView.bounds.midY) 157 | attachmentBehavior = UIAttachmentBehavior(item: imageView, 158 | offsetFromCenter: centerOffset, attachedToAnchor: location) 159 | 160 | // 3 161 | redSquare.center = attachmentBehavior.anchorPoint 162 | blueSquare.center = location 163 | 164 | // 4 165 | animator.addBehavior(attachmentBehavior) 166 | ``` 167 | 168 | 让我们来看看每一步都做了些什么: 169 | 170 | 1. 第一步,先移除animtor中任何可能残留的动画行为。 171 | 2. 然后,你创建了一个**UIAttachmentBehavior**,用来连接用户在image view内点击的点和一个锚点(刚好这两个点是重合的)。晚点你将改变这个锚点,从而使image view跟着移动。给视图添加一个锚点,就好比在view上一个固定的点和锚点之间添加了一个隐形的拉杆。 172 | 3. 更新red square来表示锚点,而blue square就是表示在image view中和锚点相连的点。当手势刚开始时,这两个点是重合的。 173 | 4. 在引擎animator添加这个行为使其生效。 174 | 175 | 下一步你要告诉锚点跟随你的指尖。在**handleAttachmentGesture(_:)**,替换掉**default:**中的**break**语句为下列代码: 176 | 177 | ```swift 178 | attachmentBehavior.anchorPoint = sender.locationInView(view) 179 | redSquare.center = attachmentBehavior.anchorPoint 180 | ``` 181 | 182 | 当用户拖拽时候,default里面的代码内容来响应手势一直更新这个事件。上面的代码就是简单的将锚点,red square和手指当前位置合为一体。当用户手指移动时,手势识别则调用这个方法更新锚点至指尖移动到的位置。同时,animator也会随着锚点的移动自动更新view。 183 | 184 | 编译并运行,现在你可以把view拖来拖去了: 185 | 186 | ![](http://cdn2.raywenderlich.com/wp-content/uploads/2014/07/004_DraggedView-281x500.png) 187 | 188 | 注意观察view并不是在屏幕中简单的变动;如果手势起于图像的某一个角,你会发现当你移动时view会旋转,这就要归功于锚点了。 189 | 190 | 不过,当你拖拽完后,视图最好能回到自己的初始位置。我们类中添加一个新的方法来完善这个功能: 191 | 192 | ```swift 193 | func resetDemo() { 194 | animator.removeAllBehaviors() 195 | 196 | UIView.animateWithDuration(0.45) { 197 | self.imageView.bounds = self.originalBounds 198 | self.imageView.center = self.originalCenter 199 | self.imageView.transform = CGAffineTransformIdentiy 200 | } 201 | } 202 | ``` 203 | 204 | 然后在**handelAttachmentGesture(_:)**中,在**.Ended**中的**println**语句下添加: 205 | 206 | ```swift 207 | resetDemo() 208 | ``` 209 | 210 | 编译运行,现在当你拖拽完之后,视图就应该回到它自己初始的位置了。 211 | 212 | #UIPushBehavior 213 | 214 | 接下来,我们要实现的是:当你停止拖拽时,使你的view飞离(detach),我们会给这个view赋予动能,所以当你的手松开时,它能会延续它的既定轨道去运动。我们将通过**UIPushBehavior**来实现这一点。 215 | 216 | 首先,你需要两个常量。在文件头部添加它们: 217 | 218 | ``` 219 | let ThrowingThreshold: CGFloat = 1000 220 | let ThrowingVelocityPadding: CGFloat = 35 221 | ``` 222 | 223 | **ThrowingThreshold**表明view必须移动多块才能在手指松开后继续移动(而不是马上回到它的初始位置)。**ThrowingVelocityPadding**是一个神奇的常量,它对抛掷view速度快慢产生影响(这个数值是经过反复试错而得出的)。 224 | 225 | 最后,在**handleAttachmentGesture(_:)**中,在**.Ended**分支下,用下面的代码替换原来的**resetDemo()**: 226 | 227 | ```swift 228 | animator.removeAllBehaviors() 229 | 230 | // 1 231 | let velocity = sender.velocityInView(view) 232 | let magnitude = sqrt((velocity.x * velocity.x) + (velocity.y * velocity.y)) 233 | 234 | if magnitude > ThrowingThreshold { 235 | // 2 236 | let pushBehavior = UIPushBehavior(items: [imageView], mode: .Instantaneous) 237 | pushBehavior.pushDirection = CGVector(dx: velocity.x / 10, dy: velocity.y / 10) 238 | pushBehavior.magnitude = magnitude / ThrowingVelocityPadding 239 | 240 | self.pushBehavior = pushBehavior 241 | animator.addBehavior(pushBehavior) 242 | 243 | // 3 244 | let angle = Int(arc4random_uniform(20)) - 10 245 | 246 | itemBehavior = UIDynamicItemBehavior(items: [imageView]) 247 | itemBehavior.friction = 0.2 248 | itemBehavior.allowsRotation = true 249 | itemBehavior.addAngularVelocity(CGFloat(angle), forItem: imageView) 250 | animator.addBehavior(itemBehavior) 251 | 252 | // 4 253 | let timeOffset = Int64(0.4 * Double(NSEC_PER_SEC)) 254 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeOffset), dispatch_get_main_queue()) { 255 | self.resetDemo() 256 | } 257 | } else { 258 | resetDemo() 259 | } 260 | ``` 261 | 262 | 让我们再来看看每一步干了些什么: 263 | 264 | 1. 向手势获取拖拽的速度。通过速度和你的老朋友勾股定理,你可以计算出速度的量级 - 也就是x轴方向的速度和y轴方向的速度所构成的三角形的斜边(弦)。如果想了解这个定理背后的知识,可以瞅瞅[Trigonometry for Game Programming Tutorial](http://www.raywenderlich.com/?p=90520). 265 | 266 | 2. 当速度的量级超过了你设定的临界值,则创建一个push behavior。Push behavior会给特定的对象施加一种力,在这里,我们通过push behavior给图像施加一种瞬间,猝发的力。而运行的方向则是由x轴速度和y轴速度合起来转换出的一个方向向量来决定的。当你创建好push behavior之后,将其添加到animator的行为序列中。 267 | 268 | 3. 这一步则将添加一下旋转特效,让图像看起来像是“飞离”一样。你可以在这里了解背后复杂的[数学知识](https://github.com/u10int/URBMediaFocusViewController/blob/master/URBMediaFocusViewController.m#L636-L686)。其中有部分效果取决于当你开始手势时,你的手指离边界的距离。你可以尝试设置其他的值然后观察它们的移动效果。这里用的值,会给出一个非常流畅平滑和酷毙了的旋转效果。 269 | 270 | 4. 在指定的一段时间过后,图像将会被重置回原来初始的位置,所以“颼”的一下,图像又回到屏幕中 - 就想一个撞到墙弹回来的球一样。 271 | 272 | 编译并运行,这回你应该可以愉快地把你的view随意的拖拽了。 273 | 274 | ![](http://cdn1.raywenderlich.com/wp-content/uploads/2014/07/UIKitDynamics3.gif) 275 | 276 | #接下来可以做什么? 277 | 278 | 这篇UIKit Dynamics教程中的项目可以在这里[下载](http://cdn5.raywenderlich.com/wp-content/uploads/2015/04/DynamicToss-Final-1.2.zip)。 279 | 280 | 恭喜你,你现在已经学了如何添加一些UIKit Dynamics动画特效让你的app的UI看起来更加的酷炫。 281 | 282 | 如果你想学更多UIKit Dynamics的技巧, 记得去看看[iOS 7 by Tutorials](http://www.raywenderlich.com/?page_id=48020)中有两章节是专门讲UIKit Dynamics的。 283 | 284 | 没事就到论坛去留个言分享一下你的成功经验或者问些关于如何在iOS中实现一些酷的动画特效的问题等等。然后善用你新get的技能! 285 | -------------------------------------------------------------------------------- /issue-8/WWDC中那些令人感兴趣的事物-iOS9,-Swift2.md: -------------------------------------------------------------------------------- 1 | ## WWDC中那些令人感兴趣的事物:iOS 9, Swift 2 2 | 3 | > - 原文链接 : [Notes: The interesting things from WWDC, iOS 9, Swift 2](http://iosdevtips.co/post/121053658888/wwdc-ios-9-swift-2-notes) 4 | - 原文作者: [Rounak Jain](http://iosdevtips.co) 5 | - 译文出自: [开发技术前线http://www.devtf.cn/](http://www.devtf.cn/) 6 | - 译者 : [Lollypo](https://github.com/Lollypo) 7 | - 校对者:[Mr.Simple](https://github.com/bboyfeiyu) 8 | - 状态 : 完成 9 | 10 | 今天WWDC上的Keynote非常棒,即便到目前为止还未结束.在开发者方面,已经有大量的新事物出现,其中包括有Swift 2, iOS 9, CloudKit等等. 11 | 12 | 这里是我在浏览发布日志,API差异,新特性等内容时记录的笔记 13 | 14 | - 你不需要加入$99的开发者计划就可以在真机上调试应用程序.此外,Mac与iOS开发者计划合并,可以节省一半支出: 15 | ![](http://40.media.tumblr.com/ab8cf0d95f459e63bb10ded612e2fa34/tumblr_inline_npnb64YpK31qh9cw7_500.png) 16 | 17 | - Interface Builder支持实时预览模糊与透明效果 18 | ![](http://40.media.tumblr.com/ed19a7cbd80fe5ccb49f5a9102744eec/tumblr_inline_npnb8aLtBC1qh9cw7_500.png) 19 | 20 | - Objective-C支持泛型,这意味着集合对象,如数组,字典可以像Swift一样指定类型 21 | ![](http://40.media.tumblr.com/4ed77823bca27d89db84f654080d7d28/tumblr_inline_npnbbdudd01qh9cw7_500.png) 22 | 23 | - 新的名为 [UIStackView](https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIStackView_Class_Reference/index.html#//apple_ref/doc/uid/TP40015256)UI类,是Mac中NSStackView的一种实现. 24 | 25 | - [CloudKit现在也可以在Web中使用](https://developer.apple.com/library/prerelease/ios/documentation/DataManagement/Conceptual/CloutKitWebServicesReference/Introduction/Introduction.html#//apple_ref/doc/uid/TP40015240) 26 | 27 | - 开放 CASpringAnimation! 28 | ![](http://40.media.tumblr.com/65c41c82d295034c23ef986214404c6d/tumblr_inline_npnbg2mCrc1qh9cw7_500.png) 29 | 30 | - 应用程序现在可以作为拓展插入到Safari中,听起来有点像广告拦截器.此外,应用程序可以在Safari的共享链接填充内容 31 | 32 | - 开发者现在可以使用 `SFSafariViewController`, 这可以共享Safari中中cookies, 自动填充以及其他Safari中的特性. 33 | 34 | - `UIPickerView` 可转换为任意尺寸,而不必再使用那些丑陋的转换技巧. 35 | 36 | - iOS应用程序可在发出提醒展示一段文本域,此前仅限于Messages使用. 37 | 38 | - 通讯录可通过面向对象的API来访问,而不是之前的C 39 | 40 | - 电话API在低版本设备上不支持,这也许会导致编译时错误.现在有新的判断条件来决定是否执行代码,这可以有效避免使用新的API导致的应用程序崩溃: 41 | ![](http://41.media.tumblr.com/c1875f851f168467e8e3bd435fa31f8d/tumblr_inline_npnbnvrUp31qh9cw7_500.png) 42 | 43 | - `UICollectionView` 新添加了许多方法来支持交互式移动 44 | 45 | - 新的名为 `UIFieldBehavior` 的 UIKit Dynamics类 46 | 47 | - Xcode Assets Catalog: 设备不会一次性下载所有尺寸的图片,而根据其自身的需要下载 1x, 2x 或 3x 的图片,大大减少了应用程序的大小. 48 | 49 | ## 其他一些你可能感兴趣的链接: 50 | 51 | - [API差异](https://t.co/cOdmXdQ0EQ) 52 | - [Swift 2提前发布版电子书](https://itunes.apple.com/us/book/swift-programming-language/id1002622538?mt=11) 53 | - [Xcode 7 发布日志](http://t.co/5pU8P4iNEH) -------------------------------------------------------------------------------- /issue-8/使用CAShapeLayer来实现圆形图片加载动画.md: -------------------------------------------------------------------------------- 1 | 使用CAShapeLayer来实现圆形图片加载动画 2 | --- 3 | 4 | > * 原文链接 : [How To Implement A Circular Image Loader Animation with CAShapeLayer](http://www.raywenderlich.com/94302/implement-circular-image-loader-animation-cashapelayer) 5 | * 原文作者 : [Rounak Jain](http://www.raywenderlich.com/u/rounak) 6 | * 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn) 7 | * 译者 : [Sam Lau](https://github.com/samlaudev) 8 | * 校对者: [Lollypo](https://github.com/Lollypo) 9 | * 状态 : 校正完 10 | 11 | 几个星期之前,Michael Villar在Motion试验中创建一个非常有趣的加载动画。 12 | 13 | 下面的GIF图片展示这个加载动画,它将一个圆形进度指示器和圆形渐现动画结合。这个组合的效果有趣,独一无二和有点迷人。 14 | 15 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2015/02/Circle.gif) 16 | 17 | 这个教程将会教你如何使用Swift和Core Animatoin来重新创建这个效果。让我们开始吧! 18 | 19 | 20 | #基础 21 | 22 | 首先下载这个教程的[启动项目](http://cdn1.raywenderlich.com/wp-content/uploads/2015/02/ImageLoaderIndicator-Starter.zip),然后编译和运行。过一会之后,你应该看到一个简单的image显示: 23 | 24 | ![](http://cdn2.raywenderlich.com/wp-content/uploads/2015/02/StarterProject.png) 25 | 26 | 这个启动项目已经预先在恰当的位置将views和加载逻辑编写好了。花一分钟来浏览来快速了解这个项目;那里有一个**ViewController**,**ViewController**里有一个命名为**CustomImageView**的**UIImageView**子类, 还有一个**SDWebImage**的方法被调用来加载image。 27 | 28 | 你可能注意到当你第一次运行这个app的时候,当image下载时这个app似乎会暂停几秒,然后image会显示在屏幕。当然,此刻没有圆形进度指示器 - 你将会在这个教程中创建它! 29 | 30 | 你会在两个步骤中创建这个动画: 31 | 32 | 1. **圆形进度**。首先,你会画一个圆形进度指示器,然后根据下载进度来更新它。 33 | 2. **扩展圆形图片**。第二,你会通过扩展的圆形窗口来揭示下载图片。 34 | 35 | 紧跟着下面步骤来逐步实现! 36 | 37 | 38 | #创建圆形指示器 39 | 40 | 想一下关于进度指示器的基本设计。这个指示器一开始是空来展示0%进度,然后逐渐填满直到image完成下载。通过设置**CAShapeLayer**的**path**为circle来实现是相当简单。 41 | 42 | > 注意:如果你不熟悉**CAShapeLayer**(或**CALayers**)的基本概念,可以查看Scott Gardner的[CALayer in iOS with Swift](http://www.raywenderlich.com/90488/calayer-in-ios-with-swift-10-examples)文章。 43 | 44 | 你可以通过**CAShapeLayer**的**strokeStart**和**strokeEnd**属性来控制开始和结束位置的外观。通过改变**strokeEnd**的值在0到1之间,你可以恰当地填充下载进度。 45 | 46 | 让我们试一下。通过**iOS\Source\Cocoa Touch Class template**来创建一个新的文件,文件名为**CircularLoaderView**。设置它为**UIView**的子类。 47 | 48 | ![](http://cdn2.raywenderlich.com/wp-content/uploads/2015/02/Screenshot-2015-01-25-19.25.43.png) 49 | 50 | 点击**Next**和**Create**。新的子类**UIView**将用来保存动画的代码。 51 | 52 | 打开**CircularLoaderView.swift**和添加以下属性和常量到这个类: 53 | 54 | ``` 55 | let circlePathLayer = CAShapeLayer() 56 | let circleRadius: CGFloat = 20.0 57 | ``` 58 | 59 | **circlePathLayer**表示这个圆形路径,而**circleRadius**表示这个圆形路径的半径。 60 | 61 | 添加以下初始化代码到**CircularLoaderView.swift**来配置这个shape layer: 62 | 63 | ``` 64 | override init(frame: CGRect) { 65 | super.init(frame: frame) 66 | configure() 67 | } 68 | 69 | required init(coder aDecoder: NSCoder) { 70 | super.init(coder: aDecoder) 71 | configure() 72 | } 73 | 74 | func configure() { 75 | circlePathLayer.frame = bounds 76 | circlePathLayer.lineWidth = 2 77 | circlePathLayer.fillColor = UIColor.clearColor().CGColor 78 | circlePathLayer.strokeColor = UIColor.redColor().CGColor 79 | layer.addSublayer(circlePathLayer) 80 | backgroundColor = UIColor.whiteColor() 81 | } 82 | ``` 83 | 84 | 两个初始化方法都调用**configure**方法,**configure**方法设置一个shape layer的line width为2,fill color为clear,stroke color为red。将添加circlePathLayer添加到view's main layer。然后设置view的 **backgroundColor** 为white,那么当image加载时,屏幕的其余部分就忽略掉。 85 | 86 | ##添加路径 87 | 88 | 你会注意到你还没赋值一个path给layer。为了做到这点,添加以下方法(还是在**CircularLoaderView.swift**文件): 89 | 90 | ``` 91 | func circleFrame() -> CGRect { 92 | var circleFrame = CGRect(x: 0, y: 0, width: 2*circleRadius, height: 2*circleRadius) 93 | circleFrame.origin.x = CGRectGetMidX(circlePathLayer.bounds) - CGRectGetMidX(circleFrame) 94 | circleFrame.origin.y = CGRectGetMidY(circlePathLayer.bounds) - CGRectGetMidY(circleFrame) 95 | return circleFrame 96 | } 97 | ``` 98 | 99 | 上面那个方法返回一个**CGRect**的实例来界定指示器的路径。这个边框是**2*circleRadius**宽和**2*circleRadius**高,放在这个view的正中心。 100 | 101 | 每次这个view的size改变时,你会需要都重新计算**circleFrame**,所以你可能将它放在一个独立的方法。 102 | 103 | 现在添加以下方法来创建你的路径: 104 | 105 | ``` 106 | func circlePath() -> UIBezierPath { 107 | return UIBezierPath(ovalInRect: circleFrame()) 108 | } 109 | ``` 110 | 111 | 这只是根据**circleFrame**限定来返回圆形的**UIBezierPath**。由于**circleFrame()**返回一个正方形,在这种情况下”椭圆“会最终成为一个圆形。 112 | 113 | 由于layers没有**autoresizingMask**这个属性,你需要在**layoutSubviews**方法更新**circlePathLayer**的frame来恰当地响应view的size变化。 114 | 115 | 下一步,覆盖**layoutSubviews()**方法: 116 | 117 | ``` 118 | override func layoutSubviews() { 119 | super.layoutSubviews() 120 | circlePathLayer.frame = bounds 121 | circlePathLayer.path = circlePath().CGPath 122 | } 123 | ``` 124 | 125 | 由于改变了frame,你要在这里调用**circlePath()**方法来触发重新计算路径。 126 | 127 | 现在打开**CustomImageView.swift**文件和添加以下**CircularLoaderView**实例作为一个属性: 128 | 129 | ``` 130 | let progressIndicatorView = CircularLoaderView(frame: CGRectZero) 131 | ``` 132 | 133 | 下一步,在之前下载图片的代码添加这几行代码到**init(coder:)**方法: 134 | 135 | ``` 136 | addSubview(self.progressIndicatorView) 137 | progressIndicatorView.frame = bounds 138 | progressIndicatorView.autoresizingMask = .FlexibleWidth | .FlexibleHeight 139 | ``` 140 | 141 | 上面代码添加进度指示器作为一个subview添加到自定义的image view。**autoresizingMask**确保进度指示器view保持与image view的size一样。 142 | 143 | 编译和运行你的项目;你会看到一个红的、空心的圆形出现,就像这样: 144 | 145 | ![](http://cdn1.raywenderlich.com/wp-content/uploads/2015/02/Screenshot-2015-01-25-21.44.17.png) 146 | 147 | 好的 - 你已经有进度指示器画在屏幕上。你的下一个任务就是根据下载进度变化来stroke。 148 | 149 | 150 | ##修改Stroke长度 151 | 152 | 回到**CircularLoaderView.swift**文件和在这个文件的其他属性直接添加以下代码: 153 | 154 | ``` 155 | var progress: CGFloat { 156 | get { 157 | return circlePathLayer.strokeEnd 158 | } 159 | set { 160 | if (newValue > 1) { 161 | circlePathLayer.strokeEnd = 1 162 | } else if (newValue < 0) { 163 | circlePathLayer.strokeEnd = 0 164 | } else { 165 | circlePathLayer.strokeEnd = newValue 166 | } 167 | } 168 | } 169 | ``` 170 | 171 | 以上代码创建一个**computed property** - 也就是一个属性没有任何后背的变量 - 它有一个自定义的setter和getter。这个getter只是返回**circlePathLayer.strokeEnd**,setter验证输入值要在0到1之间,然后恰当地设置layer的**strokeEnd**属性。 172 | 173 | 在第一次运行的时候,添加下面这行代码到**configure()**来初始化进度: 174 | 175 | ``` 176 | progress = 0 177 | ``` 178 | 179 | 编译和运行工程;除了一个空白的屏幕,你应该什么也没看到。相信我,这是一个好消息。设置**progress**为0,反过来会设置**strokeEnd**也为0,这就意味着shape layer什么也没画。 180 | 181 | 182 | 唯一剩下要做的就是你的指示器在image下载回调方法中更新**progress**。 183 | 184 | 回到**CustomImageView.swift**文件和用以下代码来代替注释**Update progress here**: 185 | 186 | ``` 187 | self!.progressIndicatorView.progress = CGFloat(receivedSize)/CGFloat(expectedSize) 188 | ``` 189 | 190 | 这主要通过**receivedSize**除以**expectedSize**来计算进度。 191 | 192 | > 注意:你会注意到block使用weak self引用 - 这样能够避免retain cycle。 193 | 194 | 编译和运行你的工程;你会看到进度指示器像这样开始移动: 195 | 196 | ![](http://cdn2.raywenderlich.com/wp-content/uploads/2015/02/indicator.gif) 197 | 198 | 即使你自己没有添加任何动画代码,**CALayer**在layer轻松地发现任何animatable属性和当属性改变时平滑地animate。 199 | 200 | 上面已经完成第一个阶段。现在进入第二和最后阶段。 201 | 202 | 203 | #创建Reveal动画 204 | 205 | reveal阶段在window显示image然后逐渐扩展圆形环的形状。如果你已经读过[前面教程](http://www.raywenderlich.com/86521/how-to-make-a-view-controller-transition-animation-like-in-the-ping-app),那个教程主要讲创建一个Ping风格的view controller动画,你就会知道这是一个很好的关于**CALayer**的**mask**属性的使用案例。 206 | 207 | 添加以下方法到**CircularLoaderView.swift**文件: 208 | 209 | ``` 210 | func reveal() { 211 | 212 | // 1 213 | backgroundColor = UIColor.clearColor() 214 | progress = 1 215 | // 2 216 | circlePathLayer.removeAnimationForKey("strokeEnd") 217 | // 3 218 | circlePathLayer.removeFromSuperlayer() 219 | superview?.layer.mask = circlePathLayer 220 | } 221 | ``` 222 | 223 | 这是一个很重要的方法需要理解,让我们逐段看一遍: 224 | 225 | 1. 设置view的背景色为clear,那么在view后面的image不再隐藏,然后设置**progress**为1或100%。 226 | 227 | 2. 使用**strokeEnd**属性来移除任何待定的implicit animations,否则干扰reveal animation。关于implicit animations的更多信息,请查看[iOS Animations by Tutorials](http://www.raywenderlich.com/store/ios-animations-by-tutorials). 228 | 229 | 3. 从它的**superLayer**移除**circlePathLayer**,然后赋值给**superView**的layer maks,借助circular mask “hole”,image是可见的。这样让你复用已存在的layer和避免重复代码。 230 | 231 | 现在你需要在某个地方调用**reveal()**。在**CustomImageView.swift**文件用以下代码替换**Reveal image here**注释: 232 | 233 | ``` 234 | self!.progressIndicatorView.reveal() 235 | ``` 236 | 237 | 编译和运行你的app;一旦image开始下载,你会看见一部分小的ring在显示。 238 | 239 | ![](http://cdn3.raywenderlich.com/wp-content/uploads/2015/02/Screenshot-2015-01-26-02.49.54.png) 240 | 241 | 你能在背景看到你的image - 但几乎什么也没有! 242 | 243 | ##扩展环 244 | 245 | 你的下一步就是在内外扩展这个环。你可以两个分离的、同轴心的**UIBezierPath**来做到,但你也可以一个更加有效的方法,只是使用一个Bezier path来完成。 246 | 247 | 怎样做呢?你只是增加圆的半径(**path**属性)来向外扩展,同时增加line的宽度(**lineWidth**属性)来使环更加厚和向内扩展。最终,两个值都增长到足够时就在下面显示整个image。 248 | 249 | 回到**CircularLoaderView.swift**文件和添加以下代码到reveal()方法的最后: 250 | 251 | ``` 252 | // 1 253 | let center = CGPoint(x: CGRectGetMidX(bounds), y: CGRectGetMidY(bounds)) 254 | let finalRadius = sqrt((center.x*center.x) + (center.y*center.y)) 255 | let radiusInset = finalRadius - circleRadius 256 | let outerRect = CGRectInset(circleFrame(), -radiusInset, -radiusInset) 257 | let toPath = UIBezierPath(ovalInRect: outerRect).CGPath 258 | 259 | // 2 260 | let fromPath = circlePathLayer.path 261 | let fromLineWidth = circlePathLayer.lineWidth 262 | 263 | // 3 264 | CATransaction.begin() 265 | CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions) 266 | circlePathLayer.lineWidth = 2*finalRadius 267 | circlePathLayer.path = toPath 268 | CATransaction.commit() 269 | 270 | // 4 271 | let lineWidthAnimation = CABasicAnimation(keyPath: "lineWidth") 272 | lineWidthAnimation.fromValue = fromLineWidth 273 | lineWidthAnimation.toValue = 2*finalRadius 274 | let pathAnimation = CABasicAnimation(keyPath: "path") 275 | pathAnimation.fromValue = fromPath 276 | pathAnimation.toValue = toPath 277 | 278 | // 5 279 | let groupAnimation = CAAnimationGroup() 280 | groupAnimation.duration = 1 281 | groupAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) 282 | groupAnimation.animations = [pathAnimation, lineWidthAnimation] 283 | groupAnimation.delegate = self 284 | circlePathLayer.addAnimation(groupAnimation, forKey: "strokeWidth") 285 | ``` 286 | 287 | 现在逐段解释以上代码是究竟做了什么: 288 | 289 | 1. 确定圆形的半径之后就能完全限制image view。然后计算**CGRect**来完全限制这个圆形。**toPath**表示**CAShapeLayer** mask的最终形状。 290 | 291 | 2. 设置**lineWidth**和**path**初始值来匹配当前layer的值。 292 | 293 | 3. 设置**lineWidth**和**path**的最终值;这样能防止它们当动画完成时跳回它们的原始值。**CATransaction**设置**kCATransactionDisableActions**键对应的值为**true**来禁用layer的implicit animations。 294 | 295 | 4. 创建一个两个**CABasicAnimation**的实例,一个是路径动画,一个是**lineWidth**动画,**lineWidth**必须增加到两倍跟半径增长速度一样快,这样圆形向内扩展与向外扩展一样。 296 | 297 | 5. 将两个animations添加到一个**CAAnimationGroup**,然后添加animation group到layer。将**self**赋值给delegate,等下你会使用到它。 298 | 299 | 编译和运行你的工程;你会看到一旦image完成下载,reveal animation就会弹出来。但即使reveal animation完成,部分圆形还是会保持在屏幕上。 300 | 301 | ![](http://cdn4.raywenderlich.com/wp-content/uploads/2015/02/StillVisible.png) 302 | 303 | 为了修复这种情况,添加以下实现**animationDidStop(_:finished:)** 到 **CircularLoaderView.swift**: 304 | 305 | ``` 306 | override func animationDidStop(anim: CAAnimation!, finished flag: Bool) { 307 | superview?.layer.mask = nil 308 | } 309 | ``` 310 | 311 | 这些代码从super layer上移除mask,这会完全地移除圆形。 312 | 313 | 再次编译和运行你的工程,和你会看到整个动画的效果: 314 | 315 | ![](http://cdn4.raywenderlich.com/wp-content/uploads/2015/02/indicator-final-final.gif) 316 | 317 | 恭喜你,你已经完成创建圆形图像加载动画! 318 | 319 | 320 | #下一步 321 | 322 | 你可以[在这里下载整个工程](http://cdn2.raywenderlich.com/wp-content/uploads/2015/02/ImageLoaderIndicator-Final.zip)。 323 | 324 | 基于本教程,你可以进一步来微调动画的时间、曲线和颜色来满足你的需求和个人设计美学。一个可能需要改进就是设置shape layer的**lineCap**属性值为**kCALineCapRound**来四舍五入圆形进度指示器的尾部。你自己思考还有什么可以改进的地方。 325 | 326 | 如果你喜欢这个教程和愿意学习怎样创建更多像这样的动画,请查看Marin Todorov的书[iOS Animations by Tutorials](http://www.raywenderlich.com/store/ios-animations-by-tutorials)。它是从基本的动画开始,然后逐步讲解layer animations, animating constraints, view controller transitions和更多 327 | 328 | 如果你有什么关于这个教程的问题或评论,请在下面参与讨论。我很乐意看到你在你的App中添加这么酷的动画。 -------------------------------------------------------------------------------- /issue-9/Swift 2 有哪些新特性.md: -------------------------------------------------------------------------------- 1 | Swift 2 有哪些新特性 2 | --- 3 | 4 | > * 原文链接 : [What’s New in Swift 2](http://www.raywenderlich.com/108522/whats-new-in-swift-2) 5 | * 原文作者 : [Greg Heo](http://www.raywenderlich.com/u/gregheo) 6 | * 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn) 7 | * 译者 : [Sam Lau](https://github.com/samlaudev) 8 | * 校对者: [Mr.Simple](https://github.com/bboyfeiyu) 9 | * 状态 : 校正完 10 | 11 | 在WWDC我们发现Swift团队没有浪费时间在无谓的地方,而是致力于改善Swift 2。 12 | 13 | 我们将会为你编写和录制很多关于Swift 2的教程,但在此期间我想强调Swift最令人兴奋的改变,为你可以在秋天迁移到Swift 2做准备。 14 | 15 | ![](http://cdn2.raywenderlich.com/wp-content/uploads/2015/06/swift-new.jpg) 16 | 17 | 18 | #错误处理 19 | 20 | 正如Ray在[WWDC 2015 Initial Impressions](http://www.raywenderlich.com/108379/wwdc-2015-initial-impressions)文章中提及,错误处理已经在Swift 2改进了。我们已经迁移到新的系统就像异常处理,而不是**NSError**对象和双指针。 21 | 22 | 你可能对以下代码比较熟悉: 23 | 24 | ``` 25 | if drinkWithError(nil) { 26 | print("Could not drink beer! :[") 27 | return 28 | } 29 | ``` 30 | 31 | 一般在Cocoa,你传入一个**NSError**对象的引用(一个**inout**参数在Swift),然后方法会赋值给错误变量。但问题是你可以传入一个nil到这里来完全忽略这个错误;或者,你可以传入**NSError**但从不检查它。 32 | 33 | Swift 2 为错误检查添加额外保护层。你可以使用**throws**关键字来指定那个函数和方法能够抛出一个错误。然后当你调用某样东西时,可以用**do**, **try**和**catch**多个关键字来捕捉和处理错误。 34 | 35 | ``` 36 | // 1 37 | enum DrinkError: ErrorType { 38 | case NoBeerRemainingError 39 | } 40 | 41 | // 2 42 | func drinkWithError() throws { 43 | if beer.isAvailable() { 44 | // party! 45 | } else { 46 | // 3 47 | throw DrinkError.NoBeerRemainingError 48 | } 49 | } 50 | 51 | func tryToDrink() { 52 | // 4 53 | do { 54 | try drinkWithError() 55 | } catch { 56 | print("Could not drink beer! :[") 57 | return 58 | } 59 | } 60 | 61 | ``` 62 | 63 | 这里有几样东西需要强调的: 64 | 65 | 1. 为了创建一个错误可以抛出,只是创建一个继承**ErrorType**的**enum**。 66 | 2. 你需要使用**throws**关键字来标志任何函数可以抛出一个错误。 67 | 3. 这里抛出一个错误,它将会在section 4中被捕捉。 68 | 4. 你在一个**do**块中包含任何可以抛出一个错误的代码,而不是其他语言类似的**try**块中。然后,你添加一个**try**关键字到函数被调用的前面,而且这个函数能够抛出一个错误。 69 | 70 | 新语法是非常简洁和易读。任何API当前使用**NSError**以后都会使用这种错误处理方式。 71 | 72 | ![](http://cdn4.raywenderlich.com/wp-content/uploads/2015/06/throw-all-the-things-415x320.jpg) 73 | 74 | 75 | #绑定 76 | 77 | 在Swift 1.2,我们失去了金字塔的厄运和能够在一行代码测试多个绑定的optionals: 78 | 79 | ``` 80 | if let pants = pants, frog = frog { 81 | // good stuff here! 82 | } 83 | ``` 84 | 85 | 这样勉强能够工作,但对于有些人需要缩进那个嵌套着很多optionals才能访问的值的“首选”的代码路径是一个问题。这意味着你需要深入查看缩进主线部分的代码块,而错误条件却在外面。 86 | 87 | 如果有些方式来检查一些没有值的optionals,然后早点退出。这正是Swift 2提供的guard语句: 88 | 89 | ``` 90 | guard let pants = pants, frog = frog else { 91 | // sorry, no frog pants here :[ 92 | return 93 | } 94 | 95 | // at this point, frog and pants are both unwrapped and bound! 96 | ``` 97 | 98 | 使用**guard**意味着你可以执行optional binding (或其他操作)和如果条件失败就提供一个代码块在**else**运行。然后,你可以继续执行。在这种情况下,optionals **frog**和**pants**在作用域内被unwrap。 99 | 100 | 使用**guard**指定某种你希望得到状态而不是检查错误情况之后,使代码更加简洁。 101 | 102 | > **注意:** 如果你仍然不明白为什么使用**guard**语句比**if-else**语句更加有用,请查看Swift团队[Eric Cerney‘s post](http://www.raywenderlich.com/u/ecerney)在[Swift guard statement](http://ericcerney.com/swift-guard-statement/)。 103 | 104 | 105 | #协议扩展 106 | 107 | 面向对象编程?函数式编程?Swift其实还是一种面向协议的编程语言! 108 | 109 | 在Swift 1,协议就像接口一样可以指定一些属性和方法,然后类,结构体或枚举会遵循它。 110 | 111 | 现在在Swift 2,你可以扩展协议和给属性和方法添加默认实现。你之前已经可以在类或结构体添加新的方法到**String**或**Array**,但现在你可以添加这些到协议,这让你更加广泛地应用。 112 | 113 | ``` 114 | extension CustomStringConvertible { 115 | var shoutyDescription: String { 116 | return "\(self.description.uppercaseString)!!!" 117 | } 118 | } 119 | 120 | let greetings = ["Hello", "Hi", "Yo yo yo"] 121 | 122 | // prints ["Hello", "Hi", "Yo yo yo"] 123 | print("\(greetings.description)") 124 | 125 | // prints [HELLO, HI, YO YO YO]!!! 126 | print("\(greetings.shoutyDescription)") 127 | 128 | ``` 129 | 130 | 注意**Printable**协议现在被命名为**CustomStringConvertible**,而大多数的Foundation对象都遵循__Printable__协议。有了协议扩展之,你可以用自定义功能来扩展系统。相比于向很多类、结构体和枚举添加少量的自定义代码,你可以编写一个通用实现,然后应用到不同的数据类型。 131 | 132 | Swift团队已经忙着做这个了 - 如果你在Swift已经使用__map__或__filter__,你可能也认为以方法的方式比全局函数来使用它们更好。多亏了强大的协议扩展,已经有一些新的方法添加到集合类型,例如:__map__,__filter__,__indexOf__和更多! 133 | 134 | ``` 135 | et numbers = [1, 5, 6, 10, 16, 42, 45] 136 | 137 | // Swift 1 138 | find(filter(map(numbers, { $0 * 2}), { $0 % 3 == 0 }), 90) 139 | 140 | // Swift 2 141 | numbers.map { $0 * 2 }.filter { $0 % 3 == 0 }.indexOf(90) // returns 2 142 | ``` 143 | 144 | 多亏了协议一致性,你的Swift 2代码会变得更加简洁和易读。在Swift 1版本,你需要查看调用函数内部来理解它的工作原理;在Swfit 2版本,函数链会变得清晰。 145 | 146 | 如果你打算使用面向协议编程 - 请查看[WWDC session on this topic](https://developer.apple.com/videos/wwdc/2015/?id=408)和留意这个网站的教程和文章。 147 | 148 | 149 | #汇总 150 | 151 | 在WWDC大会中发布很多东西,所以我想强调几样重要的东西: 152 | 153 | * __Objective-C 泛型__ - Apple已经开始标注所有的Objective-C代码以便Swift类型能够获取正确类型的optional。使用Objective-C泛型也能正常工作,这样给Swift开发者更好的类型提示。如果你希望出现一些__UITouch__对象或字符串数组,那就会出现你想要的而不是一些__AnyObjects__。 154 | 155 | * __重命名 语法__ - __println__已经离开我们一年了;现在它是普通旧的__print__,现在它有第二个参数的默认值设置为__true__来决定是否换行。__do__关键字主要用来错误处理,do-while循环现在是使用__repeat-while__。类似地,有很多协议名都改变了,例如:__Printable__改为__CustomStringConvertible__。 156 | 157 | * __Migrator__ - 有很多小的语法改变,你怎样使得你代码变得最新?Swift 1-to-2 migrator会将代码变成最新的标准和改变语法。这个migrator智能到能够更新你的代码使用新的错误处理,和更新块注释到新的格式风格! 158 | 159 | * __开源!__ - 对码农有一个重大消息就是在秋天发布Swift 2的时候,Swift将会开源!这意味着不仅可以使用它来iOS开发,更重要的是学习它的源代码。不仅如此,这将是很好的机会来深入源代码,甚至为项目贡献代码,然后在swift编译器提交历史上留下你的名字。 160 | 161 | 162 | #下一步 163 | 164 | 这只是所有发布特性中的一些简单示例;想了解更多,请查看[WWDC session videos](https://developer.apple.com/videos/wwdc/2015/)和已更新的[Swift Programming Language book](https://itunes.apple.com/us/book/swift-programming-language/id1002622538?mt=11) 165 | 166 | 如果还有一些人记得在Swift第一个beta版和发布的1.0之间有很多改变,那么将来肯定会有更多地特性出现。我们团队将会持续关注所有的更新,深入挖掘令人兴奋的改变,所以请密切留意教程,书籍和视频。 167 | 168 | Swift 2哪部分最令你兴奋?哪部分你想我们第一时间报道?在下面评论让我们知道! 169 | -------------------------------------------------------------------------------- /issue-9/Swift-EventKit的初学者指南--请求权限.md: -------------------------------------------------------------------------------- 1 | # Swift EventKit的初学者指南--请求权限 2 | --- 3 | 4 | > * 原文链接 : [Beginner’s Guide to EventKit in Swift – Requesting Permission](http://www.andrewcbancroft.com/2015/05/14/beginners-guide-to-eventkit-in-swift-requesting-permission/) 5 | * 原文作者 : [Andrew](http://www.andrewcbancroft.com/) 6 | * [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn) 7 | * 译者 : [MollyMmm](https://github.com/MollyMmm) 8 | * 校对者: [Mr.Simple](https://github.com/bboyfeiyu) 9 | * 状态 : 校对完成 10 | 11 | EventKit为获取和操作用户日历事件和提醒提供了一系列的类.在下面的教程中,我的目标是带领你走出利用EventKit建立一个应用程序的第.我的目标是带领你迈出利用EventKit建立一个应用程序的第一步.我将演示如何向用户的日历请求许可,我还将展示几个处理用户响应的例子(当他们授予访问权限,或者拒绝). 12 | #Example scenario 13 | #场景 14 | 15 | 让我们先提出一个基本方案,作为本教程的例子。 16 | 17 | 假设我们正在构建一个应用程序,现在,有一个单一的视图控制器。在得到用户授权允许的情况下,我们希望这个视图控制器显示日历列表。如果用户拒绝访问,我们将向用户展示一个消息,用来说明我们的应用程序在没有访问权限时不能运行,我们也将允许他们通过单击一个按钮能够在他们的设备的设置中设置授权访问. 18 | 19 | 我已经创建一个那样的应用程序作为例子--跳到GitHub中查看并研究这个例子的代码. 20 | 21 | >资源 22 | 23 | > [Xcode工程示例](https://github.com/andrewcbancroft/EventTracker/tree/ask-for-permission) 24 | 25 | #Storyboard setup 26 | #故事面板设置 27 | 28 | 你使用EventKit的第一步就是需要为自己创建一个用户界面来处理当你第一个程序启动时用户对"该程序可以访问你的日历吗?"对出不同的响应,不久,我们将得到如何请求这个许可的详情.但首先,让我们来剖析我们如何用对于一个许可操作导致的给定响应能够做正确的操作的一些视图来安排我们的故事板. 29 | 30 | 用户可以授予访问权限,也可以拒绝授予访问权限来通知他们的日历或者提醒.我们需要为这两种情况做好准备. 31 | 32 | #当被授予访问权限时,tableview显示日历列表 33 | 34 | 我今天持乐观态度,所以让我们开始处理从一开始用户就授予我们访问他们日历的权限的情况. 35 | 36 | 当用户授予我们访问权限,我们想列出一个表视图的日历.在接下来的教程中,我们将担心数据源的设置.现在,我们将从实用工具栏中拽一个表格视图过来. 37 | 38 | 为了得到填满整个屏幕的表视图,我做了几件事情.通常,当你从实用工具栏中拽一个表视图过来的时候,它会在故事板中填满整个场景.在布局中我向下拖顶部边缘知道它"捕捉"到我所期望的状态栏底部被定位的那行.然后,我设置了以下限制: 39 | 40 | >* Center X 41 | * Center Y 42 | * Equal width to Superview 43 | * Top space to Top Layout Guide for height. 44 | 45 | 我已经创建了一个设置表视图的简短截屏,如果你想要一个完整的练习,可以参看下面链接的内容: 46 | 47 | >Resources 48 | 资源 49 | 50 | > [Screencast: Setting Up a Table View](http://bit.ly/WatchSetUpTVFromACB) 51 | 52 | >[ Full transcript](http://www.andrewcbancroft.com/2015/05/18/swift-how-to-setting-up-a-table-view/) 53 | 54 | 这里有这些约束的详细视图,以及故事板看起来像装表视图的视觉效果. 55 | 56 | ![Detailed view of constraints](http://ww1.sinaimg.cn/mw690/a10328aajw1eshuj6c424j20850ciwfn.jpg) 57 | 58 | ![Table view in Storyboard](http://ww2.sinaimg.cn/mw690/a10328aajw1eshuj777a3j212u0oxguc.jpg) 59 | 60 | 最后一点,在故事板中我已经将这个表视图的hidden属性设置为true.根据用户允许或者拒绝对日历的访问后,我将切换表的可见性,但我认为值得指出的是在我例子中表视图的初始状态是被隐藏. 61 | 62 | 63 | #访问被拒绝时的"需要许可"视图 64 | 65 | 但有时,用户拒绝授权访问日历,在意识到这样做将导致基本上停止你应用程序所有的功能之前,如果你的整个应用程序或者只是该应用程序的一部分需要访问功能,你需要一种方法来告知用户,并为他们提供一种方法跳到用户设置,如果可能的话让用户手动授予访问权限. 66 | 67 | 我在示例项目中的方法是在故事板场景中组织一个新的视图,该视图包含一个展示操作说明的标签和一个点击后使用户进入我们应用程序的设置界面的按钮. 68 | 69 | 再次,一些约束涉及到在运行时使一些事物正确的显示.在这里我不会讲述这个细节,因为它很可能因为每一个执行操作而有一点不同. 70 | 71 | 我将指出的意见事情是,这个视图的透明度已经被设置为0,以便如果用户拒绝授权,我能够展示一个逐渐消失的效果.下面就来看看在设置了隐藏“NeedPermissionsView”的场景: 72 | 73 | ![Need permission view](http://ww3.sinaimg.cn/mw690/a10328aajw1eshuj6ra19j214p0ou12b.jpg) 74 | 75 | # Event Store的角色 76 | 77 | EventKit的核心是EKEventStore.EKEventStore是事物的中心.创建EKEventStore的一个实例,为开发人员提供了对用户的日历和提醒列表中执行各种读/写操作的API. 78 | 79 | 一个与日历交互的视图控制器应该有一个引用EKEventStore的实例.这很容易被创建--这里是一个例子: 80 | 81 | >ViewController.swift 82 | 83 | >``` 84 | class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 85 | let eventStore = EKEventStore() 86 | // ... 87 | } 88 | ``` 89 | 90 | #检查日历的授权 91 | 92 | 一旦我们有了引用EKEventStore的实例,我们可以做像检查用户是否授权访问他们的日历这样的事情.根据这里,我们可以做是否需要请求许可的决定,随后确定要显示的视图(表视图或者需要许可视图). 93 | 94 | 我们在哪里检查日历授权很重要.我的建议是每次在视图出现时检查(即在viewWillAppear()中),因为用户首次授予访问权限,切换设置,拒绝访问的情况是完全有可能的.我们的应用程序需要做出适当的响应. 95 | 96 | In the example project provided with this article, I’ve created a function named checkCalendarAuthorizationStatus(). Here a peek at what it does: 97 | 98 | 在这个文章提供的示例工程中,我已经创建了一个名为checkCalendarAuthorizationStatus()的函数. 99 | 接下来看看它的实现: 100 | 101 | >ViewController.swift 102 | >``` 103 | class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 104 | // ... 105 | override func viewWillAppear(animated: Bool) { 106 | checkCalendarAuthorizationStatus() 107 | } 108 | func checkCalendarAuthorizationStatus() { 109 | let status = EKEventStore.authorizationStatusForEntityType(EKEntityTypeEvent) 110 | switch (status) { 111 | case EKAuthorizationStatus.NotDetermined: 112 | // This happens on first-run 113 | requestAccessToCalendar() 114 | case EKAuthorizationStatus.Authorized: 115 | // Things are in line with being able to show the calendars in the table view 116 | loadCalendars() 117 | refreshTableView() 118 | case EKAuthorizationStatus.Restricted, EKAuthorizationStatus.Denied: 119 | // We need to help them give us permission 120 | needPermissionView.fadeIn() 121 | default: 122 | let alert = UIAlertView(title: "Privacy Warning", message: "You have not granted permission for this app to access your Calendar", delegate: nil, cancelButtonTitle: "OK") 123 | alert.show() 124 | } 125 | } 126 | // ... 127 | } 128 | ``` 129 | 130 | 这里关键的功能是EKEventStore的 authorizationStatusForEntityType实现的.传入的EKEntityTypeEvent用于跟用户日历进行交互.如果我们想要检查他们的提醒的权限,我们将在这里使用EKEntityTypeReminder. 131 | 132 | EKAuthorizationStatus的可能值根据switch里的相应的case来执行封装好的方便阅读的独立功能的逻辑代码. 133 | 134 | 让我们一步步来看一看这些功能. 135 | 136 | #请求访问日历 137 | 138 | 正如标题所说的,所有的事情从这里开始.每当我们的应用程序加载和调用authorizationStatusForEntityType的时候,将返回NotDetermined的状态.就是在这一点上我们想请求访问日历. 139 | 140 | 为了这样做,按照下面的方法定义requestAccessToCalendar函数: 141 | 142 | >requestAccessToCalendar() 143 | >``` 144 | class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 145 | // ... 146 | func requestAccessToCalendar() { 147 | eventStore.requestAccessToEntityType(EKEntityTypeEvent, completion: { 148 | (accessGranted: Bool, error: NSError!) in 149 | if accessGranted == true { 150 | // Ensure that UI refreshes happen back on the main thread! 151 | dispatch_async(dispatch_get_main_queue(), { 152 | self.loadCalendars() 153 | self.refreshTableView() 154 | }) 155 | } else { 156 | // Ensure that UI refreshes happen back on the main thread! 157 | dispatch_async(dispatch_get_main_queue(), { 158 | self.needPermissionView.fadeIn() 159 | }) 160 | } 161 | }) 162 | } 163 | // ... 164 | } 165 | ``` 166 | 167 | 我们的EKEventStore实例提供了一个名为requestAccessToEntityType的函数.再次将EKEntityTypeEvent作为我们请求访问日历的参数传递.剩余的有趣的部分在我们提供的封装完的闭包里能够找到. 168 | 169 | 在实现里有三个主要的事情需要注意: 170 | 171 | 1.传递到闭包里的两个参数一个是用来说明访问权限是否被授予的Bool类型的,另一个是NSError. 172 | 173 | 2.我们需要调用dispatch_async(),并表明我们要调回主队列中执行刷新UI的操作. 174 | 175 | 3.self.needPermissionView.fadeIn()作为我操作中的一个UIView的拓展,[Swift中渐入/淡出动画的拓展类(Fade In / Out Animations as Class Extensions in Swift)](https://github.com/andrewcbancroft/EventTracker/tree/ask-for-permission). 176 | 177 | #授予访问权限!加载日历和刷新表视图 178 | 179 | 当被允许访问的时候,我们可以调用eventStore实例中的calendarsForEntityType函数,并传递EKEntityTypeEvent去抓取用户日历的数组在我们的表视图中显示.下面就来看看: 180 | 181 | >loadCalendars() 182 | >``` 183 | class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 184 | // ... 185 | var calendars: [EKCalendar]? 186 | // ... 187 | func loadCalendars() { 188 | self.calendars = eventStore.calendarsForEntityType(EKEntityTypeEvent) as? [EKCalendar] 189 | } 190 | func refreshTableView() { 191 | calendarsTableView.hidden = false 192 | calendarsTableView.reloadData() 193 | } 194 | // ... 195 | } 196 | ``` 197 | 198 | #拒绝访问--显示需要许可视图 199 | 200 | 当访问被拒绝的时候,我们需要弹出在故事板场景中创建的“Needs Permission View”. 201 | 202 | 在这个视图中,上面的函数重新被调用,这样有一个能够让用户直接跳转到我们应用程序的设置页面中,以便他们能够从那里授权日历访问.这个按钮连线到了一个IBAction.下面有实现IBAction的例子: 203 | 204 | >goToSettingsButtonTapped() 205 | >``` 206 | class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 207 | // ... 208 | @IBAction func goToSettingsButtonTapped(sender: UIButton) { 209 | let openSettingsUrl = NSURL(string: UIApplicationOpenSettingsURLString) 210 | UIApplication.sharedApplication().openURL(openSettingsUrl!) 211 | } 212 | // ... 213 | } 214 | ``` 215 | 216 | #结束语 217 | 218 | 这几乎完成了使用 Event Kit的开始工作!对于checkCalendarAuthorizationStatus()函数的其余案例简单的重用,我只是简单的剖析了请求允许的过程. 219 | 220 | 我鼓励你们[跳到Github](https://github.com/andrewcbancroft/EventTracker/tree/ask-for-permission),并且作为你应用程序中利用Event Kit的开始,自己深入研究这些代码. -------------------------------------------------------------------------------- /issue-9/Swift的异步机制-Future.md: -------------------------------------------------------------------------------- 1 | # Swift的异步机制-Future 2 | --- 3 | 4 | > * 原文链接 : [Back to the Future](https://realm.io/news/swift-summit-javier-soto-futures/) 5 | * 原文作者 : [Javier Soto](https://twitter.com/Javi) 6 | * [译文出自 : 开发技术前线 www.devtf.cn](http://www.devtf.cn) 7 | * 译者 : [sdq](http://shidanqing.net) 8 | 9 | 我们在使用Objective-C写异步代码时常常会出现许多问题。它没有很好的错误处理机制,当你发现你需要在你的异步函数中创建新的异步函数它也没有一个很好的可伸缩实现。在这次演讲中,Javier Soto将演示实现一个能够简化异步API的Swift Future类型。并且创建一个Resule类型,我们可以在这个类型上调用map和andThen等操作。对于Swift来说,Futures的未来可能很会基于信号和eactiveCocoa 3。 10 | 11 | 你可以在[这里](https://github.com/JaviSoto/Talks#swift-summit-2015-back-to-the-futures)看到这次演讲中的代码。 12 | 13 | ## 更优雅地使用Swift 14 | 15 | 谢谢各位,很高兴和大家一起来聊一聊Swift,感谢组织者把我们聚集在一起并让我在这里演讲。我的名字叫Javi,是来自Twitter的iOS工程师。 16 | 17 | 相比于Objective-C,我们在Swift里可以把很多事做得更好。我们已经在之前的演讲中看了不少例子,我想紧接着着他们,和大家谈论一下一种我想引入Swift的设计模式。这种设计模式会使我们的代码更为优雅。 18 | 19 | ##如今的异步API 20 | 21 | 今天我不会讲一些很空理论,而是通过实践和大家一起讨论。接下来会看到,在传统情况下我们是如何使用异步API的,并且在使用过程中存在的一些问题,尤其是对错误的处理。然后我会介绍一个简单的Future API,我们会看到它使如何提升我们的异步代码的。 22 | 23 | 让我们看看如今的问题在哪里? 24 | 25 | ```swift 26 | struct User { let avatarURL: NSURL } 27 | func requestUserInfo(userID: String, completion: (User?, NSError?) -> ()) 28 | func downloadImage(URL: NSURL, completion: (UIImage?, NSError?) -> ()) 29 | 30 | func loadAvatar(userID: String, completion: (UIImage?, NSError?) -> ()) { 31 | requestUserInfo(userID) { user, error in 32 | if let user = user { 33 | downloadImage(user.avatarURL) { avatar, error in 34 | if let avatar = avatar { completion(avatar, nil) } 35 | else { completion(nil, error!) } 36 | } 37 | } else { completion(nil, error!) } 38 | } 39 | } 40 | ``` 41 | 42 | 这是我们参照Objective-C异步API的方法写的Swift代码,其中包含大量的嵌套回调函数。我们可以看到loadAvatar方法,该方法的输入为用户ID,函数体完成对该用户头像的下载。其中,complete block需要传入一个image及一个error,但其实这个方法没有任何逻辑可言。方法本身不知道该如何进一步处理,其内部包含了一些异步方法来完成整个操作,包括采用requestUserInfo方法异步获取用户信息,以及一个下载图片的方法downloadImage。每一次我们都需要检查错误,执行完以后再调用completion block。 43 | 44 | 你们中的大部分可能已经习惯了这样的代码,但这样的模式非常不容易扩展。即使你没有使用过PHP,你也肯定看到过这样的场景(演讲者展示了一张异步代码层层嵌套的图)。所以让我们避免这样的情况发生吧,我们可以做的更好。此外,这段代码还有一些其它特殊的问题。其中的一个异步API将会查看参数类型。completion block需要传入的一个元祖或者两个数值;它们都是optional型的,因为下载失败的话image会返回nil,而下载成功没有错误的话error会返回nil。 45 | 46 | 这些问题会让你不爽,但你要知道问题其实一直存在,只是你从没有发现而已。在Objective-C中它们比较隐晦,因为任何类型都可以产生nil值。 47 | 48 | 如果我们看一下所有可能发生的情况,我们会发现总共有两种:一种是图片下载成功没有error,另一种是图片下载失败发生error。有趣的是其实还存在其余两种可能的情况,但完全没有任何意义。我的app该如何处理即有图片又有错误的状况呢?又或者当既没有图片又没有错误时,会发生什么? 49 | 50 | ##错误处理的重要性 51 | 52 | 我常常喜欢说我认为的计算机是不会犯错的完美机器,每当我们遇到了bug,那都是因为攻城狮所希望的与实际要求计算机去做的产生了冲突。如果我们没有一个很好地方式来表达如何能完成我们的设想,那很可能导致我们对计算机下达了错误的命令。 53 | 54 | 我认为错误处理非常重要,我们需要用合适的方式在应用程序中处理错误。上述这类API会令你在很多情况下用错,举个例子,你可能会首先检查error而不是先查看数值,虽然已经执行成功了,但你仍然会担心产生错误。 55 | 56 | ##我讨厌NSError 57 | 58 | 然后是NSError,我非常讨厌NSError,但我不能责怪Apple,因为老实说这的确是Objective-C中针对error最好的做法了。让我们再看一个例子看一下为什么NSError容易产生问题。 59 | 60 | ```swift 61 | var error: NSError? 62 | let string = NSString(contentsOfFile:path encoding:NSUTF8Encoding error:&error) 63 | 64 | if string == nil { 65 | // Oops: 66 | if error.code == NSFileReadNoSuchFileError { 67 | // ... 68 | } 69 | else if ... 70 | } 71 | ``` 72 | 73 | 这段代码有错误,但是并不是非常明显。我们调用了Foundation API,如果查看文档,会发现我们完全不知道返回的是什么类型的错误,既没有提到领域,也没有提及错误代码。这段代码首先查看错误代码而不是先查看领域,这是没有任何意义的,因为在完全不同的领域可能产生相同的错误代码,非常容易误导开发者。另一种情况是,我们可以传nil给error,告诉Foundation“我不在乎error”,但是我认为鲁棒性强的软件应该认真处理错误。 74 | 75 | 我们每天都会遇到这样令人抓狂的情况,但我们无法责备开发者。如果我们的工具不能让这一切变得简单、让错误处理更方便,那我们只会变得更懒,我们将会向方法里传nil值,然后忽略这些error。我们不会去了解那些错误是什么,而是让它们显示在控制台然后继续。 76 | 77 | ##一个提议 78 | 79 | 这里我有一个提议:我们可以用自己定义的类型来封装我们API中可能产生的错误,而不是使用NSError。我们可以写一个protocol包含所有可能发生的错误类型。 80 | 81 | ```swift 82 | protocol ErrorType { } 83 | 84 | enum UserInfoErrorDomain: ErrorType { 85 | case UserDoesNotExist 86 | case UserRequestFailure(reason: String) 87 | case NetworkRequestFailure(reason: String) 88 | } 89 | 90 | extension NSError: ErrorType { } 91 | ``` 92 | 93 | 举个例子,如果我们有一个API,我们可以声明一个遵循此protocol的枚举类型,包含了三种清楚的错误情况。这里甚至可以提供原因的信息,之后可以看到我们如何进行使用。如果确实想用NSError,我们也可以将其包含在内。 94 | 95 | 另一件看似矛盾但是却很酷的事情是,我们甚至可以创建一个NoError类型。 96 | 97 | ```swift 98 | enum NoError: ErrorType { } 99 | 100 | let error = NoError(?) 101 | ``` 102 | 103 | 104 | 很奇怪是吧?由于它是空的枚举,我们实际上不能创建这种类型,因为它不存在任何构造函数。虽然没有意义,但是我们知道null类型是可以被构造的。如果API返回的error值是NOError类型,那API就会报错,我们不再需要通过单元测试进行检验,编译器就会替我们发现。 105 | 106 | ##采用Future建立Result类型 107 | 108 | 如Brian在之前的演讲里所叙述的那样,这里我们会在Result类型的基础上展开一些工作,Result无论成功或者失败都可以产生一个值。在这页slide里我做了点手脚,因为由于编译器的限制,目前这个代码在Swift里是无法实现的,你必须用class的形式来想办法实现,但我还是先用简单的形式在这里展示以免影响理解。 109 | 110 | ```swift 111 | enum Result { 112 | case Success(T) 113 | case Error(E) 114 | } 115 | ``` 116 | 117 | 现在让我们来看一下Future,我们如何来实现呢?希望通过研究其实现的过程,我们能够更加理解其价值,这会很有趣!那什么是Future呢?你可能在其它语言或者框架里听到过它的另外一个名字——Promise。它们的概念是完全相同的。 118 | 119 | ```swift 120 | struct Future { 121 | typealias ResultType = Result 122 | typealias Completion = ResultType -> () 123 | typealias AsyncOperation = Completion -> () 124 | 125 | private let operation: AsyncOperation 126 | } 127 | 128 | ``` 129 | 130 | Future包含了不一样的处理方式,可以对一段时间后才能获取数据先进行抽象的处理,比如网络延迟的情况。或许有人会说 “那是不是像闭包?”或者 “这不就是回调函数!” 不过Future的优势在于,我们可以像已经获取了数据那样进行操作,并且不用关心那些无关的异步操作,使我们的代码变得更简洁。 131 | 132 | ##实施Future 133 | 134 | 让我们开始实践吧。我们创建一个struct,类似Result它具有T和E的泛型,因为我们需要使用Result。我定义了一些别名用来帮助我们处理这些类型。我们之前看到过Result,同时需要一个可以触发的completion用于当你向Future要求,“现在给我值吧!”。你将会进到completion block里,它会获取到result并且不会返回任何东西。异步合并是一个闭包,当需要一个函数返回值的时候被调用。这时候completion块被调用,并且告知其函数工作的完成,并且不会返回任何值。Future封装了一些操作处理,我们可以在实例化中直接传入这些操作符。最重要的共有API是start方法,API的使用者可以通过此方法来告诉Future,“Ok,你现在可以去获取数据了,获取完之后交给我”。但是到现在为止用处不大,除了使用Result的方法不同外,它看起来完全和completion block一样。 135 | 136 | ```swift 137 | struct Future { 138 | init(operation: AsyncOperation) { 139 | self.operation = operation 140 | } 141 | 142 | func start(completion: Completion) { 143 | self.operation() { result in 144 | completion(result) 145 | } 146 | } 147 | } 148 | ``` 149 | 150 | ###map & andThen 151 | 152 | Future真正酷的是在我们实现一些操作的时候。我想要实现map和andThen,让我们先从map开始吧。我们可以用map方法实现的一些功能。比如我们有了用户的ID,然后我们想取得用户头像的链接地址。我们创建一个方法来完成这个功能,另外我们还有一个已经创建出的方法用以下载用户信息并给我们一个基本的用户信息的结构体,也就是URL。如果我们想不用一堆回调来将其中一个转换为另一个,那可以先map,然后通过返回user内部的其它数据进行转换。 153 | 154 | ```swift 155 | struct User { let avatarURL: NSURL } 156 | 157 | func requestUserInfo(userID: String) -> Future 158 | 159 | 160 | func requestUserAvatarURL(userID: String) -> Future { 161 | return requestUserInfo(userID) 162 | .map { $0.avatarURL } 163 | } 164 | ``` 165 | 166 | 这就是函数签名的样子。比较一下它与map的函数签名你会发现很有意思,举个例子,两种类型都是出自swift标准库,Array和Optional。如果我们看一下类型,它们是完全一样的。他们会用函数来将获取的T类型值生成U类型的结果,其中T是容器内部的类型,而U将是新容器的类型。 167 | 168 | ```swift 169 | struct Array { 170 | func map(f: T -> U) -> [U] 171 | } 172 | 173 | enum Optional { 174 | func map(f: T -> U) -> U? 175 | } 176 | 177 | struct Future { 178 | func map(f: T -> U) -> Future 179 | } 180 | ``` 181 | 182 | ##实现map 183 | 184 | 那么让我们实现它吧。我们需要创建一个新的Future,因为它必须是U型的。我们用下列的方法来验证Future模式的机制。就像我们看到的,方法传给Future一个运算操作。为了转变数值,我们首先需要获得数值,所以我们调用开始方法从而得到结果。该结果可以是成功或失败,因此我们需要根据其情况处理。 185 | 186 | ```swift 187 | func map(f: T -> U) -> Future { 188 | return Future(operation: { completion in 189 | // Retrieve the value from self... 190 | self.start { result in 191 | 192 | } 193 | } 194 | } 195 | ``` 196 | 197 | 首先让我们看一下成功的情况。成功的情况下,如同类型签名那样,告诉我们需要通过F函数转化得到想要的数值 - 我们将再次在成功条件下运行它,然后会调用completion block。错误的情况下就更简单了。我们没有value,我们不能进行转换,只需要调用completion block。这就和幻灯片里显示的一样。 198 | 199 | 在错误情况下发生的短路情况和Brain之前展示的面向铁路对象编程概念基本一样。但是现在我们想要用最初Future给我们的value做一些事,但我们希望获取一个新的value。获取该value不像调用头像URL一样简单。获取该value会导致一些延迟,比如当我们需要另一个网络请求。 200 | 201 | ##声明andThen 202 | 203 | 这是行不通的,因为我们需要通过一个函数来返回另一个Future。因此,我们想要再Future API上声明一个Future和一个andThen函数。我们会这样使用,例如:如果我们有一个像刚才看到的请求头像URL,可以在之前的操作后紧接着下载图片,返回头像图片的Future。我们只需要调用andThen函数并传其它的函数。 204 | 205 | ```swift 206 | func requestUserAvatarURL(userID: String) -> Future 207 | 208 | func downloadImage(URL: NSURL) -> Future 209 | 210 | 211 | func downloadUserAvatar(userID: String) -> Future { 212 | return requestUserAvatarURL(userID) 213 | .andThen(downloadImage) 214 | } 215 | ``` 216 | 217 | 这就是签名的样子,类似map,就像我说的重要的区别是,F函数不返回U,它返回的是一个新的Future。实现它非常非常相似。我们重新建立一个Future,初始化并取得value,然后选择Result。在成功的情况下,当我们在value上调用F,我们没有可以返回的value。现在我们有了另一个Future,我们需要启动它,当我们完成得到最后的Result。 218 | 219 | 在错误的情况下,我们需要对其短路。如果我们没有一个value,就不能转换它,以便继续做更多的操作。我们需要马上调用completion block。这就是整个的实现过程,你会意识到它看起来很像map。事实上,有趣的是你可以在其他情况下实现它,但我选择了单独实现来看看他们会做些什么。 220 | 221 | 让我们来看看已经完成的工作。我们从嵌套混乱复杂的回调函数以及每次单独调用都会涉及错误处理的情形,转变为通过一个声明方法来告诉我们需要获取头像:我们需要请求用户信息,将它map到头像的URL,然后下载图像。这不是关于代码的能力,它是关于我们在代码中要表达我们的意图的能力。不过其最大的优势在于,代码中不再有错误处理的内容,所有的error都会在运行中自动被识别。 222 | 223 | ```swift 224 | func andThen(f: T -> Future) -> Future { 225 | return Future(operation: { completion in 226 | self.start { firstFutureResult in 227 | switch firstFutureResult { 228 | case .Success(let value): // ... 229 | case .Error(let error): // ... 230 | } 231 | } 232 | } 233 | } 234 | 235 | ``` 236 | 237 | ##Future的局限性 238 | 239 | 我希望你们会和我一样喜欢Future,Future也的确和我想的一样具有实用价值。但是,了解Future的缺陷也同样重要。文中的例子在我们的移动应用中很常见,会存在网络请求,会存在异步请求等等,但是问题是我们不仅仅只处理异步事件;另外,当返回值不只有一个值时Future就会失效,Future只能处理一个值,而当我们处理用户交互的时候,像手势识别,往往会存在多个值的情况。 240 | 241 | 另一个问题是我们在上文处理的情景并不是用户驱动的,就像交互时你不会告诉app “我现在准备触碰手机了”。 这些行为完全是用户决定的,程序无法预测。 242 | 243 | ##Signals 244 | 245 | 好消息是Future正在变得更好。Signals将会解决以上遇到的问题。Signals是一个非常有效的工具。 246 | 247 | 另一件令人兴奋得事情是我们不必自己去实现它。你可能已经听过了ReactiveCocoa可以用来实现signals并允许你的app使用这些概念。ReactiveCocoa 3是用swift写的,可以在Github上找到。 248 | -------------------------------------------------------------------------------- /issue-9/在Swift开发中通过UINavigationController来访问Sub-Controllers.md: -------------------------------------------------------------------------------- 1 | 在Swift开发中通过UINavigationController来访问Sub-Controllers 2 | === 3 | 4 | > * 原文链接 : [Access Sub-Controllers from a UINavigationController in Swift](http://www.andrewcbancroft.com/2015/06/02/access-sub-controllers-from-a-uinavigationcontroller-in-swift/) 5 | * 原文作者 : [Andrew Bancroft](http://www.andrewcbancroft.com) 6 | * 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn) 7 | * 译者 : [samw00](http://www.andrewcbancroft.com/2015/06/02/access-sub-controllers-from-a-uinavigationcontroller-in-swift/) 8 | * 校对者: [mrchenhao](https://github.com/mrchenhao) 9 | * 状态 : 完成 10 | 11 | 通过`AppDelegate`或者通过`prepareForSegue(_:sender:)`来访问`UINavigationController`的第一个子元素的访问顺序总是让我有点纳闷。这篇博文中有几段代码可以帮助你和我迅速的弄清楚如何在我们的Swift应用程序开发中和navigation controllers打交道。 12 | 13 | ##AppDelegate 14 | 15 | 每一个iOS应用在其启动加载完成后都会显示一个根视图控制器。假设我们要搭建一个以导航控制器(navigation controller)为主的一个app...,也就是说我们这个app的第一(根)视图控制器是一个UINavigationController。在我们的Storyboard中,我们创建了一个简单的场景,给一个视图控制器添加了些UI控件和一些属性,然后我们将这个视图控制器嵌入在一个导航控制器中。 16 | 17 | 但是如果我想在app启动之后再去设置视图控制器中的某些个属性呢?我们要如何才能做到这点呢? 18 | 19 | 我一直把我在Storyboard中添加UI要素的第一个场景(first scene)视为“第一视图控制器”。但是,对iOS来说,导航控制器才是实际意义上的第一(根)视图控制器。 20 | 21 | 当一个app把导航控制器视为其第一(根)视图器时,我们就需要在视图层级结构中“挖”的更深一点才能访问到我们之前所以为的“第一视图控制器“。 22 | 23 | ###找寻第一视图控制器 24 | 25 | 下面的代码段展示了如果在`UINavigationController`的视图控制器层级中找到第一个控制器并给其设置一些虚构属性,这一次都发生在AppDelegate中: 26 | 27 | ```swift 28 | class AppDelegate: UIResponder, UIApplicationDelegate { 29 | var window: UIWindow? 30 | 31 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 32 | // Override point for customization after application launch. 33 | 34 | let navigationController = window?.rootViewController as! UINavigationController 35 | let firstVC = navigationController.viewControllers[0] as! NameofFirstViewController 36 | // set whatever properties you might want to set 37 | // such as an NSManagedObjectContext reference 38 | 39 | return true 40 | } 41 | 42 | // ... 43 | } 44 | ``` 45 | 46 | 整个流程是这样的: 47 | 48 | * 获取window的根视图控制器(在我们的例子中就是导航控制器) 49 | * 通过导航控制器的视图控制器数组获取其第一个子视图控制器(也就是原先我一直以为的“第一”视图控制器) 50 | * 设置任何你需要设置的属性 51 | 52 | 在上面这段代码中,你可能担心我用了隐式拆包这方法来看可选值变量是否有值。通常情况下我也是避免用这种方法的,但在这里我用这种方法的理由是我的这个app的根视图控制器就是一个`UINavigationController`,这点确保了我可以使用隐式拆包这种用法。因为不管如何,只要改变了app的导航模式都会出错。 53 | 54 | 如果上面的理由没有说服你也别担心 - 你可以把`?`换成`!`然后添加一些`if-let`语句来确保你不会碰到`nil`。举个例子: 55 | 56 | ```swift 57 | class AppDelegate: UIResponder, UIApplicationDelegate { 58 | 59 | // ... 60 | if let navigationController = window?.rootViewController as? UINavigationController { 61 | if let firstVC = navigationController.viewControllers[0] as? NameOfFirstViewController { 62 | firstVC.someproperty = someValue 63 | } 64 | } 65 | // ... 66 | } 67 | ``` 68 | 69 | ##Prepare for segue 70 | 71 | 那么`prepareForSegue(_:sender:)`又是怎么一回事?什么时候不得不走这条路呢? 72 | 73 | 假设,我们有一个应用跳转进一个导航控制器。我们可能需要给下一个视图控制器传值,但是“下一个视图控制器”严格来说其实是一个导航控制器,并不是我们声明属性的那个控制器。 74 | 75 | 和通过AppDelegate时的情形一样,我们也需要在视图层级中深挖一点来访问第一视图控制器来,这样才能传递我们的数据。下面是一个实现的例子: 76 | 77 | ```swift 78 | public class ViewController: UIViewController { 79 | 80 | // ... 81 | override public func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 82 | let destinationVC = segue.destinationViewController as! UINavigationController 83 | let nextViewController = destinationVC.viewControllers[0] as! SecondViewController 84 | 85 | nextViewController.someProperty = someValue 86 | } 87 | // ... 88 | } 89 | ``` 90 | 91 | 在`AppDelegate`例子和`prepareForSegue`的例子中唯一改变的就是我们是从哪里如何获取`UINavigationController`的。在`AppDelegate`例子中,导航控制器是来自window的根视图控制器。在`prepareForSegue`中则来自segue的目标视图控制器。 92 | 93 | 除此之外,获取导航控制器的第一个子元素的步骤都是一样的。 94 | 95 | ##总结 96 | 97 | 如何去访问一个导航控制器的视图控制器层级结构让我觉得有点含糊不清,所以我给我自己写了这篇小博文作为日后的一个参考,但是我希望你也能从这篇博文中受益。 98 | -------------------------------------------------------------------------------- /markdown简单教程.md: -------------------------------------------------------------------------------- 1 | # 标题1,文章的第一句就是标题1 2 | 3 | ## 这是标题2,其他小标题按照层级划分 4 | ### 标题3 5 | ### 标题4 6 | 7 | ## 文字加粗 8 | **hello** 9 | 10 | ## 引用 11 | 引用的格式为: >加上一个空格,然后加上文字内容。 12 | 在一些注意点时,我们可以使用引用来引起读着的注意。例如 : 13 | 14 | > 注意 : 不能在子线程中更新UI。 15 | 16 | 17 | ## 换行 18 | 19 | 句尾有四个以上的空格代表换行。示例 : 20 | 21 | **四个空格换行(注意句尾的空格)** 22 | 这里是一行,重新换行。 23 | 新的一行。 24 | 25 | 26 | **回车换行** 27 | 这里是一行,重新换行。 28 | 29 | 新的一行。 30 | 31 | ## 超链接 32 | **格式1 :** 33 | [链接名](地址) 34 | 35 | 示例 : 36 | [www.baidu.com](www.baidu.com) 37 | 38 | **格式2** 39 | [链接名][唯一标识] 40 | [唯一标识]: url地址 41 | 42 | 这种格式一般用在某段话中链接比较多的情况,为了保持原文简洁,将链接单独提出来。 43 | 44 | 示例 : 45 | [开发技术前线][devtf] 46 | 47 | [devtf]: http://www.devtf.cn 48 | 49 | ## 代码 50 | 51 | ```java 52 | public class Name { 53 | 54 | } 55 | ``` 56 | 57 | 适用于Android代码。需要注意的是开始处的```上面必须要有一个空行。 58 | 59 | 使用于iOS方面的代码 : 60 | 61 | ``` 62 | let customPresentAnimationController = CustomPresentAnimationController() 63 | 64 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 65 | 66 | if segue.identifier == "showAction" { 67 | let toViewController = segue.destinationViewController as UIViewController 68 | toViewController.transitioningDelegate = self 69 | } 70 | } 71 | ``` 72 | 73 | ## 显示图片 74 | 格式 : 75 | ![图片名字](图片链接) 76 | 77 | 示例 : 78 | ![p1](http://img.blog.csdn.net/20150416165133627) 79 | 80 | 81 | ## 列表 82 | 列表前面也是需要一个空行,并且在标识符后面需要一个空格,比如: 1. 和* 。 83 | 84 | 这是有序列表 : 85 | 86 | 1. 第一 87 | 2. 第二 88 | 3. 第三 89 | 90 | 无序列表 : 91 | 92 | * 翻译正确 93 | * 中文流畅 94 | * 高质量 95 | 96 | 另一种列表 : 97 | 98 | - 翻译项目 99 | - 认真校对撒 100 | - 发布文章 101 | 102 | ## 表格 103 | 格式为 : 104 | 105 | | 这是表头1 | 表头2 | 106 | |:-------------|:-------------:| 107 | | 第一行第一列,左对齐| 第一行第二列居中 | 108 | | 第二行第一列,左对齐| 第二行第二列,居中 | 109 | 110 | 111 | Markdown更详细的教程请查看[Markdown简明教程](http://wowubuntu.com/markdown/)。 -------------------------------------------------------------------------------- /template.md: -------------------------------------------------------------------------------- 1 | 这里写中文标题 2 | --- 3 | 4 | > * 原文链接 : [原文标题](原文url) 5 | * 原文作者 : [作者](作者的博客或者其他社交页面) 6 | * 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn) 7 | * 译者 : [这里写你的github用户名](github链接) 8 | * 校对者: [这里校对者的github用户名](github链接) 9 | * 状态 : 未完成 / 校对中 / 完成 10 | 11 | **注意 : 翻译完之后请认真的审核一遍有没有错字、语句通不通顺,谢谢~** 12 | 13 | 14 | `这里是翻译原文,注意翻译时英文和译文都要留在该文档中,并且是一段英文原文下面直接跟着写译文,便于校对。如下示例 : ` 15 | 16 | Over the last months and after having friendly discussions at Tuenti with colleagues like @pedro_g_s and @flipper83 (by the way 2 badass of android development), I have decided that was a good time to write an article about architecting android applications. 17 | The purpose of it is to show you a little approach I had in mind in the last few months plus all the stuff I have learnt from investigating and implementing it. 18 | 19 | 过去几个月以来,通过在Tuenti网站上与@pedro_g_s和@flipper83(安卓开发两位大牛)进行友好讨论之后,我决定写这篇关于架构安卓应用的文章。 20 | 21 | 我写这篇文章的目的是想把我在过去几个月体悟到的小方法以及在调查和应用中学到的有用的东西分享给大家。 22 | 23 | Getting Started 24 | We know that writing quality software is hard and complex: It is not only about satisfying requirements, also should be robust, maintainable, testable, and flexible enough to adapt to growth and change. This is where “the clean architecture” comes up and could be a good approach for using when developing any software application. 25 | The idea is simple: clean architecture stands for a group of practices that produce systems that are: 26 | 27 | ## 入门指南 28 | 大家都知道要写一款精品软件是有难度且很复杂的:不仅要满足特定要求,而且软件还必须具有稳健性,可维护、可测试性强,并且能够灵活适应各种发展与变化。这时候,“清晰架构”就应运而生了,这一架构在开发任何软件应用的时候用起来非常顺手。 29 | 30 | 这个思路很简单:简洁架构 意味着产品系统中遵循一系列的习惯原则: -------------------------------------------------------------------------------- /翻译项目协作流程.md: -------------------------------------------------------------------------------- 1 | Github协作流程 2 | --- 3 | 4 | 5 | ## fork一份到你的账户下 6 | 在[android-tech-frontier](https://github.com/bboyfeiyu/android-tech-frontier)项目中选择右边的"fork",然后选择你的头像,将该项目fork一份到你的账户下。 7 | 8 | ![fork](http://img.blog.csdn.net/20150323230517092) 9 | 10 | ## clone你fork的仓库到本地 11 | 输入如下命令clone一份你fork的仓库 (相当于拷贝了一份到你的账户下),例如 : 12 | ![clone](http://img.blog.csdn.net/20150323230449588) 13 | 14 | > git clone git@github.com:bboyfeiyu/android-tech-frontier.git 15 | 16 | 等待clone完毕。此时,在你的当前目录下会有android-tech-frontier目录。进入该android-tech-frontier,在相应的目录下按照[readme.md](readme.md)中的命名规则创建你的翻译项目,然后完成翻译。 17 | 18 | 19 | 20 | ## 认领翻译任务 21 | 你可以在主仓库下看到一些翻译任务的状态: 22 | 23 | * 待认领 : 表示还没有人接这个翻译任务; 24 | * 翻译中 : 表明已经有人翻译了; 25 | * 翻译完成 : 表明已经翻译完成; 26 | * 寻找校对中 : 表明此时文章翻译完成,正在等待校对; 27 | * 校对中 : 表明正在校对; 28 | * 校对完成 : 表明校对完成,等待发布。 29 | 30 | 当你看到某个状态为"待认领"的issue时,你可以撸起袖子在该issue下面添加评论,将你的github账户评论到下面,然后管理员会将该issue设置为"翻译中"。当你看到“寻找校对中”的任务,则说明该任务已经翻译完,但是需要校对,你也可以在该issue中将你的github账户添加到评论中,然后将该任务设置为“校对中”。 31 | 校对完毕,管理员确认没啥问题之后会发布该文章,发布后会将该issue关闭。 32 | 33 | ![is](http://img.blog.csdn.net/20150323231540412) 34 | 35 | 36 | ## 推荐文章 37 | 如果你看到好的文章,也可以在仓库的issue中添加一个,将推荐理由、原文链接填上。如图 : 38 | 39 | ![submit](http://img.blog.csdn.net/20150323231923706) 40 | 41 | ## 提交翻译内容到你的仓库中 42 | 在android-tech-frontier下依次输入如下三条命令提交你修改的内容到你的仓库。 43 | 44 | ``` 45 | git add . 46 | git commit -m "我翻译了xxx项目" 47 | git push origin master 48 | ``` 49 | 50 | ## 将你的更新提交到主仓库 51 | 经过上面push命令之后,你的内容已经更新到你自己fork的仓库,此时你需要把你的更新提交到主仓库。因此你需要发一个pull request。在你fork的项目的右边点击"pull request",然后选择"new pull request"。注意是从你的仓库master分支(左边)到我的仓库的master分支(右边)。然后选择"create new pull request",最后填写相关的内容后确认即可。 52 | 53 | ![pr](http://img.blog.csdn.net/20150323230526312) 54 | 55 | 提交了pull request之后,管理员会找相关人员进行校对。校对完成之后管理员会将你的文章发布。 56 | 57 | --------------------------------------------------------------------------------