├── .idea ├── .gitignore ├── learning-notes.iml ├── misc.xml ├── modules.xml └── vcs.xml ├── LICENSE ├── README.md ├── apex └── code-style.md ├── c++ ├── .DS_Store ├── C++面向对象高级编程(侯捷).md └── c++primer │ ├── 01 - 第一章-开始.md │ ├── 02 - 第二章-变量和基本类型.md │ ├── 03 - 第三章-字符串、向量和数组.md │ ├── 04 - 第四章-表达式.md │ ├── 05 - 第五章-语句.md │ ├── 06 - 第六章-函数.md │ └── 07 - 第七章-类.md ├── corepack └── index.md ├── css └── note.md ├── data-structure-and-algorithm ├── 01 - 数组和字典.md ├── 02 - 链表.md ├── 03 - 栈 (Stack).md ├── 04 - 队列 (Queue).md ├── 05 - 树 (Tree).md ├── 06 - 二叉树 (Binary Tree).md ├── 07 - 二叉搜索树 (Binary Search Tree).md ├── 08 - 平衡二叉搜索树 (AVL Tree).md ├── 09 - 数据结构 堆 (Heap).md ├── 10 - 优先队列 (Priority Queue).md ├── 11 - 二分查找 (Binary Search).md ├── 12 - 时间复杂度为O(n^2)的排序算法.md ├── 13 - 归并排序 (Merge Sort).md ├── 14 - 快速排序 (Quicksort).md ├── 15 - 图表 (Graph).md ├── 16 - 广度优先搜索和深度优先搜索.md └── 17 - 迪克斯特拉 (Dijkstra) 算法.md ├── design-pattern ├── 01 - 什么是设计模式.md ├── 02 - MVC模式 (Model-View-Controller Pattern).md ├── 03 - 代理模式 (Delegation Pattern).md ├── 04 - 策略模式 (Strategy Pattern).md ├── 05 - 单例模式 (Singleton Pattern).md ├── 06 - 备忘录模式 (Memento Pattern).md ├── 07 - 观察者模式 (Observer Pattern).md ├── 08 - 建造者模式 (Builder Pattern).md ├── 09 - MVVM模式 (Model-View-ViewModel Pattern).md ├── 10 - 工厂模式 (Factory Pattern).md ├── 11 - 适配器模式 (Adapter Pattern).md ├── 12 - 迭代器模式 (Iterator Pattern).md ├── 13 - 原型模式 (Prototype Pattern).md ├── 14 - 状态模式 (State Pattern).md ├── 15 - 多播委托模式 (Multicast Delegate Pattern).md ├── 16 - 门面模式 (Facade Pattern).md ├── 17 - 享元模式 (Flyweight Pattern).md ├── 18 - 中介者模式 (Mediator Pattern).md ├── 19 - 组合模式 (Composite Pattern).md ├── 20 - 命令模式 (Command Pattern).md └── 21 - 责任链模式 (Chain-of- Responsibility Pattern).md ├── docs └── alamofire5 │ ├── 01 - Alamofire 5.0 迁移指南.md │ ├── 02 - Alamofire 5 的使用 - 基本用法.md │ └── 03 - Alamofire 5 的使用 - 高级用法.md ├── fastlane ├── 01 - Fastlane 介绍、安装和初始化.md ├── 02 - 创建 app.md ├── 03 - lane.md ├── 04 - 证书管理.md ├── 05 - Provisioning.md ├── 06 - Appfile 的使用.md ├── 07 - 团队代码签名.md ├── 08 - 打包和分发.md ├── 09 - 上线前检查.md ├── 10 - 截图.md ├── 11 - 截图边框.md ├── 12 - 提交审核.md ├── 13 - 集成 git.md ├── 14 - 单元测试.md ├── 15 - 代码格式检查.md ├── 16 - 生成文档.md └── images │ ├── 234944ED-FE2A-4BA5-9454-381F0F8C134A.png │ ├── 4DB46B5B-65DF-4C00-8599-241DA5B57231.png │ ├── 88BE07FE-43E8-4FCF-BA8F-A2FED58275C9.png │ ├── C1A0500C-9862-429E-8EEF-D992B8D73731.png │ └── C48EBFF5-AF06-42FD-8161-EFDDD0437951.png ├── flutter └── firebase │ └── setup-firebase-with-multiple-flavors.md ├── git ├── 01 - git.md └── connecting-to-github-with-ssh.md ├── ios ├── combine │ ├── 01 - Combine 基础.md │ ├── 02 - Publishers 和 Subscribers.md │ ├── 03 - transform 操作符.md │ ├── 04 - Filter 操作符.md │ ├── 05 - Combine 操作符.md │ ├── 06 - 时间管理操作符.md │ ├── 07 - Sequence 操作符.md │ ├── 08 - 网络请求.md │ ├── 09 - Timers.md │ ├── 09 - 调试.md │ ├── 10 - Key-Value Observing.md │ ├── 11 - 资源管理.md │ ├── 12 - 错误管理.md │ ├── 13 - Scheduler (调度器).md │ └── 14 - 自定义 Publisher 和背压 (Backpressure) 处理.md ├── swiftui │ └── wwdc2019 │ │ ├── 01 - 关于 SwiftUI 的思考.md │ │ ├── 02 - SwiftUI 开发必须要了解的知识.md │ │ ├── 03 - SwiftUI 的数据流.md │ │ └── 04 - SwiftUI的 View 如何布局.md └── tdd │ ├── 01 - 什么是 TDD.md │ ├── 02 - TDD 循环.md │ └── 其他注意事项.md ├── nvm └── note.md ├── react └── test │ └── notes.md ├── ruby └── metaprogramming-ruby-2 │ ├── 01 - The M Word.md │ ├── 02 - The Object Model.md │ ├── 03 - Methods.md │ ├── 04 - Blocks.md │ ├── 05 - Class Definitions.md │ └── images │ ├── 01663F00-5296-4E99-877A-A4508549A2AB.png │ ├── 4B275B77-64F9-40EB-BB68-A0B28B8903A5.png │ ├── 5F1CFC9E-F655-4942-AF93-7CA1A00C9E97.png │ ├── 6E390D6F-8F67-434D-8825-D3EDBEC90876.png │ ├── 74A8E7E8-7540-4ECE-AE17-7EF11EFF506B.png │ ├── 844F1F50-F1D3-425B-88D5-2E097CD02766.png │ └── B5D6EE57-BCFC-45A6-B81C-2F5EDAB8983E.png ├── source-code-analysis ├── alamofire4 │ ├── 01 - 前言.md │ ├── 02 - AFError & Notifications & DispatchQueue+Alamofire.md │ ├── 03 - Timeline.md │ ├── 04 - MultipartFormData.md │ ├── 05 - NetworkReachabilityManager.md │ ├── 06 - Alamofire.md │ ├── 07 - SessionManager.md │ ├── 08 - Request.md │ ├── 09 - ParameterEncoding.md │ ├── 10 - SessionDelegate.md │ ├── 11 - TaskDelegate.md │ ├── 12- Response.md │ ├── 13- ResponseSerialization.md │ ├── 14- Result.md │ ├── 15- ServerTrustPolicy.md │ └── 16- Validation.md └── kickstarter-ios │ ├── 01 - 项目相关.md │ ├── 02 - MVVM 架构.md │ ├── 03 - Environment 和 AppEnvironment.md │ ├── 04 - 网络请求的处理和 Deep Linking.md │ ├── 05 - UI 的管理.md │ ├── 06 - 测试.md │ └── 07 - 第三方工具.md ├── storybook └── 01-why-storybook.md ├── swift ├── modern-swift-concurrency │ ├── 01 - 为什么需要现代化的 Swift 并发 (Why Modern Swift Concurrency).md │ ├── 02 - 从 async await 开始.md │ ├── 03 - 异步序列和中间任务.md │ ├── 04 - 用 AsyncStream 自定义异步序列.md │ ├── 05 - 中级 async await 和 CheckedContinuation.md │ ├── 07 - TaskGroup 并发代码.md │ ├── 08 - Actor 基础.md │ ├── 09 - 全局 Actor.md │ ├── 10 - 分布式系统中的 Actor.md │ └── images │ │ ├── 01.png │ │ ├── 02.png │ │ ├── 03.png │ │ ├── 04.png │ │ └── 05.png ├── notes.md ├── pro-swift │ ├── 01 - 语法.md │ ├── 02 - 类型.md │ ├── 03 - 引用类型和值类型.md │ ├── 04 - 方法.md │ ├── 05 - 错误处理.md │ ├── 06 - 函数式编程.md │ ├── 07 - 设计模式.md │ └── images │ │ └── 2057254-60f42d356f865e93.jpg ├── swift-language-guide │ ├── 01 - 基础知识 (The Basics).md │ ├── 02 - 基本运算符 (Basic Operators).md │ ├── 03 - 字符串和字符 (Strings and Characters).md │ ├── 04 - 集合类型 (Collection Types).md │ ├── 05 - 控制流 (Control Flow).md │ ├── 06 - 方法 (Functions).md │ ├── 07 - 闭包 (Closures).md │ ├── 08 - 枚举 (Enumerations).md │ ├── 09 - 类和结构 (Classes and Structures).md │ ├── 10 - 属性 (Properties).md │ ├── 11 - 方法 (Functions).md │ ├── 12 - 下标 (Subscripts).md │ ├── 13 - 继承 (Inheritance).md │ ├── 14 - 初始化 (Initialization).md │ ├── 15 - 反初始化 (Deinitialization).md │ ├── 16 - 自动引用计数 (Automatic Reference Counting).md │ ├── 17 - 可选链 (Optional Chaining).md │ ├── 18 - 错误处理 (Error Handling).md │ ├── 19 - 类型转换 (Type Casting).md │ ├── 20 - 嵌套类型 (Nested Types).md │ ├── 21 - 扩展 (Extensions).md │ ├── 22 - 协议 (Protocols).md │ ├── 23 - 泛型 (Generics).md │ └── 24 -访问权限 (Access Control).md └── swift-new-features │ ├── 01 - Swift 4 新增内容.md │ ├── 02 - Swift 5.0 新特性.md │ └── 03 - Swift 5.1 新特性.md └── typescript ├── 01 - 入门.md ├── 02 - 基础.md ├── 03 - TypeScript 编译器.md ├── 04 - Class & Interface.md ├── 05 - 高级类型.md ├── 06 - 泛型.md ├── 07 - 装饰器.md └── 08 - 在 TypeScript 中使用 JavaScript 库.md /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/learning-notes.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Lebron 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # learning-notes 2 | 3 | ## 目录 4 | 5 | - **[c++](/c++)**: C++ 学习笔记 6 | - **[data-structure-and-algorithm](/data-structure-and-algorithm)**: [Data Structures & Algorithms in Swift](https://store.raywenderlich.com/products/data-structures-and-algorithms-in-swift) 的学习笔记 7 | - **[design-pattern](/design-pattern)**: [Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials) 的学习笔记 8 | - **doc** 9 | - **[alamofire5](/docs/alamofire5)**: Alamofire 5 的相关文档,翻译至 [Alamofire](https://github.com/Alamofire/Alamofire) 的 readme 10 | - **[fastlane](/fastlane)**: [Fastlane for iOS](https://www.raywenderlich.com/1259223-fastlane-for-ios) 的学习笔记 11 | - **[git](/git)**: 自己总结的常用 git 命令 12 | - **iOS** 13 | - **[combine](/ios/combine)**: [Combine: Asynchronous Programming with Swift](https://store.raywenderlich.com/products/combine-asynchronous-programming-with-swift) 的学习笔记 14 | - **swiftui** 15 | - **[wwdc2019](/ios/swiftui/wwdc2019)**: WWDC2019 中 SwiftUI 相关的视频学习笔记 16 | - **[tdd](/ios/tdd)**: [iOS Test-Driven Development by Tutorials](https://store.raywenderlich.com/products/ios-test-driven-development) 的学习笔记 17 | - **ruby** 18 | - **[metaprogramming-ruby-2](/ruby/metaprogramming-ruby-2)**: 《Ruby元编程 (第2版) 》的学习笔记 19 | - **source-code-analysis** 20 | - **[alamofire4](/source-code-analysis/alamofire4)**: Alamofire 4 的源码分析 21 | - **[kickstarter-ios](/source-code-analysis/kickstarter-ios)**: [kickstarter-ios](https://github.com/kickstarter/ios-oss) 的源码分析 22 | - **swift** 23 | - **[modern-swift-concurrency](/swift/modern-swift-concurrency)**: [Modern Concurrency in Swift](https://www.raywenderlich.com/books/modern-concurrency-in-swift) 的学习笔记 24 | - **[pro-swift](/swift/pro-swift)**: [Pro Swift](https://gumroad.com/l/proswift) 的学习笔记 25 | - **[swift-language-guide](/swift/swift-language-guide)**: [Swift 官方文档](https://docs.swift.org/swift-book/LanguageGuide/TheBasics.html) 的学习笔记 26 | - **[swift-new-features](/swift/swift-new-features)**: Swift 新特性总结 27 | - **[typescript](/typescript)**: [Understanding TypeScript - 2020 Edition](https://www.udemy.com/course/understanding-typescript/) 的学习笔记 -------------------------------------------------------------------------------- /apex/code-style.md: -------------------------------------------------------------------------------- 1 | # Code Style 2 | 3 | 1. Class name should match the file name. 4 | 2. Class name should be in `PascalCase`. -------------------------------------------------------------------------------- /c++/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/c++/.DS_Store -------------------------------------------------------------------------------- /c++/C++面向对象高级编程(侯捷).md: -------------------------------------------------------------------------------- 1 | ## 头文件与类的声明 2 | 3 | 1. 防卫式声明 4 | ```c++ 5 | // complex.h 6 | #ifndef __COMPLEX__ 7 | #define __COMPLEX__ 8 | 9 | // ... 10 | 11 | #endif 12 | ``` 13 | 14 | ## 构造函数 15 | 16 | 1. 构造函数可以重载 17 | 18 | 2. 构造函数写法: 19 | ```c++ 20 | // class complex 21 | double re, im; 22 | 23 | // 好的写法 24 | complex(double r = 0, double i = 0) 25 | : re(r), im(i) // 设置初始值,效率高 26 | { } 27 | 28 | // 不好的写法 29 | complex(double r = 0, double i = 0) { 30 | // 重新赋值 31 | re = r; 32 | im = i; 33 | } 34 | ``` 35 | 36 | 37 | 3. inline 函数 38 | 39 | 函数若在 class body 内定义完成,则自动成为 inline 候选人 40 | 41 | 4. 访问级别 42 | 43 | `public` 外界可以访问;`private` 只有类自己才能访问 -------------------------------------------------------------------------------- /c++/c++primer/01 - 第一章-开始.md: -------------------------------------------------------------------------------- 1 | 1. 打印语句最后应该加上 `std::endl`: 2 | 3 | ```c++ 4 | std::cout << "Enter two numbers: " << std::endl; 5 | ``` 6 | 7 | `endl` 被称为操纵符的特殊值。它的效果是结束当前行,并将与设备关联的缓冲区中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出都真正写入到输出流中,而不是仅停留在内存中等待写入流。 -------------------------------------------------------------------------------- /corepack/index.md: -------------------------------------------------------------------------------- 1 | # Corepack 2 | 3 | [Corepack](https://nodejs.org/api/corepack.html) 是一个实验性工具,用于帮助管理您的包管理器的版本。它为每个支持的包管理器(Yarn:yarn/yarnpkg, pnpm:pnpm/pnpx)暴露了二进制代理,当被调用时,将识别当前项目配置的任何包管理器,如果需要,将其下载,并最终运行它。 4 | 5 | 此功能简化了两个核心工作流程: 6 | - 它简化了新贡献者的入职流程,因为他们不再需要遵循特定于系统的安装过程,只需使用您希望他们使用的包管理器。 7 | - 它确保您团队中的每个人都将使用您希望他们使用的确切包管理器版本,而无需每次更新时手动同步。 8 | 9 | ## Workflows 10 | 11 | ### Enable the feature 12 | 13 | 由于其实验性状态,Corepack 目前需要明确启用才能生效。要做到这一点,请运行 `corepack enable`,这将会在您的环境中设置与 node 二进制文件相邻的符号链接(并在必要时覆盖现有的符号链接)。 14 | 15 | ### Configuring a package 16 | 17 | Corepack 代理将会在您当前的目录层次结构中找到最近的 `package.json` 文件,以提取其 `packageManager` 属性。 18 | 19 | 如果该值对应于受支持的包管理器,Corepack 将确保所有对相关二进制文件的调用都针对请求的版本运行,如有需要,将按需下载,并在无法成功检索时中止。 20 | 21 | 指定包管理器: 22 | 23 | ```bash 24 | corepack use pnpm@7.x # sets the latest 7.x version in the package.json 25 | corepack use yarn@* # sets the latest version in the package.json 26 | ``` 27 | 28 | ### Running npm install -g yarn doesn't work 29 | 30 | npm 在进行全局安装时会防止意外覆盖 Corepack 的二进制文件。为避免此问题,请考虑以下选项之一: 31 | 32 | - 不要运行此命令;Corepack 会提供包管理器的二进制文件,并确保所请求的版本始终可用,因此不需要显式安装包管理器。 33 | - 在 `npm install` 中添加 `--force` 标志;这将告诉 npm 可以覆盖二进制文件,但您将在此过程中删除 Corepack 的二进制文件。(运行 `corepack enable` 以将它们添加回来。) 34 | 35 | ```bash 36 | corepack enable 37 | corepack yarn install 38 | ``` 39 | -------------------------------------------------------------------------------- /css/note.md: -------------------------------------------------------------------------------- 1 | # Note 2 | 3 | ## Sticky Header 4 | 5 | ```css 6 | .sticky { 7 | position: sticky; 8 | top: 0; 9 | } 10 | ``` -------------------------------------------------------------------------------- /data-structure-and-algorithm/01 - 数组和字典.md: -------------------------------------------------------------------------------- 1 | 在学习其他更复杂的数据结构之前,我们先简单看看Swift标准库自带的数组和字典。 2 | 3 | ### 数组 4 | 5 | 数组是我们在开发中经常用到的,是一个有序的集合。可以通过索引来获取其中的元素。例如: 6 | 7 | ```swift 8 | let persons = ["Lebron", "Love", "JR", "Korver"] 9 | persons[0] // "Lebron" 10 | ``` 11 | 12 | 从数组中取出元素所需时间为常量时间**O(1)**,因为数组是在一块连续的内存中保存的,索引就是他的位置,不管数组有多大,都可以直接取出来。 13 | 14 | #### 插入操作 15 | 16 | 最简单的是新的元素是在数组最后面插入;最坏的情况就是从数组的最前面插入。一般我们需要考虑最坏的情况,所以插入操作的时间是线程时间**O(n)**,`n`是元素的个数。可以想象一下我们在排队购买iPhone,有个流氓想要插到最前面,那么所有队伍中的人都后往后移。 17 | 18 | 另外一个影响插入时间的数组的容量。我们刚刚说到:数组是在一块连续的内存中保存的,当我们创建数组的时候,系统就会默认在内存中开一块连续空间来保存数组;如果后面发现这块内存不够用了,系统就必须在内存的另外一个位置,开辟一块更大的空间,然后把原有的元素复制到新的空间。所以,在开发中,如果我们知道数组需要存储多少个元素,可以调用`array.reserveCapacity(100)`告诉Swift在内存中预留出足够的空间。其实在Swift中,如果发现数组内存不够用的话,它会把数组的内存加1倍。 19 | 20 | 因为插入操作在数组中比较耗时,如果在开发中需要频繁的将新的元素插入到最前面,那么可以考虑用**链表**来代替数组。 21 | 22 | ### 字典 23 | 24 | 字典存储的是键值对,例如保存商品的库存: 25 | 26 | ```swift 27 | let inventory = ["雪碧": 10, "可乐": 10] 28 | 29 | // 插入新元素 30 | inventory["薯片": 10] 31 | ``` 32 | 33 | 字典的元素是无序的,也不能插入到指定的位置;字典中的**键**必须遵循`Hashable`。 34 | 35 | 因为是无序的,所有新元素插入到字典中所有的时间是常量时间**O(1)**。 36 | 37 | 这里简单总结了数组和字典的一些特性。我们可以利用这两个基本的数据结构来实现更复杂的数据结构。 38 | -------------------------------------------------------------------------------- /data-structure-and-algorithm/03 - 栈 (Stack).md: -------------------------------------------------------------------------------- 1 | 栈在开发中是很常见的,例如 iOS 中的 `UINavigationController` 就是通过栈数据结构来管理它的子控制器。 2 | 3 | 栈,非常简单而又很有用。当向栈中添加数据时,直接放在栈的上面;要移除数据时,直接把最上面的那个移除掉。它主要有两个操作: 4 | 5 | - **push**:在栈顶上添加元素 6 | - **pop**:把栈顶的元素移除 7 | 8 | 所以我们只能在栈的顶部对元素进行操作,在计算机的专业术语中,我们把栈的成为** LIFO** (last in first out) 的数据结构。简单地说就是后面进来的元素先出去;最先进去的最后出去。 9 | 10 | ### 实现 11 | 12 | 用数组来存储栈中的元素,把 `Stack` 定义如下: 13 | 14 | ```swift 15 | struct Stack { 16 | private var elements: [Element] = [] 17 | init() { } 18 | } 19 | 20 | extension Stack: CustomStringConvertible { 21 | var description: String { 22 | let topDivider = "====top====\n" 23 | let bottomDivider = "\n====bottom====\n" 24 | let stackElements = elements 25 | .reversed() 26 | .map { "\($0)" } 27 | .joined(separator: "\n") 28 | return topDivider + stackElements + bottomDivider 29 | } 30 | } 31 | ``` 32 | 33 | 另外还实现了 `CustomStringConvertible`。 34 | 35 | #### push 和 pop 操作 36 | 37 | ```swift 38 | extension Stack { 39 | mutating func push(_ element: Element) { 40 | elements.append(element) 41 | } 42 | 43 | @discardableResult 44 | mutating func pop() -> Element? { 45 | return elements.popLast() 46 | } 47 | } 48 | ``` 49 | 50 | `push` 的元素直接添加到数组后面,`pop` 把数组最后的元素移除。`push` 和 `pop` 操作的时间复杂度都是**O(1)**。 51 | 52 | #### 使用 53 | 54 | ```swift 55 | var stack = Stack() 56 | stack.push(1) 57 | stack.push(2) 58 | stack.push(3) 59 | stack.push(4) 60 | 61 | print(stack) 62 | 63 | stack.pop() 64 | 65 | print(stack) 66 | 67 | // 结果 68 | ====top==== 69 | 4 70 | 3 71 | 2 72 | 1 73 | ====bottom==== 74 | 75 | ====top==== 76 | 3 77 | 2 78 | 1 79 | ====bottom==== 80 | ``` 81 | 82 | #### 添加一些实用的方法 83 | 84 | ```swift 85 | // MARK: - Getters 86 | extension Stack { 87 | var top: Element? { 88 | return elements.last 89 | } 90 | 91 | var isEmpty: Bool { 92 | return elements.isEmpty 93 | } 94 | 95 | var count: Int { 96 | return elements.count 97 | } 98 | } 99 | ``` 100 | 101 | `top`:栈顶的元素,`isEmpty`:栈是否为空,`count`:栈元素个数。 102 | 103 | #### 实现 Swift 的 Collection 协议? 104 | 105 | 在上一篇的链表中,实现了 Swift 的 Collection 协议。但是对于栈来说,我们是不能去实现 Collection 协议。因为栈数据结构的很大一个特性就是不能让我们可以通过很多方法去轻易访问里面的元素,如果实现 Collection 协议,就会违反这个规则了。 106 | 107 | #### 总结 108 | 109 | 栈的实现相对来说比较简单,栈的元素是先进后出的。 110 | 111 | ### [完整代码 >>](https://github.com/Lebron1992/swift-algorithm-demo/blob/master/swift-algorithm/Stack/Stack.swift) 112 | 113 | ### 参考资料 114 | 115 | > [Data Structures and Algorithms in Swift](https://store.raywenderlich.com/products/data-structures-and-algorithms-in-swift) --- [raywenderlich.com](https://www.raywenderlich.com/),如果想看原版书籍,请点击链接购买。 -------------------------------------------------------------------------------- /data-structure-and-algorithm/04 - 队列 (Queue).md: -------------------------------------------------------------------------------- 1 | 队列在生活中非常常见。排队等位吃饭、在火车站买票、通过高速路口等,这些生活中的现象很好的描述了队列的特点:先进先出 (FIFO, first in first out),排在最前面的先出来,后面来的只能排在最后面。 2 | 3 | ### 实现 4 | 5 | 这里我们利用 Swift 的数组来实现。 6 | 7 | ```swift 8 | struct Queue { 9 | 10 | private var elements: [Element] = [] 11 | 12 | init() { } 13 | 14 | // MARK: - Getters 15 | 16 | var count: Int { 17 | return elements.count 18 | } 19 | 20 | var isEmpty: Bool { 21 | return elements.isEmpty 22 | } 23 | 24 | var peek: Element? { 25 | return elements.first 26 | } 27 | 28 | // MARK: - Enqueue & Dequeue 29 | 30 | mutating func enqueue(_ element: Element) { 31 | elements.append(element) 32 | } 33 | 34 | @discardableResult 35 | mutating func dequeue() -> Element? { 36 | return isEmpty ? nil : elements.removeFirst() 37 | } 38 | } 39 | 40 | extension Queue: CustomStringConvertible { 41 | var description: String { 42 | return elements.description 43 | } 44 | } 45 | ``` 46 | 47 | 用数组实现队列比较简单: 48 | 49 | - 1) 用数组存储所有队列的元素 50 | - 2) 常用的一些属性: `count`、`isEmpty`、`peek` (访问第一个元素) 51 | - 3) `enqueue(_:)`: 在队列最后添加元素 52 | - 4) `dequeue()`: 移除队列的第一个元素 53 | - 5) 遵循 `CustomStringConvertible`,直接使用`elements`的`description` 54 | 55 | #### 使用 56 | 57 | ```swift 58 | var queue = Queue() 59 | queue.enqueue(1) 60 | queue.enqueue(2) 61 | queue.enqueue(3) 62 | queue.enqueue(4) 63 | 64 | print(queue) 65 | 66 | queue.dequeue() 67 | 68 | print(queue) 69 | 70 | // 结果 71 | [1, 2, 3, 4] 72 | [2, 3, 4] 73 | ``` 74 | 75 | 上面代码先插入四个元素,然后再把第一个移除。 76 | 77 | #### 性能分析 78 | 79 | |方法名 | 方法概述 | 时间复杂度 | 80 | | -----------------|:---------------------------|:-------------:| 81 | | `enqueue(_:)` | 在队列最后添加元素 | O(1) | 82 | | `dequeue()` | 移除队列的第一个元素 | O(n) | 83 | 84 | - 入队的时间复杂度为`O(1)`,如果队列比较大,可能造成刚开始开辟的内存不够用,需要重新开辟内存空间。重新开辟空间的时间复杂度为`O(n)`,因为要把元素复制到新的内存。但是 Swift 在每次增加内存空间时,都会增加到原来的两倍,重新开辟内存操作的次数一般比较少,所以我们还是认为入队的时间复杂度为`O(1)`。 85 | - 而出队为`O(n)`,因为在内存中,移除数组的第一个元素之后,后面的元素要往前移动(就像排队付款,前面的付完款之后,后面的往前走),所以造成时间复杂度为`O(n)`。 86 | 87 | 88 | #### 总结 89 | 90 | 用数组来实现队列是非常简单的,但是`dequeue()`的时间复杂度是`O(n)`,有没有办法可以让解决这个缺点呢?有,我们可以使用双向链表来实现队列,但是双向链表的节点要记录前后节点的地址,所以占用的内存比较大。所以在实际使用中,要根据情况来选择。 91 | 92 | ### [完整代码 >>](https://github.com/Lebron1992/swift-algorithm-demo/blob/master/swift-algorithm/Queue/Queue.swift) 93 | 94 | ### 参考资料 95 | 96 | > [Data Structures and Algorithms in Swift](https://store.raywenderlich.com/products/data-structures-and-algorithms-in-swift) --- [raywenderlich.com](https://www.raywenderlich.com/),如果想看原版书籍,请点击链接购买。 97 | -------------------------------------------------------------------------------- /data-structure-and-algorithm/06 - 二叉树 (Binary Tree).md: -------------------------------------------------------------------------------- 1 | 二叉树中的每个节点最多只有两个子节点,左子节点和右子节点: 2 | 3 | ![](http://upload-images.jianshu.io/upload_images/2057254-f73dcf3a9248fc05.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | ### 实现 6 | 7 | 根据上面描述的二叉树的特点,创建`BinaryTreeNode`: 8 | 9 | ```swift 10 | final class BinaryTreeNode { 11 | var value: T 12 | var leftChild: BinaryTreeNode? 13 | var rightChild: BinaryTreeNode? 14 | 15 | init(_ value: T) { 16 | self.value = value 17 | } 18 | } 19 | ``` 20 | 21 | - `value`:存储当前节点的值 22 | - `leftChild`:左子节点 23 | - `leftChild`:右子节点 24 | 25 | 我们来尝试使用一下: 26 | 27 | ```swift 28 | let zero = BinaryTreeNode(0) 29 | let one = BinaryTreeNode(1) 30 | let two = BinaryTreeNode(2) 31 | let three = BinaryTreeNode(3) 32 | let four = BinaryTreeNode(4) 33 | let five = BinaryTreeNode(5) 34 | let six = BinaryTreeNode(6) 35 | 36 | three.leftChild = one 37 | three.rightChild = five 38 | 39 | one.leftChild = zero 40 | one.rightChild = two 41 | 42 | five.leftChild = four 43 | five.rightChild = six 44 | ``` 45 | 46 | 上面的代码创建了这样一个树结构: 47 | 48 | ![](http://upload-images.jianshu.io/upload_images/2057254-95e0da30975ee22f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 49 | 50 | ### 遍历算法 51 | 52 | 二叉树的遍历算法有三种:1)中序遍历;2)前序遍历;3)后序遍历。 53 | 54 | #### 中序遍历 55 | 56 | 遍历顺序:对当前节点的左子节点进行中序遍历 --> 当前节点 --> 对当前节点的右子节点进行中序遍历,简称**左根右**。 57 | 58 | 代码实现: 59 | 60 | ```swift 61 | func traverseInOrder(_ closure: (T) -> Void) { 62 | leftChild?.traverseInOrder(closure) 63 | closure(value) 64 | rightChild?.traverseInOrder(closure) 65 | } 66 | ``` 67 | 68 | 对上面创建的树形结构,用中序遍历: 69 | 70 | ```swift 71 | three.traverseInOrder { print($0) } 72 | 73 | // 结果 74 | 0 75 | 1 76 | 2 77 | 3 78 | 4 79 | 5 80 | 6 81 | ``` 82 | 83 | #### 前序遍历 84 | 85 | 遍历顺序:当前节点 --> 对当前节点的左子节点进行前序遍历 --> 对当前节点的右子节点进行前序遍历,简称**根左右**。 86 | 87 | 代码实现: 88 | 89 | ```swift 90 | func traversePreOrder(_ closure: (T) -> Void) { 91 | closure(value) 92 | leftChild?.traversePreOrder(closure) 93 | rightChild?.traversePreOrder(closure) 94 | } 95 | ``` 96 | 97 | 对上面创建的树形结构,用前序遍历: 98 | 99 | ```swift 100 | three.traversePreOrder { print($0) } 101 | 102 | // 结果 103 | 3 104 | 1 105 | 0 106 | 2 107 | 5 108 | 4 109 | 6 110 | ``` 111 | 112 | #### 后序遍历 113 | 114 | 遍历顺序:对当前节点的左子节点进行后序遍历 --> 对当前节点的右子节点进行后序遍历 --> 当前节点,简称**左右根**。 115 | 116 | 代码实现: 117 | 118 | ```swift 119 | func traversePostOrder(_ closure: (T) -> Void) { 120 | leftChild?.traversePostOrder(closure) 121 | rightChild?.traversePostOrder(closure) 122 | closure(value) 123 | } 124 | ``` 125 | 126 | 对上面创建的树形结构,用后序遍历: 127 | 128 | ```swift 129 | three.traversePostOrder { print($0) } 130 | 131 | // 结果 132 | 0 133 | 2 134 | 1 135 | 4 136 | 6 137 | 5 138 | 3 139 | ``` 140 | 141 | 如何区分这三种遍历方法?看根部遍历的顺序即可:根部在前,则为前序遍历;根部在中,则为中序遍历;根部在后,则为后序遍历。 142 | 143 | ### [完整代码 >>](https://github.com/Lebron1992/swift-algorithm-demo/blob/master/swift-algorithm/Tree/BinaryTree.swift) 144 | 145 | ### 参考资料 146 | 147 | > [Data Structures and Algorithms in Swift](https://store.raywenderlich.com/products/data-structures-and-algorithms-in-swift) --- [raywenderlich.com](https://www.raywenderlich.com/),如果想看原版书籍,请点击链接购买。 148 | -------------------------------------------------------------------------------- /data-structure-and-algorithm/10 - 优先队列 (Priority Queue).md: -------------------------------------------------------------------------------- 1 | 在[04 - 队列 (Queue)](https://github.com/Lebron1992/learning-notes/blob/master/data-structure-and-algorithm/04%20-%20%E9%98%9F%E5%88%97%20(Queue).md)文章里面,我们已经讲过队列,采用**先进先出**的规则。这边文章我们要学习的是优先队列,根据优先级的高低来决定出队的顺序。 2 | 3 | ### 实现 4 | 5 | 在上一篇文章[09 – 数据结构 堆 (Heap)](https://github.com/Lebron1992/learning-notes/blob/master/data-structure-and-algorithm/09%20-%20%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%20%E5%A0%86%20(Heap).md)中,我们了解了堆的特点,最大堆能在 `O(1)` 时间内得到最大值,最小堆能在 `O(1)` 时间内得到最小值。优先队列是根据优先级的高低来决定出队的顺序,堆的这个特点非常适合用来实现优先队列。 6 | 7 | 采用堆实现优先队列比较简单,直接给出实现代码如下: 8 | 9 | ```swift 10 | struct PriorityQueue { 11 | private var heap: Heap 12 | 13 | init(order: @escaping (Element, Element) -> Bool) { 14 | heap = Heap(order: order) 15 | } 16 | 17 | var isEmpty: Bool { 18 | return heap.isEmpty 19 | } 20 | 21 | var peek: Element? { 22 | return heap.peek 23 | } 24 | 25 | mutating func enqueue(_ element: Element) { 26 | heap.insert(element) 27 | } 28 | 29 | mutating func dequeue() -> Element? { 30 | return heap.removePeek() 31 | } 32 | } 33 | 34 | extension PriorityQueue: CustomStringConvertible { 35 | var description: String { 36 | return heap.description 37 | } 38 | } 39 | ``` 40 | 41 | 下面我们来测试一下: 42 | 43 | ```swift 44 | var priorityQueue = PriorityQueue(order: >) 45 | for i in 1...7 { 46 | priorityQueue.enqueue(i) 47 | } 48 | 49 | while !priorityQueue.isEmpty { 50 | print(String(describing: priorityQueue.dequeue())) 51 | } 52 | 53 | // 结果 54 | Optional(7) 55 | Optional(6) 56 | Optional(5) 57 | Optional(4) 58 | Optional(3) 59 | Optional(2) 60 | Optional(1) 61 | ``` 62 | 63 | 数值较大的先出列,符合我们传入的 `order` 参数。 64 | 65 | ### [完整代码 >>](https://github.com/Lebron1992/swift-algorithm-demo/blob/master/swift-algorithm/Priority%20Queue/PriorityQueue.swift) 66 | 67 | ### 参考资料 68 | 69 | > [Data Structures and Algorithms in Swift](https://store.raywenderlich.com/products/data-structures-and-algorithms-in-swift) --- [raywenderlich.com](https://www.raywenderlich.com/),如果想看原版书籍,请点击链接购买。 70 | -------------------------------------------------------------------------------- /data-structure-and-algorithm/11 - 二分查找 (Binary Search).md: -------------------------------------------------------------------------------- 1 | 二分查找是高效搜索算法中的一员,时间复杂度为`O(log n)`。使用搜索算法前,需要满足两个条件: 2 | 3 | - 集合中的元素必须可以使用索引直接访问,在 Swift 中,这个集合必须是`RandomAccessCollection`类型。 4 | - 集合中的元素必须是有序的 5 | 6 | ### 实现 7 | 8 | 假设要查找的元素为 A,集合从小到大的顺序排列,二分查找的步骤如下: 9 | 10 | - 首先找到中间元素 11 | - 拿 A 与中间元素对比,如果 A 等于中间元素,直接返回索引,否则继续下列步骤 12 | - 如果 A 小于中间元素,则用所有左边元素组成的集合进行递归;如果 A 大于中间元素,则用所有有边元素组成的集合进行递归 13 | 14 | 实现代码如下: 15 | 16 | ```swift 17 | extension RandomAccessCollection where Element: Comparable { 18 | func binarySearch(for value: Element, 19 | in range: Range? = nil) -> Index? { 20 | 21 | let range = range ?? startIndex..>](https://github.com/Lebron1992/swift-algorithm-demo/blob/master/swift-algorithm/Binary%20Search/BinarySearch.swift) 67 | 68 | ### 参考资料 69 | 70 | > [Data Structures and Algorithms in Swift](https://store.raywenderlich.com/products/data-structures-and-algorithms-in-swift) --- [raywenderlich.com](https://www.raywenderlich.com/),如果想看原版书籍,请点击链接购买。 71 | -------------------------------------------------------------------------------- /data-structure-and-algorithm/13 - 归并排序 (Merge Sort).md: -------------------------------------------------------------------------------- 1 | 归并排序算法是一个效率很高的排序算法,时间复杂度为 `O(n log n)`。它的算法思想是先分组,各小组排好序后,再把结果合并。 2 | 3 | ### 原理解析 4 | 5 | 以下面的数组为例来讲解一下归并排序的原理: 6 | 7 | ``` 8 | +--------+ +--------+ +--------+ +--------+ 9 | | | | | | | | | 10 | | 8 | | 7 | | 14 | | 5 | 11 | | | | | | | | | 12 | +--------+ +--------+ +--------+ +--------+ 13 | ``` 14 | 15 | 1. 首先把数组分成两半: 16 | 17 | ``` 18 | +--------+ +--------+ +--------+ +--------+ 19 | | | | | | | | | 20 | | 8 | | 7 | | 14 | | 5 | 21 | | | | | | | | | 22 | +--------+ +--------+ +--------+ +--------+ 23 | ``` 24 | 25 | 2. 继续把上面的两个子数组分别分成两半: 26 | 27 | ``` 28 | +--------+ +--------+ +--------+ +--------+ 29 | | | | | | | | | 30 | | 8 | | 7 | | 14 | | 5 | 31 | | | | | | | | | 32 | +--------+ +--------+ +--------+ +--------+ 33 | ``` 34 | 35 | 到目前为止,每个子数组只有一个元素,不能再继续往下分组,分组的操作到此完成。 36 | 37 | 3. 子数组按照小到大的顺序合并: 38 | 39 | ``` 40 | +--------+ +--------+ +--------+ +--------+ 41 | | | | | | | | | 42 | | 7 | | 8 | | 5 | | 14 | 43 | | | | | | | | | 44 | +--------+ +--------+ +--------+ +--------+ 45 | ``` 46 | 47 | 4. 再继续合并: 48 | 49 | ``` 50 | +--------+ +--------+ +--------+ +--------+ 51 | | | | | | | | | 52 | | 5 | | 7 | | 8 | | 14 | 53 | | | | | | | | | 54 | +--------+ +--------+ +--------+ +--------+ 55 | ``` 56 | 57 | 完成排序。 58 | 59 | ### 实现 60 | 61 | ```swift 62 | func mergeSort(_ array: [Element]) -> [Element] { 63 | guard array.count > 1 else { 64 | return array 65 | } 66 | let middle = array.count / 2 67 | let left = mergeSort(Array(array[..(_ left: [Element], 73 | _ right: [Element]) -> [Element] { 74 | var leftIndex = 0 75 | var rightIndex = 0 76 | var result: [Element] = [] 77 | 78 | while leftIndex < left.count && rightIndex < right.count { 79 | let leftElement = left[leftIndex] 80 | let rightElement = right[rightIndex] 81 | 82 | if leftElement < rightElement { 83 | result.append(leftElement) 84 | leftIndex += 1 85 | } else if leftElement > rightElement { 86 | result.append(rightElement) 87 | rightIndex += 1 88 | } else { 89 | result.append(leftElement) 90 | leftIndex += 1 91 | result.append(rightElement) 92 | rightIndex += 1 93 | } 94 | } 95 | 96 | if leftIndex < left.count { 97 | result.append(contentsOf: left[leftIndex...]) 98 | } 99 | if rightIndex < right.count { 100 | result.append(contentsOf: right[rightIndex...]) 101 | } 102 | 103 | return result 104 | } 105 | ``` 106 | 107 | **`mergeSort`方法**:利用递归把数组分割到最小,然后调用 `merge`方法 108 | 109 | **`merge`方法**: 110 | 111 | - `leftIndex` 和 `rightIndex` 记录左右两个子数组当前索引;`result` 用于存储左右两个子数组合并的结果 112 | - `while` 循环里面,判断当前左右子索引对应元素的大小,谁小就先把谁添加到 `result` 中,如果一样大,两个都添加 113 | - `while` 循环结束后,如果左右子数组还有剩下的元素,则直接添加到最后面。 114 | 115 | 116 | #### 测试 117 | 118 | ```swift 119 | let ints = [4,5,5467,73,234,678,87,989] 120 | let sortedInts = mergeSort(ints) 121 | print(sortedInts) 122 | 123 | // 结果 124 | [4, 5, 73, 87, 234, 678, 989, 5467] 125 | ``` 126 | 127 | 结果正确。 128 | 129 | ### [完整代码 >>](https://github.com/Lebron1992/swift-algorithm-demo/blob/master/swift-algorithm/Sort/MergeSort.swift) 130 | 131 | ### 参考资料 132 | 133 | > [Data Structures and Algorithms in Swift](https://store.raywenderlich.com/products/data-structures-and-algorithms-in-swift) --- [raywenderlich.com](https://www.raywenderlich.com/),如果想看原版书籍,请点击链接购买。 134 | -------------------------------------------------------------------------------- /design-pattern/01 - 什么是设计模式.md: -------------------------------------------------------------------------------- 1 | 设计模式不是具体的实现,而是一套可以重复利用的模板解决方案,用于解决开发中常见的问题。它可以让我们在写代码的之前思考如何去写代码。 2 | 3 | 不管是什么语言还是什么平台,设计模式是非常有用的。我们每一个开发者都应该了解它们,知道何时使用它们,如何使用它们。 4 | 5 | ### 如何理解设计模式 6 | 7 | 我们先来看一个生活中的例子: 8 | 9 | 假设我家要建一套房子,需要把砖、水泥、沙子之类的材料运送到建房子的地方,但是因为车子不能直接开到建房子的地方,只能放到一个比较开阔的地方,然后通过人工去转移。 10 | 11 | 但是用什么办法去转移这些材料呢?这时我就去问那些有建房子经验的人,他告诉我说:你们可以用手推车来转移,省时省力。 12 | 13 | 上面的例子中,**使用手推车** 在我们软件开发来说,就是一个设计模式,我们可以利用这个模式,然后用手推车去运送其他东西;**用手推车去转移建筑材料** 就是一个具体实现。 14 | 15 | ### 设计模式的类型 16 | 17 | 设计模式主要有三大类: 18 | 19 | - **结构设计模式:** 描述各种对象如何组成一个大的结构,例如我们熟知的MVC和MVVM等。 20 | - **行为设计模式:** 描述对象之间如何沟通,例如代理、观察者等。 21 | - **创建型设计模式:** 描述如何创建或者初始化对象,例如单例等。 22 | 23 | 如果在一个项目中很好的利用设计模式,我们可以更好的管理代码,同时其他熟悉设计模式的开发人员可以很容易接手这个项目。所以为了写出更好的代码,我觉得了解设计模式是非常有必要的。 24 | -------------------------------------------------------------------------------- /design-pattern/02 - MVC模式 (Model-View-Controller Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | MVC (model-view-controller) 把所有类分成三种类型:模型、视图和控制器。 6 | 7 | MVC在iOS开发中是非常常见的,因为这是Apple重度使用的一种设计模式。 8 | 9 | **Model**:保存应用数据,通常是Struct和Class类型。在Swift中,我们优先选择Struct,因为Struct是值类型,可以避免不必要的循环引用问题。 10 | **View**:在屏幕上显示应用界面,通常继承于`UIView`。 11 | **Controller**:控制和协调Model和View,通常继承于`UIViewContorller`。 12 | 13 | Controller可以强引用Model和View,所以可以直接访问Model和View。但是反过来Model和View不能强引用Controller,否则会造成引用循环。 14 | 15 | Model可以通过属性观察者来告诉Controller数据发生变化,例如`willSet`和`didSet`;而View可以通过手势或者`valueChanged`事件等告诉Controller。 16 | 17 | ### 简单Demo 18 | 19 | 需求是展示一个用户的名字和年龄: 20 | 21 | 1. **Model**:定义了一个Person,有`name`和`age`属性 22 | 2. **View**:`nameLabel`和`ageLabel` 23 | 3. **Controller**:`PersonInfoViewController`,强引用了两个views,`nameLabel`和`ageLabel`;还强引用了`person`。 24 | 25 | 在`viewDidLoad`我们从服务器获取数据,获取到数据之后,我们修改`person`模型的值;模型被修改后告诉控制器,控制器更新`nameLabel`和`ageLabel`。这就是一个简单的MVC。 26 | 27 | ```swift 28 | import UIKit 29 | 30 | struct Person { 31 | var name: String 32 | var age: Int 33 | } 34 | 35 | final class PersonInfoViewController: UIViewController { 36 | 37 | @IBOutlet var nameLabel: UILabel! 38 | @IBOutlet var ageLabel: UILabel! 39 | 40 | var person: Person? { 41 | didSet { 42 | nameLabel.text = person?.name 43 | ageLabel.text = person?.age 44 | } 45 | } 46 | 47 | override func viewDidLoad() { 48 | super.viewDidLoad() 49 | fetchPersonInfo() 50 | } 51 | 52 | private func fetchPersonInfo() { 53 | // 假设name和age是从服务器获取的 54 | let name = "Lebron" 55 | let age = 25 56 | person = Person(name: name, age: age) 57 | } 58 | } 59 | ``` 60 | 61 | ### 总结 62 | 63 | 在我们刚刚开始学iOS开发时,一般都是从MVC模式开始的。但是MVC有个问题,如果是比较大型的项目,Controller的代码将会变得非常复杂,不方便维护。所以在实际开发中,大型的项目,我们一般考虑使用其他设计模式,例如MVVM等。 64 | -------------------------------------------------------------------------------- /design-pattern/03 - 代理模式 (Delegation Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 代理模式可以让一个对象用另外一个对象来提供一些数据或者执行某些任务。这种模式有三部分: 6 | 7 | - **需要代理的对象**:也就是有一个`delegate`属性的对象,通常使用`weak`修饰,以避免引用循环(需要代理的对象引用着代理,而代理又引用着需要代理的对象)。 8 | - **代理协议**:定义了代理需要实现的方法。 9 | - **代理**:实现代理协议的对象。 10 | 11 | 有了代理协议,那么代理协议的实现变得非常灵活,任何对象都可以实现这个协议,成为代理。 12 | 13 | ### 什么时候使用 14 | 15 | 当我们要创建一些通用的、可重复利用的组件时,可以考虑使用代理模式。代理模式在iOS的框架中普遍使用,特别是`UIKit`,例如`xxxDataSource`和`xxxDelegate`都是代理模式。 16 | 17 | 为什么在iOS的框架中,要把`xxxDataSource`和`xxxDelegate`分开呢? 18 | 19 | 其实是为了更好的管理,`xxxDataSource`专门用来提供数据,`xxxDelegate`用来接收数据或者事件。我们在自定义控件的时候也可以用这种思想。 20 | 21 | ### 简单Demo 22 | 23 | 假设我们有一个侧滑菜单,用户点击菜单中的每一项,然后显示不同的View Controller,我们就可以通过代理来实现。 24 | 25 | - 我们用一个枚举`MenuItem`列举出菜单有哪些选项 26 | - 实现UITableViewDataSource 27 | - 当用户点击了某一项之后,我们代理协议来告诉代理用户点击了哪一项,所以定义`MenuViewControllerDelegate` 28 | - 定义`delegate`属性,然后在`didSelectRowAt`中调用代理方法 29 | - 当其他对象成为`MenuViewController`的代理之后,它就能收到用户的点击事件 30 | 31 | ```swift 32 | protocol MenuViewControllerDelegate: class { 33 | func menuViewController(_ menuViewController: MenuViewController, 34 | didSelectItem item: MenuItem) 35 | } 36 | 37 | enum MenuItem: String { 38 | case home = "首页" 39 | case me = "我的" 40 | case settings = "设置" 41 | } 42 | 43 | class MenuViewController: UIViewController { 44 | 45 | weak var delegate: MenuViewControllerDelegate? 46 | 47 | @IBOutlet var tableView: UITableView! { 48 | didSet { 49 | tableView.dataSource = self 50 | tableView.delegate = self 51 | } 52 | } 53 | 54 | private let items: [MenuItem] = [.home, .me, .settings] 55 | } 56 | 57 | // MARK: - UITableViewDataSource & UITableViewDelegate 58 | extension MenuViewController: UITableViewDataSource, UITableViewDelegate { 59 | func tableView(_ tableView: UITableView, 60 | numberOfRowsInSection section: Int) -> Int { 61 | return items.count 62 | } 63 | 64 | func tableView(_ tableView: UITableView, 65 | cellForRowAt indexPath: IndexPath) 66 | -> UITableViewCell { 67 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 68 | cell.textLabel?.text = items[indexPath.row].rawValue 69 | return cell 70 | } 71 | 72 | func tableView(_ tableView: UITableView, 73 | didSelectRowAt indexPath: IndexPath) { 74 | let item = items[indexPath.row] 75 | delegate?.menuViewController(self, didSelectItem: item) 76 | } 77 | } 78 | ``` 79 | 80 | ### 总结 81 | 82 | 代理模式在开发中经常使用,但是不要过度使用。如果一个对象定义很多delegate,说明我们这个类做了太多的事情,要考虑把它拆分了。 83 | -------------------------------------------------------------------------------- /design-pattern/04 - 策略模式 (Strategy Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 策略模式定义了多个在运行中可以切换的对象。这个模式由三部分组成: 6 | 7 | 8 | ![策略模式UML图](http://upload-images.jianshu.io/upload_images/2057254-6c7ca6de0273bec0.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 9 | 10 | - **使用策略的对象**:在iOS开发中,通常是一个view controller,但是在理论上来说,可以是任何类型的对象。 11 | - **策略协议**:定义了每一种策略需要实现的方法。 12 | - **所有策略**:所有遵循策略协议的策略。 13 | 14 | ### 什么时候使用 15 | 16 | 当我们有多个不同的可切换的行为时,可以使用策略模式。 17 | 18 | 策略模式有点类似于代理模式,这两种模式都依赖于某个协议;在代理模式中,遵循了协议的对象就可以成为代理;在策略模式中,遵循了协议的就可以成一个策略。 19 | 20 | 但不同的是,策略模式有多个策略组成的集合,可以在运行时根据需要切换。 21 | 22 | ### 简单Demo 23 | 24 | 假设我们要做一个可以查看餐饮商家在不同平台的评分情况,例如大众点评、口碑等,我们就可以使用策略模式。 25 | 26 | - 首先定义一个策略协议`FoodMerchantRatingStrategy`,要求有平台的名称`ratingServiceName`和获取评分的方法 27 | - 定义大众点评和口碑两个平台:`DianPingClient`和`KouBeiClient`,并实现`FoodMerchantRatingStrategy`协议 28 | - 在`FoodMerchantRatingViewController`中,我们就可以根据传入的`foodMerchantRatingClient`做相应的显示 29 | 30 | 代码如下: 31 | 32 | ```swift 33 | import UIKit 34 | 35 | protocol FoodMerchantRatingStrategy { 36 | var ratingServiceName: String { get } 37 | func fetchRatingforMerchant(named name: String, 38 | success: (_ rating: String, _ review: String) -> Void) 39 | } 40 | 41 | class DianPingClient: FoodMerchantRatingStrategy { 42 | let ratingServiceName = "大众点评" 43 | func fetchRatingforMerchant(named name: String, 44 | success: (String, String) -> Void) { 45 | let rating = "⭐️⭐️⭐️⭐️" 46 | let review = "还不错!!!" 47 | success(rating, review) 48 | } 49 | } 50 | 51 | class KouBeiClient: FoodMerchantRatingStrategy { 52 | let ratingServiceName = "口碑" 53 | func fetchRatingforMerchant(named name: String, 54 | success: (String, String) -> Void) { 55 | let rating = "⭐️⭐️⭐️⭐️⭐️" 56 | let review = "非常好!" 57 | success(rating, review) 58 | } 59 | } 60 | 61 | class FoodMerchantRatingViewController: UIViewController { 62 | 63 | private let foodMerchantRatingClient: FoodMerchantRatingStrategy 64 | 65 | // MARK: - Initializers 66 | 67 | init(withRaingClient client: FoodMerchantRatingStrategy) { 68 | foodMerchantRatingClient = client 69 | super.init(nibName: nil, bundle: nil) 70 | } 71 | 72 | required init?(coder aDecoder: NSCoder) { 73 | fatalError("init(coder:) has not been implemented") 74 | } 75 | 76 | // MARK: - Views 77 | 78 | @IBOutlet private var merchantNameTextField: UITextField! 79 | @IBOutlet private var ratingServiceNameLabel: UILabel! 80 | @IBOutlet private var ratingLabel: UILabel! 81 | @IBOutlet private var reviewLabel: UILabel! 82 | 83 | // MARK: - View Lifecycles 84 | 85 | override func viewDidLoad() { 86 | super.viewDidLoad() 87 | ratingServiceNameLabel.text = foodMerchantRatingClient.ratingServiceName 88 | } 89 | 90 | // MARK: - Actions 91 | 92 | @IBAction private func searchButtonTapped() { 93 | guard let merchantName = merchantNameTextField.text, 94 | !merchantName.isEmpty else { 95 | return 96 | } 97 | foodMerchantRatingClient 98 | .fetchRatingforMerchant(named: merchantName, 99 | success: { [weak self] (rating, review) in 100 | self?.ratingLabel.text = rating 101 | self?.reviewLabel.text = review 102 | }) 103 | } 104 | } 105 | ``` 106 | 107 | ### 总结 108 | 109 | 如果有多个不同的行为,可以使用策略模式。但如果只有一个行为,并且不会再发生改变,直接把逻辑写到View Controller里面就好了。 110 | -------------------------------------------------------------------------------- /design-pattern/05 - 单例模式 (Singleton Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 单例模式限制一个类只能有一个实例,每一个对这个类的引用,都指向同一个实例。 6 | 7 | ### 什么时候使用 8 | 9 | 当一个类有多个实例会出现问题的时候,我们使用单例模式。 10 | 11 | ### 简单Demo 12 | 13 | iOS中我们经常会用到一些单例模式,例如: 14 | 15 | ```swift 16 | UIApplication.shared 17 | 18 | NotificationCenter.default 19 | 20 | UserDefaults.standard 21 | 22 | URLSession.shared 23 | 24 | FileManager.default 25 | ``` 26 | 27 | 通常以`shared`、`default`、`standard`命名的,都是用单例。 28 | 29 | 我们可以创建自己的单例: 30 | 31 | ```swift 32 | class MySingleton { 33 | static let shared = MySingleton() 34 | private init() { } 35 | } 36 | 37 | let singleton = MySingleton.shared 38 | //let singleton2 = MySingleton() // 报错 39 | ``` 40 | 41 | 在上面这个例子中,我们把`MySingleton`的初始化函数定义成了`private`,保证外部不能再自己创建另外一个`MySingleton`实例。 42 | 43 | ### 另外一种“单例模式” 44 | 45 | 我们上面讲到的单例,是一种严格的单例模式,保证一个类只能有一个实例,外部不能再创建第二个实例。 46 | 47 | 实际上我们还可以有另外一种类似的单例模式,同样有一个`shared`实例,但是它允许再创建另外一个实例。例如iOS中的`FileManager`: 48 | 49 | ```swift 50 | let defaultFileManager = FileManager.default 51 | let customFileManager = FileManager() 52 | ``` 53 | 54 | 在上面的那个`MySingleton`例子中,我们把初始化函数的`private`关键字去掉,就是我们的这部分讲到的单例模式: 55 | 56 | ```swift 57 | class MySingleton { 58 | static let shared = MySingleton() 59 | init() { } 60 | } 61 | 62 | let singleton = MySingleton.shared 63 | let singleton2 = MySingleton() 64 | ``` 65 | 66 | ### 总结 67 | 68 | 单例模式非常容易滥用。 69 | 70 | 当遇到某些需求想要使用单例模式来实现的时候,要优先考虑是否有其他方式可以实现? 71 | 72 | 当确定要使用单例模式时,考虑下是否可以让外部自己创建另外一个实例? 73 | 74 | 单例模式最大的问题是测试。如果有多个状态存储在一个单例中,测试案例的顺序跟单例的状态有密切的联系,那么在测试的时候就会比较麻烦。 75 | -------------------------------------------------------------------------------- /design-pattern/06 - 备忘录模式 (Memento Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 备忘录模式可以用来存储或者恢复一个对象,它包含以下三个部分: 6 | 7 | ![](http://upload-images.jianshu.io/upload_images/2057254-b9d3b4caa9ad8590.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 8 | 9 | - **Originator**:被存储或者恢复的对象 10 | - **Memento**:备忘录,代表一个已经存储的状态 11 | - **CareTaker**:发起保存Originator的请求,然后收到一个Memento。CareTaker的作用对备忘录进行管理,负责保存备忘录,然后提供已保存的备忘录给Originator,用于恢复Originator的状态。 12 | 13 | 在iOS中,我们使用`Encoder`把originator的状态编码到一个备忘录中,然后使用`Decoder`解码备忘录,生成一个originator。例如,我们可以把一个遵循`Codable`的类型,用`JSONEncoder`和`JSONDecoder`进行编码和解码。 14 | 15 | ### 什么时候使用 16 | 17 | 当我们需要保存一个对象的状态,后续要恢复这个对象时,使用备忘录模式。 18 | 19 | ### 简单Demo 20 | 21 | 我们简单地设计一个游戏系统: 22 | 23 | - **Originator**:`Game`,记录游戏的状态和动作 24 | - **Memento**:在本例中,是`Data`,用`JSONEncoder`把`Game`编码成数据,再用`JSONDecoder`解码成`Game`对象 25 | - **CareTaker**:`GameSystem`,执行存储和恢复的过程 26 | 27 | 代码如下: 28 | 29 | #### Game 30 | 31 | 记录游戏的分数和级别,并执行一些有些的操作 32 | 33 | ```swift 34 | class Game: Codable { 35 | class State: Codable { 36 | var score = 0 37 | var level = 1 38 | } 39 | lazy var state = State() 40 | 41 | func enterNextLebrl() { 42 | state.level += 1 43 | } 44 | 45 | func earnSomePoints() { 46 | state.score += 100 47 | } 48 | } 49 | ``` 50 | 51 | #### GameSystem 52 | 53 | 定义了`encoder`和`decoder`,用`encoder`把`game`编码成数据,用`UserDefaults`保存;用`decoder`把数据转化成`Game`对象。 54 | 55 | ```swift 56 | class GameSystem { 57 | private let encoder = JSONEncoder() 58 | private let decoder = JSONDecoder() 59 | 60 | func save(_ game: Game, for player: String) throws { 61 | let data = try encoder.encode(game) 62 | UserDefaults.standard.set(data, forKey: player) 63 | } 64 | 65 | func loadGame(for player: String) throws -> Game? { 66 | guard let data = UserDefaults.standard.data(forKey: player), 67 | let game = try? decoder.decode(Game.self, from: data) else { 68 | return nil 69 | } 70 | return game 71 | } 72 | } 73 | ``` 74 | 75 | #### 游戏模拟 76 | 77 | 第一个玩家开始玩游戏,赚了100分,进入下一等级;然后把这个玩家的数据以`player1`作为key保存。 78 | 79 | ```swift 80 | var game1 = Game() 81 | game1.earnSomePoints() 82 | game1.enterNextLebrl() 83 | 84 | let gameSystem = GameSystem() 85 | try gameSystem.save(game1, for: "player1") 86 | ``` 87 | 88 | 第二个玩家开始玩游戏: 89 | 90 | ```swift 91 | var game2 = Game() 92 | print("第二个玩家的分数: \(game2.state.score)") // 第二个玩家的分数: 0 93 | ``` 94 | 95 | 如果这时第二个玩家不想玩了,第一个玩家接着玩,恢复第一个玩家的数据: 96 | 97 | ```swift 98 | var restoredGame1 = try! gameSystem.loadGame(for: "player1") 99 | print("第一个玩家的分数: \(restoredGame1?.state.score ?? "nil")") 100 | // 第一个玩家的分数: 100 101 | ``` 102 | -------------------------------------------------------------------------------- /design-pattern/07 - 观察者模式 (Observer Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 观察者模式可以让一个对象观察另一个对象的变化。在iOS中,我们一般使用KVO来实现。 6 | 7 | 观察者模式主要有两个对象组成: 8 | 9 | - **subject**:被观察的对象。 10 | - **observer**:观察subject的对象。 11 | 12 | ### 什么时候使用 13 | 14 | 当我们想关注一个对象的变化时,可以使用观察者模式。 15 | 16 | ### 简单Demo 17 | 18 | 创建一个`User`类,继承于`NSObject`: 19 | 20 | ```swift 21 | @objcMembers class User: NSObject { 22 | dynamic var name: String 23 | public init(name: String) { 24 | self.name = name 25 | } 26 | } 27 | ``` 28 | 29 | 从Swift 4开始,继承自`NSObject`的类不会自动把他们的属性暴露给Objective-C runtime。因为`NSObject`使用Objective-C runtime来执行KVO,所以我们手动添加`@objcMembers`,它的作用相当于在每一个属性前面加上`@objc`。 30 | 31 | `dynamic`意思是用Objective-C runtime动态调度系统来调用属性的getter和setter,意味着静态或者虚拟调用将永远不会被使用。我们要加上`dynamic`,KVO才能正常进行。 32 | 33 | 下面我们创建一个`User`实例,并且观察`name`的变化: 34 | 35 | ```swift 36 | let user = User(name: "Lebron") 37 | 38 | var nameObservation: NSKeyValueObservation? = 39 | user.observe(\.name, options: [.initial, .new]) { (user, change) in 40 | print("名字是:\(user.name)") 41 | } 42 | 43 | // 名字是:Lebron 44 | ``` 45 | 46 | 这里我们使用Swift 4的KVO写法,`\.name`是`\KVOUser.name`的简写,是一个`KeyPath`;`options`参数是用来指定想要观察哪些类型的数据,这里传入`[.initial, .new]`,意味着可以观察到初始的实例时`name`的值和`name`变化时的值;最后一个参数是闭包,闭包有`user`和`change`两个对象,`user`是所有变化完成后的对象,如果`.new`事件被触发,`change`会包含一个旧值`oldValue`。 47 | 48 | 在`options`参数中,我们传入了`.initial`,所以我们能看到这个打印`名字是:Lebron`。 49 | 50 | 如果尝试把`name`改为`Love`: 51 | 52 | ``` 53 | user.name = "Love" 54 | 55 | // 名字是:Love 56 | ``` 57 | 58 | 控制台出现:`名字是:Lebron`。我们成功地观察了`name`的变化。 59 | 60 | 在Swift 4的KVO中,我们不需要手动的移除观察者或者闭包,因为观察者是被弱引用着的,当观察者变为`nil`时,闭包会自动移除。而在以前的Swift版本或者Objective-C中,我们必须调用`removeObserver`来移除,否则当我们访问被回收了的观察者时,应用会crash。 61 | 62 | 虽然Swift 4的KVO不需要我们手动移除观察则,但是被观察的对象必须继承自`NSObject`和使用Objective-C runtime。 63 | -------------------------------------------------------------------------------- /design-pattern/08 - 建造者模式 (Builder Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 建造者模式允许我们一步一步的创建复杂的对象,而不是通过初始化函数一次性创建。这种模式有三个主要的部分组成: 6 | 7 | - **Director**:接收输入,并控制Builder;通常是一个View Controller 8 | - **Product**:需要创建的复杂对象 9 | - **Builder**:接受一步一步传入的输入,并负责Product的创建;通常是一个Class类型 10 | 11 | ### 什么时候使用 12 | 13 | 当想要一步一步的创建一个复杂对象时,使用建造者模式。 14 | 15 | ### 简单Demo 16 | 17 | 我们来看一个经典的例子:实现汉堡包的制作。**Product**是`Hamburger`;**Director**是员工`Employee`,**Builder**是`HamburgerBuilder`。 18 | 19 | #### Product 20 | 21 | ```swift 22 | struct Hamburger { 23 | let meat: Meat 24 | let toppings: Toppings 25 | let sauces: Sauces 26 | } 27 | extension Hamburger: CustomStringConvertible { 28 | var description: String { 29 | return meat.rawValue + " Hamburger" 30 | } 31 | } 32 | 33 | enum Meat: String { 34 | case beef 35 | case chicken 36 | } 37 | 38 | struct Toppings: OptionSet { 39 | static let cheese = Toppings(rawValue: 1 << 0) 40 | static let lettuce = Toppings(rawValue: 1 << 1) 41 | static let tomatoes = Toppings(rawValue: 1 << 2) 42 | 43 | let rawValue: Int 44 | 45 | init(rawValue: Int) { 46 | self.rawValue = rawValue 47 | } 48 | } 49 | 50 | struct Sauces: OptionSet { 51 | static let mustard = Sauces(rawValue: 1 << 0) 52 | static let ketchup = Sauces(rawValue: 1 << 1) 53 | 54 | let rawValue: Int 55 | 56 | init(rawValue: Int) { 57 | self.rawValue = rawValue 58 | } 59 | } 60 | ``` 61 | 62 | `Hamburger`由肉`Meat`、配料`Toppings`和调味汁`Sauces`组成;另外实现了`CustomStringConvertible`,待会我们打印的时候能看到是什么汉堡。。其中肉可以选择牛肉和肌肉中的一种;配料可以多选:奶酪、生菜和番茄;调味汁也可以多选:芥末和番茄酱。 63 | 64 | #### Builder 65 | 66 | ```swift 67 | class HamburgerBuilder { 68 | private(set) var meat: Meat = .chicken 69 | private(set) var toppings: Toppings = [] 70 | private(set) var sauces: Sauces = [] 71 | 72 | func setMeat(_ meat: Meat) { 73 | self.meat = meat 74 | } 75 | 76 | func addToppings(_ toppings: Toppings) { 77 | self.toppings.insert(toppings) 78 | } 79 | 80 | func removeToppings(_ toppings: Toppings) { 81 | self.toppings.remove(toppings) 82 | } 83 | 84 | func addSauces(_ sauces: Sauces) { 85 | self.sauces.insert(sauces) 86 | } 87 | 88 | func removeSauces(_ sauces: Sauces) { 89 | self.sauces.remove(sauces) 90 | } 91 | 92 | func build() -> Hamburger { 93 | return Hamburger(meat: meat, 94 | toppings: toppings, 95 | sauces: sauces) 96 | } 97 | } 98 | ``` 99 | 100 | 定义了制作汉堡需要的三种原料,并且使用了`private(set)`,防止被外部篡改(我们在写程序的时候,也要有这种思想,只暴露外部需要的api);还有其他添加或者移除材料的方法;最后是最终完成制作的方法。 101 | 102 | #### Director 103 | 104 | ```swift 105 | class Employee { 106 | private let builder = HamburgerBuilder() 107 | 108 | func createBeefHamburger() -> Hamburger { 109 | builder.setMeat(.beef) 110 | builder.addSauces(.ketchup) 111 | builder.addToppings([.lettuce, .tomatoes]) 112 | return builder.build() 113 | } 114 | 115 | func createChickenHamburger() -> Hamburger { 116 | let builder = HamburgerBuilder() 117 | builder.setMeat(.chicken) 118 | builder.addSauces(.mustard) 119 | builder.addToppings([.lettuce, .tomatoes]) 120 | return builder.build() 121 | } 122 | } 123 | ``` 124 | 125 | 有两个方法分别用于制作鸡肉汉堡和牛肉汉堡。 126 | 127 | #### 使用 128 | 129 | ```swift 130 | let employee = Employee() 131 | 132 | let beefHamburger = employee.createBeefHamburger() 133 | print(beefHamburger.description) // beef hamburger 134 | 135 | let chickenHamburger = employee.createChickenHamburger() 136 | print(chickenHamburger.description) // chicken hamburger 137 | ``` 138 | 139 | 创建一个员工对象,然后制作汉堡就非常简单了。 140 | 141 | ### 总结 142 | 143 | 当创建对象时,需要通过一系列的步骤来传入参数时,非常适合使用建造者模式。如果我们的Product不需要很多的参数,或者需要一次性创建,建议还是使用初始化函数。 144 | -------------------------------------------------------------------------------- /design-pattern/10 - 工厂模式 (Factory Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 工厂模式可以在不需要暴露创建逻辑的情况下,创建对象。主要有两部分组成: 6 | 7 | - **Factory**:负责创建对象。 8 | - **Products**:Factory创建的对象。 9 | 10 | ### 什么时候使用 11 | 12 | 当想要把Product的创建逻辑独立出来,而不是让使用者直接去创建时,使用这种模式。 13 | 14 | ### 简单Demo 15 | 16 | 假设我们在开发一个HR专用的邮箱,其中有一个需求是回复求职者的职位申请时,可以根据求职者目前的状态来创建模板邮件: 17 | 18 | 首先我们有两个模型,求职者`JobApplicant`和邮件`Email`: 19 | 20 | ```swift 21 | struct JobApplicant { 22 | let name: String 23 | let email: String 24 | var status: Status 25 | 26 | enum Status { 27 | case new 28 | case interview 29 | case hired 30 | case rejected 31 | } 32 | } 33 | 34 | struct Email { 35 | let subject: String 36 | let messageBody: String 37 | let recipientEmail: String 38 | let senderEmail: String 39 | } 40 | ``` 41 | 42 | 然后是我们的邮件工厂`EmailFactory`: 43 | 44 | ```swift 45 | struct EmailFactory { 46 | 47 | let senderEmail: String 48 | 49 | func createEmail(to recipient: JobApplicant, messageBody: String? = nil) -> Email { 50 | switch recipient.status { 51 | case .new: 52 | return Email( 53 | subject: "已收到你的求职申请", 54 | messageBody: messageBody ?? "感谢你申请我们的职位,我们会在24小时内回复你。", 55 | recipientEmail: recipient.email, 56 | senderEmail: senderEmail) 57 | 58 | case .interview: 59 | return Email( 60 | subject: "面试邀请", 61 | messageBody: messageBody ?? "你的简历已经通过筛选,请于明天上午10点到我们公司面试。", 62 | recipientEmail: recipient.email, 63 | senderEmail: senderEmail) 64 | 65 | case .hired: 66 | return Email( 67 | subject: "你已通过面试", 68 | messageBody: messageBody ?? "恭喜你,你已经通过我们公司的面试,请于下周一到我们公司报道。", 69 | recipientEmail: recipient.email, 70 | senderEmail: senderEmail) 71 | 72 | case .rejected: 73 | return Email( 74 | subject: "面试未通过", 75 | messageBody: messageBody ?? "因不符合我公司的要求,此次面试不通过。谢谢!", 76 | recipientEmail: recipient.email, 77 | senderEmail: senderEmail) 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | 因为发邮件的时候,需要一个发件人,所以创建邮件工厂时,需要一个`senderEmail`参数;在`createEmail`根据求职者的不同状态来创建模板,并且还提供了一个可选的`messageBody`参数,如果不提供`messageBody`,我们就会使用默认的。 84 | 85 | 使用: 86 | 87 | ```swift 88 | var lebron = JobApplicant(name: "Lebron James", 89 | email: "lebronjames@example.com", 90 | status: .hired) 91 | 92 | let emailFactory = EmailFactory(senderEmail: "hr@company.com") 93 | let emial = emailFactory.createEmail(to: lebron) 94 | ``` 95 | 96 | ### 总结 97 | 98 | 当想要把实例的创建逻辑独立出来,可以使用工厂模式。但如果想要的实例非常简单,直接在用到的地方直接创建即可。如果这个实例需要一系列的步骤才能创建,最好使用Builder模式。 99 | -------------------------------------------------------------------------------- /design-pattern/11 - 适配器模式 (Adapter Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 适配器模式可以让不兼容的类型在一起正常的工作,它包含以下四个组成部分: 6 | 7 | ![](http://upload-images.jianshu.io/upload_images/2057254-4c8674f217a50efd.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 8 | 9 | - **使用adapter的对象**:这个对象拥有遵循新协议的adapter 10 | - **新的协议**:为了能让不兼容的类型一起正常工作而新建的协议 11 | - **适配器**:遵循新的协议,并且拥有旧的对象,调用就对象的方法 12 | - **旧对象**:在新协议建立之前就已经存在的对象,并且不能直接修改它让他去遵循新的协议 13 | 14 | 看到现在,可能比较难懂。我们举个例子:最新的iPhone都把3.5mm的耳机接口给干掉了,如果只有3.5mm接口的耳机,我们又想用耳机听歌,那么必须要有一个3.5mm转lightning的转换器才能使用3.5mm接口的耳机。在这个例子中,转接头就是**适配器**;3.5mm接口的耳机就是**旧对象**;3.5mm转lighting的技术实现就是**新的协议**;最新的iPhone就是**使用adapter的对象**。 15 | 16 | ### 什么时候使用 17 | 18 | 有些方法、类或者模块不能直接修改,特别是当他们来自第三方库的时候,当我们又需要让他们遵循我们的设计的规则时,我们可以使用适配器模式。 19 | 20 | ### 简单Demo 21 | 22 | 国内的很多应用支持微信、QQ、微博等进行登录,他们的登录api肯定会不大一样。现在我们想用一个统一的api来处理这些第三方的登录,就可以用到适配器模式。 23 | 24 | 这里以微博登录为例。 25 | 26 | #### 第三方库的api 27 | 28 | 我们假设下面的api来自第三方库,不能直接被修改: 29 | 30 | ```swift 31 | public struct WeiboUser { 32 | public let username: String 33 | public let email: String 34 | public let token: String 35 | } 36 | 37 | public final class WeiboAuthenticator { 38 | public func login(email: String, 39 | password: String, 40 | completion: (WeiboUser?, Error?) -> Void) { 41 | 42 | let token = "a_token_value" 43 | let username = "a_user_name" 44 | let user = WeiboUser(username: username, 45 | email: email, 46 | token: token) 47 | completion(user, nil) 48 | } 49 | } 50 | ``` 51 | 52 | #### 统一第三方登录的api 53 | 54 | 为了统一第三方登录的api,我们需要一个新的协议`AuthenticationServiceable`: 55 | 56 | ```swift 57 | struct User { 58 | let email: String 59 | let password: String 60 | } 61 | 62 | struct Token { 63 | let value: String 64 | } 65 | 66 | protocol AuthenticationServiceable { 67 | func login(email: String, 68 | password: String, 69 | success: (User, Token) -> Void, 70 | failure: (Error?) -> Void) 71 | } 72 | ``` 73 | 74 | 接下来我们改创建适配器了: 75 | 76 | ```swift 77 | final class WeiboAuthenticatorAdapter: AuthenticationServiceable { 78 | 79 | private lazy var authenticator = WeiboAuthenticator() 80 | 81 | func login(email: String, 82 | password: String, 83 | success: (User, Token) -> Void, 84 | failure: (Error?) -> Void) { 85 | 86 | authenticator.login(email: email, password: password) { (weiboUser, error) in 87 | guard let weiboUser = weiboUser else { 88 | failure(error) 89 | return 90 | } 91 | 92 | let user = User(username: weiboUser.username, 93 | email: weiboUser.email) 94 | let token = Token(value: weiboUser.token) 95 | success(user, token) 96 | } 97 | } 98 | } 99 | ``` 100 | 101 | 我们让`WeiboAuthenticatorAdapter`适配器拥有了一个旧的`WeiboAuthenticator`实例,然后实现`AuthenticationServiceable`协议。 102 | 103 | 我们把`WeiboAuthenticator`包装到`WeiboAuthenticatorAdapter`适配器了,如果微博的登录api改变,我们只需要在适配器更新代码即可。 104 | 105 | #### 使用 106 | 107 | ```swift 108 | let authService = WeiboAuthenticatorAdapter() 109 | authService.login(email: "test@example.com", 110 | password: "password", 111 | success: { (user, token) in 112 | print("登录成功: 1) 邮件:\(user.email), 2)token: \(token.value)") 113 | }, 114 | failure: { (error) in 115 | print("登录失败:\(error?.localizedDescription ?? "")") 116 | }) 117 | ``` 118 | 119 | ### 总结 120 | 121 | 新的协议在适配器模式中是必须的,保证adapter在我们的应用中能正常使用。如果第三方的代码改变,我们更新adapter中的代码即可。 122 | -------------------------------------------------------------------------------- /design-pattern/12 - 迭代器模式 (Iterator Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 迭代器模式提供了一种遍历集合的方式。这个模式设计两种类型:1) **迭代器协议**;2) **自定义的可遍历的类型**。 6 | 7 | Swift里面的集合可以通过`for-in`来遍历的类型,就是利用了迭代器协议`IteratorProtocol`。 8 | 9 | ### 什么时候使用 10 | 11 | 当我们的class或者struct拥有一组排好序的对象,并且我们想通过`for-in`来遍历这组对象时,使用迭代器模式。 12 | 13 | ### 简单demo 14 | 15 | 我们实现一个队列,然后通过遵循`Sequence`协议,实现使用`for-in`来遍历队列里面的元素。 16 | 17 | #### 队列 18 | 19 | 跟数组和字典一样,队列也是一种数据结构。队列遵循的规则是**先进先出**。 20 | 21 | ```swift 22 | struct Queue { 23 | private var array: [T?] = [] 24 | private var head = 0 25 | 26 | var isEmpty: Bool { 27 | return count == 0 28 | } 29 | 30 | var count: Int { 31 | return array.count - head 32 | } 33 | 34 | mutating func enqueue(_ element: T) { 35 | array.append(element) 36 | } 37 | 38 | mutating func dequeue() -> T? { 39 | guard head < array.count, 40 | let element = array[head] else { 41 | return nil 42 | } 43 | array[head] = nil 44 | head += 1 45 | return element 46 | } 47 | } 48 | 49 | extension Queue: Sequence { 50 | func makeIterator() -> IndexingIterator> { 51 | let values = array[head..() 74 | queue.enqueue(lebron) 75 | queue.enqueue(love) 76 | queue.enqueue(korver) 77 | 78 | // 出队 79 | queue.dequeue() 80 | 81 | for person in queue { 82 | print(person?.name ?? "此人不存在") 83 | } 84 | 85 | // 结果 86 | Kevin Love 87 | Kyle Korver 88 | ``` 89 | 90 | 这里我们定义了一个`Person`类型,用于定义`Person`队列。添加了三个人,然后将第一个人剔除,最终剩下两个。 91 | 92 | ### 总结 93 | 94 | Swift里面有迭代器协议`IteratorProtocol`,但是我们不需要直接去遵循这个协议,而是通过遵循`Sequence`来实现,好处是我们不需要自己自定义一个迭代器,而且还可以获得集合的很多方法,例如`filter`、`map`和`sort`等等。 95 | -------------------------------------------------------------------------------- /design-pattern/13 - 原型模式 (Prototype Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 原型模式属于创建型模式,允许我们对一个对象进行复制。它包含两个部分:1) **Copying**协议;2) 遵循**Copying**协议的Class类型。 6 | 7 | 复制又分为两种类型:浅复制和深复制: 8 | 9 | - **浅复制**:创建了一个新的实例,但是不复制原实例的属性,而是所有属性都指向原实例。 10 | 11 | - **深复制**:创建新的实例,并且复制原有实例的属性。 12 | 13 | ### 什么时候使用 14 | 15 | 当想要复制一个对象的时候,使用这个模式。 16 | 17 | ### 简单demo 18 | 19 | 在Objective-C的时候,有一个`NSCopying`协议,虽然在Swift还是可以使用,但使用起来不是非常友好,所以这里我们就自己定义一个`Copying`协议: 20 | 21 | ```swift 22 | protocol Copying: class { 23 | init(_ prototype: Self) 24 | } 25 | 26 | extension Copying { 27 | func copy() -> Self { 28 | return type(of: self).init(self) 29 | } 30 | } 31 | ``` 32 | 33 | 首先我们定义了一个`Copying`协议,并规定了一个把当前类型的实例作为参数初始化函数;然后通过扩展实现了`copy()`方法。 34 | 35 | 接下来定义一个宠物类,并实现`Copying`协议: 36 | 37 | ```swift 38 | class Pet: Copying { 39 | let name: String 40 | let weight: Double 41 | 42 | init(name: String, weight: Double) { 43 | self.name = name 44 | self.weight = weight 45 | } 46 | 47 | // MARK: - Copying 48 | 49 | required convenience init(_ pet: Pet) { 50 | self.init(name: pet.name, weight: pet.weight) 51 | } 52 | } 53 | ``` 54 | 55 | 试试我们刚刚定义的宠物类: 56 | 57 | ```swift 58 | let pet1 = Pet(name: "Lili", weight: 10) 59 | let pet2 = pet1.copy() 60 | print("pet1====name: \(pet1.name)====weight: \(pet1.weight)") 61 | print("pet2====name: \(pet2.name)====weight: \(pet2.weight)") 62 | 63 | // 结果 64 | pet1====name: Lili====weight: 10.0 65 | pet2====name: Lili====weight: 10.0 66 | ``` 67 | 68 | 结果完全一样,这样我们就完成了对一个实例的复制。 69 | -------------------------------------------------------------------------------- /design-pattern/15 - 多播委托模式 (Multicast Delegate Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 多播委托模式属于行为模式,属于代理模式的变种。我们可以通过这种模式来创建一对多的代理模式。 6 | 7 | 这个模式涉及四个部分: 8 | 9 | - **需要代理的对象**:这个对象可以有一个或者多个delegates 10 | - **代理协议**:规定代理需要实现的方法 11 | - **代理**:实现代理协议的对象 12 | - **多播委托**:持有多个代理,并对代理进行管理 13 | 14 | 多播委托模式和代理模式的主要区别在于:多播委托模式多了一个多播委托对象。 15 | 16 | ### 什么时候使用 17 | 18 | 使用这个模式来创建一对多的代理模式。 19 | 20 | ### 简单demo 21 | 22 | 在这个demo中,我们实现这个例子:特朗普大厦发生火灾,大厦的安保中心及时通知消防中心和医院的急救中心。 23 | 24 | 从这个例子可以分析得出:1)**需要代理的对象**是大厦的安保中心`SecurityCenter`;2)**代理**是消防中心`FireStation`和医院的急救中心`HospitalEmergencyCenter`;3)Swift默认没有提供**多播委托**给我们,需要自己创建`MulticastDelegate`;4)**代理协议**:`FireEmergencyResponding`。 25 | 26 | #### MulticastDelegate 27 | 28 | ```swift 29 | final class MulticastDelegate { 30 | 31 | // MARK: - Helper Types 32 | 33 | private final class DelegateWrapper { 34 | weak var delegate: AnyObject? 35 | init(delegate: AnyObject) { self.delegate = delegate } 36 | } 37 | 38 | // MARK: - Properties 39 | 40 | private var delegateWrappers: [DelegateWrapper] 41 | private var delegates: [ProtocolType] { 42 | delegateWrappers = delegateWrappers.filter { $0.delegate != nil } 43 | return delegateWrappers.map { $0.delegate } as! [ProtocolType] 44 | } 45 | 46 | // MARK: - Initializers 47 | 48 | init(delegates: [ProtocolType] = []) { 49 | delegateWrappers = delegates 50 | .map { DelegateWrapper(delegate: $0 as AnyObject)} 51 | } 52 | 53 | // MARK: - Delegate Management 54 | 55 | func addDelegate(_ delegate: ProtocolType) { 56 | let wrapper = DelegateWrapper(delegate: delegate as AnyObject) 57 | delegateWrappers.append(wrapper) 58 | } 59 | 60 | func removeDelegate(_ delegate: ProtocolType) { 61 | guard let index = delegateWrappers 62 | .index(where: { $0.delegate === (delegate as AnyObject)}) else { 63 | return 64 | } 65 | delegateWrappers.remove(at: index) 66 | } 67 | 68 | func notifyDelegates(_ closure: (ProtocolType) -> Void) { 69 | delegates.forEach { closure($0) } 70 | } 71 | } 72 | ``` 73 | 74 | 为了保证`MulticastDelegate`的通用性,把它定义为泛型。 75 | 76 | 另外,为了不让`MulticastDelegate`强引用代理,定义了一个内部类`DelegateWrapper`来把代理包装起来,内部类里面弱引用代理。在内部类里面,`delegate`被定义为`AnyObject?`,而不是`ProtocolType?`,因为`weak`只能用于Class类型。 77 | 78 | 还添加了管理代理的方法。 79 | 80 | #### 代理协议 81 | 82 | ```swift 83 | protocol FireEmergencyResponding: class { 84 | func notifyFire(at location: String) 85 | } 86 | ``` 87 | 88 | 火灾紧急响应,只有一个方法,通知在某个地方发生了火灾。 89 | 90 | #### 代理 91 | 92 | ```swift 93 | final class FireStation: FireEmergencyResponding { 94 | func notifyFire(at location: String) { 95 | print("已经通知消防员在\(location)发生了火灾") 96 | } 97 | } 98 | 99 | final class HospitalEmergencyCenter: FireEmergencyResponding { 100 | func notifyFire(at location: String) { 101 | print("已经通知医护人员在\(location)发生了火灾") 102 | } 103 | } 104 | ``` 105 | 106 | 消防中心和医院的急救中心。 107 | 108 | #### 需要代理的对象 109 | 110 | ```swift 111 | final class SecurityCenter { 112 | static let shared = SecurityCenter() 113 | let multicastDelegate = MulticastDelegate() 114 | } 115 | ``` 116 | 117 | 大厦的安保中心,因为安保中心通常只有一个,所以这里使用了单例。 118 | 119 | #### 使用 120 | 121 | ```swift 122 | let securityCenter = SecurityCenter.shared 123 | var fireStation: FireStation! = FireStation() 124 | let hospital = HospitalEmergencyCenter() 125 | 126 | securityCenter.multicastDelegate.addDelegate(fireStation) 127 | securityCenter.multicastDelegate.addDelegate(hospital) 128 | 129 | securityCenter.multicastDelegate.notifyDelegates { 130 | $0.notifyFire(at: "特朗普大厦") 131 | } 132 | 133 | print("======分割线======") 134 | 135 | fireStation = nil 136 | 137 | securityCenter.multicastDelegate.notifyDelegates { 138 | $0.notifyFire(at: "特朗普大厦") 139 | } 140 | 141 | // 结果 142 | 已经通知消防员在特朗普大厦发生了火灾 143 | 已经通知医护人员在特朗普大厦发生了火灾 144 | ======分割线====== 145 | 已经通知医护人员在特朗普大厦发生了火灾 146 | ``` 147 | 148 | 首先创建了两个代理:`fireStation`和`hospital`。把`fireStation`定义为`FireStation!`,这样后面我们可以把`fireStation`设置为`nil`。然后把两个代理添加到`multicastDelegate`,接着通知特朗普大厦发生火灾,得到了我们预期的打印结果。 149 | 150 | 把`fireStation`设置为`nil`之后,只有医院接收到了通知。 151 | 152 | ### 总结 153 | 154 | 多播委托模式非常适合用于把信息告诉代理的场景。如果是需要多个代理提供数据,这种模式就不适合了。 155 | -------------------------------------------------------------------------------- /design-pattern/16 - 门面模式 (Facade Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 门面模式属于结构模式,它给一个复杂的系统提供了简单的接口。它涉及到两个部分: 6 | 7 | - **Facade**:提供了与系统交互的简单方法,它让使用者直接使用facade,而不需要知道系统里的类,也不需要跟他们交互。 8 | - **Dependencies**:Facade持有的对象,每个dependency处理一部分复杂的任务。 9 | 10 | ### 什么时候使用 11 | 12 | 当我们有一个由很多组件组成的系统时,并且需要提供一个简单的方式给用户执行复杂的任务时,使用这个模式。例如电商的下单系统,他会涉及到顾客、产品、库存清单等等。 13 | 14 | ### 简单demo 15 | 16 | 这里我们就以刚刚提到的电商下单系统为例。首先我们需要`Customer`和`Product`数据模型;然后是库存`InventoryDatabase`;用户下单后,把已购买的产品存到待发货数据库`ShippingDatabase`中。实际开发中,肯定还需要顾客数据库和发票的处理等等,这里为了简便,我们就不考虑这些了。 17 | 18 | #### `Customer`和`Product`数据模型 19 | 20 | ```swift 21 | // MARK: - Customer 22 | struct Customer { 23 | let id: String 24 | var address: String 25 | var name: String 26 | } 27 | extension Customer: Hashable { 28 | var hashValue: Int { 29 | return id.hashValue 30 | } 31 | 32 | static func ==(lhs: Customer, rhs: Customer) -> Bool { 33 | return lhs.id == rhs.id 34 | } 35 | } 36 | 37 | // MARK: - Product 38 | struct Product { 39 | let id: String 40 | var name: String 41 | var price: Double 42 | } 43 | extension Product: Hashable { 44 | var hashValue: Int { 45 | return id.hashValue 46 | } 47 | 48 | static func ==(lhs: Product, rhs: Product) -> Bool { 49 | return lhs.id == rhs.id 50 | } 51 | } 52 | ``` 53 | 54 | 这里简单地创建了`Customer`和`Product`。因为后面要把他们作为字典的key,所以都实现了`Hashable`协议。 55 | 56 | #### `InventoryDatabase`和`ShippingDatabase`数据库 57 | 58 | ```swift 59 | final class InventoryDatabase { 60 | var inventory: [Product: Int] = [:] 61 | init(inventory: [Product: Int]) { self.inventory = inventory } 62 | } 63 | 64 | final class ShippingDatabase { 65 | var pendingShipments: [Customer: [Product]] = [:] 66 | } 67 | ``` 68 | 69 | `InventoryDatabase`记录了某个产品对应的数量有多少个;`ShippingDatabase`记录了某个客户对应的待发货的产品。 70 | 71 | #### Facade 72 | 73 | 上面已经准备好了一系列的工作,下面该创建我们的门面了。 74 | 75 | ```swift 76 | final class OrderFacade { 77 | let inventoryDatabase: InventoryDatabase 78 | let shippingDatabase: ShippingDatabase 79 | 80 | init(inventoryDatabase: InventoryDatabase, 81 | shippingDatabase: ShippingDatabase) { 82 | self.inventoryDatabase = inventoryDatabase 83 | self.shippingDatabase = shippingDatabase 84 | } 85 | 86 | func placeOrder(for product: Product, by customer: Customer) { 87 | let productCount = inventoryDatabase.inventory[product, default: 0] 88 | 89 | guard productCount > 0 else { 90 | print("\(product.name)缺货") 91 | return 92 | } 93 | 94 | inventoryDatabase.inventory[product] = productCount - 1 95 | 96 | var pendingShipmentProducts = shippingDatabase.pendingShipments[customer, default: []] 97 | pendingShipmentProducts.append(product) 98 | shippingDatabase.pendingShipments[customer] = pendingShipmentProducts 99 | 100 | print("\(customer.name)购买了\(product.name)") 101 | } 102 | } 103 | ``` 104 | 105 | 在订单门面里,持有了`InventoryDatabase`和`ShippingDatabase`实例。还提供了一个下单的方法:先查询要下单的产品的库存,如果库存为0,则提示缺货;如果库存足够,把库存减一,并加到待发货数据库中。 106 | 107 | #### 使用 108 | 109 | ```swift 110 | let cola = Product(id: UUID().uuidString, name: "可乐", price: 3) 111 | let sprite = Product(id: UUID().uuidString, name: "雪碧", price: 3) 112 | 113 | let inventoryDatabase = InventoryDatabase(inventory: [cola: 10, sprite: 10]) 114 | let shippingDatabase = ShippingDatabase() 115 | 116 | let orderFacade = OrderFacade(inventoryDatabase: inventoryDatabase, 117 | shippingDatabase: shippingDatabase) 118 | 119 | let customer = Customer(id: UUID().uuidString, 120 | address: "深圳南山区xx街道xx小区10栋", 121 | name: "Lebron James") 122 | 123 | orderFacade.placeOrder(for: cola, by: customer) 124 | 125 | // 打印 126 | Lebron James购买了可乐 127 | ``` 128 | 129 | 这里,存入了10瓶可乐和10瓶雪碧到库存中,然后用`orderFacade`去下单。 130 | 131 | 至此,我们完成了一个订单门面。 132 | 133 | ### 总结 134 | 135 | 在实际开发中,我们可能会创建一个或多个门面。如果一个门面有多个方法分别在不同的类使用,那么需要考虑把这个门面分成多一个门面。 136 | -------------------------------------------------------------------------------- /design-pattern/17 - 享元模式 (Flyweight Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 享元模式属于创建型模式,通过复用内存中的对象,减少内存的占用。 6 | 7 | 这个模式涉及两个部分:1)flyweight对象;2)创建 flyweight 的方法。 8 | 9 | ``` 10 | +---------------------------------------------+ 11 | | Flyweight | 12 | +---------------------------------------------+ 13 | | static flyweight: SomeObjectType | 14 | +---------------------------------------------+ 15 | | static flyweight(for:) -> SomeObjectType | 16 | +---------------------------------------------+ 17 | ``` 18 | 19 | 其实,享元模式很像单例模式,他们都有让整个程序共享某个对象的特性。但是,在享元模式中,可以拥有多个同一类型的对象。例如,我们`UIColor`中的各种系统自带的颜色,每一种颜色都是一个单例。 20 | 21 | ### 什么时候使用 22 | 23 | 在需要使用多个不同配置的单例时使用。 24 | 25 | ### 简单demo 26 | 27 | #### iOS 中用到的享元模式 28 | 29 | 上面提到了`UIColor`其实是使用了享元模式,我们来验证一下: 30 | 31 | ```swift 32 | let black1 = UIColor.black 33 | let black2 = UIColor.black 34 | print(black1 === black2) // true 35 | ``` 36 | 37 | 结果是 `true`,很明显`black`是一个单例。 38 | 39 | 来看看我们自己创建的黑色: 40 | 41 | ```swift 42 | let black3 = UIColor(red: 0, green: 0, blue: 0, alpha: 1) 43 | let black4 = UIColor(red: 0, green: 0, blue: 0, alpha: 1) 44 | print(black3 === black4) // false 45 | ``` 46 | 47 | 都是黑色,但是他们是不同的对象。 48 | 49 | 我们再看看`UIFont`: 50 | 51 | ```swift 52 | let font1 = UIFont.systemFont(ofSize: 15) 53 | let font2 = UIFont.systemFont(ofSize: 15) 54 | print(font1 === font2) // true 55 | ``` 56 | 57 | 结果也是 `true`,说明`UIFont`也是使用了享元模式。 58 | 59 | #### Demo 60 | 61 | 在开发中,难免会遇到自定义字体的需求,这里以自定义字体为例,假设在项目中要用到**SFUIText**这个系列的字体: 62 | 63 | ```swift 64 | extension UIFont { 65 | 66 | enum SFUITextFontStyle: String { 67 | case medium = "SFUIText-Medium" 68 | case regular = "SFUIText-Regular" 69 | case heavy = "SFUIText-Heavy" 70 | case bold = "SFUIText-Bold" 71 | case semibold = "SFUIText-Semibold" 72 | } 73 | 74 | static var SFUITextFonts: [String: UIFont] = [:] 75 | 76 | static func SFUITextFont(style: SFUITextFontStyle, 77 | size: CGFloat) -> UIFont? { 78 | 79 | let key = "\(style.rawValue)\(size)" 80 | if let font = SFUITextFonts[key] { 81 | return font 82 | } 83 | 84 | let font = UIFont(name: style.rawValue, size: size) 85 | SFUITextFonts[key] = font 86 | return font 87 | } 88 | } 89 | ``` 90 | 91 | 首先用枚举列举了字体的粗细程度;然后定义`SFUITextFonts`来存储创建过的字体;最后定义一个创建字体的方法,如果已经创建过的字体,则直接在缓存中取出。 92 | 93 | 我们来验证一下: 94 | 95 | ```swift 96 | let font1 = UIFont.SFUITextFont(style: .heavy, size: 20) 97 | let font2 = UIFont.SFUITextFont(style: .heavy, size: 20) 98 | print(font1 == font2) // true 99 | ``` 100 | 101 | 结果是 `true`。 102 | 103 | 104 | ### 总结 105 | 106 | 在使用享元模式的过程中,需要考虑缓存数据是否很增长得很大。虽然我们用缓存来保存了一些对象,避免重复创建,以减少内存的使用,但是也有可能缓存会占用比较大的内存。所以我们可能要考虑设定缓存的最大占用内存,如果超过了设定值,则把以前的数据删掉,保留最近的数据。 107 | 108 | 另外还要注意的是,享元模式只能用于 Class 类型,不能用于 Struct 类型,因为 Struct 类型是值类型, 109 | -------------------------------------------------------------------------------- /design-pattern/19 - 组合模式 (Composite Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 组合模式属于结构型模式,可以把多个对象整理成一个树状结构,把这些对象当做一个对象来处理。涉及以下三种类型: 6 | 7 | - **Component 协议**:这个协议保证了可以使用同样的方式来处理整棵树中的对象 8 | - **Leaf**:树结构中的一个没有子元素的组件 9 | - **Composite**:用来存储 Leaf 对象和 Composite 的容器 10 | 11 | 所有的 Leaf 和 Composite 都遵循 Component 协议,所以我们可以在 Composite 中存储不同类型的 Leaf 对象。例如,数组是一个 Composite,Component可以是 String、Int 等,也可以是一个数组。 12 | 13 | ### 什么时候使用 14 | 15 | 如果类的层级结构形成了一个分支模式,我们分别创建分支和节点两种类型来处理的话,这些类之间将会变得难以交互。这种情况下,我们可以使用组合模式,通过让分支和节点遵循同一个协议,就可以把他们当做一个对象来处理。Component 协议在这个模式中,起到一个抽象的层的作用,减少他们的复杂程度。 16 | 17 | ### 简单demo 18 | 19 | 仅从上面的理论描述,是比较难理解这个模式的。这里以我们常见的电脑中的文件的层级关系来 demo 一下这个模式。 20 | 21 | 文件夹中,可以存储某一类具体类型的文件,例如`.pdf`、`.mp3`等,还可以存储文件夹。不管是具体的文件还是文件夹,在他们上面点击右键有一些共同的操作,例如**打开**、**删除**和**重命名**等。我们可以在文件夹中存储不同类型的文件和文件夹,就是因为他们都遵循了 Component 协议。 22 | 23 | 下面来看看代码。 24 | 25 | #### Component 协议 26 | 27 | 首先定义 Component 协议: 28 | 29 | ```swift 30 | protocol File { 31 | var name: String { get set } 32 | func open() 33 | } 34 | ``` 35 | 36 | 定义了`File`协议,所有的 Leaf 和 Composite 对象都要遵循这个协议。 37 | 38 | #### Leaf 39 | 40 | ```swift 41 | final class PDF: File { 42 | var name: String 43 | init(name: String) { 44 | self.name = name 45 | } 46 | 47 | func open() { 48 | print("正在打开\(name)") 49 | } 50 | } 51 | 52 | final class Music: File { 53 | var name: String 54 | var artist: String 55 | 56 | init(name: String, artist: String) { 57 | self.name = name 58 | self.artist = artist 59 | } 60 | 61 | func open() { 62 | print("正在播放\(artist)的\(name)") 63 | } 64 | } 65 | ``` 66 | 67 | 定义了两个 Leaf 类型,分别是`PDF`和`Music`,并且遵循`File`协议,各自实现了`open()`方法。 68 | 69 | #### Composite 70 | 71 | ```swift 72 | final class Folder: File { 73 | var name: String 74 | private(set) var files: [File] = [] 75 | 76 | init(name: String) { 77 | self.name = name 78 | } 79 | 80 | func addFile(_ file: File) { 81 | files.append(file) 82 | } 83 | 84 | func open() { 85 | print("\n") 86 | print("正在显示以下文件:") 87 | files.forEach { print("--- \($0.name)") } 88 | } 89 | } 90 | ``` 91 | 92 | `Folder`属于 Composite,实现了`File`协议,另外有一个数组可以存储其他 `File` 类型,也就意味着 `Folder`不仅可以存储`PDF`和`Music`,也可以存储其他`Folder`对象。 93 | 94 | #### 使用 95 | 96 | ```swift 97 | // 创建两个文件夹 98 | let desktop = Folder(name: "桌面") 99 | let musicFolder = Folder(name: "我最爱的音乐") 100 | 101 | // 创建具体的文件 102 | let iOSDesignPattern = PDF(name: "iOS设计模式") 103 | 104 | let diYiCi = Music(name: "第一次", artist: "光良") 105 | let liXiang = Music(name: "理想", artist: "赵雷") 106 | 107 | // 桌面文件夹添加音乐文件夹和 PDF 文件 108 | desktop.addFile(musicFolder) 109 | desktop.addFile(iOSDesignPattern) 110 | 111 | // 把两个音乐添加到音乐文件夹 112 | musicFolder.addFile(diYiCi) 113 | musicFolder.addFile(liXiang) 114 | 115 | // 打开文件 116 | iOSDesignPattern.open() 117 | liXiang.open() 118 | 119 | // 打开文件夹 120 | desktop.open() 121 | musicFolder.open() 122 | 123 | 124 | // 结果 125 | 正在打开iOS设计模式 126 | 正在播放赵雷的理想 127 | 128 | 正在显示以下文件: 129 | --- 我最爱的音乐 130 | --- iOS设计模式 131 | 132 | 正在显示以下文件: 133 | --- 第一次 134 | --- 理想 135 | ``` 136 | 137 | 从例子中可以看到,我们可以对不同的对象调用同一个方法,让程序变得非常简单。 138 | 139 | ### 总结 140 | 141 | 利用组合模式,我们可以对不同的对象以相同的方式处理。想象一下,如果没有 Component 协议,要创建一个文件的容器会变得有多复杂。但是在使用组合模式之前,首先要保证应用有分支结构。 142 | -------------------------------------------------------------------------------- /design-pattern/20 - 命令模式 (Command Pattern).md: -------------------------------------------------------------------------------- 1 | > 这篇文章是我阅读[raywenderlich.com](https://store.raywenderlich.com)的[Design Patterns by Tutorials](https://store.raywenderlich.com/products/design-patterns-by-tutorials)的总结,文中的代码是我阅读书本之后根据自己的想法修改的。如果想看原版书籍,请点击链接购买。 2 | 3 | *** 4 | 5 | 命令模式属于行为模式,他把命令的相关信息封装到一个命令对象里。这个模式设计三个类型: 6 | 7 | - **Invoker**:存储并执行命令。 8 | - **Command**:封装命令的具体行为。 9 | - **Receiver**:接收命令的对象。 10 | 11 | ### 什么时候使用 12 | 13 | 当我们要创建可以让不同的Receivers接收的行为时,使用命令模式。 14 | 15 | ### 简单demo 16 | 17 | 现在的小区房,一般都有保安在看门,我们就保安开关门为例:1)因为保安是执行开门命令的人,所以是 Invoker;2)封装了如何开门的对象就是 Command,;3)接受命令的就是门,所以门是 Receiver。 18 | 19 | #### Command 和 Receiver 20 | 21 | 这里,我们把封装如何开门的类命名为 `DoorCommand`;Receiver 命名为`Door`: 22 | 23 | ```swift 24 | final class Door { 25 | var isOpen = false 26 | } 27 | 28 | class DoorCommand { 29 | let door: Door 30 | init(door: Door) { self.door = door } 31 | func execute() {} 32 | } 33 | 34 | final class OpenCommand: DoorCommand { 35 | override func execute() { 36 | print("正在开门") 37 | door.isOpen = true 38 | } 39 | } 40 | 41 | final class CloseCommand: DoorCommand { 42 | override func execute() { 43 | print("正在关门") 44 | door.isOpen = false 45 | } 46 | } 47 | ``` 48 | 49 | 1) 首先定义了`Door`,只有一个属性`isOpen`;2) `DoorCommand`作为一个基类,持有`door`属性,还有一个命令执行的方法`excecute()`;3) 分别创建开门和关门的命令。 50 | 51 | #### Invoker 52 | 53 | ```swift 54 | final class Doorman { 55 | let door: Door 56 | 57 | let closeCommand: CloseCommand 58 | let openCommand: OpenCommand 59 | 60 | init(door: Door) { 61 | closeCommand = CloseCommand(door: door) 62 | openCommand = OpenCommand(door: door) 63 | self.door = door 64 | } 65 | 66 | func closeDoor() { 67 | closeCommand.execute() 68 | } 69 | 70 | func openDoor() { 71 | openCommand.execute() 72 | } 73 | } 74 | ``` 75 | 76 | 这里定义了`Doorman`,角色属于 Invoker,持有`Door`和开关门的命令,另外还定义了发送开关门命令的方法。 77 | 78 | #### 使用 79 | 80 | ```swift 81 | let door = Door() 82 | let doorman = Doorman(door: door) 83 | 84 | doorman.openDoor() 85 | doorman.closeDoor() 86 | 87 | // 结果 88 | 正在开门 89 | 正在关门 90 | ``` 91 | 92 | 创建Door 和 Doorman,然后 Doorman 发送开关门的命令。 93 | 94 | ### 总结 95 | 96 | 命令模式会产生很多命令对象,最终可能导致代码很难维护。所以,如果我们的命令不需要稍后执行,直接调用 Recevier 的方法即可。 97 | -------------------------------------------------------------------------------- /fastlane/01 - Fastlane 介绍、安装和初始化.md: -------------------------------------------------------------------------------- 1 | # 01 - Fastlane 介绍、安装和初始化 2 | 3 | ## 介绍 4 | 5 | Fastlane 是一个命令行工具的集合,用来改进和自动化与 App Store 生态的交互。它包含了大量的原子操作,每个操作用于处理一个特定的任务。他不进让很多事情变得更简单,更是简化了你和你的团队的整个 iOS 开发周期。例如,如果我们手动去准备 screenshots 和相关的元数据,在 Fastlane 中我们可以定义一个 lane 去处理这件事情,然后只需要执行一个命令就可以处理手动处理的工作。 6 | 7 | Fastlane 是 2014 年开源的,我们可以在 GitHub 中找到它,[GitHub - fastlane/fastlane](https://github.com/fastlane/fastlane),使用 Ruby 编写。我们可能会问:为什么不用 Swift 编写?其实 Fastlane 团队有使用 Swift 编写了 beta 版本,但团队还是建议在生产环境中使用 Ruby 来编写。目前 Swift 版本的 Fastlane 还没有开发完成。我们需要使用 Ruby 来编写 lane,但不太懂 ruby 也没太大关系。 8 | 9 | ## 安装 10 | 11 | ### 安装 command line developer tools 12 | 13 | 执行 `xcode-select --install` 命令: 14 | - 如果提示 `xcode-select: error: command line tools are already installed, use "Software Update" to install updates`,说明已经安装好; 15 | - 否则会弹出一个窗口,点击 install 来安装。 16 | 17 | 安装好之后,执行 `xcode-select --print-path` 查看 command line 的路径,如果存在,说明安装好了。 18 | 19 | ### 安装 homebrew 20 | 21 | 如果已经安装好了 homebrew,则可以跳过这一步。还没安装的,则运行这个命令来安装:`/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` 22 | 23 | ### 安装 ruby 24 | 25 | 目前的 mac 电脑都应该自带了 ruby,我们可以执行 `ruby -v` 检查一下: 26 | - 如果版本号大于 `2.3.7`,那就可以了; 27 | - 否则执行 `brew install ruby` 来安装 ruby,ruby 安装好之后,要把刚刚安装好的 ruby 的路径指定到 shell 的配置文件,shell 的配置文件的路径可能是 `.profile` 和 `.bash_profile`,或者是其他;在配置文件中加上 `export PATH = "usr/local/opt/rby/bin:$PATH"`并且保存。 28 | 29 | 最后执行 `gem install bundler` 完成 ruby 的安装。 30 | 31 | ## 安装 git 32 | 33 | 一般 Mac 电脑自带 git,如果没有的,执行 `brew install git`。 34 | 35 | ## 安装 fastlane 36 | 37 | 执行以下其中一条命令: 38 | 39 | ```ruby 40 | # 使用 RubyGems 41 | sudo gem install fastlane 42 | 43 | # 使用 Homebrew 44 | brew cask install fastlane 45 | ``` 46 | 47 | ## 初始化 48 | 49 | 在项目的根目录中执行 `fastlane init`: 50 | - 首先会询问你用 fastlane 来干嘛,我们选择第四个 `Manual setup`,输入 4,然后按 enter 键 51 | - 剩下的其他提示,都按 enter 键 52 | 53 | 打开根目录,你会看到一些自动生产的文件,后续的自定义 lanes 会保存在 `fastlane/Fastfile` 文件中。 -------------------------------------------------------------------------------- /fastlane/02 - 创建 app.md: -------------------------------------------------------------------------------- 1 | # 02 - 创建 app 2 | 3 | Fastlane 的功能非常丰富,教程中不可能涉及到所有的功能,很多时候还是需要自己去看文档。所以在这篇文章,我以创建一个新的app为例,详细讲解下如何使用 fastlane 的文档。 4 | 5 | ## Actions 6 | 7 | 每当要把一个手动的任务变成自动化,我们需要使用 fastlane 中与任务对应的工具,这个工具就叫做 action。 8 | 9 | 打开文档主页[fastlane docs](https://docs.fastlane.tools/),在左边的侧栏找到 `Available Actions`,点击进去,可以看到很多不同的分类,以后遇到新的自动化需求可以在这里查找。 10 | 11 | ## produce action 12 | 13 | 在这篇文章中,我们使用一个 action 来创建 app,这个 action 叫做 `produce`, 属于 `App Store Connect`分类。找到它之后,点击进去可以看到具体文档。`produce` action 其实是 `create_app_online` 的别名。 14 | 15 | 下面我们在终端应用程序中尝试一下这个 action: 16 | 17 | ``` 18 | # 1. 执行 fastlane produce,这是执行一个 action 的方法:fastlane "action 名称" 19 | fastlane produce 20 | 21 | # 2. 提示: Your Apple ID Username (让你输入 Apple Id) 22 | lebron@test.com 23 | 24 | # 3. 如果你苹果开发者账号下有多个 team,则选择你需要创建 app 的 team 25 | 26 | # 4. 提示: App Identifier (Bundle ID, e.g. com.krausefx.app) (让你输入 bundle id) 27 | com.lebron.iosapp 28 | 29 | # 5. 提示: App Name (让你输入应用名称) 30 | My First App 31 | 32 | # 6. 如果你的 App Store Connect 账号下有多个 team,则选择你需要创建 app 的 team 33 | 34 | # 7. 最终创建成功,输出 app id 35 | ``` 36 | 37 | 上面的例子中,我们执行的是最原始的命令,他会提示你输入账号和 app 的各项具体信息。其实我们可以在执行 `fastlane produce` 时带上所有参数。可以通过查看网页文档或者在终端输入 `fastlane produce --help` 来查看如何加上参数。看完文档后,我们可以把上面的例子写成一个命令: 38 | 39 | ```ruby 40 | fastlane produce \ 41 | --username lebron@test.com \ 42 | --app_identifier com.lebron.iosapp \ 43 | --app_name "My First App" \ 44 | --team_name "Lebron Team" \ 45 | --itc_team_name "Lebron Team" 46 | ``` 47 | 48 | `\` 意思是把下一行的命令连接起来。 49 | 50 | 另外,还可以执行 `fastlane actions produce` 来更好的查看各项参数的具体描述。 51 | -------------------------------------------------------------------------------- /fastlane/03 - lane.md: -------------------------------------------------------------------------------- 1 | # 03 - lane 2 | 这篇文章我们来学习 fastlane 的一个核心内容 `lane`。每一个 lane 都在项目中的 `Fastfile` 文件定义,并且都跟一个特定的 action 绑定。 3 | 4 | ## Ruby 5 | 6 | 有些开发者可能对 ruby 不熟悉,这里以一个 Fastfile 文件为例,讲解一下Ruby 的语法: 7 | 8 | ```ruby 9 | platform :iOS do 10 | 11 | lane :first do 12 | archive 13 | sign 14 | upload 15 | end 16 | 17 | lane :second do 18 | build_app(scheme: "myAmazingApp") 19 | upload_to_testflight 20 | end 21 | 22 | end 23 | ``` 24 | 25 | 在这个例子中,可以明显的看到最外层的 block (第一个 `do` 开始到最有一个 `end` 结束)只在系统是 iOS 时才运行,然后里面包含了两个 lane: `first` 和 `second`。 下面看一下具体代码: 26 | - 代码中的 `platform` 和 `lane` 是 Ruby 方法,在 ruby 方法中,当没有或者只有一个参数时,可以省略括号,`do-end` 表示一个 block。 27 | - 代码中的`:iOS`、 `:first` 和 `:second` 是 Ruby 中的 symbol,其实可以看做是 Swift 中的常量。 28 | - 在每一个 lane 里面的代码是 actions,其实是 ruby 方法,可以根据需求带上参数,并且会按照编写顺序执行。 29 | 30 | ## 编写 lane 31 | 32 | 了解以上的 Ruby 语法,我们可以把上一篇文章的 produce action 写入 Fastfile 中,代码如下: 33 | 34 | ```ruby 35 | default_platform(:ios) 36 | 37 | platform :ios do 38 | 39 | lane :register_app do 40 | produce( 41 | username: "lebron@test.com", 42 | app_identifier: "com.lebron.iosapp", 43 | app_name: "My First App", 44 | team_name "Lebron Team", 45 | itc_team_name: "Lebron Team" 46 | ) 47 | end 48 | 49 | end 50 | ``` 51 | 52 | 代码中 `produce` 方法可以具体添加哪些参数,可以查看produce 文档 [produce - fastlane docs](https://docs.fastlane.tools/actions/produce/#produce) 后面的 Parameters 部分。 53 | 54 | 然后在项目的根目录中下,就可以在终端执行 `fastlane register_app` 命令来创建 app。 55 | 56 | 但是在实际开发中,因为对于一个项目,创建 app 只需要做一次,所以通常不会写成 lane,而是直接在终端使用命令执行。需要重复操作的步骤我们才写成lane。 57 | -------------------------------------------------------------------------------- /fastlane/04 - 证书管理.md: -------------------------------------------------------------------------------- 1 | # 04 - 证书管理 2 | 3 | Fastlane 有一个 action 用于证书的管理,叫做 `cert`,是 `get_certificates` 的别名。文档链接:[cert - fastlane docs](https://docs.fastlane.tools/actions/cert/#cert) 4 | 5 | 像最开始学习 `produce` 一样,我们可以在项目的根目录中执行最原始的命令: `fastlane cert`,看看结果如何。这里就不演示了,在执行过程中根据提示输入对应的信息即可。 6 | 7 | 在这个命令的底层中,它会检查是否有有效的 iOS 证书,如果有它就会把证书下载到本地并且安装好;如果没有,它就会创建一个新的 private key,并保存在 keychain 中,然后提交一个新的签名请求,最后获得新的证书。 8 | 9 | lane 的编写如下: 10 | 11 | ```ruby 12 | lane :sanbox do 13 | cert( 14 | username: "lebron@test.com", 15 | team_name: "Lebron Team", 16 | development: true # 指定生产还是开发版本,默认是生产 17 | ) 18 | end 19 | ``` 20 | -------------------------------------------------------------------------------- /fastlane/05 - Provisioning.md: -------------------------------------------------------------------------------- 1 | # 05 - Provisioning 2 | 3 | Fastlane 有一个 action 用于 provisioning profile 的管理,叫做 `sigh`,是 `get_provisioning_profile` 的别名。文档链接:[sigh - fastlane docs](https://docs.fastlane.tools/actions/sigh/#sigh) 4 | 5 | `sigh` 通常与 `cert` 一起使用,它支持所有四种 provisioning profiles 类型。 6 | 7 | lane的写法如下: 8 | 9 | ```ruby 10 | lane :sanbox do 11 | cert( 12 | username: "lebron@test.com", 13 | team_name: "Lebron Team", 14 | development: true # 指定生产还是开发版本,默认是生产 15 | ) 16 | sigh( 17 | username: "lebron@test.com", 18 | team_name: "Lebron Team", 19 | app_identifier: "com.lebron.iosapp", 20 | development: true 21 | ) 22 | end 23 | ``` 24 | -------------------------------------------------------------------------------- /fastlane/06 - Appfile 的使用.md: -------------------------------------------------------------------------------- 1 | # 06 - Appfile 的使用 2 | 3 | 在上一篇文章中,我们把 `cert` 和 `sigh` 一起使用,代码如下: 4 | 5 | ```ruby 6 | lane :sanbox do 7 | cert( 8 | username: "lebron@test.com", 9 | team_name: "Lebron Team", 10 | development: true 11 | ) 12 | sigh( 13 | username: "lebron@test.com", 14 | team_name: "Lebron Team", 15 | app_identifier: "com.lebron.iosapp", 16 | development: true 17 | ) 18 | end 19 | ``` 20 | 21 | 我们发现这里有很多参数是重复的。这篇文章的 Appfile 就是用来解决这个问题的。Appfile 的文档:[Appfile - fastlane docs](https://docs.fastlane.tools/advanced/Appfile/#appfile) 22 | 23 | 在 Appfile 中保存相同的参数,代码如下: 24 | 25 | ```ruby 26 | team_id "Q2CBPJ58CA" # team_name 为 Lebron Team 的 id 27 | apple_id "lebron@test.com" 28 | app_identifier "com.lebron.iosapp" 29 | ``` 30 | 31 | 然后 Fastfile 可以修改为: 32 | 33 | ```ruby 34 | lane :sanbox do 35 | cert(development: true) 36 | sigh(development: true) 37 | end 38 | ``` 39 | -------------------------------------------------------------------------------- /fastlane/07 - 团队代码签名.md: -------------------------------------------------------------------------------- 1 | # 07 - 团队代码签名 2 | 3 | 在前面的文章所学习的 actions 都是针对于个人开发者的。单通常在实际开发中,我们都处于一个团队。有经验的开发者都知道,在一个团队中,保持每个人的各种证书和 provisioning profile 同步是很麻烦的。所以我们为什么不把团队成员开发中需要用到的各种配置文档放在一个地方,然后每个成员去那个地方下载呢? `git` 是一个很好的存放位置,因为它是中心化的。 4 | 5 | 我们可能会担心把证书和 provisioning profile之类的放在 git 会不会有安全问题?Fastlane 有评估过这个问题:首先 git 仓库必须是私有的;默认情况下,我们的代码和代码签名需要用到的文件在存储到 git 仓库之前,已经自动通过 OpenSSL 加密。所以其实无须担心这个问题。退一步来说,项目的代码都可以放心地存储在私有 git 仓库中,代码签名需要用到的文件何尝不可?为了安全,Fastlane 建议我们的 git 账号开启两步验证,保证账号安全。 6 | 7 | ## match 8 | 9 | Fastlane 中用于处理团队代码签名问题的 action 叫做 `match`,是 `sync_code_signing` 的别名。文档:[match - fastlane docs](https://docs.fastlane.tools/actions/match/#match) 10 | 11 | ### 使用步骤 12 | 13 | - 在使用 match 之前,我们要有一个私有的 git 仓库,用于存储代码签名需要用到的文件 14 | - 在项目根目录执行 `fastlane match init` 15 | - 终端提示输入仓库的 url,输入正确的仓库 url 16 | - 完成后,在项目的`fastlane` 文件夹会多了一个文件`Matchfile` 17 | - 最后,就可以执行 `fastlane match development(adhoc/appstore)` 来创建对应的证书和配置文件。在命令的执行过程中,终端会提示输入一个私钥,记得一定要保存好这个密码。 18 | - 查看 git 仓库就可以看到我们刚刚创建的配置文件了。 19 | 20 | ## 编写 lane 21 | 22 | 随着开发的进行,可能会有新的测试设备要加入,所以我们希望在每次使用 match 创建配置文件时能把新的测试设备加进来。打开 Matchfile 加入 `force_for_new_devices(true)` 即可。 23 | 24 | 通过查看文档,可以把 development 的 lane 写成: 25 | 26 | ```ruby 27 | Lane :sync_all_development do 28 | match(type: "development") 29 | end 30 | ``` 31 | 32 | 通常在执行match之前,先清理以前存在的 certs 和 provisioning files:`fastlane match nuke development(distribution/enterprise)` 33 | 34 | 另外我们还可以直接使用 fastlane 来注册新的设备,这个 action 叫做 `register_devices` ,链接:[register_devices - fastlane docs](https://docs.fastlane.tools/actions/register_devices/)。写法如下: 35 | 36 | ```ruby 37 | lane :sync_device_info do 38 | register_devices( 39 | devices: { 40 | "Luka iPhone 6" => "1234567890123456789012345678901234567890", 41 | "Felix iPad Air 2" => "abcdefghijklmnopqrstvuwxyzabcdefghijklmn" 42 | } 43 | # devices_file: "./devices.txt" 44 | ) 45 | end 46 | ``` 47 | 48 | 我们可以把设备信息直接传入到参数中,或者保存一个 txt 文件中。设备信息在 txt 文件中的保存格式请查看:http://devimages.apple.com/downloads/devices/Multiple-Upload-Samples.zip 49 | 50 | 如果想要同步各个开发阶段的证书和配置文件,可以这样写 lane: 51 | 52 | ```ruby 53 | lane :bootstrap_code_signing do 54 | sync_device_info 55 | match(type: "development") 56 | match(type: "adhoc") 57 | match(type: "appstore") 58 | end 59 | ``` 60 | Fastlane 使用 tips 61 | 62 | ## tips 63 | 64 | 我们可以为每个 lane 加上描述,例子如下: 65 | 66 | ```ruby 67 | desc "Update iOS UDIDs on Developer Protal" 68 | lane :sync_device_info do 69 | register_devices( 70 | devices: { 71 | "Luka iPhone 6" => "1234567890123456789012345678901234567890", 72 | "Felix iPad Air 2" => "abcdefghijklmnopqrstvuwxyzabcdefghijklmn" 73 | } 74 | ) 75 | end 76 | ``` 77 | 78 | 加上 `desc` 之后,执行这个 lane 时,这个 lane 的文档就会自动写入到 readme 中。 79 | -------------------------------------------------------------------------------- /fastlane/08 - 打包和分发.md: -------------------------------------------------------------------------------- 1 | # 08 - 打包和分发 2 | 3 | ## 打包 4 | 5 | Fastlane 有一个 action 用于打包,`gym` ,是 `build_ios_app` 的别名。链接为:[gym - fastlane docs](https://docs.fastlane.tools/actions/gym/) 6 | 7 | ### 用法 8 | 9 | 为了不直接把相关参数直接传入 lane 中,所以我们先执行 `fastlane gym init`,然后会在 fastlane 文件夹中生成一个文件 Gymfile,把文件内容替换为: 10 | ```ruby 11 | clean true # 打包前是否需要 clean 12 | scheme "MyFirstApp" # Xcode 左上角显示的 Scheme 名称 13 | ``` 14 | 15 | 接着我们可以编写打包 ad-hoc 类型的 lane: 16 | 17 | ```ruby 18 | lane :build_adhoc do 19 | gym( 20 | output_directory: "build_Adhoc", 21 | export_method: "ad-hoc" 22 | ) 23 | end 24 | ``` 25 | 26 | 编写打包 app-store 类型的 lane: 27 | 28 | ```ruby 29 | lane :build_appstore do 30 | gym( 31 | output_directory: "build_Appstore", 32 | export_method: "app-store" 33 | ) 34 | end 35 | ``` 36 | 37 | ### increment_build_number action 38 | 39 | 在打包的时候,通常我们要更新build 的版本号。Fastlane 提供了 `increment_build_number` ,文档链接:[increment_build_number - fastlane docs](https://docs.fastlane.tools/actions/increment_build_number/#increment_build_number)。默认情况下他会自动加 1,也可以自己设置版本号。 40 | 41 | 在Xcode 中还有一个地方要设置. 默认情况下,Xcode 没有使用任何的版本号系统,所以要让 `increment_build_number` 工作,我们需要做一些设置。在 `Build Settings` 中找到 `Versioning System`,把它改为 `Apple Generic`,这样就可以了。 42 | 43 | ![](images/C1A0500C-9862-429E-8EEF-D992B8D73731.png) 44 | 45 | ## 分发到 TestFlight 46 | 47 | Fastlane 有一个 action 用于分发到 TestFlight,`pilot` ,是 `upload_to_testflight` 的别名。链接为:[pilot - fastlane docs](https://docs.fastlane.tools/actions/pilot/) 48 | 49 | 以下是一些常用命令: 50 | - 上传包到 App Store Connect: `fastlane pilot upload` 51 | - 查看已经上传的包: `fastlane pilot builds` 52 | - 查看所有测试用户:`fastlane pilot list` 53 | - 查看特定测试用户的信息:`fastlane pilot find lebron@test.com` 54 | 55 | 一个简单的 lane 编写如下: 56 | 57 | ```ruby 58 | lane :distribute_to_appstore do 59 | build_appstore 60 | pilot( 61 | team_name: "Team Lebron", 62 | notes: "Version {lane_context[SharedValues::VERSION_NUMBER]}, Build {lane_context[SharedValues::BUILD_NUMBER]}" 63 | ) 64 | end 65 | ``` 66 | 67 | 更详细的使用教程请查看 [pilot - fastlane docs](https://docs.fastlane.tools/actions/pilot/)的 Examples 部分。其中的 `lane_context` 可以读取其他 actions 生成的信息,具体可以查看[Lanes - fastlane docs](https://docs.fastlane.tools/advanced/lanes/#lane-context) 68 | 69 | ## 生成专用密码 70 | 在一个[苹果账号管理页面](https://appleid.apple.com/account/manage)专用密码,放到 `.bash_profile` 文件中: 71 | ``` 72 | export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD="xxxx" 73 | ``` 74 | -------------------------------------------------------------------------------- /fastlane/09 - 上线前检查.md: -------------------------------------------------------------------------------- 1 | # 09 - 上线前检查 2 | 3 | Fastlane 有一个 action 可以帮助我们在 app 上线之前做检查,叫做 `precheck`,是 `check_app_store_metadata` 的别名,文档链接:[precheck - fastlane docs](https://docs.fastlane.tools/actions/precheck/) 4 | 5 | ## 用法 6 | 7 | 既然是检查,那必须有一定的规则。首先我们通过执行 `fastlane precheck init` 让 fastlane 帮我们创建Precheckfile 文件,文件模板如下: 8 | 9 | ```ruby 10 | # For more information about this configuration visit 11 | # https://docs.fastlane.tools/actions/precheck/ 12 | 13 | # In general, you can use the options available 14 | # fastlane precheck --help 15 | 16 | # Remove the # in front of the line to enable the option 17 | 18 | # You have three possible values for each rule options 19 | # :skip 20 | # indicates that your metadata will not be checked by this rule 21 | 22 | # :warn 23 | # when triggered, this rule will warn you of a potential problem 24 | 25 | # :error 26 | # when triggered, this rule will cause an error to be displayed and it will prevent any further fastlane commands from running after precheck finishes 27 | 28 | # Examples: 29 | # negative_apple_sentiment(level: :skip) 30 | # curse_words(level: :warn) 31 | # future_functionality(level: :error) 32 | # other_platforms(level: :error) 33 | # placeholder_text(level: :error) 34 | # test_words(level: :error) 35 | # unreachable_urls(level: :error) 36 | # custom_text(data: ["fabric"], level: :warn) 37 | ``` 38 | 39 | 我们可以根据需求设置规则,然后编写 lane: 40 | 41 | ```ruby 42 | lane :release do 43 | precheck 44 | end 45 | ``` 46 | -------------------------------------------------------------------------------- /fastlane/10 - 截图.md: -------------------------------------------------------------------------------- 1 | # 10 - 截图 2 | 3 | 在应用提交审核之前,需要准备截图,通常我们要手动去做。 Fastlane的 `snapshot` action 可以帮助我们解决这个问题。文档链接:[snapshot - fastlane docs](https://docs.fastlane.tools/actions/snapshot/) 。它是通过 UITest 来实现的。 4 | 5 | ## 用法 6 | 7 | 1. 首先要有一个 UITest Target。如果项目中已经有 UI 测试,则可以跳过这一个,没有则去创建一个 8 | 9 | ![](images/234944ED-FE2A-4BA5-9454-381F0F8C134A.png) 10 | 11 | ![](images/88BE07FE-43E8-4FCF-BA8F-A2FED58275C9.png) 12 | 13 | 3. 创建一个新的 scheme:Taraget 选项选择刚刚创建的 UITest Target,Name 参数填写 `Fastlane Snapshots`。 14 | 4. 让刚刚创建的 scheme 支持 run,如下图那个位置勾上: 15 | 16 | ![](images/C48EBFF5-AF06-42FD-8161-EFDDD0437951.png) 17 | 18 | 4. 执行 `fastlane snapshot init`,在 fastlane 目录会生成 `Snapfile` 和 `SnapshotHelper.swift` 文件. 19 | 5. 把 `SnapshotHelper.swift` 文件添加到 UITest Target,然后在tests 文件的 `setUp()` 方法加上以下代码: 20 | 21 | ```swift 22 | let app = XCUIApplication() 23 | setupSnapshot(app) 24 | app.launch() 25 | ``` 26 | 27 | 6. 录制 UI 测试,然后在需要截图的地方使用 snapshot 截图,例如: 28 | 29 | ```swift 30 | func testSnapshot() { 31 | let app = XCUIApplication() 32 | snapshot("1launch") 33 | app.buttons["How’s it Work?"].tap() 34 | snapshot("2HowItWorks") 35 | app.buttons["Sounds Fun!"].tap() 36 | snapshot("3SoundsFun") 37 | } 38 | ``` 39 | 40 | 7. 配置 `Snapfile`: 41 | 42 | ```ruby 43 | scheme("Fastlane Snapshots") 44 | 45 | devices([ 46 | "iPhone 8 Plus" 47 | "iPhone Xs", 48 | "iPhone Xs Max", 49 | ]) 50 | 51 | languages([ 52 | "en-US", 53 | "es-ES" 54 | ]) 55 | 56 | stop_after_first_error true 57 | erase_simulator true 58 | clear_previous_screenshots true 59 | reinstall_app true 60 | ``` 61 | 62 | 8. 终端执行 `fastlane snapshot` 就可以截图了,截图相关文件存储在 `fastlane/screenshots`。 63 | 64 | ## lane 65 | 66 | lane的编写如下: 67 | 68 | ```ruby 69 | lane :release do 70 | snapshot 71 | precheck 72 | end 73 | ``` 74 | -------------------------------------------------------------------------------- /fastlane/11 - 截图边框.md: -------------------------------------------------------------------------------- 1 | # 11 - 截图边框 2 | 3 | 截图弄好之后,我们可以加上对应设备的边框。 Fastlane的 `frameit` action 可以帮助我们解决这个问题。文档链接:[frameit - fastlane docs](https://docs.fastlane.tools/actions/frameit/) 4 | 5 | ## 用法 6 | 7 | 1. 在使用 `frameit` 之前,我们需要安装一个图片处理库: 8 | 9 | ``` 10 | brew install imagemagick 11 | ``` 12 | 13 | 2. 编写 lane 14 | 15 | ```ruby 16 | lane :release do 17 | snapshot 18 | frameit( 19 | path: "./fastlane/screenshots", 20 | rose_gold: true, # 指定真机颜色 21 | ) 22 | precheck 23 | end 24 | ``` 25 | 26 | 3. 我们还可以做更多的自动以,例如:在截图上加文字、设置背景等。具体步骤如下: 27 | 1) 在 `screenshots` 文件夹中创建一个 `Framefile.jsonfile` 文件,用于保存文字的样式配置。 28 | 29 | ```json 30 | { 31 | "default_frame_version": "latest", 32 | "default": { 33 | "keyword": { 34 | "font": "./fonts/Chalkduster.ttf" // 相对于 screenshots 的路径 35 | }, 36 | "title": { 37 | "color": "#B35800" 38 | }, 39 | "padding": 50, 40 | "title_below_image": true, 41 | "background": "./background.jpg", 42 | "show_complete_frame": true, 43 | "data": [] 44 | } 45 | } 46 | ``` 47 | 48 | 2) 设置每张截图的文字。在每一个语言截图的文件夹下,创建 `title.strings` 文件。假设我们只有两张截图,英文目录下的设置如下: 49 | 50 | ``` 51 | "1_Launch" = "Make training Fun!" 52 | "2_HowItWorks" = "As Simple as ABC!" 53 | ``` 54 | 55 | 3) 执行对应的 lane,就可以生成类似下面这样的图片: 56 | 57 | ![](images/4DB46B5B-65DF-4C00-8599-241DA5B57231.png) 58 | -------------------------------------------------------------------------------- /fastlane/12 - 提交审核.md: -------------------------------------------------------------------------------- 1 | # 12 - 提交审核 2 | 3 | Fastlane 的 `deliver` action 用于自动提交应用审核。文档链接:[deliver - fastlane docs](https://docs.fastlane.tools/actions/deliver/) 4 | 5 | ## 用法 6 | 7 | 1. 执行 `fastlane deliver init` 生成 `metadata`文件夹和 `Deliverfile` 文件, `metadata`文件夹存储了 app 的相关信息。 8 | 2. 编辑`Deliverfile` 文件。这里只是简单的写一个例子,更详细的参数更具自己的需求来查看文档: 9 | 10 | ```ruby 11 | languages(["en-US", "de-De"]) 12 | team_name("Lebron Team") 13 | ``` 14 | 15 | 3. 编写 lane: 16 | 17 | ```ruby 18 | lane :release do 19 | precheck 20 | build_appstore 21 | snapshot 22 | frameit( 23 | path: "./fastlane/screenshots", 24 | rose_gold: true, # 指定真机颜色 25 | ) 26 | deliver( 27 | ipa: "./build_Appstore/MyFirstApp.ipa", 28 | force: true, # 跳过 HTML 文件(在真正提交前,fastlane 默认会弹出 app 信息的预览)确认 29 | submit_for_review: true # 直接提交审核 30 | ) 31 | end 32 | ``` 33 | -------------------------------------------------------------------------------- /fastlane/13 - 集成 git.md: -------------------------------------------------------------------------------- 1 | # 13 - 集成 git 2 | 3 | Fastlane 很好地支持了 git。与 git 相关的 action:[Source-Control - fastlane docs](https://docs.fastlane.tools/actions/#source-control) 4 | 5 | 下面是一下常用的 actions: 6 | 7 | ```ruby 8 | lane :build_appstore do 9 | # 集成 git 10 | ensure_git_status_clean # 保证没有未提交的代码 11 | ensure_git_branch(branch: "master") # 保证在 master 分支 12 | git_pull # 更新到最新的代码 13 | increment_build_number 14 | gym( 15 | output_directory: "build_Appstore", 16 | export_method: "app-store" 17 | ) 18 | commit_version_bump( # 提交 increment_build_number 修改的代码 19 | force: true, 20 | message: "Version bumped by fastlane" 21 | ) 22 | push_to_git_remote # push 到 Git仓库 23 | end 24 | ``` 25 | 26 | 更多用法可以查看文档:[Source-Control - fastlane docs](https://docs.fastlane.tools/actions/#source-control) 27 | -------------------------------------------------------------------------------- /fastlane/14 - 单元测试.md: -------------------------------------------------------------------------------- 1 | # 14 - 单元测试 2 | 3 | Fastlane 的 `scan` action 用于自动化单元测试。文档链接:[scan - fastlane docs](https://docs.fastlane.tools/actions/scan/) 4 | 5 | ## 用法 6 | 7 | 1. 执行 `fastlane scan init`, 生成 `Scanfile` 文件。以下是这个文件的一个例子: 8 | 9 | ```ruby 10 | scheme("MyFirstAppTests") 11 | clean(true) 12 | open_report(true) 13 | ``` 14 | 15 | 2. 编写 lane 16 | 17 | ```ruby 18 | lane :build_appstore do 19 | ensure_git_status_clean 20 | ensure_git_branch(branch: "master") 21 | # 在更改版本号之前添加 22 | scan 23 | increment_build_number 24 | gym( 25 | output_directory: "build_Appstore", 26 | export_method: "app-store" 27 | ) 28 | commit_version_bump( 29 | force: true, 30 | message: "Version bumped by fastlane" 31 | ) 32 | push_to_git_remote 33 | end 34 | ``` 35 | -------------------------------------------------------------------------------- /fastlane/15 - 代码格式检查.md: -------------------------------------------------------------------------------- 1 | # 15 - 代码格式检查 2 | 3 | Fastlane 有一个 `swiftlint` action 用于自动检查代码格式问题。这个 action 需要用到一个第三方的工具:[SwiftLint](https://github.com/realm/SwiftLint) 。如果还没使用过的可以看我之前的一篇文章:[【iOS开发】Swift代码风格检查库 —— SwiftLint - 简书](https://www.jianshu.com/p/f872484fcd50) 4 | 5 | ## 用法 6 | 7 | 编写的 lane 如下: 8 | 9 | ```ruby 10 | lane :lint do 11 | swiftlint( 12 | mode: :lint, 13 | config_file: ".swiftlint.yml", 14 | output_file: "swiftlintOutput.txt", 15 | ignore_exit_status: false 16 | ) 17 | end 18 | ``` 19 | -------------------------------------------------------------------------------- /fastlane/16 - 生成文档.md: -------------------------------------------------------------------------------- 1 | # 【Fastlane - iOS】17 - 生成文档 2 | 3 | - [GitHub - realm/jazzy: Soulful docs for Swift & Objective-C](https://github.com/realm/jazzy) 4 | - [jazzy - fastlane docs](https://docs.fastlane.tools/actions/jazzy/) 5 | -------------------------------------------------------------------------------- /fastlane/images/234944ED-FE2A-4BA5-9454-381F0F8C134A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/fastlane/images/234944ED-FE2A-4BA5-9454-381F0F8C134A.png -------------------------------------------------------------------------------- /fastlane/images/4DB46B5B-65DF-4C00-8599-241DA5B57231.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/fastlane/images/4DB46B5B-65DF-4C00-8599-241DA5B57231.png -------------------------------------------------------------------------------- /fastlane/images/88BE07FE-43E8-4FCF-BA8F-A2FED58275C9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/fastlane/images/88BE07FE-43E8-4FCF-BA8F-A2FED58275C9.png -------------------------------------------------------------------------------- /fastlane/images/C1A0500C-9862-429E-8EEF-D992B8D73731.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/fastlane/images/C1A0500C-9862-429E-8EEF-D992B8D73731.png -------------------------------------------------------------------------------- /fastlane/images/C48EBFF5-AF06-42FD-8161-EFDDD0437951.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/fastlane/images/C48EBFF5-AF06-42FD-8161-EFDDD0437951.png -------------------------------------------------------------------------------- /git/01 - git.md: -------------------------------------------------------------------------------- 1 | 2 | 开源的分布式版本控制工具,在所有的分布式版本控制工具中,git是最快、最简单、最流行的。 3 | 4 | ## GIT和SVN对比 5 | 6 | - 在很多情况下,git的速度远远比SVN快 7 | - SVN是集中式管理,git是分布式管理 8 | - SVN使用分之比较笨拙,git可以轻松拥有无限个分支 9 | - SVN必须联网才能正常工作,git支持本地版本控制工作 10 | - 旧版本的SVN会在每一个目录放置一个.svn,git只会在根目录拥有一个.git 11 | 12 | ## 本地 13 | 14 | - `git config user.name Lebron`: 配置用户名 15 | - `git config user.email youremail@exmaple.com`: 配置邮箱 16 | - `git config --global user.name Lebron`: global,将此设置应用到整个系统中 17 | - `git config --global user.email youremail@exmaple.com`: global,将此设置应用到整个系统中 18 | - `git --help`: git指令帮助 19 | - `git config -l`: 查看配置信息 20 | - `git config -e`: 编辑配置信息 (用vim编辑,输入i进行编辑,:wq是退出编辑) 21 | - `git config alias.别名 原指令名称`: 设置指令别名 22 | - `git config -alias.别名 原指令名称 参数`: 设置带参数指令的别名 23 | - `git status`: 查看文件状态 24 | - `git log`: 查看文件的修改日志;1)用一行的方式查看简单的日志信息:`git log --pretty=oneline`;2)查看最近n次修改:`git log -n` 25 | - `git diff`: 查看文件最新的改动地方 26 | - `git init`: 初始化一个空的本地仓库 27 | - `git add`: 将工作区的文件保存到缓存区;保存当前路径的所有文件到缓存区:`git add .` (注意后面的点) 28 | - `git commit -m "注释" 文件名称`:将缓存区的文件提交到当前分支 29 | - `git reflog`: 查看分支引用记录,能查看所有版本号 30 | - `git reset`: 版本回退(建议加上--hard参数,git支持无限次后悔);2)`git reset --hard HEAD^`:会退到上一个版本;3)`git reset --hard HEAD^^`:回退到上上一个版本;4)`git reset --hard HEAD~n`:回退到上n个版本;5)`git reset --hard 版本号`:回退到任意一个版本,版本号用7位即可 31 | - `git rm`: 删除文件,删除完之后要进行commit操作,才能同步到版本库 32 | - `git clone`: 下载远程仓库到本地 33 | - `git pull`: 下载远程仓库的最新信息到本地仓库 34 | - `git push`: 将本地仓库信息推送到远程仓库 35 | - `git rebase -i`: 合并 36 | - `git checkout test`: 切换分支到test 37 | - `git push origin lz-flurry-99:lz-flurry-99`: 推送本地分支到远程分支 38 | - `git branch -d xxxxx`: 删除本地分支 39 | - `git cherry-pick 9f63dd6`: 把commit放到另一个分支上 40 | - `git push origin --delete `: 删除远程分支,或者用 `git push origin :` 41 | - `git branch -m 原分支名 新分支名`: 分支重命名 42 | 43 | ## `git add -A`、`git add -u` 和 `git add .` 的区别 44 | 45 | - `git add .`: 他会监控工作区的状态树,使用它会把工作时的所有变化提交到暂存区,包括文件内容修改(modified)以及新文件(new),但不包括被删除的文件。 46 | - `git add -u`: 他仅监控已经被add的文件(即tracked file),他会将被修改的文件提交到暂存区,不会提交新文件(untracked file) 47 | - `git add -A`: 上面两个功能的合集(`git add --all`的缩写) 48 | 49 | **总结:** 50 | 51 | - `git add -A`: 提交所有变化 52 | - `git add -u`: 提交被修改(modified)和被删除(deleted)文件,不包括新文件(new) 53 | - `git add .`: 提交新文件(new)和被修改(modified)文件,不包括被删除(deleted)文件 54 | 55 | ## 上传本地代码到github 56 | 57 | - `git remote add origin 地址` 58 | - `git pull origin master`,如果提示`refusing to merge unrelated histories`,使用 `git pull origin master --allow-unrelated-histories` 59 | - `git push -u origin master` 60 | 61 | ## Git的文件状态 62 | 63 | ``` 64 | M = Locally modified 本地修改 65 | 66 | U = Updated in repository 在仓库中被更新 67 | 68 | A = Locally added 在本地添加 69 | 70 | D = Locally deleted 在本地删除 71 | 72 | I = Ignored 被忽略 73 | 74 | R = Replaced in the repository 在仓库中被替换 75 | 76 | – = The contents of the folder have mixed status; display the contents to see individual status // 文件夹中的文件有多种状态(被修改、更新、添加、删除等等) 77 | 78 | ? = Not under source control // 没有加入版本控制 79 | ``` 80 | -------------------------------------------------------------------------------- /git/connecting-to-github-with-ssh.md: -------------------------------------------------------------------------------- 1 | # **connecting to GitHub with SSH** 2 | 3 | ## Generate a new SSH key 4 | ```bash 5 | ssh-keygen -t rsa -b 4096 -C "your_email@example.com" 6 | ``` 7 | 8 | ## Add your SSH key to the ssh-agent 9 | 1. Edit the `~/.ssh/config` file 10 | ```bash 11 | Host github.com 12 | AddKeysToAgent yes 13 | UseKeychain yes 14 | IdentityFile ~/.ssh/id_rsa 15 | ``` 16 | 17 | 2. Start the ssh-agent in the background 18 | ```bash 19 | eval "$(ssh-agent -s)" 20 | > Agent pid 59566 21 | ``` 22 | 23 | 3. Add your SSH private key to the ssh-agent 24 | ```bash 25 | ssh-add --apple-use-keychain ~/.ssh/id_rsa 26 | ``` 27 | 28 | ## Add your SSH key to your GitHub account 29 | 1. Copy the SSH key to your clipboard 30 | ```bash 31 | pbcopy < ~/.ssh/id_rsa.pub 32 | ``` 33 | 2. Go to **GitHub** > **Settings** > **Access** > **SSH and GPG keys** > **New SSH key** 34 | 3. Paste the SSH key to the key field 35 | 4. Click **Add SSH key** 36 | 37 | ## Authorizing an SSH key for use with SAML single sign-on 38 | 1. Go to **GitHub** > **Settings** > **Access** > **SSH and GPG keys** 39 | 2. Click **Configure SSO** 40 | 3. Click **Authorize** 41 | -------------------------------------------------------------------------------- /ios/combine/01 - Combine 基础.md: -------------------------------------------------------------------------------- 1 | > 本文是阅读 [Combine: Asynchronous Programming with Swift](https://store.raywenderlich.com/products/combine-asynchronous-programming-with-swift) 后的学习笔记,有需要的请点击链接购买。 2 | 3 | # 01 - Combine 基础 4 | 5 | 在 Combine 框架面世之前,我们通常会使用以下几种方式来处理异步问题: 6 | - NotificationCenter 7 | - 代理模式 8 | - GCD 和 Operation 9 | - Closure (闭包) 10 | 11 | 但是这几种处理异步问题的方式不好处理执行顺序的问题。并且在实际开发中,我们会根据情况使用不同的方法来处理异步的问题。Combine 可以帮助我们在异步编程中更好地处理执行顺序的问题。 12 | 13 | 苹果已经在 Foundation 框架中集成了 Combine 的 API,例如 `Timer`、`NotificationCenter`和 **Core Data** 等等。 14 | 15 | ## Combine 的背景 16 | 17 | 声明式和响应式编程并不是一个新的概念。早在 2009 年,微软的一个团队就发布了 Rx.NET,并且在 2012 年开源。从那时开始,这个概念就扩展到了其他语言,例如 RxJava, RxJS, RxKotlin, RxScala, RxPHP 等等。 18 | 19 | 在苹果的平台上,也有类似的第三方库,例如 RxSwift 和 ReactiveSwift。终于在今年 2019 年,苹果发布了官方的响应式编程框架 Combine。Combine 只能兼容 iOS 13+, macOS 10.15+, tvOS 13.0+ 和 watchOS 6.0+ 的系统。因为 Combine 对系统的版本要求比较高,所以把 Combine 应用到实际开发中还为时尚早。但是相信这绝对是以后苹果平台开发的一个趋势。提前掌握这个核心的技术还是非常有必要的。 20 | 21 | 下面我们来看一下 Combine 框架的基础。Combine 的三个关键组成部分是:**Publisher**、**Operator** 和 **Subscriber**。 22 | 23 | ## Publisher 24 | 25 | Publisher 可以不定时地向 一个或多个 Subscriber 发出某种类型的值。不管 Publisher 内部的逻辑如何,例如处理网络请求、用户事件等等,他们最终都可以发出下面这三种类型的事件: 26 | 1)一个 Publisher 的泛型 `Output` 类型的值; 27 | 2)一个成功的结束; 28 | 3)一个带有 Error 的结束,类型是 Publisher 的泛型 `Failure` 类型。 29 | 30 | 一个 Publisher 可以发出一个或多个 `Output` 类型的值,如果发出了一个结束事件,不管是成功还是带有 Error 的结束,他都不再发出任何事件。 31 | 32 | 例如下面一个可以发出 Int 类型的 Publisher: 33 | 34 | ``` 35 | Publisher 36 | 值: ---- 1 ---- 3 ---- 5 ---- 8 ---- 9 ---- 20 ---- 39 ---- 50 ---|> 37 | 时间: 0:01 0:03 0:06 0:10 0:19 0:15 0:20 0:50 38 | ``` 39 | 40 | 虚线中间的数字是发出的值;最右边的 `|` 代表一个成功的结束。 41 | 42 | 我们来看一下 `Publisher` 的定义: 43 | 44 | ```swift 45 | public protocol Publisher { 46 | associatedtype Output 47 | associatedtype Failure : Error 48 | func receive(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input 49 | } 50 | ``` 51 | 52 | 从定义中我们可以看到,`Publisher` 包含了两个泛型: 53 | - `Output` : Publisher 发出的值类型。如果一开始定义为了 `Int` 类型,那么它就不能发出 `String`或者其他类型的值。 54 | - `Failure` :Publisher 可以发出的错误结束的类型。如果从头到尾都不发出错误结束,那么可以把 `Failure` 定义为 `Never` 类型。 55 | 56 | 当我们订阅 publisher 时,我们就知道了它发出的值的类型和错误结束的类型。 57 | 58 | ## Operators 59 | 60 | Operators 是 在 Publisher 协议的基础上定义的一系列方法,可以返回同一个或新的 publisher,这样我们就可以把多个 operators 链接起来。它的任务是从上一个operator 接收到的数据通过处理,然后输出处理后的数据,传给下一个 operator。这样我们就保证的代码逻辑的处理顺序。 61 | 62 | 例如 `` 在经过了两个 Operators 之后变成了 ``: 63 | 64 | ``` 65 | ---> Operator() ---> Operator() ---> 66 | ``` 67 | 68 | 我们可以根据不同的需要调整 operators 的顺序。 69 | 70 | ## Subscribers 71 | 72 | Subscribers 从 Publisher 中订阅相关事件,例如发出的值或者结束事件,得到这些事件后,去处理后续的逻辑。 73 | 74 | ``` 75 | ---> 在屏幕上显示 76 | - 77 | - 78 | 2 ---> 4 ---> 6 ---> Subscriber 79 | - 80 | - 81 | ---> 发送到服务器 82 | ``` 83 | 84 | 目前 Combine 提供了两个内置的 subscribers: 85 | - **sink**:可以让我们提供一个 closure 来接收发出的值和结束事件。 86 | - **assign**:可以让我们直接把发出的值绑定到数据模型或者 UI 控件的属性上,直接把最新的数据显示在 UI 上,不需要我们编写任何自定义代码。 87 | 88 | 如果这两个内置的 subscribers 无法满足需求,我们可以很容易地自定义 subscribers。 89 | 90 | ## Subscriptions 91 | 92 | 当我们订阅结束后添加一个 subscriber,他就会激活 publisher。如果没有 subscribers 去订阅 publisher,那么它就不会发出任何值。 93 | 94 | Subscription 可以让我们一次性写好一系列的异步事件。当熟悉了 Combine 之后,我们可以通过 subscriptions 来描述整个 app 的逻辑。一旦把 subscriptions 的代码写好,我们就可以不用去管它了。 95 | 96 | 另外,我们不需要另外对 subscriptions 做内存管理。因为 Combine 提供了一个 `Cancellable` 协议,系统自带的 subscribers 都遵循了这个协议,在 subscription 创建成功之后就会返回一个 `Cancellable` 对象,当这个对象被释放后,整个订阅也会被释放。所以我们只需要把这个 `Cancellable` 对象当做一个属性存储在类似 ViewController 里就可以了。如果有多个 subscriptions,还可以把多个 `Cancellable` 对象存储到 `[Cancellable]`中。 -------------------------------------------------------------------------------- /ios/combine/08 - 网络请求.md: -------------------------------------------------------------------------------- 1 | > 本文是阅读 [Combine: Asynchronous Programming with Swift](https://store.raywenderlich.com/products/combine-asynchronous-programming-with-swift) 后的学习笔记,有需要的请点击链接购买。 2 | 3 | # 08 - 网络请求 4 | 5 | Combine 提供了一些 APIs 来帮助以声明方式执行常见的任务。这些 APIs 围绕着现代应用程序的两个关键组件: 6 | 7 | - `URLSession` 8 | - 通过 `Codable` 协议编码和解码 JSON 9 | 10 | ## URLSession 扩展 11 | 12 | `URLSession` 是执行网络数据传输任务的推荐方法。它提供了一个现代的异步 API,具有强大的配置选项和完全透明的后台支持。它支持多种操作,例如: 13 | 14 | - 获取 URL 内容的数据传输任务。 15 | - 获取 URL 内容并将其保存到文件中下载任务。 16 | - 将文件和数据上传到 URL 的上传任务。 17 | - 在双方之间传输数据的流任务。 18 | - 连接到 Websocket 的 Websocket 任务。 19 | 20 | 但是只有第一个数据传输任务才有对应的 Combine publisher。下面看下如何使用这个 API: 21 | 22 | ```swift 23 | let url = URL(string: "https://mysite.com/mydata.json")! 24 | let subscription = URLSession.shared 25 | .dataTaskPublisher(for: url) 26 | .sink( 27 | receiveCompletion: { completion in 28 | if case .failure(let err) = completion { 29 | print("Retrieving data failed with error \(err)") 30 | } 31 | }, 32 | receiveValue: { data, response in 33 | print("Retrieved data of size \(data.count), response = \(response)") 34 | }) 35 | ``` 36 | 37 | ## Codable 支持 38 | 39 | `Codable` 是一种现代的、强大的、快速的编码和解码机制。Foundation 支持通过 `JSONEncoder` 和 `JSONDecoder` 对 JSON 进行编码和解码。 40 | 41 | 例如,我们从网络下载 JSON 数据,然后解码成自定义 `Codable` 类型: 42 | 43 | ```swift 44 | let subscription = URLSession.shared 45 | .dataTaskPublisher(for: url) 46 | .tryMap { data, _ in 47 | try JSONDecoder().decode(MyType.self, from: data) 48 | } 49 | .sink( 50 | receiveCompletion: { completion in 51 | if case .failure(let err) = completion { 52 | print("Retrieving data failed with error \(err)") 53 | } 54 | }, 55 | receiveValue: { object in 56 | print("Retrieved object \(object)") 57 | }) 58 | ``` 59 | 60 | 上面代码中使用 `tryMap` 操作符来解码 JSON。然而 Combine 还提供了一个 `decode(type:decoder:)` 方法让我们直接解码,修改上面的代码如下: 61 | 62 | ```swift 63 | let subscription = URLSession.shared 64 | .dataTaskPublisher(for: url) 65 | .map(\.data) 66 | .decode(type: MyType.self, decoder: JSONDecoder()) 67 | .sink( 68 | receiveCompletion: { completion in 69 | if case .failure(let err) = completion { 70 | print("Retrieving data failed with error \(err)") 71 | } 72 | }, 73 | receiveValue: { object in 74 | print("Retrieved object \(object)") 75 | }) 76 | ``` 77 | 78 | 使用这个方法解码的唯一好处是 `JSONDecoder` 只需要创建一次,而使用 `tryMap` 会在闭包中每次都要创建。 79 | 80 | ## 向多个 subscribers 发送网络数据 81 | 82 | 每次你向 publisher 订阅,它就会开始工作。在网络请求的情况下,这意味着如果多个 subscribers 需要结果,则多次发送相同的请求。 83 | 84 | 我们可能可以使用 `share()` 运算符,但这不太好处理,因为您需要在结果返回之前订阅所有 subscribers。 85 | 86 | 我们应该使用另外一种解决方案:`multicast()` 操作符。它创建一个 `ConnectablePublisher`,通过一个 `Subject` 发布值。它允许您多次订阅 subject,然后在准备好后调用 publisher 的 `connect()` 方法: 87 | 88 | ```swift 89 | let url = URL(string: "https://www.apple.com")! 90 | 91 | let publisher = URLSession.shared 92 | .dataTaskPublisher(for: url) 93 | .map(\.data) 94 | .multicast { PassthroughSubject() } // 使用 `multicast` 操作符 95 | 96 | // 第一次订阅,`publisher` 不会马上进行网络请求 97 | let subscription1 = publisher 98 | .sink( 99 | receiveCompletion: { completion in 100 | if case .failure(let err) = completion { 101 | print("Sink1 Retrieving data failed with error \(err)") 102 | } 103 | }, 104 | receiveValue: { object in 105 | print("Sink1 Retrieved object \(object)") 106 | }) 107 | 108 | // 第二次订阅,`publisher` 不会马上进行网络请求 109 | let subscription2 = publisher 110 | .sink( 111 | receiveCompletion: { completion in 112 | if case .failure(let err) = completion { 113 | print("Sink2 Retrieving data failed with error \(err)") 114 | } 115 | }, 116 | receiveValue: { object in 117 | print("Sink2 Retrieved object \(object)") 118 | }) 119 | 120 | // 调用 `connect`,开始进行网络请求,然后发出值 121 | let subscription = publisher.connect() 122 | ``` 123 | 124 | 通过这个例子,我们就可以进行一次请求,然后把请求结果发给两个 subscribers。 125 | 126 | > **注意:** 记得要保存所有的 `Cancellable`,否则订阅将会在离开当前代码域后被取消。 127 | -------------------------------------------------------------------------------- /ios/combine/09 - Timers.md: -------------------------------------------------------------------------------- 1 | > 本文是阅读 [Combine: Asynchronous Programming with Swift](https://store.raywenderlich.com/products/combine-asynchronous-programming-with-swift) 后的学习笔记,有需要的请点击链接购买。 2 | 3 | # 09 - Timers 4 | 5 | ## 使用 `RunLoop` 6 | 7 | `RunLoop` 实现了 `Scheduler` 协议。它定义了几个相对底层的方法,和唯一一个允许您创建 cancellable timer 的方法: 8 | 9 | ```swift 10 | let runLoop = RunLoop.main 11 | 12 | let subscription = runLoop.schedule( 13 | after: runLoop.now, 14 | interval: .seconds(1), 15 | tolerance: .milliseconds(100) 16 | ) { 17 | print("Timer fired") 18 | } 19 | ``` 20 | 21 | 这个 timer 不会发出任何值,也不创建 publisher。它只是在指定的参数下启动,然后运行闭包里的代码。这里唯一跟 Combine 相关的是,它会返回 `subscription` 是一个 `Cancellable` 类型,让你可以随时这个 timer。 22 | 23 | 例如: 24 | 25 | ```swift 26 | runLoop 27 | .schedule( 28 | after: .init(Date(timeIntervalSinceNow: 3))) { 29 | subscription.cancel() 30 | } 31 | ``` 32 | 33 | 但综合考虑,`RunLoop` 并不是创建 timer 的最佳方法。你最好用 `Timer` 类! 34 | 35 | ## 使用 `Timer` 类 36 | 37 | 你可以使用下面这种方式创建一个重复的 timer publisher: 38 | 39 | ```swift 40 | let publisher = Timer.publish(every: 1.0, on: .main, in: .common) 41 | ``` 42 | 43 | - `on` 参数是指定 timer 的放在哪个 `RunLoop`。这里是 main RunLoop。 44 | - `in` 参数是指定 timer 在 run loop 的哪种模式下运行。这里是 `.common` run loop 模式。 45 | 46 | 除非您了解 run loop 是如何运行的,否则应该坚持使用这些默认值。通过调用 `RunLoop.current`,您可以为自己创建的或从 Foundation 获得的任何线程获取 `RunLoop`,这样您也可以编写以下内容: 47 | 48 | ```swift 49 | let publisher = Timer.publish(every: 1.0, on: .current, in: .common) 50 | ``` 51 | 52 | > **注意**:把这段代码放在一个 Dispatch queue 而不是 `DispatchQueue.main` 可能会导致不可预料的结果。Dispatch 框架没有使用 run loop 来管理它的线程。因为 run loop 需要调用它的其中一个 `run` 方法来处理事件。你永远不会看到除主队列以外的任何队列上的 timer 执行。因为为了你的 timer 保持安全,应该直接使用 `RunLoop.main`。 53 | 54 | 上面的 timer 返回 publisher 是一个 `ConnectablePublisher`。它是 `Publisher` 的一个变种,在你调用 `connect()` 方法之前,timer 不会启动。另外,你也可以调用 `autoconnect()` 来让它收到第一个 subscriber 订阅时就自动启动。 55 | 56 | 因此,创建在订阅时启动 timer 的 publisher 的最佳方法是写入: 57 | 58 | ```swift 59 | let publisher = Timer 60 | .publish(every: 1.0, on: .main, in: .common) 61 | .autoconnect() 62 | ``` 63 | 64 | 这样这个 timer 就会在有 subscriber 订阅时定时重复的发出当前的日期,因为它的 `Publisher.Output` 类型是 `Date`。 65 | 66 | 下面是一个例子: 67 | 68 | ```swift 69 | let subscription = Timer 70 | .publish(every: 1.0, on: .main, in: .common) 71 | .autoconnect() 72 | .scan(0) { counter, _ in counter + 1 } 73 | .sink { counter in 74 | print("Counter is \(counter)") 75 | } 76 | ``` 77 | 78 | ## 使用 `DispatchQueue` 79 | 80 | 可以使用 `DispatchQueue` 生成 timer 事件。虽然 Dispatch 框架有 `DispatchTimerSource` 事件源,但令人惊讶的是,Combine 没有为其提 timer 接口。相反,您将使用另一种方法在队列中生成 timer 事件。这可能有点复杂: 81 | 82 | ```swift 83 | 84 | let queue = DispatchQueue.main 85 | 86 | // 创建 Subject,用于发布 timer 的值 87 | let source = PassthroughSubject() 88 | 89 | var counter = 0 90 | 91 | // 计划一个重复的任务,这个任务会马上启动 92 | let cancellable = queue.schedule( 93 | after: queue.now, 94 | interval: .seconds(1) 95 | ){ 96 | source.send(counter) 97 | counter += 1 98 | } 99 | 100 | // 订阅 Subject 101 | let subscription = source.sink { 102 | print("Timer emitted \($0)") 103 | } 104 | ``` 105 | -------------------------------------------------------------------------------- /ios/combine/09 - 调试.md: -------------------------------------------------------------------------------- 1 | > 本文是阅读 [Combine: Asynchronous Programming with Swift](https://store.raywenderlich.com/products/combine-asynchronous-programming-with-swift) 后的学习笔记,有需要的请点击链接购买。 2 | 3 | # 09 - 调试 4 | 5 | 理解异步程序中的事件流一直是一个挑战。在 Combine 中尤其如此,因为 publisher 在操作符链中可能不会同时发出所有事件,所以您需要了解内部到底了发生了什么。Combine 提供了一些操作符来帮助调试,了解它们有助于你解决令人费解的情况。 6 | 7 | ## 打印事件 8 | 9 | 当您不确定是否有任何内容正在通过您的 publisher 时,应该首先使用 `print(_:to:)` 操作符。它是一个 passthrough publisher,打印了很多关于正在发生的事情的信息。 10 | 11 | 例如下面这个例子: 12 | 13 | ```swift 14 | let subscription = (1...3).publisher 15 | .print("publisher") 16 | .sink { _ in } 17 | ``` 18 | 19 | 运行结果如下: 20 | 21 | ``` 22 | publisher: receive subscription: (1...3) 23 | publisher: request unlimited 24 | publisher: receive value: (1) 25 | publisher: receive value: (2) 26 | publisher: receive value: (3) 27 | publisher: receive finished 28 | ``` 29 | 30 | 上面这个例子中,使用 `print(_:to:)` 操作符来打印信息: 31 | 32 | - 当 publisher 接收到订阅时,打印 `publisher: receive subscription: (1...3)` 33 | - 打印 subscriber 的 demand 请求,以便您可以看到正在请求的项目数。 34 | - 打印 publisher 发出的每个值。 35 | - 最后,打印结束事件。 36 | 37 | `print(_:to:)` 方法还可以接收第二个参数 `TextOutputStream`。例如: 38 | 39 | ```swift 40 | class TimeLogger: TextOutputStream { 41 | private var previous = Date() 42 | private let formatter = NumberFormatter() 43 | 44 | init() { 45 | formatter.maximumFractionDigits = 5 46 | formatter.minimumFractionDigits = 5 47 | } 48 | 49 | func write(_ string: String) { 50 | let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines) 51 | guard !trimmed.isEmpty else { return } 52 | let now = Date() 53 | print("+\(formatter.string(for: now.timeIntervalSince(previous))!)s: \(string)") 54 | previous = now 55 | } 56 | } 57 | 58 | let subscription = (1...3).publisher 59 | .print("publisher", to: TimeLogger()) 60 | .sink { _ in } 61 | ``` 62 | 63 | 执行结果: 64 | 65 | ``` 66 | +0.00580s: publisher: receive subscription: (1...3) 67 | +0.02573s: publisher: request unlimited 68 | +0.01150s: publisher: receive value: (1) 69 | +0.00026s: publisher: receive value: (2) 70 | +0.00018s: publisher: receive value: (3) 71 | +0.00018s: publisher: receive finished 72 | ``` 73 | 74 | ## 对事件采取操作 - 产生副作用 75 | 76 | 除了打印出信息外,对特定事件执行操作通常也很有用。我们称之为执行副作用,因为您采取的“侧边”操作不会直接影响下流的 publishers,但可以产生类似修改外部变量的效果。 77 | 78 | 这个 79 | `handleEvents(receiveSubscription:receiveOutput:receiveCompletion:rece iveCancel:receiveRequest:)`方法允许您拦截 publisher 生命周期中的任何和所有事件,然后在每个步骤执行想要的操作。 80 | 81 | 假设您正在跟踪 publisher 执行网络请求,然后发出一些数据的问题。当您运行它时,它从不接收任何数据。我们就会问:怎么了?请求真的有效吗? 82 | 83 | 例如下面的代码: 84 | 85 | ```swift 86 | let request = URLSession.shared 87 | .dataTaskPublisher(for: URL(string: "https://www.apple.com/")!) 88 | 89 | request 90 | .sink(receiveCompletion: { completion in 91 | print("Sink received completion: \(completion)") 92 | }, receiveValue: { (data, _) in 93 | print("Sink received data: \(data)") 94 | }) 95 | ``` 96 | 97 | 看起来代码没啥问题,但是一运行,发现没有任何打印。那我们使用 `handleEvents` 方法调试,在 `sink` 前面加上: 98 | 99 | ```swift 100 | .handleEvents(receiveSubscription: { _ in 101 | print("Network request will start") 102 | }, receiveOutput: { _ in 103 | print("Network request data received") 104 | }, receiveCancel: { 105 | print("Network request cancelled") 106 | }) 107 | ``` 108 | 109 | 运行后发现打印: 110 | 111 | ``` 112 | Network request will start 113 | Network request cancelled 114 | ``` 115 | 116 | 很明显,订阅被取消了。就会想到我们没有把订阅保存起来,修改代码: 117 | 118 | ```swift 119 | let subscription = request 120 | .handleEvents... 121 | ``` 122 | 123 | 运行结果就正常了: 124 | 125 | ``` 126 | Network request will start 127 | Network request data received 128 | Sink received data: 69077 bytes 129 | Sink received completion: finished 130 | ``` 131 | 132 | ## 使用 debugger 作为最后手段 133 | 134 | 最后一种方法是在调试器中的某些时候需要反省的情况下使用,因为没有其他方法可以帮助您找出问题所在。 135 | 136 | 第一个简单操作符是 `breakpointOnError()`。顾名思义,当您使用此运算符时,如果任何上游 publisher 发出错误,Xcode 将在调试器打断点,让您查看堆栈,并找到 publisher 错误的原因和位置。 137 | 138 | 另外一个更复杂的操作符是 `breakpoint(receiveSubscription:receiveOutput:receiveCompletion:)`。它允许您拦截各种事件,并根据具体情况决定是否要打断点。 139 | 140 | 例如,只有当某些值通过 publisher 时,才能断开: 141 | 142 | ```swift 143 | .breakpoint(receiveOutput: { value in 144 | return value > 10 && value < 15 145 | }) 146 | ``` 147 | 148 | 上面的例子中,当 publisher 发布的 `value` 大于 `10` 并且小于 `15`,就会自动打断点 (如果是在 Playground 中就直接报错)。 149 | -------------------------------------------------------------------------------- /ios/combine/10 - Key-Value Observing.md: -------------------------------------------------------------------------------- 1 | > 本文是阅读 [Combine: Asynchronous Programming with Swift](https://store.raywenderlich.com/products/combine-asynchronous-programming-with-swift) 后的学习笔记,有需要的请点击链接购买。 2 | 3 | # 10 - Key-Value Observing 4 | 5 | 前面已经学习了 `assign(to:on:)`,它使我们能够在每次 publisher 发出新值时更新给定对象的属性值。 6 | 7 | 但是,我们如何观察一个变量的变化呢?Combine 提供了以下几个方法: 8 | 9 | - 它为支持 KVO 的对象的任何属性提供 publisher。 10 | - `ObservableObject` 协议用于处理多个变量发生变化的情况。 11 | 12 | ## `publisher(for:options:)` 13 | 14 | 我们很容易观察到支持 KVO 的属性。下面是使用 `OperationQueue` 的示例: 15 | 16 | ```swift 17 | let subscription = queue.publisher(for: \.operationCount) 18 | .sink { 19 | print("Outstanding operations in queue: \($0)") 20 | } 21 | ``` 22 | 23 | 每次向队列中添加新 operation 时,它的 `operationCount` 都会增加,subscriber 就会收到新计数。当队列已执行了一个 operation 时,计数将递减,然后 subscriber 接收更新的计数。 24 | 25 | ## 准备并订阅自己的 KVO 属性 26 | 27 | 我们可以在代码中实现自己的 `KVO` 属性,需要满足的条件如下: 28 | 29 | - 对象必须是 class (不能是 struct),并且继承自 `NSObject`。 30 | - 把属性标记为 `@objc dynamic`。 31 | 32 | 然后我们的属性就可以被 Combine 观察。 33 | 34 | > 虽然 Swift 语言不直接支持 KVO,但是用 `@objc dynamic` 标记属性会强制编译器生成触发 KVO 机制的隐藏方法。可以说,这个机制在很大程度上依赖于 `NSObject` 的特定方法,这就解释了为什么您的对象需要从继承它。 35 | 36 | 下面是一个例子: 37 | 38 | ```swift 39 | class TestObject: NSObject { 40 | @objc dynamic var integerProperty: Int = 0 41 | } 42 | 43 | let obj = TestObject() 44 | 45 | let subscription = obj.publisher(for: \.integerProperty) 46 | .sink { 47 | print("integerProperty changes to \($0)") 48 | } 49 | 50 | obj.integerProperty = 100 51 | obj.integerProperty = 200 52 | ``` 53 | 54 | 运行结果: 55 | 56 | ``` 57 | integerProperty changes to 0 58 | integerProperty changes to 100 59 | integerProperty changes to 200 60 | ``` 61 | 62 | 从打印可以看到,初始值 `0` 也被观察到了。 63 | 64 | 另外要注意的是,KVO 对于任何 Objective-C 类型和任何桥接到 Objective-C 的 Swift 类型都可以正常工作。这包括所有本地 Swift 类型以及数组和字典,前提是它们的值都可以桥接到 Objective-C。 65 | 66 | 如果把一个没有桥接到 Objective-C 的纯 Swift 类型定义成 `@objc dynamic` 类型,那就回报错。 67 | 68 | 例如: 69 | 70 | ```swift 71 | struct PureSwift { 72 | let a: (Int, Bool) 73 | } 74 | ``` 75 | 76 | 在刚刚的 `TestObject` 添加: 77 | 78 | ```swift 79 | @objc dynamic var structProperty: PureSwift = .init(a: (0,false)) 80 | ``` 81 | 82 | 报错: 83 | 84 | ``` 85 | Property cannot be marked @objc because its type cannot be represented in Objective-C 86 | ``` 87 | 88 | ## 观察选项 89 | 90 | `publisher(for:options:)` 方法还有第二个参数,是一个 `OptionSet` 类型,可选的值为:`.initial` 、 `.prior` 、 `.old` 和 `.new`。默认值是 `[.initial, .new]`,其中包含了 `.initial`,这也就为什么刚刚能看到打印初始值的原因。 91 | 92 | 以下是每个值的解释: 93 | 94 | - `.initial`:发出初始值。 95 | - `.prior`:发出前一个值和当前值。 96 | - `.old` 和 `.new` 在这里不起任何作用,也就是发出新的值,没有它们也一样。 97 | 98 | 如果不想要初始值,直接像下面这样写即可: 99 | 100 | ```swift 101 | obj.publisher(for: \.integerProperty, options: []) 102 | ``` 103 | 104 | 如果设置了 `.prior`: 105 | 106 | ```swift 107 | let subscription = obj.publisher(for: \.integerProperty options: [.prior]) 108 | ``` 109 | 110 | 则打印结果如下: 111 | 112 | ``` 113 | integerProperty changes to 0 114 | integerProperty changes to 100 115 | integerProperty changes to 100 116 | integerProperty changes to 200 117 | ``` 118 | 119 | 每次改变属性的值,都会收到两个值:前一个老值和新的值。 120 | 121 | ## `ObservableObject` 122 | 123 | Combine 的 `ObservableObject` 协议对 Swift 对象有效,而不仅仅是对继承自 NSObject 的对象有效。它与 `@Published` 属性包装器协作,帮助我们创建带有编译器生成的 `objectWillChange` publisher 的类。 124 | 125 | 它避免了编写大量模板文件,并允许创建自监视其自身属性,并在其中任何属性发生更改时通知的对象。 126 | 127 | 例如: 128 | 129 | ```swift 130 | class MonitorObject: ObservableObject { 131 | @Published var someProperty = false 132 | @Published var someOtherProperty = "" 133 | } 134 | 135 | let object = MonitorObject() 136 | let subscription = object.objectWillChange.sink { 137 | print("object will change") 138 | } 139 | 140 | object.someProperty = true 141 | object.someOtherProperty = "Hello world" 142 | ``` 143 | 144 | 运行结果: 145 | 146 | ``` 147 | object will change 148 | object will change 149 | ``` 150 | 151 | `ObservableObject` 协议使编译器自动生成 `objectWillChange` 属性。它是一个可观察的 `ObservableObjectPublisher`,它发出 `Void` 值,错误类型为 `Never`。 152 | 153 | 每次对象的 `@Published` 变量之一发生更改时,都会让 `objectWillChange` 触发。不幸的是,你不知道到底是哪个属性改变了。它被设计成与 SwiftUI 一起非常好地工作,SwiftUI 将事件合并到一起以简化 UI 更新。 154 | -------------------------------------------------------------------------------- /ios/swiftui/wwdc2019/01 - 关于 SwiftUI 的思考.md: -------------------------------------------------------------------------------- 1 | 前段时间,利于工作之余把 WWDC2019 中关于 SwiftUI 的视频看完了,现在把从视频中学到的东西总结一下。在总结之前,我先来聊聊 SwiftUI。 2 | 3 | SwiftUI 可能是 WWDC2019 让 iOS 开发者最激动的框架了。可能很多iOS 的开发者根本不会想到苹果会推出 SwiftUI,但是仔细想想,从马后炮的心里来看,也是意料之中的事情。从最近几年移动开发的发展来看,FaceBook 和 Google 先后推出了 React Native 和 Flutter,对这两个框架稍微有点了解的开发者应该都知道,它们都是用了声明式的思想来进行 UI 的开发。除了本身有的坑之外,使用它们来进行 iOS 开发会很简单,可以比原生开发用更少的代码、更少的时间来构建同样的 UI。苹果可能也是意识到了这个问题,所以推出了 SwiftUI。关于 SwiftUI 的具体介绍,大家可以查看 [官方的介绍页面](https://developer.apple.com/xcode/swiftui/)。 4 | 5 | 另外,苹果还带来了另外一个全新响应式编程框架 [Combine](https://developer.apple.com/documentation/combine),思想类似于 RxSwift 和 ReactiveSwift。在使用 SwiftUI 开发时,我们可以用这个框架来进行数据和 UI 的绑定,保证 UI 和数据的一致性。可以这么说,你用 SwiftUI 进行开发,就会用到 Combine 框架,所以这个框架也是iOS 开发者必学的。如果之前有过RxSwift 和 ReactiveSwift开发经验的开发者,那么上手 Combine 将会很容易。 6 | 7 | 既然 SwiftUI 这里厉害,我们什么时候能在实际开发中使用啊???就国内这环境,感觉在过两年也不会有太多公司使用。不过如果还想继续做 iOS 开发,我们也还是得学啊,对吧!现在的学习是为了以后更好的切换到使用 SwiftUI 开发。 8 | 9 | 接下来的文章会对关于 SwiftUI 的视频进行总结。 10 | -------------------------------------------------------------------------------- /ios/tdd/01 - 什么是 TDD.md: -------------------------------------------------------------------------------- 1 | # 01 - 什么是 TDD 2 | 3 | > 本文是阅读 [iOS Test-Driven Development by Tutorials](https://store.raywenderlich.com/products/ios-test-driven-development) 后的学习笔记,有需要的请点击链接购买。 4 | 5 | 测试驱动开发(Test-driven development, 简称 TDD),是一种通过迭代进行许多由测试支持的小更改的迭代开发软件的方法。 6 | 7 | 它有四个步骤: 8 | 1. 写一个失败的测试 9 | 2. 使测试通过 10 | 3. 重构 11 | 4. 重复 12 | 13 | 这个步骤也被称为 TDD 循环,能彻底和准确地测试代码。 14 | 15 | ## 为什么应该使用 TDD? 16 | 17 | TDD 是确保软件能够正常工作并在未来继续良好工作的唯一最佳方法。为什么? 18 | 19 | 你可以不按照 TDD 的方式来写测试代码。例如,先编写所有代码,然后在写测试;或者完全跳过编写测试代码,直接用手动测试。为什么 TDD 比这些方法更好? 20 | 21 | 因为 TDD 提供了确保测试良好的方法: 22 | 23 | - 第一步是编写一个失败的测试。根据定义,这证明了测试是可能失败的。 24 | 25 | - 在编写新的测试之前,所有其他以前的测试都必须通过。这确保了测试的可重复性:不只是运行正在进行的单个测试,而是不断地运行所有测试。 26 | 27 | - 通过频繁地运行每个测试,您会受到激励,以确保测试能够快速运行。所有的测试仅需要几秒钟才能运行,最好是一秒钟或更短。 28 | 29 | - 重构时,同时更新代码和测试代码。这可以确保测试得到维护。 30 | 31 | - 通过并行迭代编写代码和测试,可以确保代码是可测试的。如果在完成代码后编写测试,那么代码很可能需要相当多的重构才能完成单元测试。 32 | 33 | ## 哪些是需要测试的? 34 | 35 | 更好的测试覆盖并不总是意味着你的应用程序得到了更好的测试。有些事情你应该测试,有些事情你不应该测试。以下是注意事项: 36 | 37 | - 为无法以自动化方式捕获的代码编写测试。这包括类的方法中的代码、自定义的 getter 和 setter 以及您自己编写的大多数其他内容。 38 | 39 | - 不要为自动生成的代码编写测试。例如,不值得为生成的 getter 和 setter 编写测试。 40 | 41 | - 不要为编译器可能捕捉到的问题编写测试。如果测试的问题将生成错误或警告,Xcode 将为您捕获它。 42 | 43 | - 不要为依赖代码编写测试,例如应用程序使用的系统框架或第三方框架。框架作者负责编写这些测试。例如,不应该为 UIKit 类编写测试,因为 UIKit 开发人员负责编写这些测试。但是,应该为自定义子类编写测试:这是自定义代码,因此你要负责编写测试。 44 | 45 | 上面的一个例外是编写测试以确定框架如何工作。这是非常有用的。但是,不需要长期保存这些测试。相反,后续应该删除它们。 46 | 47 | 另一个例外是“健全性测试”,它可以确保第三方代码如您所期望的那样工作。如果库不是完全稳定的,或者您不信任它,那么这类测试非常有用。 48 | 49 | ## TDD 需要花太多时间 50 | 51 | 关于 TDD 最常见的抱怨是它花费的时间太长了。 52 | 53 | 但是,一旦你习惯了,TDD 会变得更快。然而,事实是,与根本不编写任何测试相比,您最终编写的代码更多。刚刚开始用 TDD 可能需要更多的时间。 54 | 55 | 但是,你要知道:开发的成本不仅仅是最开始编写的第一个版本的代码。它还包括随着时间的推移添加新功能、修改现有代码、修复错误等等。从长远来看,遵循 TDD 比不遵循 TDD 花费的时间要少得多,因为它的代码更易于维护,错误更少。 56 | 57 | 还有另一个要考虑的成本:生产中缺陷对客户的影响。一个问题被发现的时间越长,成本就越高。它可能导致负面评论、失去信任和收入损失。 58 | 59 | 如果在开发过程中发现了问题,那么调试起来更容易,修复也更快。如果你在几周后发现它,你将花费更多的时间来加速代码的运行并找出根本原因。通过遵循 TDD,你的测试最终有助于保护你的应用程序免受 bug 的影响。 60 | 61 | ## 什么时候应该使用 TDD? 62 | 63 | TDD 可以在产品生命周期的任何时候使用:新开发的、已存在的应用程序以及介于两者之间的一切。然而,如何以及从哪里开始 TDD 确实取决于项目的状态。 64 | 65 | 然而,有一个重要的问题需要问:您的项目是否应该使用 TDD? 66 | 67 | 一般来说,如果你的应用要持续几个月以上,会有多个版本和/或需要复杂的逻辑,那么你最好还是使用 TDD。 68 | 69 | 如果你为一些临时性的东西创建一个应用程序,你应该评估 TDD 是否有意义。如果真的只有一个版本的应用程序,你可能不会遵循 TDD,或者只对关键或困难的部分进行 TDD。 70 | 71 | 归根结底,TDD是一种工具,您可以决定何时最好地使用它! -------------------------------------------------------------------------------- /ios/tdd/其他注意事项.md: -------------------------------------------------------------------------------- 1 | ## 测试用例解析 2 | 3 | 以下面这个例子为例: 4 | 5 | ```swift 6 | func testAppModel_whenStarted_isInInProgressState() { 7 | // given 8 | let sut = AppModel() 9 | 10 | // when 11 | sut.start() 12 | 13 | // then 14 | let observedState = sut.appState 15 | XCTAssertEqual(observedState, .inProgress) 16 | } 17 | ``` 18 | 19 | ### 命名规则 20 | 21 | 测试用例的名称必须能正确描述测试的目的。一个测试用例的命名应该包含以下几个部分: 22 | 23 | - 必须以 `test` 开头 24 | - 正在测试的对象,例如 `AppModel` 25 | - 测试的条件,例如 `whenStarted` 26 | - 执行测试条件后想要得到的结果,例如 `isInInProgressState` 27 | 28 | ### 具体实现的代码划分 29 | 30 | 把测试用例的具体实现划分成三个部分: 31 | 32 | - **given**:测试对象的初始状态 33 | - **when**:测试对象的一些操作或者事件和状态的变化 34 | - **given**:检查测试对象变化后状态是否是我们想要的 35 | 36 | ## 对 ViewController 的测试 37 | 38 | 测试 ViewController,并不是对 view 和 control 的直接测试,而是测试它的逻辑和状态。 -------------------------------------------------------------------------------- /nvm/note.md: -------------------------------------------------------------------------------- 1 | # Note 2 | 3 | ## Install Node.js 4 | 5 | ```bash 6 | nvm install [12.14.1] # the version number 7 | ``` 8 | 9 | ## Uninstall Node.js 10 | 11 | ```bash 12 | nvm uninstall [12.14.1] # the version number 13 | ``` 14 | 15 | ## Switch Node.js version 16 | 17 | ```bash 18 | nvm use [12.14.1] # the version number 19 | ``` 20 | 21 | ## Set default Node.js version 22 | 23 | ```bash 24 | nvm alias default [12.14.1] # the version number 25 | ``` 26 | 27 | ## List all Node.js versions 28 | 29 | ```bash 30 | nvm ls 31 | ``` 32 | -------------------------------------------------------------------------------- /react/test/notes.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | 1. Run a test file 4 | ```bash 5 | npm test -- --findRelatedTests src/components/Note.test.js 6 | ``` -------------------------------------------------------------------------------- /ruby/metaprogramming-ruby-2/01 - The M Word.md: -------------------------------------------------------------------------------- 1 | # 01 - The M Word 2 | 3 | ```ruby 4 | class Greeting 5 | def initialize(text) 6 | @text = text 7 | end 8 | 9 | def welcome 10 | @text 11 | end 12 | end 13 | 14 | my_object = Greeting.new("Hello") 15 | ``` 16 | - 获取类名:`my_object.class # => Greeting` 17 | - 获取实例方法:`my_object.class.instance_methods(false) # => [:welcome]`,参数表示是否包含继承的实例方法 18 | - 获取实例变量:`my_object.instance_variables # => [:@text]` 19 | 20 | **元编程是编写能在运行时操作语言构件的代码。** 21 | -------------------------------------------------------------------------------- /ruby/metaprogramming-ruby-2/02 - The Object Model.md: -------------------------------------------------------------------------------- 1 | # 02 - The Object Model 2 | 3 | ## 1. 直接扩展方法 4 | 5 | ```ruby 6 | class String 7 | def to_alphanumeric 8 | gsub(/[^\w\s]/, '') 9 | end 10 | end 11 | 12 | # 使用 refine 可不全局修改,refine 只在两个地方生效: 13 | # 1)在 refine 所在的 block; 14 | # 2)从使用 using 直到 module 的结束(如果在文件的顶部使用 using,则到文件的末尾) 15 | 16 | # refine 的逻辑优先于类中已存在的逻辑,也优先于通过 include 和 prepend 导入进来的代码。 17 | # `methods` 和 `ancestors` 这两个方法会忽略使用 refine 定义的方法 18 | 19 | module StringExtensions 20 | refine String do 21 | def to_alphanumeric 22 | gsub(/[^\w\s]/, '') 23 | end 24 | end 25 | end 26 | ``` 27 | 28 | ## 2. 实例变量存储在对象中,而实例方法存储在类中。 29 | 30 | ![](images/5F1CFC9E-F655-4942-AF93-7CA1A00C9E97.png) 31 | 32 | 3. 实际上,在 Ruby 中,类与模块非常接近,完全可以用任意一个来代表另外一个。保留着两个概念的主要原因是为了获得代码的清晰性,让代码的意图显得更加明确。如果你希望把自己的代码包含到别的代码中,就应该使用模块;如果你希望某段代码被实例化或者被继承,就应该使用类。 33 | 34 | ## 4. objects, classes 和 modules 的区别 35 | 36 | ![](images/74A8E7E8-7540-4ECE-AE17-7EF11EFF506B.png) 37 | 38 | 5. Constants 在程序中是以类似文件系统的树结构存储的;访问路径:`MyModule::MyConstant` 39 | 40 | ```ruby 41 | module MyMoudle 42 | MyConstant = 'Outer constant' 43 | 44 | class MyClass 45 | Myconstant = 'Inner constant' 46 | end 47 | end 48 | ``` 49 | 50 | ![](images/01663F00-5296-4E99-877A-A4508549A2AB.png) 51 | 52 | ## 6. load 和 require的区别 53 | 54 | ![](images/6E390D6F-8F67-434D-8825-D3EDBEC90876.png) 55 | 56 | ## 7. include 和 prepend 的区别 57 | 58 | - include:在原型链中,把被包含的 module 插入到 includer 的上面 59 | 60 | ```ruby 61 | module M1 62 | def my_method 63 | 'M1#my_method()' 64 | end 65 | end 66 | 67 | class C 68 | include M1 69 | end 70 | 71 | class D < C 72 | end 73 | 74 | D.ancestors # => [D, C, M1, Object, Kernel, BasicObject] 75 | ``` 76 | 77 | - prepend:在原型链中,把被包含的 module 插入到 includer 的下面 78 | 79 | ```ruby 80 | class C2 81 | prepend M2 82 | end 83 | 84 | class D2 < C2 85 | end 86 | 87 | D2.ancestors # => [D2, M2, C2, Object, Kernel, BasicObject] 88 | ``` 89 | 90 | ![](images/4B275B77-64F9-40EB-BB68-A0B28B8903A5.png) 91 | 92 | - 多个 inclusions:如果原型链中已经存在某个 module,后面再插入同一个 module,Ruby 会忽略后面的插入操作。 93 | 94 | ```ruby 95 | module M1 96 | end 97 | 98 | module M2 99 | include M1 100 | end 101 | 102 | module M3 103 | prepend M1 104 | include M2 105 | end 106 | 107 | M3.ancestors # => [M1, M3, M2] 108 | ``` 109 | 110 | ## 8. `private`方法只能使用隐式的 receiver 去调用,不能用`self.private_method`. 111 | 112 | ![](images/B5D6EE57-BCFC-45A6-B81C-2F5EDAB8983E.png) 113 | -------------------------------------------------------------------------------- /ruby/metaprogramming-ruby-2/03 - Methods.md: -------------------------------------------------------------------------------- 1 | # 03 Methods 2 | 3 | ## 1. String 和 Symbol 的区别 4 | 5 | ![](images/844F1F50-F1D3-425B-88D5-2E097CD02766.png) 6 | 7 | ## 2. 动态调用方法 8 | 9 | ```ruby 10 | def refresh(options = {}) 11 | defaults = {} 12 | attributes = [:input, :output, :commands, :print, :quiet, 13 | :exception_handler, :hooks, :custom_completions, 14 | :prompt, :memory_size, :extra_sticky_locals] 15 | 16 | attributes.each do |attribute| 17 | defaults[attribute] = Pry.send attribute 18 | end 19 | 20 | defaults.merge!(options).each do |key, value| 21 | send("#{key}=", value) if respond_to?("#{key}=") 22 | end 23 | 24 | true 25 | end 26 | ``` 27 | 28 | ## 3. 定义动态方法 29 | 30 | ```ruby 31 | class Computer 32 | def initialize(computer_id, data_source) 33 | @id = computer_id 34 | @data_source = data_source 35 | data_source.methods.grep(/^get_(.*)_info$/) { Computer.define_component $1 } 36 | end 37 | 38 | def self.define_component(name) 39 | define_method(name) do 40 | # ... 41 | end 42 | end 43 | end 44 | ``` 45 | 46 | ## 4. 重写 `method_missing` 的同时,重写 `respond_to_missing?`。直接重写 `respond_to?` 是不太好的做法。 47 | 48 | ## 5. 如果想要一个空白的类,可以继承自`BasicObject`。 49 | 50 | ## 6. The choice between **Dynamic** and **Ghost** Methods : *use Dynamic Methods if you can and Ghost Methods if you have to*. 51 | -------------------------------------------------------------------------------- /ruby/metaprogramming-ruby-2/04 - Blocks.md: -------------------------------------------------------------------------------- 1 | # 04 - Blocks 2 | 3 | ## 1. Block 的基本使用 4 | 5 | ```ruby 6 | def a_method(a, b) 7 | a + yield(a, b) # yield 代表传进来的 block 8 | end 9 | a_method(1, 2) { |x, y| (x + y) * 3 } # => 10 10 | 11 | # 判断是否传入了 block 12 | def a_method 13 | return yield if block_given? 14 | 'no block' 15 | end 16 | ``` 17 | 18 | ## 2. 全局变量 19 | 20 | ```ruby 21 | def a_scope 22 | $var = 'some value' 23 | end 24 | 25 | def another_scope 26 | $var 27 | end 28 | 29 | a_scope 30 | another_scope # => 'some value' 31 | ``` 32 | 33 | ## 3. Flat Scope:可以用`Class.new` 替换 `class`,`Module.new` 替换 `module`,`define_method` 替换 `def`。 34 | 35 | ```ruby 36 | # 通过这样修改,class 定义里面可以访问到外部的变量 37 | my_var = 'Success' 38 | 39 | MyClass = Class.new do 40 | "#{my_var} in the class definition" 41 | 42 | define_method :my_method do 43 | "#{my_var} in the method" 44 | end 45 | end 46 | ``` 47 | 48 | ## 4. Shared Scope 49 | 50 | ```ruby 51 | def define_methods 52 | shared = 0 53 | 54 | Kernel.send :define_method, :counter do 55 | shared 56 | end 57 | 58 | Kernel.send :define_method, :inc do |x| 59 | shared += x 60 | end 61 | end 62 | 63 | define_methods 64 | counter # => 0 65 | inc(4) 66 | counter # => 4 67 | ``` 68 | 69 | ## 5. `instance_eval()` 70 | 71 | ```ruby 72 | class MyClass 73 | def initialize 74 | @v = 1 75 | end 76 | end 77 | 78 | obj = MyClass.new 79 | obj.instance_val do 80 | # 执行的时候把 `self` 作为 receiver 81 | self # => # 82 | @v # => 1 83 | end 84 | 85 | # instance_val 可以访问它被定义时所在的作用域 86 | v = 2 87 | obj.instance_val { @v = v } 88 | obj.instance_val { v } # => 2 89 | ``` 90 | 91 | ## 6. `instance_exec()` : 允许传入携带参数的 block 92 | 93 | ```ruby 94 | Class C 95 | def initialize 96 | @x = 1 97 | end 98 | end 99 | 100 | class D 101 | def twisted_method 102 | @y = 2 103 | C.new.instance_exec(@y) { |y| "@x: #{@x}, @y: #{y}" } 104 | end 105 | end 106 | 107 | D.new.twisted_method # => "@x: 1, @y: 2" 108 | ``` 109 | 110 | ## 7. Proc Objects 111 | 112 | - Proc 113 | 114 | ```ruby 115 | inc = Pro.new { |x| x + 1 } 116 | inc.call(2) # => 3 117 | ``` 118 | 119 | - lambda 120 | 121 | ```ruby 122 | dec = lambda { |x| x - 1 } 123 | dec.class # => Proc 124 | dec.call(2) # => 1 125 | 126 | # 简写形式 127 | dec = ->(x) { x - 1 } 128 | ``` 129 | 130 | - **&** 运算符:block 与 Proc 互相转换 131 | 132 | ```ruby 133 | # block 转换成 Proc 134 | def math(a, b) 135 | yield(a,b ) 136 | end 137 | def do_math(a, b, &operation) 138 | math(a, b, &operation) 139 | end 140 | do_math(2, 3) { |x, y| x * y } # => 6 141 | 142 | # Proc 转换成 block 143 | def my_method(greeting) 144 | "#{greeting}, #{yield}!" 145 | end 146 | my_proc = proc { 'Bill' } 147 | my_method('Hello', &my_proc) 148 | ``` 149 | 150 | - Procs vs. Lambdas:1)`return`: 在 Lambdas 中,只是返回 lambda 的结果; 在Proc 中,从定义它的作用域返回;2)检查传入的参数规则不同: lambda 要求传入的参数个数要与所需参数相同,否则报错 `ArgumentError`;而 Proc 则无要求;3)一般优先选择 lambda。 151 | 152 | ```ruby 153 | def double(callable_object) 154 | callable_object.call * 2 155 | end 156 | l = lambda { return 10 } 157 | double(l) # => 20 158 | 159 | def another_double 160 | p = Proc.new { return 10 } 161 | result = p.call 162 | # 下面这行代码不会运行;如果 `Proc.new { return 10 }`改为 `Proc.new { 10 }` 则会运行 163 | return result * 2 164 | end 165 | ``` 166 | 167 | ```ruby 168 | p = Proc.new { |a, b| [a, b] } 169 | p.arity # => 2 170 | p.call(1, 2, 3) # => [1, 2] 171 | p.call(1) # => [1, nil] 172 | ``` 173 | -------------------------------------------------------------------------------- /ruby/metaprogramming-ruby-2/images/01663F00-5296-4E99-877A-A4508549A2AB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/ruby/metaprogramming-ruby-2/images/01663F00-5296-4E99-877A-A4508549A2AB.png -------------------------------------------------------------------------------- /ruby/metaprogramming-ruby-2/images/4B275B77-64F9-40EB-BB68-A0B28B8903A5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/ruby/metaprogramming-ruby-2/images/4B275B77-64F9-40EB-BB68-A0B28B8903A5.png -------------------------------------------------------------------------------- /ruby/metaprogramming-ruby-2/images/5F1CFC9E-F655-4942-AF93-7CA1A00C9E97.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/ruby/metaprogramming-ruby-2/images/5F1CFC9E-F655-4942-AF93-7CA1A00C9E97.png -------------------------------------------------------------------------------- /ruby/metaprogramming-ruby-2/images/6E390D6F-8F67-434D-8825-D3EDBEC90876.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/ruby/metaprogramming-ruby-2/images/6E390D6F-8F67-434D-8825-D3EDBEC90876.png -------------------------------------------------------------------------------- /ruby/metaprogramming-ruby-2/images/74A8E7E8-7540-4ECE-AE17-7EF11EFF506B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/ruby/metaprogramming-ruby-2/images/74A8E7E8-7540-4ECE-AE17-7EF11EFF506B.png -------------------------------------------------------------------------------- /ruby/metaprogramming-ruby-2/images/844F1F50-F1D3-425B-88D5-2E097CD02766.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/ruby/metaprogramming-ruby-2/images/844F1F50-F1D3-425B-88D5-2E097CD02766.png -------------------------------------------------------------------------------- /ruby/metaprogramming-ruby-2/images/B5D6EE57-BCFC-45A6-B81C-2F5EDAB8983E.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/ruby/metaprogramming-ruby-2/images/B5D6EE57-BCFC-45A6-B81C-2F5EDAB8983E.png -------------------------------------------------------------------------------- /source-code-analysis/alamofire4/01 - 前言.md: -------------------------------------------------------------------------------- 1 | 在2017年初的时候,写了两篇文章介绍了Alamofire的[基本用法](http://www.jianshu.com/p/f8c3adb056cf)和[高级用法](http://www.jianshu.com/p/903b678d2d3f)。但是这两篇文章都是仅限于如何使用,没有进行更深入的研究。考虑到在应用开发过程中,我们几乎无法避免网络相关的开发,所以决定花一些时间深入研究下Alamofire的源代码。让我们一起看看这个简单易用的网络框架是如何实现的,并且在看代码的过程中学习Swift的编程风格和良好的编程习惯。 2 | 3 | ## 类图 4 | 5 | Alamofire整个项目的类型如下图(省略了Protocol),图中被箭头指向的是父类: 6 | 7 | ![Alamofire类图](http://upload-images.jianshu.io/upload_images/2057254-8d6b0cee302f944b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 8 | 9 | ## 流程图 10 | 11 | Alamofire大概的运行流程图如下: 12 | 13 | - 我们调用`.request()`、`.download()`、`.upload()`和`.stream()`时,`SessionManager`会创建对应的请求`DataRequest`、`DownlaodRequest`、`UploadRequest `和`StreamRequest` 14 | - 然后`SessionDelegate`会处理`URLSession`对应的回调,`TaskDelegate`会处理`URLSessionTask`对应的回调 15 | - 调用`.reponse()`、`.reponseData()`、`.reponseString()`、`.reponseJSON()`和`.reponsePropertyList()`时,请求会把这些对应的response里面的代码以closure的形式加入到`delegate.queue`的operations中 16 | - 请求完成后,在`urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)`运行队列的operations,`reponse...()`的`completionHandler`就会携带有对应的`DefaultDataResponse`、`DefaultDownloadResponse`、`DataResponse`和`DownloadResponse`。 17 | 18 | 除了基本的请求和响应外,还有认证之类的,大家可以去自己看看。 19 | 20 | ![Alamofire解析](http://upload-images.jianshu.io/upload_images/2057254-a1958d821a4d3d3d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 21 | -------------------------------------------------------------------------------- /source-code-analysis/alamofire4/02 - AFError & Notifications & DispatchQueue+Alamofire.md: -------------------------------------------------------------------------------- 1 | ### 一、AFError枚举 2 | 3 | 作者首先把整个项目中遇到的error都集中在`AFError`枚举。从源码中我们可以看到,项目中遇到的error有五大类,就是`AFError`的五个case,每个case都有一个关联值([什么是关联值 >>](http://www.jianshu.com/p/4b7ab493cae7))。我们利用Swift枚举的关联值特性,在抛出错误的时候,关联一个任何类型的值。例如如果是这个错误`case invalidURL(url: URLConvertible)`,这里关联了一个url,这样就可以在遇到error的时候,告诉我们哪个url是不合法的。而其他四个case关联的都是`AFError`里面内置的enum类型(Swift允许[类型嵌套](http://www.jianshu.com/p/dd10a918b0c8)),可以告诉我们更具体的错误原因。 4 | 5 | ```swift 6 | public enum AFError: Error { 7 | 8 | // 参数编码失败的原因 9 | public enum ParameterEncodingFailureReason { 10 | 11 | } 12 | 13 | // 多表单数据编码失败的原因 14 | public enum MultipartEncodingFailureReason { 15 | 16 | } 17 | 18 | // 响应验证失败的原因 19 | public enum ResponseValidationFailureReason { 20 | 21 | } 22 | 23 | // 响应序列化失败的原因 24 | public enum ResponseSerializationFailureReason { 25 | 26 | } 27 | 28 | // 五大类错误 29 | case invalidURL(url: URLConvertible) 30 | case parameterEncodingFailed(reason: ParameterEncodingFailureReason) 31 | case multipartEncodingFailed(reason: MultipartEncodingFailureReason) 32 | case responseValidationFailed(reason: ResponseValidationFailureReason) 33 | case responseSerializationFailed(reason: ResponseSerializationFailureReason) 34 | } 35 | ``` 36 | 37 | ### 二、AdapterError 38 | 39 | 这个`AdapterError`是专门为`RequestAdapter`服务的。 40 | 41 | 这里先简单介绍下`RequestAdapter`:`RequestAdapter`是个协议,我们可以自定义一个请求适配器,并且遵循这个协议。通过我们自定义的适配器,我们在发送请求的时候可以默认添加一些请求相关的数据,具体请查看:[08 - Request](https://github.com/Lebron1992/learning-notes/blob/master/source-code-analysis/alamofire4/08%20-%20Request.md)。 42 | 43 | `AdapterError`就是在请求适配过程中可能出现的错误的结构类型。在`Error`的extension中定义的`underlyingAdaptError`就是适配过程中可能出现的错误。 44 | 45 | ### 三、Error Booleans 46 | 47 | 这里定义了一些方便使用的只读属性,方便我们在开发的时候作出判断。其实在我们的开发中,也会经常把一些判断写成只读属性或者是一个返回Bool的方法。这是一个非常好的习惯,提高代码的可读性。在Swift中,苹果已经习惯性地把Bool类型的属性以`is`开头命名,我个人也觉得用`is`开头命名,可读性会更高。 48 | 49 | ```swift 50 | extension AFError { 51 | public var isInvalidURLError: Bool { 52 | if case .invalidURL = self { return true } 53 | return false 54 | } 55 | 56 | // 更多 Error Booleans ... 57 | } 58 | ``` 59 | 60 | ### 四、Convenience Properties 61 | 62 | 这部分内容是一些方便使用的与error相关的属性,没有很特别的。 63 | 64 | ### 五、Notifications 65 | 66 | Notifications这个文件通过扩展`Notification.Name`定义了项目中用到的通知名称。我们可以学习用这个形式来定义`rawValue`:`org.alamofire.notification.name.task.didResume`。另外还定义了一个Key。 67 | 68 | ### 六、DispatchQueue+Alamofire 69 | 70 | `DispatchQueue+Alamofire`里面,通过扩展`DispatchQueue`定义了不同优先级的全局队列: 71 | * `userInteractive`:与用户交互的任务队列,通常跟UI的刷新有关,例如动画之类的 72 | * `userInitiated`:用户发起的并且需要立即得到结果的任务队列 73 | * `utility`:需要花点时间的任务队列 74 | * `background`:后台任务队列,用户不需要关心的,通常时间会比较长 75 | 76 | 另外还定义了一个方法,经过一个指定的时间后,执行一个closure,这种写法看起来更通熟易懂: 77 | 78 | ```swift 79 | func after(_ delay: TimeInterval, execute closure: @escaping () -> Void) { 80 | asyncAfter(deadline: .now() + delay, execute: closure) 81 | } 82 | ``` 83 | -------------------------------------------------------------------------------- /source-code-analysis/alamofire4/03 - Timeline.md: -------------------------------------------------------------------------------- 1 | `Timeline`这个类主要负责记录整个请求过程中的时间点。 2 | 3 | ```swift 4 | public struct Timeline { 5 | /// 请求开始的时刻 6 | public let requestStartTime: CFAbsoluteTime 7 | 8 | /// 首次从服务器接收到数据或者发送数据给服务器的时刻 9 | public let initialResponseTime: CFAbsoluteTime 10 | 11 | /// 请求完成的时刻 12 | public let requestCompletedTime: CFAbsoluteTime 13 | 14 | /// 序列化完成的时刻 15 | public let serializationCompletedTime: CFAbsoluteTime 16 | 17 | /// 从请求开始到首次收到服务器响应的时间间隔:initialResponseTime - requestStartTime 18 | public let latency: TimeInterval 19 | 20 | /// 从请求开始到请求完成的时间间隔:requestCompletedTime - requestStartTime 21 | public let requestDuration: TimeInterval 22 | 23 | /// 从请求完成到序列化完成的时间间隔:serializationCompletedTime - requestCompletedTime 24 | public let serializationDuration: TimeInterval 25 | 26 | /// 从请求开始到序列化完成的时间间隔:serializationCompletedTime - requestStartTime 27 | public let totalDuration: TimeInterval 28 | } 29 | ``` 30 | 31 | 刚开始觉得`CFAbsoluteTime`很高深莫测,其实是一个`Double`类型。 32 | 33 | ```swift 34 | public typealias CFTimeInterval = Double 35 | public typealias CFAbsoluteTime = CFTimeInterval 36 | ``` 37 | 38 | 另外还实现了`CustomStringConvertible`和`CustomDebugStringConvertible`协议,方便我们调试使用。 39 | 40 | ```swift 41 | 42 | // MARK: - CustomStringConvertible 43 | 44 | extension Timeline: CustomStringConvertible { 45 | public var description: String { 46 | } 47 | } 48 | 49 | // MARK: - CustomDebugStringConvertible 50 | 51 | extension Timeline: CustomDebugStringConvertible { 52 | public var debugDescription: String { 53 | } 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /source-code-analysis/alamofire4/06 - Alamofire.md: -------------------------------------------------------------------------------- 1 | Alamofire这个文件包含了所有请求的入口。 2 | 3 | ### 1. `URLConvertible`和`URLRequestConvertible`协议 4 | 5 | 我们知道URL可以有三个来源:1)`String`; 2) `URL`; 3) `URLComponents`。所以作者定义了一个`URLConvertible`协议,让着三个来源遵循这个协议,这样就可以直接使用`URLConvertible`类型的变量转换成URL。 6 | 7 | ```swift 8 | public protocol URLConvertible { 9 | func asURL() throws -> URL 10 | } 11 | 12 | extension String: URLConvertible { 13 | public func asURL() throws -> URL { 14 | guard let url = URL(string: self) else { throw AFError.invalidURL(url: self) } 15 | return url 16 | } 17 | } 18 | 19 | extension URL: URLConvertible { 20 | public func asURL() throws -> URL { return self } 21 | } 22 | 23 | extension URLComponents: URLConvertible { 24 | public func asURL() throws -> URL { 25 | guard let url = url else { throw AFError.invalidURL(url: self) } 26 | return url 27 | } 28 | } 29 | ``` 30 | 31 | 类似地,用同样的思想作者也定义了`URLRequestConvertible`。 32 | 33 | ### 2. 初始化 34 | 35 | ```swift 36 | extension URLRequest { 37 | // 请求的初始化方法, HTTPMethod是一个枚举;HTTPHeaders实际是[String: String] 38 | public init(url: URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws { 39 | let url = try url.asURL() 40 | 41 | self.init(url: url) 42 | 43 | httpMethod = method.rawValue 44 | 45 | if let headers = headers { 46 | for (headerField, headerValue) in headers { 47 | setValue(headerValue, forHTTPHeaderField: headerField) 48 | } 49 | } 50 | } 51 | 52 | // 使用自定义的请求适配器适配请求 53 | func adapt(using adapter: RequestAdapter?) throws -> URLRequest { 54 | guard let adapter = adapter else { return self } 55 | return try adapter.adapt(self) 56 | } 57 | } 58 | ``` 59 | 60 | `RequestAdapter`是一个协议,我们可以自定义一个类型并遵循这个协议,可以针对这个请求做一些额外的配置,在[【Alamofire源码解析】08 - Request](http://www.jianshu.com/p/e1d1331128ae)有具体的例子讲解。 61 | 62 | ### 3. 数据请求 63 | 64 | ```swift 65 | // 传入URLConvertible和其他信息 66 | @discardableResult 67 | public func request( 68 | _ url: URLConvertible, 69 | method: HTTPMethod = .get, 70 | parameters: Parameters? = nil, 71 | encoding: ParameterEncoding = URLEncoding.default, 72 | headers: HTTPHeaders? = nil) 73 | -> DataRequest 74 | { 75 | return SessionManager.default.request( 76 | url, 77 | method: method, 78 | parameters: parameters, 79 | encoding: encoding, 80 | headers: headers 81 | ) 82 | } 83 | 84 | // 传入URLRequestConvertible 85 | @discardableResult 86 | public func request(_ urlRequest: URLRequestConvertible) -> DataRequest { 87 | return SessionManager.default.request(urlRequest) 88 | } 89 | ``` 90 | 91 | ### 4. 下载请求 & 上传请求 & Stream请求 92 | 93 | 这三个类型的请求跟数据请求的代码大同小异,比较好理解。这里就不展开去讲了。 94 | -------------------------------------------------------------------------------- /source-code-analysis/kickstarter-ios/01 - 项目相关.md: -------------------------------------------------------------------------------- 1 | ## Makefile 2 | 3 | 在把项目 clone 下来之后,我们一般首先会想着怎么把它运行起来。在项目的 readme 中的 Getting Started 我们可以看到,运行 `make bootstrap`安装工具和依赖,运行 `make test-all` 构建项目并进行测试。而这两个命令就是在 **Makefile** 中定义的。 4 | 5 | 打开 Makefile 文件,我们可以从中看到:1)文件的开头定义了各种变量;2)剩下的是项目中用到的命令。我们以 `make bootstrap` 为例: 6 | 7 | ``` 8 | bootstrap: hooks dependencies 9 | brew update || brew update 10 | brew unlink swiftlint || true 11 | brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/686375d8bc672a439ca9fcf27794a394239b3ee6/Formula/swiftlint.rb 12 | brew switch swiftlint 0.29.2 13 | brew link --overwrite swiftlint 14 | ``` 15 | 16 | 执行 `make bootstrap` ,就会依次执行 bootstrap 下面包含的所有命令。 17 | 18 | 使用 Makefile 的好处是,我们可以把项目相关的一些命令操作都放到这个文件,即便是刚刚接手项目的同事也一目了然。项目还没有用 Makefile 的,可以赶紧用起来了啊。。。 😝 19 | 20 | 没了解过 Makefile 的可以自行搜索了解一下。 21 | 22 | ## Git submodule 23 | 24 | 把项目 clone 下来之后,会发现文件夹里面没有我们常用的 Podfile 和 `xcworkspace` 文件。没错,Kickstarter 不是用 Cocoapods 来管理第三方库的,而是使用 `git submodule`。 25 | 26 | 除了上面提到的两个之后,还可以用 Carthage 来管理第三方库。找到一篇文章,描述了这三种工具的优缺点,[点击前往>>](https://reallifeprogramming.com/carthage-vs-cocoapods-vs-git-submodules-9dc341ec6710)。至于选择哪一种,就看我们更看重的是什么了。 27 | 28 | ## 两个 Swift 编写的脚本工具 29 | 30 | 在根目录下的 bin 目录,我们可以看到两个用 Swift 编写的脚本:ColorScript 和 StringsScript。 31 | 32 | ### ColorScript 33 | 34 | 开发者把项目中用到的颜色,保存在`Colors.json`文件,然后通过 ColorScript 转换成 `Colors.swift`文件。开发者在使用的时候只需要通过 `UIColor.ksr_dark_grey_400`就能得到相应的颜色了。后续如果 UI 设计师想要微调颜色,直接修改颜色, json 中的 key 的值不变,我们只需要重新生成 `Colors.swift`就都搞定了,而不需要更改代码。 35 | 36 | ```json 37 | { 38 | "apricot_600": "FFCBA9", 39 | "cobalt_500": "4C6CF8", 40 | "dark_grey_400": "9B9E9E", 41 | "dark_grey_500": "656868", 42 | "facebookBlue": "3B5998", 43 | ... 44 | } 45 | ``` 46 | 47 | 这种统一管理颜色的方法,我觉得其实就是把颜色管理的工作交给 UI 设计师了。设计师写好 json 文件,交给开发者,开发者用脚本生成 `Colors.swift`,就一切都搞定了(如果颜色名字有变动或有新添加的颜色,还是需要开发者手动更改和添加)。如果不通过这种方法去做,而是开发者自己手动去写,那么可能会经常去手动修改 `Colors.swift`,这样就麻烦一些。 48 | 49 | 至于是否要使用这个思路,我觉得如果公司有专业的 UI 设计师,并且大家遵守约定的规则,用这种方法是非常好的;否则还是开发者自己手动写来的实际些吧! 50 | 51 | ### StringsScript 52 | 53 | 做过国际化的开发者应该知道,如果不通过其他处理的话,我们需要通过 `NSLocalizedString("Hello_World", comment: "")` 去获取对应的本地化字符串,这种写法非常麻烦,而且很容易出错。 54 | 55 | 在 Kickstarter-iOS 中,开发者用 StringsScript 把 `Localizable.strings`转换生成 `Strings.swift` 文件,然后我们在使用的时候,就可以像这样去获取想要的字符串 `Strings. Hello_World()`。这个脚本把 key 变成了方法名,让我们避免了在使用的时候出现错误,而且使用起来非常方便。 56 | 57 | 如果有做本地化的项目,采用这种方法可以给开发者带来很大的便利。 58 | 59 | ## 丰富的测试 60 | 61 | 测试,是软件开发中非常重要的一个环节。甚至有些公司执行 TDD (测试驱动开发(Test-Driven Development)),可以见测试的重要性。 62 | 63 | 在 Kickstarter-iOS 中,我们可以看到大量的 `xxxTests.swift`文件,包括了 Unit Test 和 UI Test。 64 | 65 | 据我了解,国内很多小公司因为进度比较紧急,都是没有写测试的。我觉得如果时间允许的话,还是要尽量写测试,否则自己写的代码都没有自信。有些人可能会问,测试要测些什么?不妨仔细去研读一下 Kickstarter-iOS 源码,看看人家的测试文件,相信都会找到一些灵感的。 66 | 67 | ## 独立的代码库 68 | 69 | 用 Xcode 打开 Kickstarter-iOS 的项目,你会发现 `KsApi`、`Library`和 `LiveStream`这三个文件夹不是存放在 `Kickstarter-iOS`文件夹里面的,而是跟它处于同一个目录。因为这三个文件夹存放的是独立于 Kickstarter-iOS 之外的 framework。 70 | 71 | ![](https://upload-images.jianshu.io/upload_images/2057254-7ed192d643388259.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 72 | 73 | 74 | 这么做的好处当然是代码可以复用。目前我看 iPad 上的 Kickstarter 应用是跟 iPhone 共用一个的,如果以后要为 iPad 单独做一个 app,这三个 frameworks 就可以直接拿过去用。 75 | -------------------------------------------------------------------------------- /source-code-analysis/kickstarter-ios/03 - Environment 和 AppEnvironment.md: -------------------------------------------------------------------------------- 1 | 有经验的 iOS 开发者应该都知道,在开发过程中我们需要设计一些对象来存储应用的全局状态,例如当前的登录用户等等。而在 Kickstarter-iOS 中,`Environment` 和 `AppEnvironment` 就是干这事的。这篇文章我们来研究下这两个 struct 。 2 | 3 | ## Environment 4 | 5 | 打开这个文件,从注释可以看到,`Environment` 是应用所需要的全局变量和单例的集合。仔细分析里面属性的定义,我们可以发现很多都是属于 protocol 类型的,例如: 6 | 7 | ```swift 8 | public let apiService: ServiceType 9 | public let cookieStorage: HTTPCookieStorageProtocol 10 | public let device: UIDeviceType 11 | public let ubiquitousStore: KeyValueStoreType 12 | public let userDefaults: KeyValueStoreType 13 | // ... 14 | ``` 15 | 16 | 这么做的好处是当有需要的时候,可以随时替换另外一个遵循对应 protocol的对象。这也就是我们所说的面向协议编程。 17 | 18 | 大家还可以结合自己的项目,去看看其他的属性,看还有什么值得学习的地方。 19 | 20 | ## AppEnvironment 21 | 刚开始看这个项目,看到有 `Environment` 和 `AppEnvironment`,可能会觉得有点困惑,为什么有了 `Environment`,还要搞一个`AppEnvironment`?下面我们来仔细看看。 22 | 23 | 先看一下 `AppEnvironment` 里面的方法: 24 | 25 | ```swift 26 | public struct AppEnvironment : AppEnvironmentType { 27 | 28 | internal static let environmentStorageKey: String 29 | 30 | internal static let oauthTokenStorageKey: String 31 | 32 | public static func login(_ envelope: AccessTokenEnvelope) 33 | 34 | public static func updateCurrentUser(_ user: User) 35 | 36 | public static func updateServerConfig(_ config: ServerConfigType) 37 | 38 | public static func updateConfig(_ config: Config) 39 | 40 | public static func updateLanguage(_ language: Language) 41 | 42 | public static func logout() 43 | 44 | public static var current: Environment! { get } 45 | 46 | public static func pushEnvironment(_ env: Environment) 47 | 48 | public static func popEnvironment() -> Environment? 49 | 50 | public static func replaceCurrentEnvironment(_ env: Environment) 51 | 52 | // 参数太长,省略了 53 | public static func pushEnvironment(...) 54 | 55 | // 参数太长,省略了 56 | public static func replaceCurrentEnvironment(...) 57 | 58 | public static func fromStorage(ubiquitousStore: KeyValueStoreType, userDefaults: KeyValueStoreType) -> Environment 59 | 60 | internal static func saveEnvironment(environment env: Environment = AppEnvironment.current, ubiquitousStore: KeyValueStoreType, userDefaults: KeyValueStoreType) 61 | } 62 | ``` 63 | 64 | 从上面的方法我们可以总结出,`AppEnvironment`是用来管理 `Environment`。如果我们不新建一个 `AppEnvironment`,那么这些管理代码就会放到 `Environment`,这会造成在一个 Model 上进行业务逻辑的处理,而这明显是不合理的。 65 | 66 | 如果你在项目中全局搜索 `pushEnvironment` 和 `popEnvironment`,你会发现,这两个方法都是在测试文件中被调用,说明这两个方法是为测试而生的。 67 | 68 | 另外 `AppEnvironment` 还提供了 `replaceCurrentEnvironment()` 方法,携带了所有对应 `Environment` 的参数,这可以让我们很容易替换当前 Environment 的某个全局变量。例如在 `AppDelegate.swift` 我们可以看到: 69 | 70 | ```swift 71 | #if DEBUG 72 | if KsApi.Secrets.isOSS { 73 | AppEnvironment.replaceCurrentEnvironment(apiService: MockService()) 74 | } 75 | #endif 76 | ``` 77 | 78 | 把 `KsApi.Secrets.isOSS` 设置为 `true` 之后,我们就可以使用 `MockService()`,实在是非常方便。 79 | -------------------------------------------------------------------------------- /source-code-analysis/kickstarter-ios/04 - 网络请求的处理和 Deep Linking.md: -------------------------------------------------------------------------------- 1 | ## 网络请求的处理 2 | 3 | 从 `Environment` 中,可以了解到 `Service` 是处理应用中所有网络请求的。进入到 `Service`, 这里编写了所有的网络请求方法。再仔细看,你会发现很多请求是通过类似 `request(.facebookConnect(facebookAccessToken: token))` 去调用的。我们就先来看看这个 `request()` 方法的参数 `Route`。 4 | 5 | ### Route 6 | 7 | `Route` 的部分代码如下: 8 | 9 | ```swift 10 | internal enum Route { 11 | case activities(categories: [Activity.Category], count: Int?) 12 | case addImage(fileUrl: URL, toDraft: UpdateDraft) 13 | case addVideo(fileUrl: URL, toDraft: UpdateDraft) 14 | case backing(projectId: Int, backerId: Int) 15 | // ... 16 | 17 | internal var requestProperties: 18 | (method: Method, path: String, query: [String: Any], file: (name: UploadParam, url: URL)?) { 19 | 20 | switch self { 21 | case let .activities(categories, count): 22 | var params: [String: Any] = ["categories": categories.map { $0.rawValue }] 23 | params["count"] = count 24 | return (.GET, "/v1/activities", params, nil) 25 | 26 | case let .addImage(file, draft): 27 | return (.POST, "/v1/projects/\(draft.update.projectId)/updates/draft/images", [:], (.image, file)) 28 | 29 | case let .addVideo(file, draft): 30 | return (.POST, "/v1/projects/\(draft.update.projectId)/updates/draft/video", [:], (.video, file)) 31 | 32 | case let .backing(projectId, backerId): 33 | return (.GET, "/v1/projects/\(projectId)/backers/\(backerId)", [:], nil) 34 | 35 | // ... 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | 如果你打开源文件,你会发现,`Route`枚举编写了所有用到的请求,并且定义了 `requestProperties` 属性,这样我们就可以通过类似 `.facebookConnect(facebookAccessToken: token)`去获取到想要的请求,然后通过 `requestProperties` 属性,获取到请求参数,接着做进一步的网络请求。 42 | 43 | 对于类似这种有多种可能情况的处理,用 enum 非常合适,而这也是开发过程中经常会遇到的。 44 | 45 | 既然各种请求都准备好了,下一步就要进行真正的网络请求了,这些代码就藏在 `Service+RequestHelpers.swift`。 46 | 47 | ### Service+RequestHelpers 48 | 49 | 这个文件暴露给外面的接口非常简单,如下: 50 | 51 | ```swift 52 | extension Service { 53 | 54 | func fetch(query: NonEmptySet) -> SignalProducer 55 | 56 | func applyMutation(mutation: B) -> SignalProducer 57 | 58 | func requestPagination(_ paginationUrl: String) 59 | -> SignalProducer where M == M.DecodedType 60 | 61 | func request(_ route: Route) 62 | -> SignalProducer where M == M.DecodedType 63 | 64 | func request(_ route: Route) 65 | -> SignalProducer<[M], ErrorEnvelope> where M == M.DecodedType 66 | 67 | func request(_ route: Route) 68 | -> SignalProducer where M == M.DecodedType 69 | } 70 | ``` 71 | 72 | 从这些方法的定义我们可以看到,全部使用了泛型,这就意味着一个方法就可以处理某一类型的请求。这六个方法就可以处理整个应用的请求,是不是觉得非常强大😁? 73 | 74 | 这也是值得我们学习的地方。所以在开发过程中,如果发现自己在重复写类似的代码,那么可以考虑使用泛型能不能解决问题。 75 | 76 | ## Deep Linking 77 | 78 | 在开发中,我们通常需要通过 Universal Link、URL Scheme 和 Push Notification 等方式跳转到应用的某一个页面。我们来看一下 Kickstarter-iOS 是怎么处理的。 79 | 80 | 打开 `Navigation.swift` ,跟网络请求一样,也是用 enum 定义了所有用户去往的目标页面。 81 | 82 | 那在 Kickstarter-iOS 中,它是怎样通过 deep linking 传入的 url 来最终得到 `Navigation` 其中的一个 case,然后跳转到目标页面呢? 83 | 84 | 首先,它用一个字典 `allRoutes: [String: (RouteParams) -> Decoded]` 保存了所有的 routes:其中 key 是 url 的模板;value 是一个闭包,这个闭包是根据url 携带的参数解析成 `Navigation`。 85 | 86 | 然后用一个 `match()` 方法,把传入的 url,最终解析成`Navigation` 这里面最关键的一个方法是 `parsedParams()` ,大家可以去仔细看一下怎么实现的。 87 | 88 | ```swift 89 | extension Navigation { 90 | public static func match(_ url: URL) -> Navigation? { 91 | return allRoutes.reduce(nil) { accum, templateAndRoute in 92 | let (template, route) = templateAndRoute 93 | return accum ?? parsedParams(url: url, fromTemplate: template).flatMap(route)?.value 94 | } 95 | } 96 | } 97 | ``` -------------------------------------------------------------------------------- /source-code-analysis/kickstarter-ios/05 - UI 的管理.md: -------------------------------------------------------------------------------- 1 | ## 用 Storyboard / Xib 创建 UI 2 | 3 | 以前,我们经常看到开发者们在争论:对于 UI 的创建,纯代码手写好还是用 Storyboard / Xib 好?这里就不对这个话题展开了,这么久过去了,相信各位开发者在自己的心里已经有了答案。下面我们看看 Kickstarter 是如何使用 Storyboard / Xib 来创建 UI 的。 4 | 5 | 首先告诉大家,Kickstarter的 UI 几乎都是用 Storyboard / Xib 来完成的。打开 `Kickstarter-iOS/Views/Storyboards` 文件夹,这里存储了应用的全部 `.storyboard` 和 `.xib` 文件。 6 | 7 | 使用 Storyboard 创建 UI,最怕的就是一个 `.storyboard` 文件包含了太多的 ViewController。所以 Kickstarter 为每一个小模块的功能单独创建了一个 Storyboard,并且当你点开每一个 Storyboard,你会发现大部分 Storyboard 只有一个 ViewController。这也很好解决了多人同时编辑一个 Storyboard 时导致的代码冲突问题,因为我们一般不会多人同时去开发一个小模块,把 Storyboard 分得很细之后,就不会出现多人同时编辑一个 Storyboard 的情况。 8 | 9 | 另外,Kickstarter 还定义了 `Storyboard` 和 `Nib` 枚举,列举了所有的 Storyboard 和 xib 文件,方便 ViewController 和 View 的初始化,这是一个非常漂亮的处理(以下代码省略了方法的具体实现): 10 | 11 | ```swift 12 | import UIKit 13 | 14 | public enum Storyboard: String { 15 | case Activity 16 | case Backing 17 | case BackerDashboard 18 | // ... 19 | 20 | public func instantiate(_ viewController: VC.Type, 21 | inBundle bundle: Bundle = .framework) -> VC 22 | } 23 | ``` 24 | 25 | ```swift 26 | import UIKit 27 | 28 | public enum Nib: String { 29 | case BackerDashboardEmptyStateCell 30 | case BackerDashboardProjectCell 31 | case CreditCardCell 32 | // ... 33 | } 34 | 35 | extension UITableView { 36 | public func register(nib: Nib, inBundle bundle: Bundle = .framework) 37 | public func registerHeaderFooter(nib: Nib, inBundle bundle: Bundle = .framework) 38 | } 39 | 40 | protocol NibLoading { 41 | associatedtype CustomNibType 42 | 43 | static func fromNib(nib: Nib) -> CustomNibType? 44 | } 45 | 46 | extension NibLoading { 47 | static func fromNib(nib: Nib) -> Self? 48 | func view(fromNib nib: Nib) -> UIView? 49 | } 50 | ``` 51 | 52 | ## PDF 格式的图标 53 | 54 | 在过去的 iOS 项目中,一般都使用 `png` 格式的图标。而在 Kickstarter 中,使用的是 `pdf` 格式的图标。我们先来看下 pdf 格式的图标有什么优点? 55 | 56 | PDF 的全称是 Portable Document Format,是用于正确显示文档和图形的图像格式。PDF文件具有强大的矢量图形基础,可以用来保矢量图像。矢量图像本质上是巨大的数学方程,每个点、线和形状都由自己的方程表示。每一个“方程式”都可以被指定一种颜色、笔画或厚度来将形状变成艺术。与光栅图像不同,矢量图像与分辨率无关。当你缩小或放大一个矢量图像时,你的形状会变大,但你不会丢失任何细节或得到任何像素。因为您的图像将始终以相同的方式呈现,无论大小如何,都不存在有损或无损矢量图像类型。矢量图像通常用于logo、图标、排版和数字插图。 57 | 58 | 从上面我们可以了解到 pdf 格式的图标最大的优点是可以无损放大。还有,只需要一个 `pdf` 文件就可以代表一个图标,而`png` 图片一般至少需要两个(`2x`和 `3x`, `1x` 一般不需要了)。除了这两个优点之外,我还发现 Kickstarter 中的 `pdf` 文件的大小只有 `5k`左右;而我们现有的项目中一个 `png` 图片就有 `15k`左右,两个 `png` 就 `30k`了,所以,使用 `pdf` 图片还可以一定程度上减少应用的大小。 59 | 60 | ## 颜色的管理 61 | 62 | Kickstarter 项目中用到的颜色,是通过 `ColorScript` 脚本去生成的,这个我在之前的文章有讲到,具体可以查看 [【Kickstarter-iOS 源码分析】02 - 项目相关](https://www.jianshu.com/p/e5caaebb9f35) 中的 ColorScript 部分。 63 | -------------------------------------------------------------------------------- /source-code-analysis/kickstarter-ios/06 - 测试.md: -------------------------------------------------------------------------------- 1 | 测试,是软件开发中非常重要的一个环节。我翻阅了 Kickstarter-iOS 中的大部分测试文件,这篇文章我来总结一下在 Kickstarter-iOS 中都测试了什么和怎样进行测试的。 2 | 3 | ## 单元测试 4 | 5 | 在 Kickstarter-iOS 中,单元测试的对象主要分两类:Model 和 ViewModel。 6 | 7 | ### Model 8 | 9 | 在定义一个 Model 时,一般都会实现 Codable,并且要测试一下对于给定的 json 数据,是否可以解析成功。Kickstarter-iOS 也是这么做的:在每一个 Model 对应的测试文件里,利用假的 json 数据,测试是否可以解析成功。 10 | 11 | 例如 `AuthorTests.swift`里: 12 | 13 | ```swift 14 | func testJSONParsing_WithCompleteData() { 15 | 16 | let author = Author.decodeJSONDictionary([ 17 | "id": 382491714, 18 | "name": "Nino Teixeira", 19 | "avatar": [ 20 | "thumb": "https://ksr-qa-ugc.imgix.net/thumb.jpg", 21 | "small": "https://ksr-qa-ugc.imgix.net/small.jpg", 22 | "medium": "https://ksr-qa-ugc.imgix.net/medium.jpg" 23 | ], 24 | "urls": [ 25 | "web": [ 26 | "user": "https://staging.kickstarter.com/profile/382491714" 27 | ], 28 | "api": [ 29 | "user": "https://api-staging.kickstarter.com/v1/users/382491714" 30 | ] 31 | ] 32 | ]) 33 | 34 | XCTAssertNil(author.error) 35 | XCTAssertEqual(382491714, author.value?.id) 36 | } 37 | ``` 38 | 39 | 除此之外,根据具体的业务,还会有一些其他测试。 40 | 41 | ### ViewModel 42 | 43 | 在 Kickstarter-iOS 中,每个 ViewModel 都会有对应的测试。这里主要讲一下有哪些小技巧值得学习的。 44 | 45 | 1. 在 `XCTestCase+AppEnvironment.swift`中, 通过扩展 `XCTestCase` 定义了 `withEnvironment()` 方法,用于替换某些全局变量,把替换后的 `Environment` push 到 stack 中作为当前的 Environment,执行完 `body()`后,再把刚刚 push 的 Environment 移除,这样可以保证不改变测试前后的 Environment。 46 | 47 | ```swift 48 | func withEnvironment(_ env: Environment, body: () -> Void) { 49 | AppEnvironment.pushEnvironment(env) 50 | body() 51 | AppEnvironment.popEnvironment() 52 | } 53 | 54 | func withEnvironment(...) # 具体看文件 55 | ``` 56 | 57 | 2. 基本上每一个 Model 都会定义一个 template 实例,用于在 ViewModel 中测试。 58 | 59 | ![](https://upload-images.jianshu.io/upload_images/2057254-e42b4e9a6f23bc38.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 60 | 61 | ## UI 测试 62 | 63 | 在 Kickstarter-iOS 中,UI 测试主要是对 ViewController 的测试,看看 UI 的显示是否有问题。 64 | 65 | 因为 Kickstarter 支持多语言,并且 iOS 设备有多种尺寸,所以定义了一个 `combos` 方法,用于组合各种语言和尺寸: 66 | 67 | ```swift 68 | internal func combos(_ xs: [A], _ ys: [B]) -> [(A, B)] { 69 | return xs.flatMap { x in 70 | return ys.map { y in 71 | return (x, y) 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | 另外还定义了一个方法,根据设备的大小和朝向最终把传入的 controller 转变成对应设备大小的 controller。 78 | 79 | ```swift 80 | internal func traitControllers(device: Device = .phone4_7inch, 81 | orientation: Orientation = .portrait, 82 | child: UIViewController = UIViewController(), 83 | additionalTraits: UITraitCollection = .init(), 84 | handleAppearanceTransition: Bool = true) 85 | -> (parent: UIViewController, child: UIViewController) 86 | ``` 87 | 88 | 最后再用 [FBSnapshotTestCase](https://github.com/facebookarchive/ios-snapshot-test-case) 生成各种尺寸语言组合的截图,具体代码如下: 89 | 90 | ```swift 91 | func testAddNewCard() { 92 | combos(Language.allLanguages, Device.allCases).forEach { language, device in 93 | withEnvironment(language: language) { 94 | let controller = AddNewCardViewController.instantiate() 95 | let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller) 96 | 97 | FBSnapshotVerifyView(parent.view, identifier: "lang_\(language)_device_\(device)") 98 | } 99 | } 100 | } 101 | ``` 102 | 103 | 这个测试就会生成以下截图: 104 | 105 | ![](https://upload-images.jianshu.io/upload_images/2057254-25c2407824dd9b65.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 106 | 107 | ## 总结 108 | 109 | 这篇文章主要对 Kickstarter-iOS 中测试的主要规律进行了总结。至于具体的测试,还请读者根据需求对具体代码进行分析。 110 | -------------------------------------------------------------------------------- /source-code-analysis/kickstarter-ios/07 - 第三方工具.md: -------------------------------------------------------------------------------- 1 | 这篇文章简单总结下 Kickstarter-iOS 中用到的第三方工具。 2 | 3 | ## [CircleCI](https://circleci.com/) 4 | 5 | CircleCI 是一个持续集成的持续部署的工具,可以让开发者们更容易、更快地构建、测试和部署应用程序。 6 | 7 | 当开发者修改代码并提交之后,这个工具会自动运行测试,测试通过后,就可以部署到设定好的服务器上,测试人员可以进行进一步的测试。有了这个工具,可以节省开发者的时间。 8 | 9 | 至于怎么使用这里就不细说了,大家可以查看文档。 10 | 11 | ## [SwiftLint](https://github.com/realm/SwiftLint) 12 | 13 | 一个检查 Swift 代码风格的工具,这可以说是 Swift 开发必备的工具。使用方法大家可以查看文档。 14 | 15 | ## [fastlane](https://fastlane.tools/) 16 | 17 | fastlane 是一个开源平台,旨在简化 Android 和 iOS 的部署。他可以让我们自动化开发和发布的工作流程。 18 | 19 | 这个工具在移动开发中使用非常广泛,我个人还是非常推荐大家使用。最开始配置可能会因为不熟悉,觉得很麻烦,但是一旦你配置好之后,以后一个命令就可以做你想做的事情,可以节省很多时间。 20 | -------------------------------------------------------------------------------- /storybook/01-why-storybook.md: -------------------------------------------------------------------------------- 1 | # Install Storybook 2 | ```bash 3 | pnpm dlx storybook@latest init 4 | ``` -------------------------------------------------------------------------------- /swift/modern-swift-concurrency/01 - 为什么需要现代化的 Swift 并发 (Why Modern Swift Concurrency).md: -------------------------------------------------------------------------------- 1 | # 01 - 为什么需要现代化的 Swift 并发 (Why Modern Swift Concurrency) 2 | 3 | 苹果上一次大谈异步框架,是在 2009 年 GCD (Grand Central Dispatch) 问世的时候。 4 | 5 | 虽然 GCD 在 2014 年帮助 Swift 从第一天开始就支持并发和异步,但这种支持不是本地的 —— 它是围绕 Objective-C 的需求和能力设计的。Swift 只是“借用”了这种并发,直到它有了自己的机制,专门为该语言设计。 6 | 7 | Swift 5.5 改变了这一切,它为编写异步并发代码引入了一个新的本地模型。新的并发模型提供了在 Swift 中编写安全、高性能程序所需的一切,包括: 8 | 9 | - 用于以结构化方式运行异步操作的新的本地语法。 10 | - 设计异步和并发代码的标准 API。 11 | - `libdispatch` 框架中的底层更改,使所有高级更改直接集成到操作系统中。 12 | - 为创建安全的并发代码提供了新级别的编译器支持。 13 | 14 | Swift 5.5 引入了新的语法和 API 来支持这些功能。在应用程序中,除了使用最新的 Swift 版本外,还需要针对某些平台版本: 15 | 16 | - 如果使用的是 Xcode 13.2 或更高版本,它会将新的并发运行时与应用程序捆绑在一起,这样就可以将 iOS 13 和 macOS 10.15 作为最低的支持版本(对于本地应用程序)。 17 | - 如果使用的是 Xcode 13,但版本低于 13.2,那么只能把 iOS 15 或 macOS 12(或更高版本)作为最低的支持版本。 18 | 19 | ## 理解异步和并发代码 20 | 21 | 大多数代码的运行方式与在代码编辑器中看到的相同:从上到下,从函数的开头开始,逐行进行到结尾。 22 | 23 | 这使得确定任何给定的代码行何时执行变得很容易。函数调用也是如此:当代码同步运行时,执行按顺序进行。 24 | 25 | 在同步的环境中,代码在单个 CPU 内核上的一个线程中运行。你可以想象同步就像一条单行道上的汽车,每辆车都跟在前面的车。即使一辆车有更高的优先级,比如救护车,它也不能“跳过”其他车辆,开得更快。 26 | 27 | 另一方面,iOS 应用程序和基于 Cocoa 的 macOS 应用程序本质上是异步的。 28 | 29 | 异步执行允许程序的不同部分在一个线程上以任意顺序运行,有时,根据许多不同的事件(如用户输入、网络连接等)在多个线程上同时运行。 30 | 31 | 在异步环境中,很难确定函数运行的确切顺序,特别是当多个异步函数需要使用同一线程时。就像在有红绿灯和交通需要让行的道路上驾驶一样,函数有时必须等到轮到它们继续,甚至必须停下来,直到它们得到绿灯才能继续。 32 | 33 | 异步调用的一个示例是发出网络请求,并提供在 web 服务器响应时运行的闭包。在等待运行完成回调时,应用程序会利用这段时间做其他任务。 34 | 35 | 为了有意识地并行运行部分程序,可以使用并发 API。一些 API 支持同时执行固定数量的任务;其他 API 支持启动一个并发组并允许任意数量的并发任务。 36 | 37 | 这也会导致大量与并发相关的问题。例如,程序的不同部分可能会相互阻止执行,或者可能会遇到非常讨厌的数据竞争,其中两个或多个函数同时访问同一个变量,导致应用程序崩溃或意外破坏应用程序的状态。 38 | 39 | 然而,如果小心使用,并发可以通过在多个 CPU 核上同时执行不同的功能来帮助程序运行得更快,就像小心的驾驶员在多车道高速公路上行驶得更快一样。 40 | 41 | 在执行代码时,高优先级任务可以在低优先级任务之前“跳转”队列,因此可以避免阻塞主线程,并让它自由地对 UI 进行关键更新。 42 | 43 | 虽然异步和并发听起来都很棒,但我们可能会问自己:“为什么 Swift 需要一个新的并发模型?”。在过去,我们可能使用过至少部分上述功能的应用程序。 44 | 45 | 接下来,我们回顾一下 Swift 5.5 之前的并发方法,并了解新的 `async`/`await` 模型的不同之处。 46 | 47 | ### 回顾以前的并发方法 48 | 49 | 在Swift 5.5之前,我们使用 GCD 通过调度队列运行异步代码。还使用了更老的 API,如 `Operation`、`Thread`,甚至直接与基于 C 的 `pthread` 库交互。 50 | 51 | 这些 API 都使用相同的基础:**POSIX** 线程,一种不依赖任何编程语言的标准化执行模型。每个执行流都是一个线程,多个线程可能重叠并同时运行。 52 | 53 | 像 `Operation` 和 `Thread` 这样的线程 wrapper 要求我们手动管理执行。也就是要我们自己负责创建和销毁线程,决定并发任务的执行顺序,并跨线程同步共享数据。这是一项容易出错且乏味的工作。 54 | 55 | GCD 基于队列的模型运行良好。但是,这通常会导致以下问题: 56 | 57 | - **线程激增**:创建太多并发线程需要在活动的线程之间不断切换。这最终会减慢应用程序。 58 | - **优先级反转**:当任意低优先级任务阻止在同一队列中等待的高优先级任务执行时。 59 | - **缺少执行层次结构**:异步代码块缺少执行层次结构,这意味着每个任务都是独立管理的。这使得取消或访问正在运行的任务变得困难。这也使得任务向调用者返回结果变得复杂。 60 | 61 | 为了解决这些缺点,Swift 引入了一种全新的并发模型。接下来,将看到 Swift 中的现代并发是怎样的! 62 | 63 | ## 介绍现代的 Swift 并发模型 64 | 65 | 新的并发模型与语言语法、Swift 运行时和 Xcode 紧密集成。它为开发人员抽象了线程的概念。其主要新功能包括: 66 | 67 | - 协作线程池。 68 | - `async`/`await` 语法。 69 | - 结构化并发。 70 | - 上下文感知代码编译。 71 | 72 | ### 1. 协作线程池 73 | 74 | 新模型透明地管理线程池,以确保它不会超过可用的 CPU 内核数。这样运行时就不需要创建和销毁线程,也不需要经常执行非常耗时的线程切换。相反,代码可以挂起,然后在池中的任何可用线程上快速恢复。 75 | 76 | ### 2. `async`/`await` 语法 77 | 78 | Swift 新的 `async`/`await` 语法让编译器和运行时知道一段代码将来可能会暂停并恢复执行一次或多次。运行时可以无缝地为我们处理这些问题,因此不必担心线程和内核的问题。 79 | 80 | 还有一个好处是,新的语法通常不再需要弱或强捕获 self 或其他变量,因为不需要将escaping closures 用作回调。 81 | 82 | ### 3. 结构化并发 83 | 84 | 现在,每个异步任务都是层次结构的一部分,具有父任务和给定的执行优先级。此层次结构允许运行时在取消父任务时取消所有子任务。此外,它允许运行时在父级完成之前等待所有子级完成。 85 | 86 | 这种层次结构提供了巨大的优势和更明显的结果,其中高优先级任务将在层次结构中的任何低优先级任务之前运行。 87 | 88 | ### 4. 上下文感知代码编译 89 | 90 | 编译器跟踪给定代码段是否可以异步运行。如果是这样,它就不会让您编写潜在的不安全代码,比如改变共享状态。 91 | 92 | 这种高水平的编译器意识支持复杂的新功能,如 **actors**,它区分了在编译时对其状态的同步和异步访问,并通过使编写不安全代码变得更加困难来防止无意中损坏数据。 93 | -------------------------------------------------------------------------------- /swift/modern-swift-concurrency/03 - 异步序列和中间任务.md: -------------------------------------------------------------------------------- 1 | # 03 - 异步序列和中间任务 2 | 3 | ## 了解异步序列 (`AsyncSequence`) 4 | 5 | `AsyncSequence` 是描述可以异步生成元素的序列的协议。从表面看,它的 API 与 Swift 标准库的 `Sequence` 相同,但有一个区别:你需要 `await` 下一个元素,因为它可能不会像在 `Sequence` 中那样立即可用。 6 | 7 | 以下是一些常见的需求,可以使用异步序列: 8 | 9 | - 使用 `await` 在 `for` 循环中对序列进行迭代,如果 `AsyncSequence` 抛出错误,使用 `try`,请重试。代码在每次循环迭代时挂起以获取下一个值: 10 | 11 | ```swift 12 | for try await item in asyncSequence { 13 | ... 14 | } 15 | ``` 16 | 17 | - 使用带有 `while` 循环的标准库迭代器的异步方法。这类似于使用同步序列:需要生成一个迭代器,并使用 `await` 重复调用 `next()`,直到序列结束: 18 | 19 | ```swift 20 | var iterator = asyncSequence.makeAsyncIterator() 21 | while let item = try await iterator.next() { 22 | ... 23 | } 24 | ``` 25 | 26 | - 使用标准序列方法,如 `dropFirst(_:)`、`prefix(_:)` 和 `filter(_:)`: 27 | 28 | ```swift 29 | for await item in asyncSequence 30 | .dropFirst(5) 31 | .prefix(10) 32 | .filter { $0 > 10 } 33 | .map { "Item: \($0)" } { 34 | ... 35 | } 36 | ``` 37 | 38 | - 使用特殊的原始字节序列包装器,例如用于文件内容或从服务器 URL 获取时: 39 | 40 | ```swift 41 | let bytes = URL(fileURLWithPath: "myFile.txt").resourceBytes 42 | 43 | for await character in bytes.characters { 44 | ... 45 | } 46 | 47 | for await line in bytes.lines { 48 | ... 49 | } 50 | ``` 51 | 52 | - 在自己的类型中遵循 `AsyncSequence` 协议来创建自定义序列。 53 | 54 | - 利用 `AsyncStream` 创建自己的自定义异步序列。 55 | 56 | ## 取消任务 57 | 58 | 取消不需要的任务对于并发模型的高效工作至关重要。 59 | 60 | 当使用新的 API 时,比如 `TaskGroup` 或 `async let`,系统通常可以在需要时自动取消任务。 61 | 62 | 但是,可以通过使用以下 `Task` API 为基于任务的代码实现更细粒度的取消策略: 63 | 64 | - `Task.isCancelled`:如果任务仍处于活动状态,但自上次暂停点以来已取消,则返回 `true`。 65 | - `Task.currentPriority`:返回当前任务的优先级。 66 | - `Task.cancel()`:尝试取消任务及其子任务。 67 | - `Task.checkCancellation()`:如果任务被取消,则抛出`CancellationError`,从而更容易退出抛出上下文。 68 | - `Task.yield()`:暂停当前任务的执行,使系统有机会自动取消该任务以执行其他具有更高优先级的任务。 69 | 70 | ## @TaskLocal 71 | 72 | 每个异步任务都在其自己的上下文中执行。但一个任务可以调用其他任务。因为每个函数都可能与许多不同的函数交互,所以在运行时隔离共享数据可能很困难。 73 | 74 | 为了解决这个问题,Swift 提供了一个新的属性包装器 `@TaskLocal`,将给定的属性标记为 **task-local**。 75 | 76 | 在 SwiftUI 中将对象注入到环境中,这样不仅可以使对象的对直接视图可用,还可以对其所有子视图可用。类似地,绑定 `task-local` 的值不仅可用于直接任务,还可用于其所有子任务: 77 | 78 | ![](images/05.png) 79 | 80 | `@TaskLocal` 属性包装器提供了一个名为 `withValue()` 的方法,可以将值绑定到异步任务,或者简单地说,将其注入到任务层次结构中。 81 | 82 | 例如: 83 | 84 | ```swift 85 | final class ViewModel: ObservableObject { 86 | @TaskLocal 87 | var supportsPartialDownloads = false 88 | 89 | func downloadFile(_ file: File) async throws -> Data { 90 | // 根据 Self.supportsPartialDownloads 的值进行不同的下载逻辑 91 | } 92 | } 93 | 94 | // 在 View 中调用 95 | try await ViewModel 96 | .$supportsPartialDownloads 97 | .withValue(file.name.hasSuffix(".jpeg")) { 98 | fileData = try await model.downloadFile(file) 99 | } 100 | ``` 101 | 102 | > **注意**:task-local 属性的类型需要是静态的,或者是全局变量。 103 | 104 | 可以通过这种方式绑定多个值,也可以覆盖内部绑定中的值,如下所示: 105 | 106 | ```swift 107 | try await $property1.withValue(myData) { 108 | ... 109 | try await $property2.withValue(myConfig1) { 110 | ... 111 | try await serverRequest() 112 | try await $property2.withValue(myConfig2) { 113 | ... 114 | } 115 | } 116 | } 117 | ``` 118 | 119 | 使用太多的 task-local 属性可能会变得难以阅读和解释,因为需要为每个绑定将代码包装在一个闭包中。 120 | 121 | **注意**:从这个意义上讲,task-local 属性对于绑定较少的值很有用:完整的配置对象或整个数据模型,而不是像上面的示例中那样单独的单个值或标志。 122 | -------------------------------------------------------------------------------- /swift/modern-swift-concurrency/04 - 用 AsyncStream 自定义异步序列.md: -------------------------------------------------------------------------------- 1 | # 04 - 用 AsyncStream 自定义异步序列 2 | 3 | ## 深入了解 `AsyncSequence``、AsyncIteratorProtocol` 和 `AsyncStream` 4 | 5 | ```swift 6 | @rethrows public protocol AsyncSequence { 7 | 8 | associatedtype AsyncIterator : AsyncIteratorProtocol 9 | 10 | associatedtype Element where Self.Element == Self.AsyncIterator.Element 11 | 12 | func makeAsyncIterator() -> Self.AsyncIterator 13 | } 14 | ``` 15 | 16 | `AsyncSequence` 协议只要求定义元素的类型和提供一个 iterator。关于如何生成元素,没有进一步的要求,对类型生存期没有任何约束。另外,打开 `AsyncSequence` 的[文档](https://developer.apple.com/documentation/swift/asyncsequence),可以看到该协议附带了一系列类似于 `Sequence` 提供的方法: 17 | 18 | ```swift 19 | func contains(_:) -> Bool 20 | func allSatisfy(_:) -> Bool 21 | func first(where:) -> Self.Element? 22 | func min() -> Self.Element? 23 | func max() -> Self.Element? 24 | ... 25 | ``` 26 | 27 | 通过遵循 `AsyncSequence` 协议,可以免费利用协议的默认实现:`prefix(while:)`、 `contains()`、 `min()`、 `max()` 等等。 28 | 29 | 提供的 iterator 需要遵循 `AsyncIteratorProtocol` 协议。它只有一个要求:返回序列中下一个元素的 `async` 方法: 30 | 31 | ```swift 32 | @rethrows public protocol AsyncIteratorProtocol { 33 | 34 | associatedtype Element 35 | 36 | mutating func next() async throws -> Self.Element? 37 | } 38 | ``` 39 | 40 | ### 自定义异步序列 41 | 42 | 下面是一个简单的自定义异步序列,用于打印一个字符串,每秒增加一个字符: 43 | 44 | ```swift 45 | struct Typewriter: AsyncSequence { 46 | typealias Element = String 47 | 48 | let phrase: String 49 | 50 | func makeAsyncIterator() -> Iterator { 51 | return Iterator(phrase) 52 | } 53 | } 54 | 55 | extension Typewriter { 56 | struct Iterator: AsyncIteratorProtocol { 57 | typealias Element = String 58 | 59 | let phrase: String 60 | var index: String.Index 61 | 62 | init(_ phrase: String) { 63 | self.phrase = phrase 64 | self.index = phrase.startIndex 65 | } 66 | 67 | mutating func next() async throws -> String? { 68 | guard index < phrase.endIndex else { 69 | return nil 70 | } 71 | try await Task.sleep(nanoseconds: 1_000_000_000) 72 | defer { 73 | index = phrase.index(after: index) 74 | } 75 | return String(phrase[phrase.startIndex...index]) 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | iterator 保存了字符串的副本。每次调用 `next()`,它都会返回初始字符串的子字符串,该子字符串比最后一个字符串长一个字符。 82 | 83 | 当它到达末尾时,无论是通过 `for wait` 循环还是直接调用 `next()` 的代码,`next()` 都返回 `nil` 以表示序列的结束。 84 | 85 | 使用 `Typewriter`: 86 | 87 | ```swift 88 | for try await item in Typewriter(phrase: "Hello, world!") { 89 | print(item) 90 | } 91 | ``` 92 | 93 | 输出结果: 94 | 95 | ``` 96 | H 97 | He 98 | Hel 99 | Hell 100 | Hello 101 | Hello, 102 | Hello, 103 | Hello, w 104 | Hello, wo 105 | Hello, wor 106 | Hello, worl 107 | Hello, world 108 | Hello, world! 109 | ``` 110 | 111 | ### 使用 `AsyncStream` 简化自定义序列 112 | 113 | 为了简化异步序列的创建,苹果增加了 `AsyncStream` 的类型,其目的是使创建异步序列尽可能简单和快速。 114 | 115 | 它遵循 `AsyncSequence` 并从单个闭包生成值,在闭包中自定义序列的逻辑。 116 | 117 | 除了从 `AsyncSequence` 继承所有默认方法外,`AsyncStream` 有只有简单的几个接口: 118 | 119 | - `init(:bufferingPolicy::)`:通过给定的闭包创建一个生成给定类型的值的新流。闭包可以通过一个称为 continuation 的结构来控制序列。生成但未使用的值保存在缓冲区中。如果不使用该选项设置该缓冲区的存储限制,则将缓冲所有未使用的值。 120 | 121 | - `init(unfolding:onCancel:)`:创建一个新的流,该流通过从 `unfolding` 闭包返回值来生成值。它可以选择在取消时执行 `onCancel` 闭包。 122 | 123 | 使用 `AsyncStream` 重写 `Typewriter`: 124 | 125 | ```swift 126 | let phrase = "Hello, world!" 127 | 128 | var index = phrase.startIndex 129 | let stream = AsyncStream { 130 | guard index < phrase.endIndex else { 131 | return nil 132 | } 133 | 134 | do { 135 | try await Task.sleep(nanoseconds: 1_000_000_000) 136 | } catch { 137 | return nil 138 | } 139 | 140 | defer { 141 | index = phrase.index(after: index) 142 | } 143 | 144 | return String(phrase[phrase.startIndex...index]) 145 | } 146 | 147 | for try await item in stream { 148 | print(item) 149 | } 150 | ``` 151 | -------------------------------------------------------------------------------- /swift/modern-swift-concurrency/07 - TaskGroup 并发代码.md: -------------------------------------------------------------------------------- 1 | # 07 - TaskGroup 并发代码 2 | 3 | `async let` 绑定是一种强大的机制,可以帮助设计异步流程,特别是在有很多任务时,其中一些任务需要并行运行,而另一些任务则相互依赖并按顺序运行。 4 | 5 | 虽然可以灵活地决定使用 `async let` 运行多少任务和哪些任务,但这种语法并不能提供真正的动态并发。 6 | 7 | 假设需要并行运行一千个任务。写异步 `async let` 一千次是不可能的! 8 | 9 | `TaskGroup` 就是用来解决这个问题的。它是一个允许在代码中创建动态并发的 API,可以减少数据争用的可能性,并能让我们安全地处理结果。 10 | 11 | ## 介绍 TaskGroup 12 | 13 | 有两种 API 用于构造任务组:`TaskGroup` 和 `ThrowingTaskGroup`。这两个 API 几乎相同,区别在于后者允许抛出错误。 14 | 15 | 可以使用以下泛型函数之一创建一个组,并帮助编译器正确检查代码的类型: 16 | 17 | - `withTaskGroup(of:returning:body:)`:使用给定的任务返回类型、结果返回类型以及作为初始化和运行组的闭包创建组。 18 | - `withThrowingTaskGroup(of:returning:body:)`:采用类似的参数,但每个任务以及整个组都可能抛出错误。 19 | 20 | 对于这些函数,要注意的是:它们仅在组完成其所有任务后返回。 21 | 22 | 例如: 23 | 24 | ```swift 25 | //1 26 | let images = try await withThrowingTaskGroup( 27 | of: Data.self // 每个任务的返回类型 28 | returning: [UIImage].self // 任务组的返回类型。可以在闭包中指定返回类型,这个参数就可以省略 29 | ) { group in 30 | for index in 0.. Success 92 | ) 93 | ``` 94 | 95 | `operation` 闭包是 `@escaping`,因为它是异步的;同时也是 `@Sendable`,在编译时验证闭包代码是线程安全的。 96 | 97 | 一旦我们自定义的某个类型遵循 `Sendable`,编译器将自动以各种方式限制它,以帮助确保其线程安全。例如,它会要求将类设为 `final`,使类属性不可变,等等。 98 | 99 | `addTask(...)` 的闭包参数也是 `Sendable`: 100 | 101 | ```swift 102 | mutating func addTask( 103 | priority: TaskPriority? = nil, 104 | operation: @escaping @Sendable () async -> ChildTaskResult 105 | ) 106 | ``` 107 | 108 | 因此,代码中的最佳实践是要求异步运行的任何闭包都是 `@Sendable`,并且异步代码中使用的任何值都遵循 `Sendable` 协议。 109 | 110 | 另外,如果 struct 或者 class 是线程安全的,那么还应该遵循 `Sendable` 协议,以便其他并发代码可以安全地使用它。 111 | 112 | ## 把安全的方法标记为 `nonisolated` 113 | 114 | 当 actor 的方法实际上并不直接修改于它自己的共享状态时,那么这些方法就不需要 actor 的特殊作用。我们可以把这些方法看做是安全的,可以通过使用 `nonisolated` 关键字标记它们来帮助运行时并移除它们的安全检查: 115 | 116 | ```swift 117 | nonisolated func loadImages() async throws 118 | ``` 119 | 120 | 使用 `nonisolated` 关键字标记后,这些方法就好像它们是普通类方法而不是 actor 方法一样。这可以提升一点性能。 121 | -------------------------------------------------------------------------------- /swift/modern-swift-concurrency/09 - 全局 Actor.md: -------------------------------------------------------------------------------- 1 | # 09 - 全局 Actor 2 | 3 | 应用程序运行在一个主线程上,所以不能创建第二个或第三个主线程。因此,有一个默认的、共享的 actor 实例,可以在任何地方安全地使用,这是合理的。 4 | 5 | 应用程序范围内单实例共享状态的一些例子如下: 6 | 7 | - 应用程序的数据库层。 8 | - 图片或数据缓存。 9 | - 用户的身份验证状态。 10 | 11 | Swift 允许创建自己的全局 actor,就像 `MainActor` 一样,适用于需要从任何地方都可以访问单例的情况。 12 | 13 | 在 Swift 中,可以使用 `@globalActor` 对 actor 进行注释,从而使其自动遵循 `GlobalActor` 协议: 14 | 15 | ```swift 16 | @globalActor actor MyActor { 17 | ... 18 | } 19 | ``` 20 | 21 | `GlobalActor` 只有一个要求:必须具有一个名为 shared 的静态属性,该属性是可全局访问的 actor 实例。 22 | 23 | 正如使用 `@MainActor` 注释方法以允许其代码更改应用程序的 UI 一样,也可以使用带 `@` 前缀的注释自动执行自己自定义的全局 actor 方法: 24 | 25 | ```swift 26 | @MyActor func say(_ text: String) { 27 | ... 自动在 MyActor 上执行 ... 28 | } 29 | ``` 30 | 31 | 为了避免由于不同线程同时写入数据而导致的并发问题,只需要注释所有相关的方法,并使它们在我们的全局 actor 上运行。 32 | 33 | 事实上,可以用一个全局 actor 对一个完整的类进行注释,这将把一个类的所有方法和属性进行注释(只要它们不是 `nonisolated` 的): 34 | 35 | ```swift 36 | @MyActor class MyClass { 37 | ... 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /swift/modern-swift-concurrency/10 - 分布式系统中的 Actor.md: -------------------------------------------------------------------------------- 1 | # 10 - 分布式系统中的 Actor 2 | 3 | 分布式系统中的 Actor 这一特性目前还处于试验阶段。 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /swift/modern-swift-concurrency/images/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/swift/modern-swift-concurrency/images/01.png -------------------------------------------------------------------------------- /swift/modern-swift-concurrency/images/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/swift/modern-swift-concurrency/images/02.png -------------------------------------------------------------------------------- /swift/modern-swift-concurrency/images/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/swift/modern-swift-concurrency/images/03.png -------------------------------------------------------------------------------- /swift/modern-swift-concurrency/images/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/swift/modern-swift-concurrency/images/04.png -------------------------------------------------------------------------------- /swift/modern-swift-concurrency/images/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/swift/modern-swift-concurrency/images/05.png -------------------------------------------------------------------------------- /swift/notes.md: -------------------------------------------------------------------------------- 1 | 1. `[Any] is [SomeType]` 类型转换为 always true 2 | -------------------------------------------------------------------------------- /swift/pro-swift/03 - 引用类型和值类型.md: -------------------------------------------------------------------------------- 1 | 书籍链接:[《Pro Swift》](https://gumroad.com/l/proswift) (链接需要梯子才能打得开)。 2 | 3 | 布尔类型(Bool)、整型(Integer)、字符串(String)、多元组(Tuple)、枚举(Enum)、数组(Array)、字典(Dictionary)、结构(Struct)都是值类型。 4 | 5 | class和closure都是引用类型。 6 | 7 | ### 一、Closures 8 | 9 | 举个例子,我们创建一个函数,并返回closure: 10 | 11 | ```swift 12 | func createIncrementer() -> () -> Void { 13 | var counter = 0 14 | return { 15 | counter += 1 16 | print(counter) 17 | } 18 | } 19 | ``` 20 | 21 | 在`createIncrementer()`里面,有一个`counter`变量,因为这个变量包含在返回的closure里面,所以他将会被捕获。运行下面代码后,会打印1和2: 22 | 23 | ```swift 24 | let incrementer = createIncrementer() 25 | incrementer() 26 | incrementer() 27 | ``` 28 | 29 | 因为closure是引用类型,所以运行下面代码之后,会打印1、2、3和4: 30 | 31 | ```swift 32 | let incrementer = createIncrementer() 33 | incrementer() 34 | incrementer() 35 | 36 | let incrementerCopy = incrementer 37 | incrementerCopy() 38 | incrementer() 39 | ``` 40 | 41 | ### 二、struct和class 42 | 43 | #### 1. struct和class的对比 44 | 45 | **Swift的类和结构有很多共同点:** 46 | 47 | - 可以定义属性 48 | - 可以定义方法 49 | - 可以定义下标 50 | - 可以定义初始化函数 51 | - 可以使用扩展 52 | - 可以遵循协议 53 | 54 | **类拥有而结构没有的功能:** 55 | 56 | - 继承 57 | - 类型转换 58 | - 类的实例可以使用Deinitializers清除他自己定义的资源 59 | - 类实例可以被一个或多个地方引用 60 | 61 | #### 2. 使用struct和class的注意事项 62 | 63 | - 如果不需要继承的话,尽量选择struct。 64 | - 如果一定要选择class,把它声明为`final`(除非class需要被继承),这样可以提升性能。 65 | - 不管是struct还是class,尽可能把属性定义为常量。 66 | 67 | #### 3. 不可变性 68 | 69 | class和struct在不可变性层面是有区别的。 70 | 71 | **定义结构类型的Person:** 72 | 73 | ```swift 74 | struct PersonStruct { 75 | var name: String 76 | var age: Int 77 | } 78 | 79 | var taylor = PersonStruct(name: "Taylor Swift", age: 26) 80 | taylor.name = "Justin Bieber" 81 | ``` 82 | 83 | 上面的代码声明`taylor`的时候,使用了`var`,这意味着后面可以修改`taylor`,但这明显是不合理的,一个Person被首次确定之后,不应该可以被修改的。如果我们改为`let`: 84 | 85 | ```swift 86 | let taylor = PersonStruct(name: "Taylor Swift", age: 26) 87 | taylor.name = "Justin Bieber" // 这行代码会报错,因为`taylor`是常量,不能在修改 88 | ``` 89 | 90 | 定义class类型的Person: 91 | 92 | ```swift 93 | final class PersonClass { 94 | var name: String 95 | var age: Int 96 | init(name: String, age: Int) { 97 | self.name = name 98 | self.age = age 99 | } } 100 | 101 | let taylor = PersonClass(name: "Taylor Swift", age: 26) 102 | taylor.name = "Justin Bieber" 103 | ``` 104 | 105 | 这里的代码编译通过。声明`taylor`的时候,使用了`let`,但是后面还可以修改`name`,因为class是引用类型,这里的`let`是指`taylor`指向的`PersonClass`实例的地址不变,当我们修改`name`的时候,`PersonClass`实例的地址是不变。 106 | 107 | 下面是其他例子: 108 | 109 | ```swift 110 | // struct变量: 可以更改属性和实例 111 | var taylor1 = PersonStruct(name: "Taylor Swift", age: 26) 112 | taylor1.name = "Justin Bieber" 113 | taylor1 = PersonStruct(name: "Justin Bieber", age: 22) 114 | 115 | 116 | // struct常量: 不可以更改属性和实例 117 | let taylor2 = PersonStruct(name: "Taylor Swift", age: 26) 118 | //taylor2.name = "Justin Bieber" 119 | //taylor2 = PersonStruct(name: "Justin Bieber", age: 22) 120 | 121 | 122 | // class变量: 可以更改属性和引用 123 | var taylor3 = PersonClass(name: "Taylor Swift", age: 26) 124 | taylor3.name = "Justin Bieber" 125 | taylor3 = PersonClass(name: "Justin Bieber", age: 22) 126 | 127 | 128 | // class常量: 可以更改属性,但不能更改引用 129 | let taylor4 = PersonClass(name: "Taylor Swift", age: 26) 130 | taylor4.name = "Justin Bieber" 131 | //taylor4 = PersonClass(name: "Justin Bieber", age: 22) 132 | ``` 133 | -------------------------------------------------------------------------------- /swift/pro-swift/05 - 错误处理.md: -------------------------------------------------------------------------------- 1 | 书籍链接:[《Pro Swift》](https://gumroad.com/l/proswift) (链接需要梯子才能打得开)。 2 | 3 | ### 一、`rethrows`的使用 4 | 5 | 我们先来看看Swift文档里的一句话:不会抛出错误的方法,属于会抛出错误的方法的一种。我们举个例子: 6 | 7 | ```swift 8 | func definitelyWontThrow() { 9 | print("Shiny!") 10 | } 11 | 12 | try definitelyWontThrow() 13 | ``` 14 | 15 | `definitelyWontThrow()`这个方法不会抛出错误,但是我们也可使用`try`去调用,虽然Xcode会有警告。这也证明了Swift文档的那句话。 16 | 17 | 下面我们通过例子来说明`rethrows`的使用:假设我们有一个应用要获取用户的数据,可从服务器或者本地获取。我们定义一个`Failure`枚举来列出可能的错误,并写两个方法分别获取服务器和本地的数据: 18 | 19 | ```swift 20 | enum Failure: Error { 21 | case badNetwork(message: String) 22 | case broken 23 | } 24 | 25 | func fetchRemote() throws -> String { 26 | // 从服务器获取数据,可能会有错误 27 | throw Failure.badNetwork(message: "Firewall blocked port.") 28 | } 29 | 30 | func fetchLocal() -> String { 31 | // 本地获取,不会抛出错误 32 | return "Taylor" 33 | } 34 | ``` 35 | 36 | 然后我们再写一个方法来统一获取用户的数据,可以把`fetchRemote()`和`fetchLocal()`作为closure参数传进去: 37 | 38 | ```swift 39 | func fetchUserData(using closure: () throws -> String) { 40 | do { 41 | let userData = try closure() 42 | print("User data received: \(userData)") 43 | } catch Failure.badNetwork(let message) { 44 | print(message) 45 | } catch { 46 | print("Fetch error") 47 | } 48 | } 49 | ``` 50 | 51 | 虽然这个方法要求传入的参数是会抛出错误的closure,但是我们之前说过:不会抛出错误的方法,属于会抛出错误的方法的一种,所以把不会抛出错误的`fetchLocal()`作为参数传入`fetchUserData(using closure: () throws -> String)`也是没有问题的。我们就可以这样使用: 52 | 53 | ```swift 54 | fetchUserData(using: fetchLocal) 55 | 56 | // 或者 57 | 58 | fetchUserData(using: fetchRemote) 59 | ``` 60 | 61 | 如果我们不想在`fetchUserData(using closure: () throws -> String)`方法里面处理错误,而是继续抛出错误,让它的使用者去处理,可以在方法后面加上`throws`关键字: 62 | 63 | ```swift 64 | func fetchUserData(using closure: () throws -> String) throws { 65 | let userData = try closure() 66 | print("User data received: \(userData)") 67 | } 68 | ``` 69 | 70 | 在使用的时候就要处理错误: 71 | 72 | ```swift 73 | do { 74 | try fetchUserData(using: fetchLocal) 75 | } catch Failure.badNetwork(let message) { 76 | print(message) 77 | } catch { 78 | print("Fetch error") 79 | } 80 | ``` 81 | 82 | 现在问题来了!!!当我传入`fetchLocal()`这个不会抛出错误的方法,根本就没有必要使用`try/catch`。这时我们可以使用`rethrows`来解决这个问题: 83 | 84 | ```swift 85 | func fetchUserData(using closure: () throws -> String) rethrows { 86 | let userData = try closure() 87 | print("User data received: \(userData)") 88 | } 89 | ``` 90 | 91 | 这时我们如果我们传入`fetchLocal()`,根本不需要处理抛出错误的问题: 92 | 93 | ```swift 94 | fetchUserData(using: fetchLocal) 95 | ``` 96 | 97 | 如果是传入`fetchRemote()`,需要处理抛出的错误: 98 | 99 | ```swift 100 | do { 101 | try fetchUserData(using: fetchRemote) 102 | } catch Failure.badNetwork(let message) { 103 | print(message) 104 | } catch { 105 | print("Fetch error") 106 | } 107 | ``` 108 | 109 | ### 二、`try` vs `try?` vs `try!` 110 | 111 | 处理Swift的抛出错误,有三种方式: 112 | 113 | - `try`: 必须与`catch`配合使用 114 | - `try?`: 如果调用的方法抛出了错误,那么会自动返回`nil`;如果没有错误,那么返回带有值的可选类型 115 | - `try!`: 如果调用的方法抛出错误的话,应用会crash;如果没有错误,那么返回对应的值。 116 | 117 | 我们该如何选择?1. 如果我们关心抛出了什么错误,使用`try`;2. 如果我们不关心抛出的错误,并且**不确定**是否会抛出错误,使用`try?`;3. 如果我们不关心抛出的错误,并且**确定**不会抛出错误,或者如果抛出了错误就让应用crash,使用`try!`。 118 | 119 | ### 三、断言 120 | 121 | 当我们在编写复杂应用时,断言是很有好处的。可以让程序满足某些条件时,才继续向下执行。例如: 122 | 123 | ```swift 124 | let success = runImportantOperation() 125 | assert(success == true, "Important operation failed!") 126 | ``` 127 | 128 | 我们先来看看`assert`方法的定义: 129 | 130 | ```swift 131 | public func assert(_ condition: @autoclosure () -> Bool, 132 | _ message: @autoclosure () -> String = String(), 133 | file: StaticString = #file, 134 | line: UInt = #line) { 135 | 136 | if _isDebugAssertConfiguration() { 137 | if !_branchHint(condition(), expected: true) { 138 | _assertionFailed("assertion failed", message(), file, 139 | line, flags: _fatalErrorFlags()) 140 | } } 141 | } 142 | ``` 143 | 144 | 后面的两个参数`file`和`line`,Swift已经提供了默认值,分别代表当前文件和触发断言的行数。前面的`condition`和`message`参数都用了`@autoclosure`,这意味着这两个closure不会马上调用。我们从方法的实现可以看到,`_isDebugAssertConfiguration()`只有在debug模式下,才会运行里面的代码;然后`!_branchHint(condition(), expected: true)`再运行我们传入的`condition`,如果结果不是`true`,运行`_assertionFailed`。 145 | 146 | ### 四、`Never`和`fatalError()` 147 | 148 | `Never`是一个很特殊的返回值类型,意思是这个方法永远不会返回;不同于`Void`,`Void`意思是返回空的东西。 149 | 150 | `fatalError()`会马上终止应用,它的返回值类型正是`Never`。`fatalError()`可以替代无意义的返回,例如,我们在使用`UITableView`时,在`cellForRowAt`方法里面,首先会从队列里面取出已有的cell,得到一个`UITableViewCell`,然后在把它转成自定义的cell(`MyCustomTableViewCell`),最后再把cell返回。假如转型失败呢?通常我们是直接返回`UITableViewCell()`。但是理论上来说,如果转型失败的话,意味着代码出现了很严重的问题,我们返回`UITableViewCell()`也是无意义的,所以我们也可以直接在转型失败的时候调用`fatalError()`,不返回任何东西。 151 | -------------------------------------------------------------------------------- /swift/pro-swift/images/2057254-60f42d356f865e93.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lebron1992/learning-notes/b6660ace22ec6df6863d2549e538a021048c9a81/swift/pro-swift/images/2057254-60f42d356f865e93.jpg -------------------------------------------------------------------------------- /swift/swift-language-guide/11 - 方法 (Functions).md: -------------------------------------------------------------------------------- 1 | # 方法 (Functions) 2 | 3 | 类、结构和枚举都可以定义实例方法和类方法。与C和OC一个主要的不同点,就是Swift的结构和枚举能定义方法,而在OC中只有类才可以定义方法。 4 | 5 | ## 实例方法 (Instance Methods) 6 | 7 | 实例方法内部可以访问其他实例方法和属性。 8 | 9 | ```swift 10 | class Counter { 11 | var count = 0 12 | func increment() { 13 | count += 1 14 | } 15 | func increment(by amount: Int) { 16 | count += amount 17 | } 18 | func reset() { 19 | count = 0 20 | } 21 | } 22 | ``` 23 | 24 | 使用点语法调用方法: 25 | 26 | ```swift 27 | let counter = Counter() 28 | // the initial counter value is 0 29 | counter.increment() 30 | // the counter's value is now 1 31 | counter.increment(by: 5) 32 | // the counter's value is now 6 33 | counter.reset() 34 | // the counter's value is now 0 35 | ``` 36 | 37 | ### self属性 (The self Property) 38 | 39 | 每一个类型的实例都有一个隐式的属性`self`,正好等于实例自己。在它自己的实例方法内部使用`self`来引用当前实例。 40 | 41 | ```swift 42 | func increment() { 43 | self.count += 1 44 | } 45 | ``` 46 | 47 | 如果我们没有写`self`,Swift会认为你正在引用当前实例的属性或方法。当参数名和实例属性名一样时,我们必须明确写出`self`,以与参数名区分开。 48 | 49 | ```swift 50 | struct Point { 51 | var x = 0.0, y = 0.0 52 | func isToTheRightOf(x: Double) -> Bool { 53 | return self.x > x 54 | } 55 | } 56 | let somePoint = Point(x: 4.0, y: 5.0) 57 | if somePoint.isToTheRightOf(x: 1.0) { 58 | print("This point is to the right of the line where x == 1.0") 59 | } 60 | // Prints "This point is to the right of the line where x == 1.0" 61 | ``` 62 | 63 | ### 在实例方法内部修改值类型 (Modifying Value Types from Within Instance Methods) 64 | 65 | 结构和枚举都是值类型。默认情况下,值类型的属性在自己的实例方法内部是不能修改的。 66 | 67 | 然而,如果我们要在方法内部修改结构或者枚举的属性,我们可以为那个方法加上*mutating*行为。然后这个方法就能在方法内部修改自己的属性,当方法结束时,任何改变都会写回给最初的那个结构。这个方法还可以完全赋一个新的实例给`self`属性,然后这个新的实例会替换掉已经存在的实例。 68 | 69 | 使用`mutating`关键字来实现这个行为: 70 | 71 | ```swift 72 | struct Point { 73 | var x = 0.0, y = 0.0 74 | mutating func moveBy(x deltaX: Double, y deltaY: Double) { 75 | x += deltaX 76 | y += deltaY 77 | } 78 | } 79 | var somePoint = Point(x: 1.0, y: 1.0) 80 | somePoint.moveBy(x: 2.0, y: 3.0) 81 | print("The point is now at (\(somePoint.x), \(somePoint.y))") 82 | // Prints "The point is now at (3.0, 4.0)" 83 | ``` 84 | 85 | 注意:我们不能使用用一个结构的常量调用mutating方法,因为它的属性值是不能被改变的,即使是可变属性。 86 | 87 | ```swift 88 | let fixedPoint = Point(x: 3.0, y: 3.0) 89 | fixedPoint.moveBy(x: 2.0, y: 3.0) 90 | // this will report an error 91 | ``` 92 | 93 | ### 在Mutating方法内部给self赋值 (Assignto self Within a Mutating Method) 94 | 95 | ```swift 96 | struct Point { 97 | var x = 0.0, y = 0.0 98 | mutating func moveBy(x deltaX: Double, y deltaY: Double) { 99 | self = Point(x: x + deltaX, y: y + deltaY) 100 | } 101 | } 102 | ``` 103 | 104 | 枚举类型的mutating方法: 105 | 106 | ```swift 107 | enum TriStateSwitch { 108 | case off, low, high 109 | mutating func next() { 110 | switch self { 111 | case .off: 112 | self = .low 113 | case .low: 114 | self = .high 115 | case .high: 116 | self = .off 117 | } 118 | } 119 | } 120 | var ovenLight = TriStateSwitch.low 121 | ovenLight.next() 122 | // ovenLight is now equal to .high 123 | ovenLight.next() 124 | // ovenLight is now equal to .off 125 | ``` 126 | 127 | ## 类方法 (Type Methods) 128 | 129 | 使用`static`来定义类方法,class类型可以使用`class`关键字,以允许子类重写父类的实现。 130 | 131 | **注意:** 在OC中,我们只能在class中定义类方法。而在Swift,可以在类、结构和枚举中定义类方法。 132 | 133 | ```swift 134 | class SomeClass { 135 | class func someTypeMethod() { 136 | // type method implementation goes here 137 | } 138 | } 139 | SomeClass.someTypeMethod() 140 | ``` 141 | 142 | 在类方法的方法体中,隐式的`self`属性引用着类型自己,而不是这个类型的实例。 143 | -------------------------------------------------------------------------------- /swift/swift-language-guide/12 - 下标 (Subscripts).md: -------------------------------------------------------------------------------- 1 | # 下标 (Subscripts) 2 | 3 | 类、结构和枚举都可以定义下标,能让我们快速访问一个集合、列表或序列的成员元素。一个类型可以定义多个下标。 4 | 5 | ## 下标语法 (Subscript Syntax) 6 | 7 | 下标语法类似于实例方法和计算属性语法。使用`subscript`关键字来定义下标,然后指定一个或多个参数和返回类型,就像实例方法一样。但不同于实例方法,下标可以读写或者只读。 8 | 9 | ```swift 10 | subscript(index: Int) -> Int { 11 | get { 12 | // return an appropriate subscript value here 13 | } 14 | set { 15 | // perform a suitable setting action here 16 | } 17 | } 18 | ``` 19 | 20 | `newValue`的类型与下标返回值类型相同,当然我们也可以不用指定参数名,Swift会默认提供一个`newValue`的参数名供我们使用。 21 | 22 | 就像只读计算属性一样,我们写只读下标时可以把`get`去掉: 23 | 24 | ```swift 25 | subscript(index: Int) -> Int { 26 | return an appropriate subscript value here 27 | } 28 | ``` 29 | 30 | 下面是一个例子: 31 | 32 | ```swift 33 | struct TimesTable { 34 | let multiplier: Int 35 | subscript(index: Int) -> Int { 36 | return multiplier * index 37 | } 38 | } 39 | let threeTimesTable = TimeTable(multiplier: 3) 40 | print("six times three is \(threeTimeTable[6])") 41 | ``` 42 | 43 | ## 下标的使用 (Subscript Usage) 44 | 45 | 下标的意义决定于它所在的上下文。下标通常作为一个捷径,用于方法集合、列表或者序列的成员元素。 46 | 47 | 例如,Swift的`Dictionary`就是实现下标来设置和获取存储在字典的值。 48 | 49 | ```swift 50 | var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4] 51 | numberOfLegs["bird"] = 2 52 | ``` 53 | 54 | ## 下标选项 (Subscript Options) 55 | 56 | 下标可以接受任意数量任意类型的参数,也可以返回任意类型的值,还可以使用可变参数,但是不能使用in-out参数和给参数提供默认值。 57 | 58 | 如果有需要的话,类和结构可以提供多个下标实现。在使用时,他会根据中括号内的值或者值的类型来选择合适的下标。定义多个下标被称为*下标重载*。 59 | 60 | 通常情况下,下标只带一个参数,但是也可以带多个参数。例如下面的矩阵结构: 61 | 62 | ```swift 63 | struct Matrix { 64 | let rows: Int, columns: Int 65 | var grid: [Double] 66 | 67 | init(rows: Int, columns: Int) { 68 | self.rows = rows 69 | self.columns = columns 70 | grid = Array(repeat: 0.0, count: rows * columns) 71 | } 72 | 73 | func indexIsValid(row: Int, column: Int) -> Bool { 74 | return row >= 0 && row < rows && column >= 0 && column < columns 75 | } 76 | 77 | subscript(row: Int, column: Int) -> Double { 78 | get { 79 | assert(indexIsValid(row: row, column: column), "Index out of range") 80 | return grid[(row * columns) + column] 81 | } 82 | set { 83 | assert(indexIsValid(row: row, column: column), "Index out of range") 84 | grid[(row * columns) + column] = newValue 85 | } 86 | } 87 | } 88 | ``` 89 | 90 | 创建一个矩阵: 91 | 92 | ```swift 93 | var matrix = Matrix(rows: 2, columns: 2) 94 | ``` 95 | 96 | 可以使用下图表示: 97 | 98 | ![matrix](http://upload-images.jianshu.io/upload_images/2057254-4a66b32cc1e7562f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 99 | 100 | 使用下标来设置矩阵里面的值: 101 | 102 | ```swift 103 | matrix[0, 1] = 1.5 104 | matrix[1, 0] = 3.2 105 | ``` 106 | 107 | 矩阵的值变为: 108 | 109 | ![matrix](http://upload-images.jianshu.io/upload_images/2057254-42190cceffedfa7c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 110 | -------------------------------------------------------------------------------- /swift/swift-language-guide/13 - 继承 (Inheritance).md: -------------------------------------------------------------------------------- 1 | # 继承 (Inheritance) 2 | 3 | ## 定义基类 (Defining a Base Class) 4 | 5 | 没有继承其他类的类,叫做基类。 6 | 7 | **注意:** Swift的类不用继承一个通用的基类。 8 | 9 | 下面是定义一个`Vehicle`基类,定义了任意机动车的共同特征: 10 | 11 | ```swift 12 | class Vehicle { 13 | var currentSpeed = 0.0 14 | var description: String { 15 | return "traveling at \(currentSpeed) miles per hour" 16 | } 17 | func makeNoise() { 18 | // do nothing - an arbitrary vehicle doesn't necessarily make a noise 19 | } 20 | } 21 | 22 | let someVehicle = Vehicle() 23 | 24 | print("Vehicle: \(someVehicle.description)") 25 | // Vehicle: traveling at 0.0 miles per hour 26 | 27 | ``` 28 | 29 | ## 子类化 (Subclassing) 30 | 31 | 子类继承了父类的特征,子类也可以添加新的特征。 32 | 33 | 继承的通用形式: 34 | 35 | ```swift 36 | class SomeSubclass: SomeSuperclass { 37 | // subclass definition goes here 38 | } 39 | ``` 40 | 41 | 下面是定义了一个`Vehicle`的子类`Bicycle`: 42 | 43 | ```swift 44 | class Bicycle: Vehicle { 45 | var hasBasket = false 46 | } 47 | ``` 48 | 49 | `Bycycle`自动获得了`Vehicle`的`currentSpeed`和`description`属性,还有`makeNoise()`方法。另外还添加了一个存储属性`hasBasket`,默认值为`false`(被推断为Bool类型)。 50 | 51 | 默认情况下自行车是没有篮子的,创建一个自行车实例后,把`hasBasket`设置为`true`: 52 | 53 | ```swift 54 | let bicycle = Bicycle() 55 | bicycle.hasBasket = true 56 | ``` 57 | 58 | 还可以修改通过继承得到的属性: 59 | 60 | ```swift 61 | bicycle.currentSpeed = 15.0 62 | print("Bicycle: \(bicycle.description)") 63 | // Bicycle: traveling at 15.0 miles per hour 64 | ``` 65 | 66 | 子类又可以被子类化。下面创建了一个`Bicycle`的子类两轮自行车`Tandem`: 67 | 68 | ```swift 69 | class Tandem: Bicycle { 70 | var currentNumberOfPassangers = 0 71 | } 72 | 73 | let tandem = Tandem() 74 | tandem.hasBasket = true 75 | tandem.currentNumberOfPassengers = 2 76 | tandem.currentSpeed = 22.0 77 | print("Tandem: \(tandem.description)") 78 | // Tandem: traveling at 22.0 miles per hour 79 | ``` 80 | 81 | `Tandem`继承了`Bicycle`的所有属性和方法,并依次继承了`Vehicle`的所有属性和方法,还另外添加了一个属性`currentNumberOfPassengers`,默认为0。 82 | 83 | ## 重写 (Overriding) 84 | 85 | 子类可以自定义从父类继承的实例方法、类方法、实例属性、类属性或者下标的实现,称为重写。使用`override`关键字进行重写。 86 | 87 | ### 访问父类的方法、属性和下标 (Accessing Superclass Method, Properties and Subscripts) 88 | 89 | 当重写父类的方法、属性或者下标时,把已经存在的父类实现作为重写的一部分是非常有用的。 90 | 91 | 使用`super`来访问父类的方法、属性或者下标的实现: 92 | 93 | - 子类的`someMethod()`方法可以通过`super.someMethod()`来访问父类的`somemMethod()`实现。 94 | - 子类的`someProperty`属性可以通过`super.someProperty`来访问父类的`someProperty`属性。 95 | - 子类的`someIndex`下标可以通过`super[someIndex]`来访问父类的同一个下标实现。 96 | 97 | ### 重写方法 (Overriding Methods) 98 | 99 | 创建`Vehicle`的一个子类`Train`,并重写`makeNoise()`方法: 100 | 101 | ```swift 102 | class Train: Vehicle { 103 | override func makeNoise() { 104 | print("Choo Choo") 105 | } 106 | } 107 | ``` 108 | 109 | 创建一个`Train`实例,并调用`makeNoise()`方法,实际调用的是子类的版本: 110 | 111 | ```swift 112 | let train = Train() 113 | train.makeNoise() 114 | // Prints "Choo Choo" 115 | ``` 116 | 117 | ### 重写属性 (Overriding Properties) 118 | 119 | 我们可以重写通过继承得到的实例属性和类属性,或者添加属性观察者来监测属性值的变化。 120 | 121 | ### 重写属性的getter和setter方法 (Overriding Property Getters and Setters) 122 | 123 | 子类是不知道通过继承得到的存储属性和计算属性的本质,他只知道这些属性的名字和类型。 124 | 125 | 我们可以把继承得到的只读属性重写为可读可写属性,但是不能把继承得到的可读可写属性重写范围只读属性。 126 | 127 | **注意:** 如果在重写属性时,提供了setter方法,我们必须也提供一个getter方法。在重写getter方法时,如果不想改变继承得到的属性值,我们可以在getter方法中返回`super.someProperty`,`someProperty`是正在重写的属性名字。 128 | 129 | ```swift 130 | class Car: Vehicle { 131 | var gear = 1 132 | override var description: String { 133 | return super.description + " in gear \(gear)" 134 | } 135 | } 136 | ``` 137 | 138 | ### 重写属性观察者 (Overriding Property Observers) 139 | 140 | 我们可以使用属性重写来把属性观察者添加到继承得到的属性中。当继承得到的属性值发生改变时,我们可以做出响应。 141 | 142 | **注意:** 不能把属性观察者添加到继承得到的常量存储属性或者只读计算属性。因为这些属性不能被设置新的值,所有不能使用`willSet`和`didSet`观察者。另外,也不能在重写属性时同时重写setter方法和属性观察者。如果要监测属性值的变化,可以在自定义的setter方法监测。 143 | 144 | ```swift 145 | class AutomaticCar: Car { 146 | override var currentSpeed: Double { 147 | didSet { 148 | gear = Int(currentSpeed / 10.0) + 1 149 | } 150 | } 151 | } 152 | 153 | let automatic = AutomaticCar() 154 | automatic.currentSpeed = 35.0 155 | print("AutomaticCar: \(automatic.description)") 156 | // AutomaticCar: traveling at 35.0 miles per hour in gear 4 157 | ``` 158 | 159 | 当设置`currentSpeed`属性时,`didSet`观察者内部给`gear`设置了一个新的值。 160 | 161 | ## 防止重写 (Preventing Overrides) 162 | 163 | 我们可以在定义方法、属性或者下标时,在最前面加上`final`关键字来阻止子类重写(例如`final var`、`final func`、`final class func`、`final subscript`)。 164 | 165 | 如果尝试修改`final`标记的方法、属性或者下标,会报编译错误。在扩展(Extension)中定义的方法、属性或者下标都可以使用`final`。 166 | 167 | 在定义类时,在`class`前面加上`final`来阻止这个类被继承。 168 | -------------------------------------------------------------------------------- /swift/swift-language-guide/15 - 反初始化 (Deinitialization).md: -------------------------------------------------------------------------------- 1 | # 反初始化 (Deinitialization) 2 | 3 | 反初始化器在一个类的实例被释放之前调用。反初始化器只适用于class类型。 4 | 5 | ## 反初始化器如果工作 (How Deinitialization Works) 6 | 7 | 当一个类的实例不再需要的时候,Swift会自动释放。Swift是通过自动引用计数(automatic reference counting,简称ARC)来管理内存的,不需要手动管理。然后,在默写情况下是需要自己去释放资源的。例如,如果我们创建了一个自定义的类去打开一个文件或者把数据写入到文件,我们必须在这个类的实例被释放之前手动关闭文件。 8 | 9 | 每个类最多只有一个反初始化器: 10 | 11 | ```swift 12 | deinit { 13 | // perform the deinitialization 14 | } 15 | ``` 16 | 17 | 反初始化器在一个类的实例被释放之前调用,我们不能自己调用反初始化器。父类的反初始化器会被子类继承,并且父类的反初始化器会在子类反初始化器的实现后面被调用。即使子类没有提供自己的反初始化器,父类的反初始化器也会被调用。 18 | 19 | 因为在反初始化器被调用之前,实例还没有被释放,所以在反初始化器里面可以访问所有实例属性,并用这些属性来执行一些相关的代码。 20 | 21 | ## 反初始化器实践 (Deinitializers in Action) 22 | 23 | 创建`Bank`和`Player`两个类: 24 | 25 | ```swift 26 | class Bank { 27 | static var coinsInBank = 10_000 28 | static func distribute(coins numberOfCoinsRequested: Int) -> Int { 29 | let numberOfCoinsToVend = min(numberOfCoinsRequested, coinsInBank) 30 | coinsInBank -= numberOfCoinsToVend 31 | return numberOfCoinsToVend 32 | } 33 | static func receive(coins: Int) { 34 | coinsInBank += coins 35 | } 36 | } 37 | 38 | class Player { 39 | var coinsInPurse: Int 40 | init(coins: Int) { 41 | coinsInPurse = Bank.distribute(coins: coins) 42 | } 43 | func win(coins: Int) { 44 | coinsInPurse += Bank.distribute(coins: coins) 45 | } 46 | deinit { 47 | Bank.receive(coins: coinsInPurse) 48 | } 49 | } 50 | ``` 51 | 52 | 创建一个`Player`实例: 53 | 54 | ```swift 55 | var playerOne: Player? = Player(coins: 100) 56 | print("A new player has joined the game with \(playerOne!.coinsInPurse) coins") 57 | // Prints "A new player has joined the game with 100 coins" 58 | print("There are now \(Bank.coinsInBank) coins left in the bank") 59 | // Prints "There are now 9900 coins left in the bank" 60 | ``` 61 | 62 | 执行`winCoins(_:)`方法: 63 | 64 | ```swift 65 | playerOne!.win(coins: 2_000) 66 | print("PlayerOne won 2000 coins & now has \(playerOne!.coinsInPurse) coins") 67 | // Prints "PlayerOne won 2000 coins & now has 2100 coins" 68 | print("The bank now only has \(Bank.coinsInBank) coins left") 69 | // Prints "The bank now only has 7900 coins left" 70 | ``` 71 | 72 | 把`playerOne`设置为nil,意味着`playerOne`不再引用`Player`实例,那么反初始化器被调用,`receive(coins:)`方法被执行,玩家所有的游戏币返回给银行。 73 | -------------------------------------------------------------------------------- /swift/swift-language-guide/19 - 类型转换 (Type Casting).md: -------------------------------------------------------------------------------- 1 | # 19 - 类型转换 (Type Casting) 2 | 3 | **类型转换**是用来检查实例的类型,或在继承链中把实例作为不同的父类或子类的一种方法。 4 | 5 | Swift中的类型转换用`is`和`as`来实现。 6 | 7 | ## 为类型转换定义一个类的层次结构 (Defining a Class Hierarchy for Type Casting) 8 | 9 | 第一个类是`MediaItem`。假设所有的媒体项目,包括电影和音乐,并且有名字: 10 | 11 | ```swift 12 | class MediaItem { 13 | var name: String 14 | init(name: String) { 15 | self.name = name 16 | } 17 | } 18 | ``` 19 | 20 | 第二个类是`MediaItem`的子类`Movie`。第三个也是`MediaItem`的子类: 21 | 22 | ```swift 23 | class Movie: MediaItem { 24 | var director: String 25 | init(name: String, director: String) { 26 | self.director = director 27 | super.init(name: name) 28 | } 29 | } 30 | 31 | class Song: MediaItem { 32 | var artist: String 33 | init(name: String, artist: String) { 34 | self.artist = artist 35 | super.init(name: name) 36 | } 37 | } 38 | ``` 39 | 40 | 最后创建了一个`library`数组,包含了两个`Movie`实例和三个`Song`实例。Swift根据字面值推断出`library`是`[MediaItem]`类型: 41 | 42 | ```swift 43 | let library = [ 44 | Movie(name: "Casablanca", director: "Michael Curtiz"), 45 | Song(name: "Blue Suede Shoes", artist: "Elvis Presley"), 46 | Movie(name: "Citizen Kane", director: "Orson Welles"), 47 | Song(name: "The One And Only", artist: "Chesney Hawkes"), 48 | Song(name: "Never Gonna Give You Up", artist: "Rick Astley") 49 | ] 50 | // the type of "library" is inferred to be [MediaItem] 51 | ``` 52 | 53 | 虽然这个数组装的是`Movie`和`Song`实例,但如果遍历这个数组,取出来的元素是`MediaItem`类型,而不是`Movie`和`Song`类型。为了使用他们的真实类型,我们需要检查他们的类型,或者向下转型。 54 | 55 | ## 检查类型 (Checking Type) 56 | 57 | 使用`is`来检查一个实例是否是一个子类类型。如果是一个子类类型,返回`true`,否则返回`false`。 58 | 59 | ```swift 60 | var movieCount = 0 61 | var songCount = 0 62 | 63 | for item in library { 64 | if item is Movie { 65 | movieCount += 1 66 | } 67 | else if item is Song { 68 | songCount += 1 69 | } 70 | } 71 | 72 | print("Media library contains \(movieCount) movies and \(songCount) songs") 73 | // Prints "Media library contains 2 movies and 3 songs" 74 | ``` 75 | 76 | ## 向下转型 (Downcasting) 77 | 78 | 一个类型的常量或变量实际上可能是子类的实例类型,我们可以使用`as?`或者`as!`来向下转型到子类类型。 79 | 80 | 因为向下转型可能会失败,所以转型运算符有两种类型。`as?`返回你想转到的那个类型的可选类型;而`as!`如果转型成功,返回你想转到的那个类型,转型失败将会报错。 81 | 82 | 如果不确定是否能转型成功,使用`as?`;如果能确定转型成功,使用`as!`。 83 | 84 | ```swift 85 | for item in library { 86 | if let movie = item as? Movie { 87 | print("Movie: \(movie.name), dir. \(movie.director)") 88 | } 89 | else if let song = item as? Song { 90 | print("Song: \(song.name), by \(song.artist)") 91 | } 92 | } 93 | ``` 94 | 95 | **注意:** 转型实际上不会修改实例或者改变它的值,在底层中,还是同一个实例。 96 | 97 | ## Any和AnyObject的类型转换 (Type Casting for Any and AnyObject) 98 | 99 | Swift提供了两种不确定的类型: 100 | 101 | - `Any`可以代表任何类型的实例,包括方法类型 102 | - `AnyObject`代表任何class类型的实例 103 | 104 | 下面是一个例子: 105 | 106 | ```swift 107 | var things = [Any]() 108 | 109 | things.append(0) 110 | things.append(0.0) 111 | things.append(42) 112 | things.append(3.14159) 113 | things.append("hello") 114 | things.append((3.0, 5.0)) 115 | things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman")) 116 | things.append({ (name: String) -> String in "Hello, \(name)" }) 117 | ``` 118 | 119 | 向下转型: 120 | 121 | ```swift 122 | for thing in things { 123 | switch thing { 124 | case 0 as Int: 125 | print("zero as an Int") 126 | case 0 as Double: 127 | print("zero as a Double") 128 | case let someInt as Int: 129 | print("an integer value of \(someInt)") 130 | case let someDouble as Double where someDouble > 0: 131 | print("a positive double value of \(someDouble)") 132 | case is Double: 133 | print("some other double value that I don't want to print") 134 | case let someString as String: 135 | print("a string value of \"\(someString)\"") 136 | case let (x, y) as (Double, Double): 137 | print("an (x, y) point at \(x), \(y)") 138 | case let movie as Movie: 139 | print("a movie called \(movie.name), dir. \(movie.director)") 140 | case let stringConverter as (String) -> String: 141 | print(stringConverter("Michael")) 142 | default: 143 | print("something else") 144 | } 145 | } 146 | 147 | // zero as an Int 148 | // zero as a Double 149 | // an integer value of 42 150 | // a positive double value of 3.14159 151 | // a string value of "hello" 152 | // an (x, y) point at 3.0, 5.0 153 | // a movie called Ghostbusters, dir. Ivan Reitman 154 | // Hello, Michael 155 | ``` 156 | 157 | **注意:** `Any`代表任何类型的值,包括可选类型。如果在需要传入`Any`类型的位置传入一个可选类型的值,Swift会给你一个警告。如果确实需要把可选类型的值作为一个`Any`类型的值,可以使用`as`来明确地转为`Any`类型: 158 | 159 | ```swift 160 | let optionalNumber: Int? = 3 161 | things.append(optionalNumber) // Warning 162 | things.append(optionalNumber as Any) // No warning 163 | ``` 164 | -------------------------------------------------------------------------------- /swift/swift-language-guide/20 - 嵌套类型 (Nested Types).md: -------------------------------------------------------------------------------- 1 | # 20 - 嵌套类型 (Nested Types) 2 | 3 | 枚举通常是用来支持类或者结构的某些功能。同样地,在一些复杂类型里面,可以定义一些工具类和结构。Swift可以让我们定义嵌套类型。 4 | 5 | ## 嵌套类型实践 (Nested Typeds in Action) 6 | 7 | ```swift 8 | struct BlackjackCard { 9 | 10 | // nested Suit enumeration 11 | enum Suit: Character { 12 | case spades = "♠", hearts = "♡", diamonds = "♢", clubs = "♣" 13 | } 14 | 15 | // nested Rank enumeration 16 | enum Rank: Int { 17 | case two = 2, three, four, five, six, seven, eight, nine, ten 18 | case jack, queen, king, ace 19 | struct Values { 20 | let first: Int, second: Int? 21 | } 22 | var values: Values { 23 | switch self { 24 | case .ace: 25 | return Values(first: 1, second: 11) 26 | case .jack, .queen, .king: 27 | return Values(first: 10, second: nil) 28 | default: 29 | return Values(first: self.rawValue, second: nil) 30 | } 31 | } 32 | } 33 | 34 | // BlackjackCard properties and methods 35 | let rank: Rank, suit: Suit 36 | var description: String { 37 | var output = "suit is \(suit.rawValue)," 38 | output += " value is \(rank.values.first)" 39 | if let second = rank.values.second { 40 | output += " or \(second)" 41 | } 42 | return output 43 | } 44 | } 45 | ``` 46 | 47 | `Suit`枚举描述了四个常见的扑克牌,并且有一个字符代表各自的符号。 48 | 49 | `Rank`枚举定义了13个扑克牌等级,并有一个`Int`类型的原始值代表他们的牌面数字(除了J/Q/K和A)。`Rank`里面嵌套了一个`Values`结构。 50 | 51 | 因为`BlackjackCard`结构没有自定义初始化器,所以他有一个默认地逐一成员初始化器: 52 | 53 | ```swift 54 | let theAceOfSpades = BlackjackCard(rank: .ace, suit: .spades) 55 | print("theAceOfSpades: \(theAceOfSpades.description)") 56 | // Prints "theAceOfSpades: suit is ♠, value is 1 or 11" 57 | ``` 58 | 初始化器中可以直接使用case的名字`.ace`和`.spades`来引用枚举的case。 59 | 60 | ## 引用嵌套类型 (Referring to Nested Types) 61 | 62 | ```swift 63 | let heartsSymbol = BlackjackCard.Suit.hearts.rawValue 64 | // heartsSymbol is "♡" 65 | ``` 66 | -------------------------------------------------------------------------------- /swift/swift-new-features/01 - Swift 4 新增内容.md: -------------------------------------------------------------------------------- 1 | ## 初始化一个多行字符串 2 | 3 | 可以直接在多行字符串中包含`"` 4 | 5 | ```swift 6 | let quotation = """ 7 | The White Rabbit put on his spectacles. "Where shall I begin, 8 | please your Majesty?" he asked. 9 | 10 | "Begin at the beginning," the King said gravely, "and go on 11 | till you come to the end; then stop." 12 | """ 13 | ``` 14 | 15 | 可以直接在多行字符串中包含`"""` 16 | 17 | ```swift 18 | let threeDoubleQuotes = """ 19 | Escaping the first quote \""" 20 | Escaping all three quotes \"\"\" 21 | """ 22 | ``` 23 | 24 | 以下两个字符串是等价的: 25 | 26 | ```swift 27 | let singleLineString = "These are the same." 28 | let multilineString = """ 29 | These are the same. 30 | """ 31 | ``` 32 | 33 | 在字符串前后加空行,可以这样写: 34 | 35 | ```swift 36 | """ 37 | 38 | This string starts with a line feed. 39 | It also ends with a line feed. 40 | 41 | """ 42 | ``` 43 | 44 | 如果一个多行字符串定义在一个方法中,实际字符串的值是不包含每一行前面的空格: 45 | 46 | ```swift 47 | func generateQuotation() -> String { 48 | let quotation = """ 49 | The White Rabbit put on his spectacles. "Where shall I begin, 50 | please your Majesty?" he asked. 51 | 52 | "Begin at the beginning," the King said gravely, "and go on 53 | till you come to the end; then stop." 54 | """ 55 | return quotation 56 | } 57 | print(quotation == generateQuotation()) 58 | // Prints "true" 59 | ``` 60 | 61 | 如果把字符串改为下面的这个样子,第二行字符串前面的空格是不能忽略的: 62 | 63 | ```swift 64 | func generateQuotation() -> String { 65 | let quotation = """ 66 | The White Rabbit put on his spectacles. "Where shall I begin, 67 | please your Majesty?" he asked. 68 | 69 | "Begin at the beginning," the King said gravely, "and go on 70 | till you come to the end; then stop." 71 | """ 72 | return quotation 73 | } 74 | print(quotation == generateQuotation()) 75 | // Prints "false" 76 | ``` 77 | 78 | ## 泛型下标 (Generic Subscripts) 79 | 80 | 下标可以是泛型的,并且可以包含`where`语句: 81 | 82 | ```swift 83 | extension Container { 84 | subscript(indices: Indices) -> [Item] 85 | where Indices.Iterator.Element == Int { 86 | var result = [Item]() 87 | for index in indices { 88 | result.append(self[index]) 89 | } 90 | return result 91 | } 92 | } 93 | ``` 94 | 95 | 上述代码的意思是:接受一个下标序列,并返回对应的元素组成的数组: 96 | - `Indices`必须遵循`Sequence`协议 97 | - `indices`必须是`Indices`类型 98 | - `where`语句要求`Indices`的元素必须是`Int`类型 99 | 100 | ## 协议组合中可以包含父类 101 | 102 | 例如下面这个例子: 103 | 104 | ```swift 105 | class Location { 106 | var latitude: Double 107 | var longitude: Double 108 | init(latitude: Double, longitude: Double) { 109 | self.latitude = latitude 110 | self.longitude = longitude 111 | } 112 | } 113 | class City: Location, Named { 114 | var name: String 115 | init(name: String, latitude: Double, longitude: Double) { 116 | self.name = name 117 | super.init(latitude: latitude, longitude: longitude) 118 | } 119 | } 120 | func beginConcert(in location: Location & Named) { 121 | print("Hello, \(location.name)!") 122 | } 123 | 124 | let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3) 125 | beginConcert(in: seattle) 126 | // Prints "Hello, Seattle!" 127 | ``` 128 | 129 | 在`beginConcert`方法中,`location`参数必须`Location`类型,并且遵循`Named`协议。 130 | 131 | ## `final`关键字不能用在协议扩展中 132 | 133 | 在协议扩展里面,我们不能使用`final`。 134 | -------------------------------------------------------------------------------- /typescript/01 - 入门.md: -------------------------------------------------------------------------------- 1 | # 01 - 入门 2 | 3 | ## 什么是 TypeScript ? 4 | 5 | TypeScript 不是一门新的语言,而是在 JavaScript 的基础上诞生的。它在 JavaScript 的基础上添加了新的特性和优点,让我们在编写 JavaScript 代码候更容易,并且更强大。但是 TypeScript 不能在 JavaScript 的环境下运行,例如浏览器等。但是我们可以使用一些工具把 TypeScript 编译成 JavaScript,所以我们编写的 TypeScript 代码最终还是以 JavaScript 代码去运行. 6 | 7 | ## 为什么使用 TypeScript ? 8 | 9 | 假设我们有以下例子: 10 | 11 | ```javascript 12 | function add(num1, num2) { 13 | return num1 + num2; 14 | } 15 | 16 | console.log(add('2', '3')); 17 | ``` 18 | 19 | 在这个例子中,编写了一个 `add` 方法,我们的本意是想求两个数的和,但是在调用的时候传入了两个字符串类型的数字,那么会返回 `23`,而不是 `5`。因为 `add` 方法的参数类型没有指定,可以是数字类型,也可以传字符串类型。这在实际开发中是有可能出现的,例如在网页中让用户输入两个数,然后求这两个数的和,我们直接从 input 控件拿到的值是字符串类型,直接相加的话就会得到不是我们想要的结果。 20 | 21 | 要想修复这个问题,我们可以把代码改为: 22 | 23 | ```javascript 24 | function add(num1, num2) { 25 | if (typeof num1 === 'number' && typeof num2 === 'number') { 26 | return num1 + num2; 27 | } else { 28 | return +num1 + +num2; 29 | } 30 | } 31 | ``` 32 | 33 | 如果我们使用 TypeScript,就可以指定参数的类型: 34 | 35 | ```typescript 36 | function add(num1: number, num2: number) { 37 | return num1 + num2; 38 | } 39 | ``` 40 | 41 | 所以,我们可以看到使用 TypeScript 能让我们提前发现问题,并且有了类型的概念能让代码更清晰。 42 | 43 | ## 安装和使用 44 | 45 | ### 安装 46 | 47 | 1. 安装 node.js 48 | 2. `npm install -g typescript` 49 | 50 | ### 使用 51 | 52 | 1. 编译 `ts` 文件: `tsc example.ts` 53 | 54 | ## IDE 55 | 56 | ### 推荐使用 [Visual Studio Code](https://code.visualstudio.com/) 57 | 58 | **常用插件:** 59 | 60 | - ESLint 61 | - TSLint 62 | - Material Icon Theme 63 | - Path Intellisense 64 | - Prettier - Code formatter 65 | -------------------------------------------------------------------------------- /typescript/03 - TypeScript 编译器.md: -------------------------------------------------------------------------------- 1 | # 03 - TypeScript 编译器 2 | 3 | ## 编译单个 `ts` 文件 4 | 5 | 命令:`tsc test.ts` 6 | 7 | ## 使用 Watch Mode 8 | 9 | 命令:`tsc test.ts -w` 10 | 11 | ## 编译整个项目或者多个 `ts` 文件 12 | 13 | 1. 在项目的根目录执行 `tsc --init`,生成 `tsconfig.json` 配置文件 14 | 2. 直接执行 `tsc` 就可以编译所有的 `ts` 文件;或者执行 `tsc -w` 使用 Watch Mode。 15 | 16 | ## 包含和移除某些文件 17 | 18 | 在 `tsconfig.json` 文件的 json 根节点下添加 `exclude` 可以在编译时跳过某些文件: 19 | 20 | ``` 21 | "exclude": [ 22 | "node_modules", 23 | "**/*.dev.ts" 24 | ] 25 | ``` 26 | 27 | 这样包含在数组中的文件将不会被编译。 28 | 29 | 如果我们只需要编译某些特定的文件,则可以添加 `include`。例如: 30 | 31 | ``` 32 | "include": [ 33 | "app.ts" 34 | ] 35 | ``` 36 | 37 | **注意:** 添加了 `include` 之后,只有包含在 `include` 数组中的文件才会被编译,同时会跳过 `exclude` 的文件。 38 | 39 | ## 设置编译 Target 40 | 41 | 浏览 `tsconfig.json` 文件,可以看到 `compilerOptions` 节点下有一个字段 `target`,它就是用于指定 TypeScript 代码最终编译成哪个版本的 JavaScript 代码。 42 | 43 | 目前我们一般设置为 `es5`。 44 | 45 | ## `noEmitOnError` 46 | 47 | 如果想在 ts 文件编译出错时不生成对应的 js 文件,可以在 `compilerOptions` 节点下添加 `"noEmitOnError": true`,这样可以避免一些不必要的错误。 48 | 49 | ## 更多编译设置 50 | 51 | - [tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) 52 | 53 | - [Compiler Options](https://www.typescriptlang.org/docs/handbook/compiler-options.html) 54 | -------------------------------------------------------------------------------- /typescript/06 - 泛型.md: -------------------------------------------------------------------------------- 1 | # 06 - 泛型 2 | 3 | ## 创建泛型函数 4 | 5 | 创建泛型函数的写法如下: 6 | 7 | ```typescript 8 | function merge(objA: T, objB: U) { 9 | return Object.assign(objA, objB); 10 | } 11 | ``` 12 | 13 | `T` 和 `U` 就是指定的泛型. 14 | 15 | ## 设置约束 16 | 17 | 对于上面的方法,我们可以传任意参数,但对于 `assign` 函数,它只对对象有效。所以我们可以做泛型作出限制: 18 | 19 | ```typescript 20 | function merge(objA: T, objB: U) { 21 | return Object.assign(objA, objB); 22 | } 23 | ``` 24 | 25 | ## keyof 约束 26 | 27 | 假设我们想写一个函数来获取一个对象中某个字段的值,一开始可能会想到的写法如下: 28 | 29 | ```typescript 30 | function extractAndConvert(obj: object, key: string) { 31 | return obj[key]; 32 | } 33 | ``` 34 | 35 | 这么写编译会报错,因为不能确定 `obj` 的 key 是什么类型,我们不能把它默认看成是 `string`。 36 | 37 | 我们可以利用 `keyof` 约束来确定 `key` 的类型。代码修改如下: 38 | 39 | ```typescript 40 | function extractAndConvert( 41 | obj: T, 42 | key: U 43 | ) { 44 | return obj[key]; 45 | } 46 | ``` 47 | 48 | ## 泛型 Class 49 | 50 | Class 也可以定义为泛型,具体写法如下: 51 | 52 | ```typescript 53 | class DataStorage { 54 | private data: T[] = []; 55 | 56 | addItem(item: T) { 57 | this.data.push(item); 58 | } 59 | 60 | removeItem(item: T) { 61 | const index = this.data.indexOf(item); 62 | if (index === -1) { 63 | return; 64 | } 65 | this.data.splice(index, 1); 66 | } 67 | 68 | getItems() { 69 | return [...this.data]; 70 | } 71 | } 72 | ``` 73 | 74 | ## 泛型工具类型 75 | 76 | 假设有以下接口 `CourseGoal`: 77 | 78 | ```typescript 79 | interface CourseGoal { 80 | title: string; 81 | description: string; 82 | completeDate: Date; 83 | } 84 | ``` 85 | 86 | ### Partial 87 | 88 | 某些情况下,我们想通过一个函数来创建一个对象。例如创建 `CourseGoal` : 89 | 90 | ```typescript 91 | function createdCourseGoal( 92 | title: string, 93 | description: string, 94 | completeDate: Date 95 | ): CourseGoal { 96 | let courseGoal: CourseGoal = {}; 97 | courseGoal.title = title; 98 | courseGoal.description = description; 99 | courseGoal.completeDate = completeDate; 100 | return courseGoal; 101 | } 102 | ``` 103 | 104 | 在上面的代码中,我们先把 `courseGoal` 初始化为 `{}`。然后再逐个属性赋值。但是在 `let courseGoal: CourseGoal = {};` 这行代码中会报错,因为 `{}` 不是 `CourseGoal` 类型。为了解决这个错误,TypeScript 给我们提供了 `Partial` 泛型,它可以让我们初始化对象时只对一部分属性赋值。修改后的代码如下: 105 | 106 | ```typescript 107 | function createdCourseGoal( 108 | title: string, 109 | description: string, 110 | completeDate: Date 111 | ): CourseGoal { 112 | let courseGoal: Partial = {}; 113 | courseGoal.title = title; 114 | courseGoal.description = description; 115 | courseGoal.completeDate = completeDate; 116 | return courseGoal as CourseGoal; 117 | } 118 | ``` 119 | 120 | ### Readonly 121 | 122 | 在开发中,有时需要定义一个常量,定义好之后不允许修改。例如: 123 | 124 | ```typescript 125 | const names = ["Lebron", "Anthony"]; 126 | ``` 127 | 128 | 但是对于 `names`,我们还是可以调用 `push` 或者 `pop` 对其修改: 129 | 130 | ```typescript 131 | names.push("Howard"); 132 | names.pop(); 133 | ``` 134 | 135 | 这显然不是我们想要的。 136 | 137 | TypeScript 给我们提供了 `Readonly` 泛型,它可以禁止后续的修改。修改后的代码如下: 138 | 139 | ```typescript 140 | const names: Readonly = ["Lebron", "Anthony"]; 141 | ``` 142 | 143 | 如果再调用 `push` 或者 `pop` 就会报错。 144 | -------------------------------------------------------------------------------- /typescript/08 - 在 TypeScript 中使用 JavaScript 库.md: -------------------------------------------------------------------------------- 1 | # 08 - 在 TypeScript 中使用第三方库 2 | 3 | ## JavaScript 库 4 | 5 | 在 TypeScript 中使用 JavaScript 库,例如 [lodash](https://lodash.com/),如果直接 import 会报错:`import _ from 'lodash';`。实际上代码是可以运行的,但是在 TypeScript 中会报错。 6 | 7 | 安装 [@types/lodash](https://www.npmjs.com/package/@types/lodash) 之后,就不会报错:`npm install --save-dev @types/lodash`。 8 | 9 | 其他流行的第三方库,也会有对应的库 `@types/xxx`,可以在 Google 搜索 `xxx types` 就能找到。 10 | 11 | ## 访问全局变量 12 | 13 | 如果我们确定有一个全局变量 `GLOBAL`,然后直接在 TypeScript 中访问会报错。例如: 14 | 15 | ```typescript 16 | console.log(GLOBAL); 17 | ``` 18 | 19 | 为了消除这个错误,可以先声明这个全局变量: 20 | 21 | ```typescript 22 | declare var GLOBAL: any; 23 | ``` 24 | --------------------------------------------------------------------------------