diff --git a/IOS/README.md b/IOS/README.md index f1bba59..3ae9186 100644 --- a/IOS/README.md +++ b/IOS/README.md @@ -1 +1,80 @@ -# IOS开发 +# iOS开发 + +> 开源贡献:李岳昆、易远哲,特别鸣谢:杨皓博、贾献华 + +## 任务说明 + +iOS开发对硬件的要求较为严格,我们建议身边有Mac/iMac相关设备同学进行学习。如果您有对iOS开发感兴趣的话,也欢迎您加入本次课程!我们将提供基于Swift的基础语法教程与基于Swift的基础算法实现,此外,我们也将提供一种在虚拟机运行MacOS的方案。 + +由于设备不同,我们提出了两种学习路线:1. MacOS端对算法内容不做硬性要求,可根据实际需要进行学习,但需要完成Swift语言搭建基础的app界面,了解app开发的一般流程;2. 其他端可仅进行Swift语法的学习,并用Swift构建基础的算法内容。对于Task 02 部分的控件功能和实现仅需了解即可。3.我们添加了一些可选任务,同学们如果有条件的话,可以选择性的做一做,加深对移动端开发的了解。 + +## MacOS + +**Task 00**:Swift基础语法入门(2天) + +- 了解Swift语言基础语法与面向协议编程特点 +- 学习可选类型、闭包特性、类与结构体 + +**Task 01**:基础插件与功能实现(4天) + +- 学习UIView、UILabel、UIButton、UIImageView、UITextfield等基础控件的使用 +- 学习UIScrollView、UITableView与Cell等的设置和启动 +- 学习动画、音频、视频与基础权限等内容实现 +- 初识Cocopods,利用优秀的第三方库简化开发流程 +- 代码启动、注册页面跳转、设置app的logo与启动页面 + +**Task 02**:项目练习(5天) + +- 酒店管理系统 +- 贪吃蛇 +- 智能识别水果app +- ARKit实现KNN简易demo + +**Task 02**:Datawhale项目练习(5天)(可选) + +- 项目介绍 + +- 完成底部Tabbar功能开发 +- 完成Me界面设置 +- 完成注册/登录功能开发 +- 提醒功能开发 + +**Task 03**:大作业Statistics in Time(3天) + +- 明确app开发的一般流程 +- 通过Cocopods引入基本的第三方库对app进行优化 +- 通过代码对控件进行设置与页面搭建 + +**Task 03**:大作业Datawhale首页开发(4天)(可选) + +- 故事板模块开发 +- 日历模块开发 +- 记录模块开发 +- 形成业务闭环 + +## iPad、Linux或Windows虚拟机 + +**Task 00**:Swift基础语法入门(2天) + +- 了解Swift语言基础语法与面向协议特点 +- 学习闭包特性与基础控件 + +**Task 01**:基础插件与功能实现(4天) + +- 学习Label、Button、Textfield等基础控件的使用 +- 学习TableView、ScrollView与Cell等的设置和启动 +- 学习动画、音频、视频与基础权限等内容实现 +- 初识Cocopods,利用优秀的第三方库简化开发流程 +- 代码启动、注册页面跳转、设置app的logo与启动页面 + +**Task 02**:算法实现(5天) + +- 三种递归问题求解 +- 搜索问题 +- 图问题 +- 动态规划与旅行商问题 + +**Task 03**:Statistics in Time的算法探究(3天) + +- 各种控件要求的数据格式与相互转换算法 +- 随机输入数据的数组传入与可视化呈现 diff --git a/IOS/Task00:Swift基础语法学习/0.Swift介绍与环境搭建.md b/IOS/Task00:Swift基础语法学习/0.Swift介绍与环境搭建.md new file mode 100644 index 0000000..de27e68 --- /dev/null +++ b/IOS/Task00:Swift基础语法学习/0.Swift介绍与环境搭建.md @@ -0,0 +1,125 @@ +# Swift介绍与环境搭建 +[TOC] + +## 关于Swift + +> **Swift** 是一种非常好的编写软件的编程语言,无论是手机,台式机,服务器,还是其他运行代码的设备。它是一种安全,快速和互动的编程语言,将现代编程语言的精华和苹果工程师文化的智慧,以及来自开源社区的多样化贡献结合了起来。 + +Swift 是一种支持多编程范式和编译式的开源编程语言,苹果于2014年WWDC(苹果开发者大会)发布,用于开发 iOS,OS X 和 watchOS 应用程序。Swift 结合了 C 和 Objective-C 的优点并且不受 C 兼容性的限制。Swift 在 Mac OS 和 iOS 平台可以和 Objective-C 使用相同的运行环境。2015年6月8日,苹果于WWDC 2015上宣布,Swift将开放源代码,包括编译器和标准库。 +### Swift好在哪 +- Swift 对于初学者来说也很友好。它是第一个既满足工业标准又像脚本语言一样充满表现力和趣味的系统编程语言。它支持代码预览(playgrounds),这个革命性的特性可以允许程序员在不编译和运行应用程序的前提下运行 Swift 代码并实时查看结果。 +- Swift 将强大的类型推理和模式匹配与现代轻巧的语法相结合,使复杂的想法能够以清晰简洁的方式表达。因此,代码不仅更容易编写,而且易于阅读和维护。 +- Swift 代码被编译和优化,以充分利用现代硬件。语法和标准库是基于指导原则设计的,编写代码的明显方式也应该是最好的。安全性和速度的结合使得 Swift 成为从 “Hello,world!” 到整个操作系统的绝佳选择。 +- 编译器LLVM对Swift性能进行了优化,其性能平均比OC快30%左右;在开发阶段进行了优化,如字符串拼接、方法默认参数、面向协议编程、高阶函数等等很大程度上提高了开发效率。 +- Swift 通过采用现代编程模式来避免大量常见编程错误: + - 变量始终在使用前初始化或解包,保证代码的安全性。 + - 检查数组索引超出范围的错误。 + - 检查整数是否溢出。 + - 可选值确保明确处理 `nil` 值。 + - 内存被自动管理。 + - 错误处理允许从意外故障控制恢复。 + +## 环境搭建 +> Swift是一门开源的编程语言,该语言用于开发OS X和iOS应用程序。 +> 在正式开发应用程序前,我们需要搭建Swift开发环境,以便更好友好的使用各种开发工具和语言进行快速应用开发。由于Swift开发环境需要在OS X系统中运行,因此其环境的搭建将不同于Windows环境,下面就一起来学习一下Swift开发环境的搭建方法。 + +成功搭建Swift开发环境的前提: +- 一台Mac/iMac 。因为集成开发环境XCode只能运行在OS X系统上。 +- 电脑系统必须在OS 10.9.3及以上。 +- 电脑必须安装Xcode集成开发环境。 + +此外,如果您的手上没有Mac/iMac,您仍然可以尝试在iPad的Playgrounds中进行Swift小项目的探索。如果您手上既没有Mac/iMac,也没有iPad相关产品,您可以选择在虚拟机上搭建相关项目,这可能会造成系统性能的下降。如果您对这些都不感兴趣,学习一门面向协议的新型函数式编程语言也不失为一种收获。 + +## 使用Xcode创建第一个程序 + +在App Store下载Xcode完毕后(这里以Xcode13为例),双击运行: + +- 点击`Create a new Xcode project` + +![Alt text](./images/1637228746939.png) + +- 选择`iOS`-`App`并`Next` + +![Alt text](./images/1637228788344.png) +> 这里代表你选择开发一个iOS平台的App +> +> **简单介绍常见项目选择:**App:创建一个标准的App模版也是我们创建新项目一般勾选的模版; +> +> ducument App:文档App模版 +> +> Game:游戏App模版 +> +> Augmented Reality App: AR项目模板 +> +> Framework:创建一个framework库(类似于封装SDK等) +> +> Static Library:创建静态库 +> +> Metal library: metal库 +> +> +> **简单介绍常见项目选择:**App:创建一个标准的App模版也是我们创建新项目一般勾选的模版; +> +> ducument App:文档App模版 +> +> Game:游戏App模版 +> +> Augmented Reality App: AR项目模板 +> +> Framework:创建一个framework库(类似于封装SDK等) +> +> Static Library:创建静态库 +> +> Metal library: metal库 +> +> **顶部平台介绍**->Mutiplatfrom是跨平台开发(如一套代码在Mac上运行、pad、iphone)、ios也就是主要在iOS平台上运行的项目、macOS主要是在macOS上运行的项目开发、watchOS运行在apple watch上的项目、tvOS苹果电视项目 + +- 输入自己的`Product Name`与`Organization Identifier`: + +![Alt text](./images/1637230229308.png) + +这里我们默认选择Swift语言,Interface选择Storyboard +> **product name**:表示项目名 +> **Team** : 当前队伍,如果是团队合作开发或企业开发会有专属的Team +> **Organization Identifier**:组织名 +> **Interface** :选择是在故事板开发还是SwiftUI开发,这里建议新手选择Interface,SwiftUI需要Swift基础才能选择 +> **Language** : 可以选择Swift与OC两种语言开发 + +- 初始页面如下,在左侧找到`ViewController.swift`,添加如下代码: + + ```swift + import UIKit + + class ViewController: UIViewController { + var label: UILabel! + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + + label = UILabel.init(frame: CGRect(x:50,y:50, width:100,height:30)) + //创建标签,并设置位置,宽度、高度 + label.text = "Hello!" + //设置标签文字 + label.textColor = UIColor.red + //设置标签字体颜色 + self.view.addSubview(label) + //将标签添加到View中 + } + } + ``` +![Alt text](./images/4.png) + +- 点击运行,结果如下: + +![Alt text](./images/5.png) + +至此,我们完成了第一个iOS程序的搭建 + +## Xcode初探 + +对于初学者而言,Xcode中的storyboard提供了简单易懂的UI界面,我们可以在storyboard上搭建自己想要的界面。![Alt text](./images/6.png) +双击编辑,输入Hello World!点击运行即可看到预览结果![Alt text](./images/7.png) +除了Storyboard,Xcode还存在另一种启动方式:代码启动。在实际工程文件与开发过程中,Storyboard上堆砌大量空间会导致运行卡顿,因此本部分仅作为演示使用。 + +## LLVM为何可以同时编译多个编程语言 diff --git a/IOS/Task00:Swift基础语法学习/1.基础部分.md b/IOS/Task00:Swift基础语法学习/1.基础部分.md new file mode 100644 index 0000000..93b7e67 --- /dev/null +++ b/IOS/Task00:Swift基础语法学习/1.基础部分.md @@ -0,0 +1,764 @@ +# Swift语法基础部分 +[TOC] +## 基础部分 + +> Swift 是一门开发 iOS, macOS, watchOS 和 tvOS 应用的新语言。然而,如果你有 C 或者 Objective-C 开发经验的话,你会发现 Swift 的很多内容都是你熟悉的。 + +**Swift** 包含了 C 和 Objective-C 上所有基础数据类型,`Int` 表示整型值; `Double` 和 `Float` 表示浮点型值; `Bool` 是布尔型值;`String` 是文本型数据。 Swift 还提供了三个基本的集合类型,`Array`、`Set` 和 `Dictionary` ,详见`集合类型`部分。 + +就像 C 语言一样,**Swift** 使用变量来进行存储并通过变量名来关联值。在 Swift 中,广泛的使用着值不可变的变量,它们就是常量,而且比 C 语言的常量更强大。在 Swift 中,如果你要处理的值不需要改变,那使用常量可以让你的代码更加安全并且更清晰地表达你的意图。 + +除了我们熟悉的类型,**Swift**还增加了 Objective-C 中没有的高阶数据类型比如元组(Tuple)。元组可以让你创建或者传递一组数据,比如作为函数的返回值时,你可以用一个元组可以返回多个值。 + +**Swift** 还增加了可选(Optional)类型,用于处理值缺失的情况。可选表示 “那儿有一个值,并且它等于 *x* ” 或者 “那儿没有值” 。可选有点像在 Objective-C 中使用 `nil` ,但是它可以用在任何类型上,不仅仅是类。可选类型比 Objective-C 中的 `nil` 指针更加安全也更具表现力,它是 Swift 许多强大特性的重要组成部分。 + +**Swift** 是一门*类型安全*的语言,这意味着 **Swift** 可以让你清楚地知道值的类型。如果你的代码需要一个 `String` ,类型安全会阻止你不小心传入一个 `Int` 。同样的,如果你的代码需要一个 `String`,类型安全会阻止你意外传入一个可选的 `String` 。类型安全可以帮助你在开发阶段尽早发现并修正错误。 + +## 常量和变量 + +常量和变量把一个名字(比如 `maximumNumberOfLoginAttempts` 或者 `welcomeMessage` )和一个指定类型的值(比如数字 `10` 或者字符串 `"Hello"` )关联起来。*常量*的值一旦设定就不能改变,而*变量*的值可以随意更改。 + +### 声明常量和变量 + +常量和变量必须在使用前声明,用 `let` 来声明常量,用 `var` 来声明变量。下面的例子展示了如何用常量和变量来记录用户尝试登录的次数: + +```swift +let maximumNumberOfLoginAttempts = 10 +var currentLoginAttempt = 0 +``` + +这两行代码可以被理解为: + +“声明一个名字是 `maximumNumberOfLoginAttempts` 的新常量,并给它一个值 `10` 。然后,声明一个名字是 `currentLoginAttempt` 的变量并将它的值初始化为 `0` 。” + +在这个例子中,允许的最大尝试登录次数被声明为一个常量,因为这个值不会改变。当前尝试登录次数被声明为一个变量,因为每次尝试登录失败的时候都需要增加这个值。 + +你可以在一行中声明多个常量或者多个变量,用逗号隔开: + +```swift +var x = 0.0, y = 0.0, z = 0.0 +``` + +> **注意** +> 如果你的代码中有不需要改变的值,请使用 `let` 关键字将它声明为常量。只将需要改变的值声明为变量。 + +### 类型注解 + +当你声明常量或者变量的时候可以加上*类型注解(type annotation)*,说明常量或者变量中要存储的值的类型。如果要添加类型注解,需要在常量或者变量名后面加上一个冒号和空格,然后加上类型名称。 + +这个例子给 `welcomeMessage` 变量添加了类型注解,表示这个变量可以存储 `String` 类型的值: + +```swift +var welcomeMessage: String +``` + +声明中的冒号代表着*“是...类型”*,所以这行代码可以被理解为: + +“声明一个类型为 `String` ,名字为 `welcomeMessage` 的变量。” + +“类型为 `String` ”的意思是“可以存储任意 `String` 类型的值。” + +`welcomeMessage` 变量现在可以被设置成任意字符串: + +```swift +welcomeMessage = "Hello" +``` + +你可以在一行中定义多个同样类型的变量,用逗号分割,并在最后一个变量名之后添加类型注解: + +```swift +var red, green, blue: Double +``` + +> **注意** +> 一般来说你很少需要写类型注解。如果你在声明常量或者变量的时候赋了一个初始值,Swift 可以推断出这个常量或者变量的类型,请参考`类型安全和类型推断`。在上面的例子中,没有给 `welcomeMessage` 赋初始值,所以变量 `welcomeMessage` 的类型是通过一个类型注解指定的,而不是通过初始值推断的。 + +### 常量和变量的命名 + +常量和变量名可以包含任何字符,包括 Unicode 字符: + +```swift +let π = 3.14159 +let 你好 = "你好世界" +let 🐶🐮 = "dogcow" +``` + +常量与变量名不能包含数学符号,箭头,保留的(或者非法的)Unicode 码位,连线与制表符。也不能以数字开头,但是可以在常量与变量名的其他地方包含数字。 + +一旦你将常量或者变量声明为确定的类型,你就不能使用相同的名字再次进行声明,或者改变其存储的值的类型。同时,你也不能将常量与变量进行互转。 + +> **注意** +> 如果你需要使用与 Swift 保留关键字相同的名称作为常量或者变量名,你可以使用反引号(`)将关键字包围的方式将其作为名字使用。无论如何,你应当避免使用关键字作为常量或变量名,除非你别无选择。 + +你可以更改现有的变量值为其他同类型的值,在下面的例子中,`friendlyWelcome` 的值从 `"Hello!"`改为了 `"Bonjour!"`: + +```swift +var friendlyWelcome = "Hello!" +friendlyWelcome = "Bonjour!" +// friendlyWelcome 现在是 "Bonjour!" +``` + +与变量不同,常量的值一旦被确定就不能更改了。尝试这样做会导致编译时报错: + +```swift +let languageName = "Swift" +languageName = "Swift++" +// 这会报编译时错误 - languageName 不可改变 +``` +### 输出常量和变量 +你可以用 `print(_:separator:terminator:)` 函数来输出当前常量或变量的值: +```swift +print(friendlyWelcome) +// 输出“Bonjour!” +``` +`print(_:separator:terminator:)` 是一个用来输出一个或多个值到适当输出区的全局函数。如果你用 Xcode,`print(_:separator:terminator:)` 将会输出内容到“console”面板上。`separator` 和 `terminator` 参数具有默认值,因此你调用这个函数的时候可以忽略它们。默认情况下,该函数通过添加换行符来结束当前行。如果不想换行,可以传递一个空字符串给 `terminator` 参数--例如,`print(someValue, terminator:"")` 。关于参数默认值的更多信息,请参考 [默认参数值](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/06_Functions.html#default_parameter_values)。 + +**Swift** 用*字符串插值(string interpolation)*的方式把常量名或者变量名当做占位符加入到长字符串中,**Swift** 会用当前常量或变量的值替换这些占位符。将常量或变量名放入圆括号中,并在开括号前使用反斜杠将其转义: + +```swift +print("The current value of friendlyWelcome is \(friendlyWelcome)") +// 输出“The current value of friendlyWelcome is Bonjour!” +``` + +> **注意** +> 字符串插值所有可用的选项,请参考 [字符串插值](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/03_Strings_and_Characters.html#string_interpolation)。 + +## 注释 + +请将你的代码中的非执行文本注释成提示或者笔记以方便你将来阅读。Swift 的编译器将会在编译代码时自动忽略掉注释部分。 + +Swift 中的注释与 C 语言的注释非常相似。单行注释以双正斜杠(`//`)作为起始标记: + +```swift +// 这是一个注释 +``` + +你也可以进行多行注释,其起始标记为单个正斜杠后跟随一个星号(`/*`),终止标记为一个星号后跟随单个正斜杠(`*/`): + +```swift +/* 这也是一个注释, +但是是多行的 */ +``` + +与 C 语言多行注释不同,Swift 的多行注释可以嵌套在其它的多行注释之中。你可以先生成一个多行注释块,然后在这个注释块之中再嵌套成第二个多行注释。终止注释时先插入第二个注释块的终止标记,然后再插入第一个注释块的终止标记: + +```swift +/* 这是第一个多行注释的开头 +/* 这是第二个被嵌套的多行注释 */ +这是第一个多行注释的结尾 */ +``` + +通过运用嵌套多行注释,你可以快速方便的注释掉一大段代码,即使这段代码之中已经含有了多行注释块。 + +## 分号 + +与其他大部分编程语言不同,Swift 并不强制要求你在每条语句的结尾处使用分号(`;`),当然,你也可以按照你自己的习惯添加分号。有一种情况下必须要用分号,即你打算在同一行内写多条独立的语句: + +```swift +let cat = "🐱"; print(cat) +// 输出“🐱” +``` + +## 整数 + +整数就是没有小数部分的数字,比如 `42` 和 `-23` 。整数可以是 `有符号`(正、负、零)或者 `无符号`(正、零)。 + +Swift 提供了8、16、32和64位的有符号和无符号整数类型。这些整数类型和 C 语言的命名方式很像,比如8位无符号整数类型是 `UInt8`,32位有符号整数类型是 `Int32` 。就像 Swift 的其他类型一样,整数类型采用大写命名法。 + +### 整数范围 + +你可以访问不同整数类型的 `min` 和 `max` 属性来获取对应类型的最小值和最大值: + +```swift +let minValue = UInt8.min // minValue 为 0,是 UInt8 类型 +let maxValue = UInt8.max // maxValue 为 255,是 UInt8 类型 +``` + +`min` 和 `max` 所传回值的类型,正是其所对的整数类型(如上例 UInt8, 所传回的类型是 UInt8),可用在表达式中相同类型值旁。 + +### Int + +一般来说,你不需要专门指定整数的长度。Swift 提供了一个特殊的整数类型 `Int`,长度与当前平台的原生字长相同: + +- 在32位平台上,`Int` 和 `Int32` 长度相同。 +- 在64位平台上,`Int` 和 `Int64` 长度相同。 + +除非你需要特定长度的整数,一般来说使用 `Int` 就够了。这可以提高代码一致性和可复用性。即使是在32位平台上,`Int` 可以存储的整数范围也可以达到 `-2,147,483,648` ~ `2,147,483,647`,大多数时候这已经足够大了。 + +### UInt + +Swift 也提供了一个特殊的无符号类型 `UInt`,长度与当前平台的原生字长相同: + +- 在32位平台上,`UInt` 和 `UInt32` 长度相同。 +- 在64位平台上,`UInt` 和 `UInt64` 长度相同。 + +> 注意 +> +> 尽量不要使用 `UInt`,除非你真的需要存储一个和当前平台原生字长相同的无符号整数。除了这种情况,最好使用 `Int`,即使你要存储的值已知是非负的。统一使用 `Int` 可以提高代码的可复用性,避免不同类型数字之间的转换,并且匹配数字的类型推断,请参考 [类型安全和类型推断](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/01_The_Basics.html#type_safety_and_type_inference)。 + +## 浮点数 + +浮点数是有小数部分的数字,比如 `3.14159`、`0.1` 和 `-273.15`。 + +浮点类型比整数类型表示的范围更大,可以存储比 `Int` 类型更大或者更小的数字。Swift 提供了两种有符号浮点数类型: + +- `Double` 表示64位浮点数。当你需要存储很大或者很高精度的浮点数时请使用此类型。 +- `Float` 表示32位浮点数。精度要求不高的话可以使用此类型。 + +> 注意 +> +> `Double` 精确度很高,至少有15位数字,而 `Float` 只有6位数字。选择哪个类型取决于你的代码需要处理的值的范围,在两种类型都匹配的情况下,将优先选择 `Double`。 + +## 类型安全和类型推断 + +Swift 是一个*类型安全(type safe)*的语言。类型安全的语言可以让你清楚地知道代码要处理的值的类型。如果你的代码需要一个 `String`,你绝对不可能不小心传进去一个 `Int`。 + +由于 Swift 是类型安全的,所以它会在编译你的代码时进行*类型检查(type checks)*,并把不匹配的类型标记为错误。这可以让你在开发的时候尽早发现并修复错误。 + +当你要处理不同类型的值时,类型检查可以帮你避免错误。然而,这并不是说你每次声明常量和变量的时候都需要显式指定类型。如果你没有显式指定类型,Swift 会使用*类型推断(type inference)*来选择合适的类型。有了类型推断,编译器可以在编译代码的时候自动推断出表达式的类型。原理很简单,只要检查你赋的值即可。 + +因为有类型推断,和 C 或者 Objective-C 比起来 Swift 很少需要声明类型。常量和变量虽然需要明确类型,但是大部分工作并不需要你自己来完成。 + +当你声明常量或者变量并赋初值的时候类型推断非常有用。当你在声明常量或者变量的时候赋给它们一个字面量(literal value 或 literal)即可触发类型推断。(字面量就是会直接出现在你代码中的值,比如 `42`和 `3.14159` 。) + +例如,如果你给一个新常量赋值 `42` 并且没有标明类型,Swift 可以推断出常量类型是 `Int` ,因为你给它赋的初始值看起来像一个整数: + +```swift +let meaningOfLife = 42 +// meaningOfLife 会被推测为 Int 类型 +``` + +同理,如果你没有给浮点字面量标明类型,Swift 会推断你想要的是 `Double`: + +```swift +let pi = 3.14159 +// pi 会被推测为 Double 类型 +``` + +当推断浮点数的类型时,Swift 总是会选择 `Double` 而不是 `Float`。 + +如果表达式中同时出现了整数和浮点数,会被推断为 `Double` 类型: + +```swift +let anotherPi = 3 + 0.14159 +// anotherPi 会被推测为 Double 类型 +``` + +原始值 `3` 没有显式声明类型,而表达式中出现了一个浮点字面量,所以表达式会被推断为 `Double` 类型。 + +## 数值型字面量 + +整数字面量可以被写作: + +- 一个*十进制*数,没有前缀 +- 一个*二进制*数,前缀是 `0b` +- 一个*八进制*数,前缀是 `0o` +- 一个*十六进制*数,前缀是 `0x` + +下面的所有整数字面量的十进制值都是 `17`: + +```swift +let decimalInteger = 17 +let binaryInteger = 0b10001 // 二进制的17 +let octalInteger = 0o21 // 八进制的17 +let hexadecimalInteger = 0x11 // 十六进制的17 +``` + +浮点字面量可以是十进制(没有前缀)或者是十六进制(前缀是 `0x` )。小数点两边必须有至少一个十进制数字(或者是十六进制的数字)。十进制浮点数也可以有一个可选的指数(exponent),通过大写或者小写的 `e` 来指定;十六进制浮点数必须有一个指数,通过大写或者小写的 `p` 来指定。 + +如果一个十进制数的指数为 `exp`,那这个数相当于基数和10^exp 的乘积: + +- `1.25e2` 表示 1.25 × 10^2,等于 `125.0`。 +- `1.25e-2` 表示 1.25 × 10^-2,等于 `0.0125`。 + +如果一个十六进制数的指数为 `exp`,那这个数相当于基数和2^exp 的乘积: + +- `0xFp2` 表示 15 × 2^2,等于 `60.0`。 +- `0xFp-2` 表示 15 × 2^-2,等于 `3.75`。 + +下面的这些浮点字面量都等于十进制的 `12.1875`: + +```swift +let decimalDouble = 12.1875 +let exponentDouble = 1.21875e1 +let hexadecimalDouble = 0xC.3p0 +``` + +数值类字面量可以包括额外的格式来增强可读性。整数和浮点数都可以添加额外的零并且包含下划线,并不会影响字面量: + +```swift +let paddedDouble = 000123.456 +let oneMillion = 1_000_000 +let justOverOneMillion = 1_000_000.000_000_1 +``` + +## 数值型类型转换 + +通常来讲,即使代码中的整数常量和变量已知非负,也请使用 `Int` 类型。总是使用默认的整数类型可以保证你的整数常量和变量可以直接被复用并且可以匹配整数类字面量的类型推断。 + +只有在必要的时候才使用其他整数类型,比如要处理外部的长度明确的数据或者为了优化性能、内存占用等等。使用显式指定长度的类型可以及时发现值溢出并且可以暗示正在处理特殊数据。 + +### 整数转换 + +不同整数类型的变量和常量可以存储不同范围的数字。`Int8` 类型的常量或者变量可以存储的数字范围是 `-128`~`127`,而 `UInt8` 类型的常量或者变量能存储的数字范围是 `0`~`255`。如果数字超出了常量或者变量可存储的范围,编译的时候会报错: + +```swift +let cannotBeNegative: UInt8 = -1 +// UInt8 类型不能存储负数,所以会报错 +let tooBig: Int8 = Int8.max + 1 +// Int8 类型不能存储超过最大值的数,所以会报错 +``` + +由于每种整数类型都可以存储不同范围的值,所以你必须根据不同情况选择性使用数值型类型转换。这种选择性使用的方式,可以预防隐式转换的错误并让你的代码中的类型转换意图变得清晰。 + +要将一种数字类型转换成另一种,你要用当前值来初始化一个期望类型的新数字,这个数字的类型就是你的目标类型。在下面的例子中,常量 `twoThousand` 是 `UInt16` 类型,然而常量 `one` 是 `UInt8` 类型。它们不能直接相加,因为它们类型不同。所以要调用 `UInt16(one)` 来创建一个新的 `UInt16` 数字并用 `one` 的值来初始化,然后使用这个新数字来计算: + +```swift +let twoThousand: UInt16 = 2_000 +let one: UInt8 = 1 +let twoThousandAndOne = twoThousand + UInt16(one) +``` + +现在两个数字的类型都是 `UInt16`,可以进行相加。目标常量 `twoThousandAndOne` 的类型被推断为 `UInt16`,因为它是两个 `UInt16` 值的和。 + +`SomeType(ofInitialValue)` 是调用 Swift 构造器并传入一个初始值的默认方法。在语言内部,`UInt16` 有一个构造器,可以接受一个 `UInt8` 类型的值,所以这个构造器可以用现有的 `UInt8`来创建一个新的 `UInt16`。注意,你并不能传入任意类型的值,只能传入 `UInt16` 内部有对应构造器的值。不过你可以扩展现有的类型来让它可以接收其他类型的值(包括自定义类型),请参考 [扩展](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/20_Extensions.html)。 + +### 整数和浮点数转换 + +整数和浮点数的转换必须显式指定类型: + +```swift +let three = 3 +let pointOneFourOneFiveNine = 0.14159 +let pi = Double(three) + pointOneFourOneFiveNine +// pi 等于 3.14159,所以被推测为 Double 类型 +``` + +这个例子中,常量 `three` 的值被用来创建一个 `Double` 类型的值,所以加号两边的数类型须相同。如果不进行转换,两者无法相加。 + +浮点数到整数的反向转换同样行,整数类型可以用 `Double` 或者 `Float` 类型来初始化: + +```swift +let integerPi = Int(pi) +// integerPi 等于 3,所以被推测为 Int 类型 +``` + +当用这种方式来初始化一个新的整数值时,浮点值会被截断。也就是说 `4.75` 会变成 `4`,`-3.9` 会变成 `-3`。 + +> 注意 +> +> 结合数字类常量和变量不同于结合数字类字面量。字面量 `3` 可以直接和字面量 `0.14159` 相加,因为数字字面量本身没有明确的类型。它们的类型只在编译器需要求值的时候被推测。 + +## 类型别名 + +*类型别名(type aliases)*就是给现有类型定义另一个名字。你可以使用 `typealias` 关键字来定义类型别名。 + +当你想要给现有类型起一个更有意义的名字时,类型别名非常有用。假设你正在处理特定长度的外部资源的数据: + +```swift +typealias AudioSample = UInt16 +``` + +定义了一个类型别名之后,你可以在任何使用原始名的地方使用别名: + +```swift +var maxAmplitudeFound = AudioSample.min +// maxAmplitudeFound 现在是 0 +``` + +本例中,`AudioSample` 被定义为 `UInt16` 的一个别名。因为它是别名,`AudioSample.min` 实际上是 `UInt16.min`,所以会给 `maxAmplitudeFound` 赋一个初值 `0`。 + +## 布尔值 + +Swift 有一个基本的*布尔(Boolean)类型*,叫做 `Bool`。布尔值指*逻辑*上的值,因为它们只能是真或者假。Swift 有两个布尔常量,`true` 和 `false`: + +```swift +let orangesAreOrange = true +let turnipsAreDelicious = false +``` + +`orangesAreOrange` 和 `turnipsAreDelicious` 的类型会被推断为 `Bool`,因为它们的初值是布尔字面量。就像之前提到的 `Int` 和 `Double` 一样,如果你创建变量的时候给它们赋值 `true` 或者 `false`,那你不需要将常量或者变量声明为 `Bool` 类型。初始化常量或者变量的时候如果所赋的值类型已知,就可以触发类型推断,这让 Swift 代码更加简洁并且可读性更高。 + +当你编写条件语句比如 `if` 语句的时候,布尔值非常有用: + +```swift +if turnipsAreDelicious { + print("Mmm, tasty turnips!") +} else { + print("Eww, turnips are horrible.") +} +// 输出“Eww, turnips are horrible.” +``` + +条件语句,例如 `if`,请参考 [控制流](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html)。 + +如果你在需要使用 `Bool` 类型的地方使用了非布尔值,Swift 的类型安全机制会报错。下面的例子会报告一个编译时错误: + +```swift +let i = 1 +if i { + // 这个例子不会通过编译,会报错 +} +``` + +然而,下面的例子是合法的: + +```swift +let i = 1 +if i == 1 { + // 这个例子会编译成功 +} +``` + +`i == 1` 的比较结果是 `Bool` 类型,所以第二个例子可以通过类型检查。类似 `i == 1` 这样的比较,请参考 [基本操作符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html)。 + +和 Swift 中的其他类型安全的例子一样,这个方法可以避免错误并保证这块代码的意图总是清晰的。 + +## 元组 + +*元组(tuples)*把多个值组合成一个复合值。元组内的值可以是任意类型,并不要求是相同类型。 + +下面这个例子中,`(404, "Not Found")` 是一个描述 *HTTP 状态码(HTTP status code)*的元组。HTTP 状态码是当你请求网页的时候 web 服务器返回的一个特殊值。如果你请求的网页不存在就会返回一个 `404 Not Found` 状态码。 + +```swift +let http404Error = (404, "Not Found") +// http404Error 的类型是 (Int, String),值是 (404, "Not Found") +``` + +`(404, "Not Found")` 元组把一个 `Int` 值和一个 `String` 值组合起来表示 HTTP 状态码的两个部分:一个数字和一个人类可读的描述。这个元组可以被描述为“一个类型为 `(Int, String)` 的元组”。 + +你可以把任意顺序的类型组合成一个元组,这个元组可以包含所有类型。只要你想,你可以创建一个类型为 `(Int, Int, Int)` 或者 `(String, Bool)` 或者其他任何你想要的组合的元组。 + +你可以将一个元组的内容分解(decompose)成单独的常量和变量,然后你就可以正常使用它们了: + +```swift +let (statusCode, statusMessage) = http404Error +print("The status code is \(statusCode)") +// 输出“The status code is 404” +print("The status message is \(statusMessage)") +// 输出“The status message is Not Found” +``` + +如果你只需要一部分元组值,分解的时候可以把要忽略的部分用下划线(`_`)标记: + +```swift +let (justTheStatusCode, _) = http404Error +print("The status code is \(justTheStatusCode)") +// 输出“The status code is 404” +``` + +此外,你还可以通过下标来访问元组中的单个元素,下标从零开始: + +```swift +print("The status code is \(http404Error.0)") +// 输出“The status code is 404” +print("The status message is \(http404Error.1)") +// 输出“The status message is Not Found” +``` + +你可以在定义元组的时候给单个元素命名: + +```swift +let http200Status = (statusCode: 200, description: "OK") +``` + +给元组中的元素命名后,你可以通过名字来获取这些元素的值: + +```swift +print("The status code is \(http200Status.statusCode)") +// 输出“The status code is 200” +print("The status message is \(http200Status.description)") +// 输出“The status message is OK” +``` + +作为函数返回值时,元组非常有用。一个用来获取网页的函数可能会返回一个 `(Int, String)` 元组来描述是否获取成功。和只能返回一个类型的值比较起来,一个包含两个不同类型值的元组可以让函数的返回信息更有用。请参考 [函数参数与返回值](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/06_Functions.html#Function_Parameters_and_Return_Values)。 + +> 注意 +> +> 当遇到一些相关值的简单分组时,元组是很有用的。元组不适合用来创建复杂的数据结构。如果你的数据结构比较复杂,不要使用元组,用类或结构体去建模。欲获得更多信息,请参考 [结构体和类](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/09_Classes_and_Structures.md)。 + +## 可选类型 + +使用*可选类型(optionals)*来处理值可能缺失的情况。可选类型表示两种可能: 或者有值, 你可以解析可选类型访问这个值, 或者根本没有值。 + +> 注意 +> +> C 和 Objective-C 中并没有可选类型这个概念。最接近的是 Objective-C 中的一个特性,一个方法要不返回一个对象要不返回 `nil`,`nil` 表示“缺少一个合法的对象”。然而,这只对对象起作用——对于结构体,基本的 C 类型或者枚举类型不起作用。对于这些类型,Objective-C 方法一般会返回一个特殊值(比如 `NSNotFound`)来暗示值缺失。这种方法假设方法的调用者知道并记得对特殊值进行判断。然而,Swift 的可选类型可以让你暗示*任意类型*的值缺失,并不需要一个特殊值。 + +来看一个例子。Swift 的 `Int` 类型有一种构造器,作用是将一个 `String` 值转换成一个 `Int` 值。然而,并不是所有的字符串都可以转换成一个整数。字符串 `"123"` 可以被转换成数字 `123` ,但是字符串 `"hello, world"` 不行。 + +下面的例子使用这种构造器来尝试将一个 `String` 转换成 `Int`: + +```swift +let possibleNumber = "123" +let convertedNumber = Int(possibleNumber) +// convertedNumber 被推测为类型 "Int?", 或者类型 "optional Int" +``` + +因为该构造器可能会失败,所以它返回一个*可选类型*(optional)`Int`,而不是一个 `Int`。一个可选的 `Int` 被写作 `Int?` 而不是 `Int`。问号暗示包含的值是可选类型,也就是说可能包含 `Int` 值也可能*不包含值*。(不能包含其他任何值比如 `Bool` 值或者 `String` 值。只能是 `Int` 或者什么都没有。) + +### nil + +你可以给可选变量赋值为 `nil` 来表示它没有值: + +```swift +var serverResponseCode: Int? = 404 +// serverResponseCode 包含一个可选的 Int 值 404 +serverResponseCode = nil +// serverResponseCode 现在不包含值 +``` + +> 注意 +> +> `nil` 不能用于非可选的常量和变量。如果你的代码中有常量或者变量需要处理值缺失的情况,请把它们声明成对应的可选类型。 + +如果你声明一个可选常量或者变量但是没有赋值,它们会自动被设置为 `nil`: + +```swift +var surveyAnswer: String? +// surveyAnswer 被自动设置为 nil +``` + +> 注意 +> +> Swift 的 `nil` 和 Objective-C 中的 `nil` 并不一样。在 Objective-C 中,`nil` 是一个指向不存在对象的指针。在 Swift 中,`nil` 不是指针——它是一个确定的值,用来表示值缺失。任何类型的可选状态都可以被设置为 `nil`,不只是对象类型。 + +### if 语句以及强制解析 + +你可以使用 `if` 语句和 `nil` 比较来判断一个可选值是否包含值。你可以使用“相等”(`==`)或“不等”(`!=`)来执行比较。 + +如果可选类型有值,它将不等于 `nil`: + +```swift +if convertedNumber != nil { + print("convertedNumber contains some integer value.") +} +// 输出“convertedNumber contains some integer value.” +``` + +当你确定可选类型确实包含值之后,你可以在可选的名字后面加一个感叹号(`!`)来获取值。这个惊叹号表示“我知道这个可选有值,请使用它。”这被称为可选值的*强制解析(forced unwrapping)*: + +```swift +if convertedNumber != nil { + print("convertedNumber has an integer value of \(convertedNumber!).") +} +// 输出“convertedNumber has an integer value of 123.” +``` + +更多关于 `if` 语句的内容,请参考 [控制流](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html)。 + +> 注意 +> +> 使用 `!` 来获取一个不存在的可选值会导致运行时错误。使用 `!` 来强制解析值之前,一定要确定可选包含一个非 `nil` 的值。 + +### 可选绑定 + +使用*可选绑定(optional binding)*来判断可选类型是否包含值,如果包含就把值赋给一个临时常量或者变量。可选绑定可以用在 `if` 和 `while` 语句中,这条语句不仅可以用来判断可选类型中是否有值,同时可以将可选类型中的值赋给一个常量或者变量。`if` 和 `while` 语句,请参考 [控制流](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html)。 + +像下面这样在 `if` 语句中写一个可选绑定: + +```swift +if let constantName = someOptional { + statements +} +``` + +你可以像上面这样使用可选绑定来重写 在 [可选类型](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/01_The_Basics.html#optionals) 举出的 `possibleNumber` 例子: + +```swift +if let actualNumber = Int(possibleNumber) { + print("\'\(possibleNumber)\' has an integer value of \(actualNumber)") +} else { + print("\'\(possibleNumber)\' could not be converted to an integer") +} +// 输出“'123' has an integer value of 123” +``` + +这段代码可以被理解为: + +“如果 `Int(possibleNumber)` 返回的可选 `Int` 包含一个值,创建一个叫做 `actualNumber` 的新常量并将可选包含的值赋给它。” + +如果转换成功,`actualNumber` 常量可以在 `if` 语句的第一个分支中使用。它已经被可选类型 *包含的*值初始化过,所以不需要再使用 `!` 后缀来获取它的值。在这个例子中,`actualNumber` 只被用来输出转换结果。 + +你可以在可选绑定中使用常量和变量。如果你想在 `if` 语句的第一个分支中操作 `actualNumber` 的值,你可以改成 `if var actualNumber`,这样可选类型包含的值就会被赋给一个变量而非常量。 + +你可以包含多个可选绑定或多个布尔条件在一个 `if` 语句中,只要使用逗号分开就行。只要有任意一个可选绑定的值为 `nil`,或者任意一个布尔条件为 `false`,则整个 `if` 条件判断为 `false`,这时你就需要使用嵌套 `if` 条件语句来处理,如下所示: + +```swift +if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 { + print("\(firstNumber) < \(secondNumber) < 100") +} +// 输出“4 < 42 < 100” + +if let firstNumber = Int("4") { + if let secondNumber = Int("42") { + if firstNumber < secondNumber && secondNumber < 100 { + print("\(firstNumber) < \(secondNumber) < 100") + } + } +} +// 输出“4 < 42 < 100” +``` + +> 注意 +> +> 在 `if` 条件语句中使用常量和变量来创建一个可选绑定,仅在 `if` 语句的句中(`body`)中才能获取到值。相反,在 `guard` 语句中使用常量和变量来创建一个可选绑定,仅在 `guard` 语句外且在语句后才能获取到值,请参考 [提前退出](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#early_exit)。 + +### 隐式解析可选类型 + +如上所述,可选类型暗示了常量或者变量可以“没有值”。可选可以通过 `if` 语句来判断是否有值,如果有值的话可以通过可选绑定来解析值。 + +有时候在程序架构中,第一次被赋值之后,可以确定一个可选类型*总会*有值。在这种情况下,每次都要判断和解析可选值是非常低效的,因为可以确定它总会有值。 + +这种类型的可选状态被定义为隐式解析可选类型(implicitly unwrapped optionals)。把想要用作可选的类型的后面的问号(`String?`)改成感叹号(`String!`)来声明一个隐式解析可选类型。 + +当可选类型被第一次赋值之后就可以确定之后一直有值的时候,隐式解析可选类型非常有用。隐式解析可选类型主要被用在 Swift 中类的构造过程中,请参考 [无主引用以及隐式解析可选属性](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/23_Automatic_Reference_Counting.html#unowned_references_and_implicitly_unwrapped_optional_properties)。 + +一个隐式解析可选类型其实就是一个普通的可选类型,但是可以被当做非可选类型来使用,并不需要每次都使用解析来获取可选值。下面的例子展示了可选类型 `String` 和隐式解析可选类型 `String` 之间的区别: + +```swift +let possibleString: String? = "An optional string." +let forcedString: String = possibleString! // 需要感叹号来获取值 + +let assumedString: String! = "An implicitly unwrapped optional string." +let implicitString: String = assumedString // 不需要感叹号 +``` + +你可以把隐式解析可选类型当做一个可以自动解析的可选类型。你要做的只是声明的时候把感叹号放到类型的结尾,而不是每次取值的可选名字的结尾。 + +> 注意 +> +> 如果你在隐式解析可选类型没有值的时候尝试取值,会触发运行时错误。和你在没有值的普通可选类型后面加一个惊叹号一样。 + +你仍然可以把隐式解析可选类型当做普通可选类型来判断它是否包含值: + +```swift +if assumedString != nil { + print(assumedString!) +} +// 输出“An implicitly unwrapped optional string.” +``` + +你也可以在可选绑定中使用隐式解析可选类型来检查并解析它的值: + +```swift +if let definiteString = assumedString { + print(definiteString) +} +// 输出“An implicitly unwrapped optional string.” +``` + +> 注意 +> +> 如果一个变量之后可能变成 `nil` 的话请不要使用隐式解析可选类型。如果你需要在变量的生命周期中判断是否是 `nil` 的话,请使用普通可选类型。 + +## 错误处理 + +你可以使用 *错误处理(error handling)* 来应对程序执行中可能会遇到的错误条件。 + +相对于可选中运用值的存在与缺失来表达函数的成功与失败,错误处理可以推断失败的原因,并传播至程序的其他部分。 + +当一个函数遇到错误条件,它能报错。调用函数的地方能抛出错误消息并合理处理。 + +```swift +func canThrowAnError() throws { + // 这个函数有可能抛出错误 +} +``` + +一个函数可以通过在声明中添加 `throws` 关键词来抛出错误消息。当你的函数能抛出错误消息时,你应该在表达式中前置 `try` 关键词。 + +```swift +do { + try canThrowAnError() + // 没有错误消息抛出 +} catch { + // 有一个错误消息抛出 +} +``` + +一个 `do` 语句创建了一个新的包含作用域,使得错误能被传播到一个或多个 `catch` 从句。 + +这里有一个错误处理如何用来应对不同错误条件的例子。 + +```swift +func makeASandwich() throws { + // ... +} + +do { + try makeASandwich() + eatASandwich() +} catch SandwichError.outOfCleanDishes { + washDishes() +} catch SandwichError.missingIngredients(let ingredients) { + buyGroceries(ingredients) +} +``` + +在此例中,`makeASandwich()`(做一个三明治)函数会抛出一个错误消息如果没有干净的盘子或者某个原料缺失。因为 `makeASandwich()` 抛出错误,函数调用被包裹在 `try` 表达式中。将函数包裹在一个 `do` 语句中,任何被抛出的错误会被传播到提供的 `catch` 从句中。 + +如果没有错误被抛出,`eatASandwich()` 函数会被调用。如果一个匹配 `SandwichError.outOfCleanDishes` 的错误被抛出,`washDishes()` 函数会被调用。如果一个匹配 `SandwichError.missingIngredients` 的错误被抛出,`buyGroceries(_:)` 函数会被调用,并且使用 `catch` 所捕捉到的关联值 `[String]` 作为参数。 + +抛出,捕捉,以及传播错误会在 [错误处理](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/17_Error_Handling.html) 章节详细说明。 + +## 断言和先决条件 + +断言和先决条件是在运行时所做的检查。你可以用他们来检查在执行后续代码之前是否一个必要的条件已经被满足了。如果断言或者先决条件中的布尔条件评估的结果为 true(真),则代码像往常一样继续执行。如果布尔条件评估结果为 false(假),程序的当前状态是无效的,则代码执行结束,应用程序中止。 + +你使用断言和先决条件来表达你所做的假设和你在编码时候的期望。你可以将这些包含在你的代码中。断言帮助你在开发阶段找到错误和不正确的假设,先决条件帮助你在生产环境中探测到存在的问题。 + +除了在运行时验证你的期望值,断言和先决条件也变成了一个在你的代码中的有用的文档形式。和在上面讨论过的 [错误处理](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/17_Error_Handling.html) 不同,断言和先决条件并不是用来处理可以恢复的或者可预期的错误。因为一个断言失败表明了程序正处于一个无效的状态,没有办法去捕获一个失败的断言。 + +使用断言和先决条件不是一个能够避免出现程序出现无效状态的编码方法。然而,如果一个无效状态程序产生了,断言和先决条件可以强制检查你的数据和程序状态,使得你的程序可预测的中止(译者:不是系统强制的,被动的中止),并帮助使这个问题更容易调试。一旦探测到无效的状态,执行则被中止,防止无效的状态导致的进一步对于系统的伤害。 + +断言和先决条件的不同点是,他们什么时候进行状态检测:断言仅在调试环境运行,而先决条件则在调试环境和生产环境中运行。在生产环境中,断言的条件将不会进行评估。这个意味着你可以使用很多断言在你的开发阶段,但是这些断言在生产环境中不会产生任何影响。 + +### 使用断言进行调试 + +你可以调用 Swift 标准库的 `assert(_:_:file:line:)` 函数来写一个断言。向这个函数传入一个结果为 `true` 或者 `false` 的表达式以及一条信息,当表达式的结果为 `false` 的时候这条信息会被显示: + +```swift +let age = -3 +assert(age >= 0, "A person's age cannot be less than zero") +// 因为 age < 0,所以断言会触发 +``` + +在这个例子中,只有 `age >= 0` 为 `true` 时,即 `age` 的值非负的时候,代码才会继续执行。如果 `age` 的值是负数,就像代码中那样,`age >= 0` 为 `false`,断言被触发,终止应用。 + +如果不需要断言信息,可以就像这样忽略掉: + +```swift +assert(age >= 0) +``` + +如果代码已经检查了条件,你可以使用 `assertionFailure(_:file:line:)` 函数来表明断言失败了,例如: + +```swift +if age > 10 { + print("You can ride the roller-coaster or the ferris wheel.") +} else if age > 0 { + print("You can ride the ferris wheel.") +} else { + assertionFailure("A person's age can't be less than zero.") +} +``` + +### 强制执行先决条件 + +当一个条件可能为假,但是继续执行代码要求条件必须为真的时候,需要使用先决条件。例如使用先决条件来检查是否下标越界,或者来检查是否将一个正确的参数传给函数。 + +你可以使用全局 `precondition(_:_:file:line:)` 函数来写一个先决条件。向这个函数传入一个结果为 `true` 或者 `false` 的表达式以及一条信息,当表达式的结果为 `false` 的时候这条信息会被显示: + +```swift +// 在一个下标的实现里... +precondition(index > 0, "Index must be greater than zero.") +``` + +你可以调用 `preconditionFailure(_:file:line:)` 方法来表明出现了一个错误,例如,switch 进入了 default 分支,但是所有的有效值应该被任意一个其他分支(非 default 分支)处理。 + +> 注意 +> +> 如果你使用 unchecked 模式(-Ounchecked)编译代码,先决条件将不会进行检查。编译器假设所有的先决条件总是为 true(真),他将优化你的代码。然而,`fatalError(_:file:line:)` 函数总是中断执行,无论你怎么进行优化设定。 +> +> 你能使用 `fatalError(_:file:line:)` 函数在设计原型和早期开发阶段,这个阶段只有方法的声明,但是没有具体实现,你可以在方法体中写上 fatalError("Unimplemented")作为具体实现。因为 fatalError 不会像断言和先决条件那样被优化掉,所以你可以确保当代码执行到一个没有被实现的方法时,程序会被中断。 diff --git a/IOS/Task00:Swift基础语法学习/2.基本运算符.md b/IOS/Task00:Swift基础语法学习/2.基本运算符.md new file mode 100644 index 0000000..2808986 --- /dev/null +++ b/IOS/Task00:Swift基础语法学习/2.基本运算符.md @@ -0,0 +1,478 @@ +# 基本运算符 + +[TOC] + +> *运算符*是检查、改变、合并值的特殊符号或短语。例如,加号(`+`)将两个数相加(如 `let i = 1 + 2`)。更复杂的运算例子包括逻辑与运算符 `&&`(如 `if enteredDoorCode && passedRetinaScan`)。 +**Swift** 支持大部分标准 C 语言的运算符,且为了减少常见编码错误做了部分改进。如:赋值符(`=`)不再有返回值,这样就消除了手误将判等运算符(`==`)写成赋值符导致代码错误的缺陷。算术运算符(`+`,`-`,`*`,`/`,`%` 等)的结果会被检测并禁止值溢出,以此来避免保存变量时由于变量大于或小于其类型所能承载的范围时导致的异常结果。当然允许你使用 Swift 的溢出运算符来实现溢出。详情参见 [溢出运算符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/26_Advanced_Operators.html#overflow_operators)。 +**Swift** 还提供了 C 语言没有的区间运算符,例如 `a.. **注意** +> 求余运算符(`%`)在其他语言也叫*取模运算符*。但是严格说来,我们看该运算符对负数的操作结果,「求余」比「取模」更合适些。 + +我们来谈谈取余是怎么回事,计算 `9 % 4`,你先计算出 `4` 的多少倍会刚好可以容入 `9` 中: + +![Art/remainderInteger_2x.png](https://docs.swift.org/swift-book/_images/remainderInteger_2x.png) + +你可以在 `9` 中放入两个 `4`,那余数是 1(用橙色标出)。 + +在 **Swift** 中可以表达为: + +```swift +9 % 4 // 等于 1 +``` + +为了得到 `a % b` 的结果,`%` 计算了以下等式,并输出 `余数`作为结果: + +``` +a = (b × 倍数) + 余数 +``` + +当 `倍数`取最大值的时候,就会刚好可以容入 `a` 中。 + +把 `9` 和 `4` 代入等式中,我们得 `1`: + +``` +9 = (4 × 2) + 1 +``` + +同样的方法,我们来计算 `-9 % 4`: + +```swift +-9 % 4 // 等于 -1 +``` + +把 `-9` 和 `4` 代入等式,`-2` 是取到的最大整数: + +``` +-9 = (4 × -2) + -1 +``` + +余数是 `-1`。 + +在对负数 `b` 求余时,`b` 的符号会被忽略。这意味着 `a % b` 和 `a % -b` 的结果是相同的。 + +### 一元负号运算符 + +数值的正负号可以使用前缀 `-`(即**一元负号符**)来切换: + +```swift +let three = 3 +let minusThree = -three // minusThree 等于 -3 +let plusThree = -minusThree // plusThree 等于 3, 或 "负负3" +``` + +一元负号符(`-`)写在操作数之前,中间没有空格。 + +### 一元正号运算符 + +*一元正号符*(`+`)不做任何改变地返回操作数的值: + +```swift +let minusSix = -6 +let alsoMinusSix = +minusSix // alsoMinusSix 等于 -6 +``` + +虽然一元正号符什么都不会改变,但当你在使用一元负号来表达负数时,你可以使用一元正号来表达正数,如此你的代码会具有对称美。 + +## 组合赋值运算符 + +如同 C 语言,Swift 也提供把其他运算符和赋值运算(`=`)组合的*组合赋值运算符*,组合加运算(`+=`)是其中一个例子: + +```swift +var a = 1 +a += 2 +// a 现在是 3 +``` + +表达式 `a += 2` 是 `a = a + 2` 的简写,一个组合加运算就是把加法运算和赋值运算组合成进一个运算符里,同时完成两个运算任务。 + +> 注意 +> +> 复合赋值运算没有返回值,`let b = a += 2` 这类代码是错误。这不同于上面提到的自增和自减运算符。 + +更多 Swift 标准库运算符的信息,请看 [运算符声明](https://developer.apple.com/documentation/swift/operator_declarations)。 ‌ + +## 比较运算符(Comparison Operators) + +所有标准 C 语言中的**比较运算符**都可以在**Swift**中使用: + +- 等于(`a == b`) +- 不等于(`a != b`) +- 大于(`a > b`) +- 小于(`a < b`) +- 大于等于(`a >= b`) +- 小于等于(`a <= b`) + +> 注意 +> +> Swift 也提供恒等(`===`)和不恒等(`!==`)这两个比较符来判断两个对象是否引用同一个对象实例。更多细节在 [类与结构](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/09_Classes_and_Structures.md) 章节的 **Identity Operators** 部分。 + +每个比较运算都返回了一个标识表达式是否成立的布尔值: + +```swift +1 == 1 // true, 因为 1 等于 1 +2 != 1 // true, 因为 2 不等于 1 +2 > 1 // true, 因为 2 大于 1 +1 < 2 // true, 因为 1 小于2 +1 >= 1 // true, 因为 1 大于等于 1 +2 <= 1 // false, 因为 2 并不小于等于 1 +``` + +比较运算多用于条件语句,如 `if` 条件: + +```swift +let name = "world" +if name == "world" { + print("hello, world") +} else { + print("I'm sorry \(name), but I don't recognize you") +} +// 输出“hello, world", 因为 `name` 就是等于 "world” +``` + +关于 `if` 语句,请看 [控制流](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html)。 + +如果两个元组的元素相同,且长度相同的话,元组就可以被比较。比较元组大小会按照从左到右、逐值比较的方式,直到发现有两个值不等时停止。如果所有的值都相等,那么这一对元组我们就称它们是相等的。例如: + +```swift +(1, "zebra") < (2, "apple") // true,因为 1 小于 2 +(3, "apple") < (3, "bird") // true,因为 3 等于 3,但是 apple 小于 bird +(4, "dog") == (4, "dog") // true,因为 4 等于 4,dog 等于 dog +``` + +在上面的例子中,你可以看到,在第一行中从左到右的比较行为。因为 `1` 小于 `2`,所以 `(1, "zebra")` 小于 `(2, "apple")`,不管元组剩下的值如何。所以 `"zebra"` 大于 `"apple"` 对结果没有任何影响,因为元组的比较结果已经被第一个元素决定了。不过,当元组的第一个元素相同时候,第二个元素将会用作比较-第二行和第三行代码就发生了这样的比较。 + +当元组中的元素都可以被比较时,你也可以使用这些运算符来比较它们的大小。例如,像下面展示的代码,你可以比较两个类型为 `(String, Int)` 的元组,因为 `Int` 和 `String` 类型的值可以比较。相反,`Bool` 不能被比较,也意味着存有布尔类型的元组不能被比较。 + +```swift +("blue", -1) < ("purple", 1) // 正常,比较的结果为 true +("blue", false) < ("purple", true) // 错误,因为 < 不能比较布尔类型 +``` + +> **注意** +> **Swift** 标准库只能比较七个以内元素的元组比较函数。如果你的元组元素超过七个时,你需要自己实现比较运算符。 + +## 三元运算符(Ternary Conditional Operator) + +**三元运算符**的特殊在于它是有三个操作数的运算符,它的形式是 `问题 ? 答案 1 : 答案 2`。它简洁地表达根据 `问题`成立与否作出二选一的操作。如果 `问题` 成立,返回 `答案 1` 的结果;反之返回 `答案 2`的结果。 + +三元运算符是以下代码的缩写形式: + +```swift +if question { + answer1 +} else { + answer2 +} +``` + +这里有个计算表格行高的例子。如果有表头,那行高应比内容高度要高出 50 点;如果没有表头,只需高出 20 点: + +```swift +let contentHeight = 40 +let hasHeader = true +let rowHeight = contentHeight + (hasHeader ? 50 : 20) +// rowHeight 现在是 90 +``` + +上面的写法比下面的代码更简洁: + +```swift +let contentHeight = 40 +let hasHeader = true +var rowHeight = contentHeight +if hasHeader { + rowHeight = rowHeight + 50 +} else { + rowHeight = rowHeight + 20 +} +// rowHeight 现在是 90 +``` + +第一段代码例子使用了三元运算,所以一行代码就能让我们得到正确答案。这比第二段代码简洁得多,无需将 `rowHeight` 定义成变量,因为它的值无需在 `if` 语句中改变。 + +三元运算为二选一场景提供了一个非常便捷的表达形式。不过需要注意的是,滥用三元运算符会降低代码可读性。所以我们应避免在一个复合语句中使用多个三元运算符。 + +## 空合运算符(Nil Coalescing Operator) + +**空合运算符**(`a ?? b`)将对可选类型 `a` 进行空判断,如果 `a` 包含一个值就进行解包,否则就返回一个默认值 `b`。表达式 `a` 必须是 Optional 类型。默认值 `b` 的类型必须要和 `a` 存储值的类型保持一致。 + +空合运算符是对以下代码的简短表达方法: + +```swift +a != nil ? a! : b +``` + +上述代码使用了三元运算符。当可选类型 `a` 的值不为空时,进行强制解封(`a!`),访问 `a` 中的值;反之返回默认值 `b`。无疑空合运算符(`??`)提供了一种更为优雅的方式去封装条件判断和解封两种行为,显得简洁以及更具可读性。 + +> 注意 +> +> 如果 `a` 为非空值(`non-nil`),那么值 `b` 将不会被计算。这也就是所谓的*短路求值*。 + +下文例子采用空合运算符,实现了在默认颜色名和可选自定义颜色名之间抉择: + +```swift +let defaultColorName = "red" +var userDefinedColorName: String? //默认值为 nil + +var colorNameToUse = userDefinedColorName ?? defaultColorName +// userDefinedColorName 的值为空,所以 colorNameToUse 的值为 "red" +``` + +`userDefinedColorName` 变量被定义为一个可选的 `String` 类型,默认值为 `nil`。由于 `userDefinedColorName` 是一个可选类型,我们可以使用空合运算符去判断其值。在上一个例子中,通过空合运算符为一个名为 `colorNameToUse` 的变量赋予一个字符串类型初始值。 由于 `userDefinedColorName` 值为空,因此表达式 `userDefinedColorName ?? defaultColorName` 返回 `defaultColorName` 的值,即 `red`。 + +如果你分配一个非空值(`non-nil`)给 `userDefinedColorName`,再次执行空合运算,运算结果为封包在 `userDefaultColorName` 中的值,而非默认值。 + +```swift +userDefinedColorName = "green" +colorNameToUse = userDefinedColorName ?? defaultColorName +// userDefinedColorName 非空,因此 colorNameToUse 的值为 "green" +``` + +## 区间运算符(Range Operators) + +**Swift** 提供了几种方便表达一个区间的值的**区间运算符**。 + +### 闭区间运算符 + +**闭区间运算符**(`a...b`)定义一个包含从 `a` 到 `b`(包括 `a` 和 `b`)的所有值的区间。`a` 的值不能超过 `b`。 + +闭区间运算符在迭代一个区间的所有值时是非常有用的,如在 `for-in` 循环中: + +```swift +for index in 1...5 { + print("\(index) * 5 = \(index * 5)") +} +// 1 * 5 = 5 +// 2 * 5 = 10 +// 3 * 5 = 15 +// 4 * 5 = 20 +// 5 * 5 = 25 +``` + +关于 `for-in` 循环,请看 [控制流](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html)。 + +### 半开区间运算符 + +**半开区间运算符**(`a.. **注意** +> **Swift** 逻辑操作符 `&&` 和 `||` 是左结合的,这意味着拥有多元逻辑操作符的复合表达式优先计算最左边的子表达式。 + +### 使用括号来明确优先级 + +为了一个复杂表达式更容易读懂,在合适的地方使用括号来明确优先级是很有效的,虽然它并非必要的。在上个关于门的权限的例子中,我们给第一个部分加个括号,使它看起来逻辑更明确: + +```swift +if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowsOverridePassword { + print("Welcome!") +} else { + print("ACCESS DENIED") +} +// 输出“Welcome!” +``` + +这括号使得前两个值被看成整个逻辑表达中独立的一个部分。虽然有括号和没括号的输出结果是一样的,但对于读代码的人来说有括号的代码更清晰。可读性比简洁性更重要,请在可以让你代码变清晰的地方加个括号吧! diff --git a/IOS/Task00:Swift基础语法学习/3.字符串和字符.md b/IOS/Task00:Swift基础语法学习/3.字符串和字符.md new file mode 100644 index 0000000..3e55047 --- /dev/null +++ b/IOS/Task00:Swift基础语法学习/3.字符串和字符.md @@ -0,0 +1,655 @@ +# 字符串和字符 +[TOC] +*字符串*是是一系列字符的集合,例如 `"hello, world"`,`"albatross"`。Swift 的字符串通过 `String`类型来表示。而 `String` 内容的访问方式有多种,例如以 `Character` 值的集合。 + +Swift 的 `String` 和 `Character` 类型提供了一种快速且兼容 Unicode 的方式来处理代码中的文本内容。创建和操作字符串的语法与 C 语言中字符串操作相似,轻量并且易读。通过 `+` 符号就可以非常简单的实现两个字符串的拼接操作。与 Swift 中其他值一样,能否更改字符串的值,取决于其被定义为常量还是变量。你可以在已有字符串中插入常量、变量、字面量和表达式从而形成更长的字符串,这一过程也被成为字符串插值。尤其是在为显示、存储和打印创建自定义字符串值时,字符串插值操作尤其有用。 + +尽管语法简易,但 Swift 中的 `String` 类型的实现却很快速和现代化。每一个字符串都是由编码无关的 Unicode 字符组成,并支持访问字符的多种 Unicode 表示形式。 + +> 注意 +> +> Swift 的 `String` 类型与 Foundation `NSString` 类进行了无缝桥接。Foundation 还对 `String` 进行扩展使其可以访问 `NSString` 类型中定义的方法。这意味着调用那些 `NSString` 的方法,你无需进行任何类型转换。 +> +> 更多关于在 Foundation 和 Cocoa 中使用 `String` 的信息请查看 *[Bridging Between String and NSString](https://developer.apple.com/documentation/swift/string#2919514)*。 + +## 字符串字面量 + +你可以在代码里使用一段预定义的字符串值作为字符串字面量。字符串字面量是由一对双引号包裹着的具有固定顺序的字符集。 + +字符串字面量可以用于为常量和变量提供初始值: + +```swift +let someString = "Some string literal value" +``` + +注意,Swift 之所以推断 `someString` 常量为字符串类型,是因为它使用了字面量方式进行初始化。 + +### 多行字符串字面量 + +如果你需要一个字符串是跨越多行的,那就使用多行字符串字面量 — 由一对三个双引号包裹着的具有固定顺序的文本字符集: + +```swift +let quotation = """ +The White Rabbit put on his spectacles. "Where shall I begin, +please your Majesty?" he asked. + +"Begin at the beginning," the King said gravely, "and go on +till you come to the end; then stop." +""" +``` + +一个多行字符串字面量包含了所有的在开启和关闭引号(`"""`)中的行。这个字符从开启引号(`"""`)之后的第一行开始,到关闭引号(`"""`)之前为止。这就意味着字符串开启引号之后(`"""`)或者结束引号(`"""`)之前都没有换行符号。(译者:下面两个字符串其实是一样的,虽然第二个使用了多行字符串的形式) + +```swift +let singleLineString = "These are the same." +let multilineString = """ +These are the same. +""" +``` + +如果你的代码中,多行字符串字面量包含换行符的话,则多行字符串字面量中也会包含换行符。如果你想换行,以便加强代码的可读性,但是你又不想在你的多行字符串字面量中出现换行符的话,你可以用在行尾写一个反斜杠(`\`)作为续行符。 + +```swift +let softWrappedQuotation = """ +The White Rabbit put on his spectacles. "Where shall I begin, \ +please your Majesty?" he asked. + +"Begin at the beginning," the King said gravely, "and go on \ +till you come to the end; then stop." +""" +``` + +为了让一个多行字符串字面量开始和结束于换行符,请将换行写在第一行和最后一行,例如: + +```swift +let lineBreaks = """ + +This string starts with a line break. +It also ends with a line break. + +""" +``` + +一个多行字符串字面量能够缩进来匹配周围的代码。关闭引号(`"""`)之前的空白字符串告诉 Swift 编译器其他各行多少空白字符串需要忽略。然而,如果你在某行的前面写的空白字符串超出了关闭引号(`"""`)之前的空白字符串,则超出部分将被包含在多行字符串字面量中。 + +![img](https://docs.swift.org/swift-book/_images/multilineStringWhitespace_2x.png) + +在上面的例子中,尽管整个多行字符串字面量都是缩进的(源代码缩进),第一行和最后一行没有以空白字符串开始(实际的变量值)。中间一行的缩进用空白字符串(源代码缩进)比关闭引号(`"""`)之前的空白字符串多,所以,它的行首将有4个空格。 + +### 字符串字面量的特殊字符 + +字符串字面量可以包含以下特殊字符: + +- 转义字符 `\0`(空字符)、`\\`(反斜线)、`\t`(水平制表符)、`\n`(换行符)、`\r`(回车符)、`\"`(双引号)、`\'`(单引号)。 +- Unicode 标量,写成 `\u{n}`(u 为小写),其中 `n` 为任意一到八位十六进制数且可用的 Unicode 位码。 + +下面的代码为各种特殊字符的使用示例。 `wiseWords` 常量包含了两个双引号。`dollarSign`、`blackHeart` 和 `sparklingHeart` 常量演示了三种不同格式的 Unicode 标量: + +```swift +let wiseWords = "\"Imagination is more important than knowledge\" - Einstein" +// "Imageination is more important than knowledge" - Enistein +let dollarSign = "\u{24}" // $,Unicode 标量 U+0024 +let blackHeart = "\u{2665}" // ♥,Unicode 标量 U+2665 +let sparklingHeart = "\u{1F496}" // 💖,Unicode 标量 U+1F496 +``` + +由于多行字符串字面量使用了三个双引号,而不是一个,所以你可以在多行字符串字面量里直接使用双引号(`"`)而不必加上转义符(`\`)。要在多行字符串字面量中使用 `"""` 的话,就需要使用至少一个转义符(`\`): + +```swift +let threeDoubleQuotes = """ +Escaping the first quote \""" +Escaping all three quotes \"\"\" +""" +``` + +### 扩展字符串分隔符 + +您可以将字符串文字放在扩展分隔符中,这样字符串中的特殊字符将会被直接包含而非转义后的效果。将字符串放在引号(**"**)中并用数字符号(**#**)括起来。例如,打印字符串文字 **#"Line 1 \ nLine 2"#** 打印换行符转义序列(**\n**)而不是进行换行打印。 + +如果需要字符串文字中字符的特殊效果,请匹配转义字符(**\**)后面添加与起始位置个数相匹配的 **#** 符。 例如,如果您的字符串是 **#"Line 1 \ nLine 2"#** 并且您想要换行,则可以使用 **#“Line 1 \ #nLine 2”#**来代替。 同样,**###"Line1 \ ### nLine2"###** 也可以实现换行效果。 + +扩展分隔符创建的字符串文字也可以是多行字符串文字。 您可以使用扩展分隔符在多行字符串中包含文本 **"""**,覆盖原有的结束文字的默认行为。例如: + +```swift +let threeMoreDoubleQuotationMarks = #""" +Here are three more double quotes: """ +"""# +``` + +## 初始化空字符串 + +要创建一个空字符串作为初始值,可以将空的字符串字面量赋值给变量,也可以初始化一个新的 `String`实例: + +```swift +var emptyString = "" // 空字符串字面量 +var anotherEmptyString = String() // 初始化方法 +// 两个字符串均为空并等价。 +``` + +你可以通过检查 `Bool` 类型的 `isEmpty` 属性来判断该字符串是否为空: + +```swift +if emptyString.isEmpty { + print("Nothing to see here") +} +// 打印输出:“Nothing to see here” +``` + +## 字符串可变性 + +你可以通过将一个特定字符串分配给一个变量来对其进行修改,或者分配给一个常量来保证其不会被修改: + +```swift +var variableString = "Horse" +variableString += " and carriage" +// variableString 现在为 "Horse and carriage" + +let constantString = "Highlander" +constantString += " and another Highlander" +// 这会报告一个编译错误(compile-time error) - 常量字符串不可以被修改。 +``` + +> 注意 +> +> 在 Objective-C 和 Cocoa 中,需要通过选择两个不同的类(`NSString` 和 `NSMutableString`)来指定字符串是否可以被修改。 + +## 字符串是值类型 + +在 Swift 中 `String` 类型是*值类型*。如果你创建了一个新的字符串,那么当其进行常量、变量赋值操作,或在函数/方法中传递时,会进行值拷贝。在前述任一情况下,都会对已有字符串值创建新副本,并对该新副本而非原始字符串进行传递或赋值操作。值类型在 [结构体和枚举是值类型](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/09_Classes_and_Structures.md#structures_and_enumerations_are_value_types) 中进行了详细描述。 + +Swift 默认拷贝字符串的行为保证了在函数/方法向你传递的字符串所属权属于你,无论该值来自于哪里。你可以确信传递的字符串不会被修改,除非你自己去修改它。 + +在实际编译时,Swift 编译器会优化字符串的使用,使实际的复制只发生在绝对必要的情况下,这意味着你将字符串作为值类型的同时可以获得极高的性能。 + +## 使用字符 + +你可通过 `for-in` 循环来遍历字符串,获取字符串中每一个字符的值: + +```swift +for character in "Dog!🐶" { + print(character) +} +// D +// o +// g +// ! +// 🐶 +``` + +`for-in` 循环在 [For 循环](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#for_loops) 中进行了详细描述。 + +另外,通过标明一个 `Character` 类型并用字符字面量进行赋值,可以建立一个独立的字符常量或变量: + +```swift +let exclamationMark: Character = "!" +``` + +字符串可以通过传递一个值类型为 `Character` 的数组作为自变量来初始化: + +```swift +let catCharacters: [Character] = ["C", "a", "t", "!", "🐱"] +let catString = String(catCharacters) +print(catString) +// 打印输出:“Cat!🐱” +``` + +## 连接字符串和字符 + +字符串可以通过加法运算符(`+`)相加在一起(或称“连接”)创建一个新的字符串: + +```swift +let string1 = "hello" +let string2 = " there" +var welcome = string1 + string2 +// welcome 现在等于 "hello there" +``` + +你也可以通过加法赋值运算符(`+=`)将一个字符串添加到一个已经存在字符串变量上: + +```swift +var instruction = "look over" +instruction += string2 +// instruction 现在等于 "look over there" +``` + +你可以用 `append()` 方法将一个字符附加到一个字符串变量的尾部: + +```swift +let exclamationMark: Character = "!" +welcome.append(exclamationMark) +// welcome 现在等于 "hello there!" +``` + +> 注意 +> +> 你不能将一个字符串或者字符添加到一个已经存在的字符变量上,因为字符变量只能包含一个字符。 + +如果你需要使用多行字符串字面量来拼接字符串,并且你需要字符串每一行都以换行符结尾,包括最后一行: + +```swift +let badStart = """ +one +two +""" +let end = """ +three +""" +print(badStart + end) +// 打印两行: +// one +// twothree + +let goodStart = """ +one +two + +""" +print(goodStart + end) +// 打印三行: +// one +// two +// three +``` + +上面的代码,把 `badStart` 和 `end` 拼接起来的字符串非我们想要的结果。因为 `badStart` 最后一行没有换行符,它与 `end` 的第一行结合到了一起。相反的,`goodStart` 的每一行都以换行符结尾,所以它与 `end` 拼接的字符串总共有三行,正如我们期望的那样。 + +## 字符串插值 + +*字符串插值*是一种构建新字符串的方式,可以在其中包含常量、变量、字面量和表达式。**字符串字面量**和**多行字符串字面量**都可以使用字符串插值。你插入的字符串字面量的每一项都在以反斜线为前缀的圆括号中: + +```swift +let multiplier = 3 +let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)" +// message 是 "3 times 2.5 is 7.5" +``` + +在上面的例子中,`multiplier` 作为 `\(multiplier)` 被插入到一个字符串常量量中。当创建字符串执行插值计算时此占位符会被替换为 `multiplier` 实际的值。 + +`multiplier` 的值也作为字符串中后面表达式的一部分。该表达式计算 `Double(multiplier) * 2.5`的值并将结果(`7.5`)插入到字符串中。在这个例子中,表达式写为 `\(Double(multiplier) * 2.5)`并包含在字符串字面量中。 + +> 注意 +> +> 插值字符串中写在括号中的表达式不能包含非转义反斜杠(`\`),并且不能包含回车或换行符。不过,插值字符串可以包含其他字面量。 + +## Unicode + +*Unicode*是一个用于在不同书写系统中对文本进行编码、表示和处理的国际标准。它使你可以用标准格式表示来自任意语言几乎所有的字符,并能够对文本文件或网页这样的外部资源中的字符进行读写操作。Swift 的 `String` 和 `Character` 类型是完全兼容 Unicode 标准的。 + +### Unicode 标量 + +Swift 的 `String` 类型是基于 *Unicode 标量* 建立的。Unicode 标量是对应字符或者修饰符的唯一的 21 位数字,例如 `U+0061` 表示小写的拉丁字母(`LATIN SMALL LETTER A`)("`a`"),`U+1F425` 表示小鸡表情(`FRONT-FACING BABY CHICK`)("`🐥`")。 + +请注意,并非所有 21 位 Unicode 标量值都分配给字符,某些标量被保留用于将来分配或用于 UTF-16 编码。已分配的标量值通常也有一个名称,例如上面示例中的 LATIN SMALL LETTER A 和 FRONT-FACING BABY CHICK。 + +### 可扩展的字形群集 + +每一个 Swift 的 `Character` 类型代表一个*可扩展的字形群*。而一个可扩展的字形群构成了人类可读的单个字符,它由一个或多个(当组合时) Unicode 标量的序列组成。 + +举个例子,字母 `é` 可以用单一的 Unicode 标量 `é`(`LATIN SMALL LETTER E WITH ACUTE`, 或者 `U+00E9`)来表示。然而一个标准的字母 `e`(`LATIN SMALL LETTER E` 或者 `U+0065`) 加上一个急促重音(`COMBINING ACTUE ACCENT`)的标量(`U+0301`),这样一对标量就表示了同样的字母 `é`。 这个急促重音的标量形象的将 `e` 转换成了 `é`。 + +在这两种情况中,字母 `é` 代表了一个单一的 Swift 的 `Character` 值,同时代表了一个可扩展的字形群。在第一种情况,这个字形群包含一个单一标量;而在第二种情况,它是包含两个标量的字形群: + +```swift +let eAcute: Character = "\u{E9}" // é +let combinedEAcute: Character = "\u{65}\u{301}" // e 后面加上 ́ +// eAcute 是 é, combinedEAcute 是 é +``` + +可扩展的字形集是一个将许多复杂的脚本字符表示为单个字符值的灵活方式。例如,来自朝鲜语字母表的韩语音节能表示为组合或分解的有序排列。在 Swift 都会表示为同一个单一的 `Character` 值: + +```swift +let precomposed: Character = "\u{D55C}" // 한 +let decomposed: Character = "\u{1112}\u{1161}\u{11AB}" // ᄒ, ᅡ, ᆫ +// precomposed 是 한, decomposed 是 한 +``` + +可拓展的字符群集可以使包围记号(例如 `COMBINING ENCLOSING CIRCLE` 或者 `U+20DD`)的标量包围其他 Unicode 标量,作为一个单一的 `Character` 值: + +```swift +let enclosedEAcute: Character = "\u{E9}\u{20DD}" +// enclosedEAcute 是 é⃝ +``` + +地域性指示符号的 Unicode 标量可以组合成一个单一的 `Character` 值,例如 `REGIONAL INDICATOR SYMBOL LETTER U`(`U+1F1FA`)和 `REGIONAL INDICATOR SYMBOL LETTER S`(`U+1F1F8`): + +```swift +let regionalIndicatorForUS: Character = "\u{1F1FA}\u{1F1F8}" +// regionalIndicatorForUS 是 🇺🇸 +``` + +## 计算字符数量 + +如果想要获得一个字符串中 `Character` 值的数量,可以使用 `count` 属性: + +```swift +let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪" +print("unusualMenagerie has \(unusualMenagerie.count) characters") +// 打印输出“unusualMenagerie has 40 characters” +``` + +注意在 Swift 中,使用可拓展的字符群集作为 `Character` 值来连接或改变字符串时,并不一定会更改字符串的字符数量。 + +例如,如果你用四个字符的单词 `cafe` 初始化一个新的字符串,然后添加一个 `COMBINING ACTUE ACCENT`(`U+0301`)作为字符串的结尾。最终这个字符串的字符数量仍然是 `4`,因为第四个字符是 `é`,而不是 `e`: + +```swift +var word = "cafe" +print("the number of characters in \(word) is \(word.count)") +// 打印输出“the number of characters in cafe is 4” + +word += "\u{301}" // 拼接一个重音,U+0301 + +print("the number of characters in \(word) is \(word.count)") +// 打印输出“the number of characters in café is 4” +``` + +> 注意 +> +> 可扩展的字形群可以由多个 Unicode 标量组成。这意味着不同的字符以及相同字符的不同表示方式可能需要不同数量的内存空间来存储。所以 Swift 中的字符在一个字符串中并不一定占用相同的内存空间数量。因此在没有获取字符串的可扩展的字符群的范围时候,就不能计算出字符串的字符数量。如果你正在处理一个长字符串,需要注意 `count` 属性必须遍历全部的 Unicode 标量,来确定字符串的字符数量。 +> +> 另外需要注意的是通过 `count` 属性返回的字符数量并不总是与包含相同字符的 `NSString` 的 `length` 属性相同。`NSString` 的 `length` 属性是利用 UTF-16 表示的十六位代码单元数字,而不是 Unicode 可扩展的字符群集。 + +## 访问和修改字符串 + +你可以通过字符串的属性和方法来访问和修改它,当然也可以用下标语法完成。 + +### 字符串索引 + +每一个 `String` 值都有一个关联的索引(*index*)类型,`String.Index`,它对应着字符串中的每一个 `Character` 的位置。 + +前面提到,不同的字符可能会占用不同数量的内存空间,所以要知道 `Character` 的确定位置,就必须从 `String` 开头遍历每一个 Unicode 标量直到结尾。因此,Swift 的字符串不能用整数(integer)做索引。 + +使用 `startIndex` 属性可以获取一个 `String` 的第一个 `Character` 的索引。使用 `endIndex` 属性可以获取最后一个 `Character` 的后一个位置的索引。因此,`endIndex` 属性不能作为一个字符串的有效下标。如果 `String` 是空串,`startIndex` 和 `endIndex` 是相等的。 + +通过调用 `String` 的 `index(before:)` 或 `index(after:)` 方法,可以立即得到前面或后面的一个索引。你还可以通过调用 `index(_:offsetBy:)` 方法来获取对应偏移量的索引,这种方式可以避免多次调用 `index(before:)` 或 `index(after:)` 方法。 + +你可以使用下标语法来访问 `String` 特定索引的 `Character`。 + +```swift +let greeting = "Guten Tag!" +greeting[greeting.startIndex] +// G +greeting[greeting.index(before: greeting.endIndex)] +// ! +greeting[greeting.index(after: greeting.startIndex)] +// u +let index = greeting.index(greeting.startIndex, offsetBy: 7) +greeting[index] +// a +``` + +试图获取越界索引对应的 `Character`,将引发一个运行时错误。 + +```swift +greeting[greeting.endIndex] // error +greeting.index(after: endIndex) // error +``` + +使用 `indices` 属性会创建一个包含全部索引的范围(`Range`),用来在一个字符串中访问单个字符。 + +```swift +for index in greeting.indices { + print("\(greeting[index]) ", terminator: "") +} +// 打印输出“G u t e n T a g ! ” +``` + +> 注意 +> +> 你可以使用 `startIndex` 和 `endIndex` 属性或者 `index(before:)` 、`index(after:)` 和 `index(_:offsetBy:)` 方法在任意一个确认的并遵循 `Collection` 协议的类型里面,如上文所示是使用在 `String` 中,你也可以使用在 `Array`、`Dictionary` 和 `Set` 中。 + +### 插入和删除 + +调用 `insert(_:at:)` 方法可以在一个字符串的指定索引插入一个字符,调用 `insert(contentsOf:at:)` 方法可以在一个字符串的指定索引插入一个段字符串。 + +```swift +var welcome = "hello" +welcome.insert("!", at: welcome.endIndex) +// welcome 变量现在等于 "hello!" + +welcome.insert(contentsOf:" there", at: welcome.index(before: welcome.endIndex)) +// welcome 变量现在等于 "hello there!" +``` + +调用 `remove(at:)` 方法可以在一个字符串的指定索引删除一个字符,调用 `removeSubrange(_:)` 方法可以在一个字符串的指定索引删除一个子字符串。 + +```swift +welcome.remove(at: welcome.index(before: welcome.endIndex)) +// welcome 现在等于 "hello there" + +let range = welcome.index(welcome.endIndex, offsetBy: -6).. 注意 +> +> 你可以使用 `insert(_:at:)`、`insert(contentsOf:at:)`、`remove(at:)` 和 `removeSubrange(_:)` 方法在任意一个确认的并遵循 `RangeReplaceableCollection` 协议的类型里面,如上文所示是使用在 `String` 中,你也可以使用在 `Array`、`Dictionary` 和 `Set` 中。 + +## 子字符串 + +当你从字符串中获取一个子字符串 —— 例如,使用下标或者 `prefix(_:)` 之类的方法 —— 就可以得到一个 `SubString` 的实例,而非另外一个 `String`。Swift 里的 `SubString` 绝大部分函数都跟 `String` 一样,意味着你可以使用同样的方式去操作 `SubString` 和 `String`。然而,跟 `String` 不同的是,你只有在短时间内需要操作字符串时,才会使用 `SubString`。当你需要长时间保存结果时,就把 `SubString` 转化为 `String` 的实例: + +```swift +let greeting = "Hello, world!" +let index = greeting.firstIndex(of: ",") ?? greeting.endIndex +let beginning = greeting[.. 注意 +> +> `String` 和 `SubString` 都遵循 `StringProtocol` 协议,这意味着操作字符串的函数使用 `StringProtocol` 会更加方便。你可以传入 `String` 或 `SubString` 去调用函数。 + +## 比较字符串 + +Swift 提供了三种方式来比较文本值:字符串字符相等、前缀相等和后缀相等。 + +### 字符串/字符相等 + +字符串/字符可以用等于操作符(`==`)和不等于操作符(`!=`),详细描述在 [比较运算符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/02_Basic_Operators.html#comparison_operators): + +```swift +let quotation = "We're a lot alike, you and I." +let sameQuotation = "We're a lot alike, you and I." +if quotation == sameQuotation { + print("These two strings are considered equal") +} +// 打印输出“These two strings are considered equal” +``` + +如果两个字符串(或者两个字符)的可扩展的字形群集是标准相等,那就认为它们是相等的。只要可扩展的字形群集有同样的语言意义和外观则认为它们标准相等,即使它们是由不同的 Unicode 标量构成。 + +例如,`LATIN SMALL LETTER E WITH ACUTE`(`U+00E9`)就是标准相等于 `LATIN SMALL LETTER E`(`U+0065`)后面加上 `COMBINING ACUTE ACCENT`(`U+0301`)。这两个字符群集都是表示字符 `é` 的有效方式,所以它们被认为是标准相等的: + +```swift +// "Voulez-vous un café?" 使用 LATIN SMALL LETTER E WITH ACUTE +let eAcuteQuestion = "Voulez-vous un caf\u{E9}?" + +// "Voulez-vous un café?" 使用 LATIN SMALL LETTER E and COMBINING ACUTE ACCENT +let combinedEAcuteQuestion = "Voulez-vous un caf\u{65}\u{301}?" + +if eAcuteQuestion == combinedEAcuteQuestion { + print("These two strings are considered equal") +} +// 打印输出“These two strings are considered equal” +``` + +相反,英语中的 `LATIN CAPITAL LETTER A`(`U+0041`,或者 `A`)不等于俄语中的 `CYRILLIC CAPITAL LETTER A`(`U+0410`,或者 `A`)。两个字符看着是一样的,但却有不同的语言意义: + +```swift +let latinCapitalLetterA: Character = "\u{41}" + +let cyrillicCapitalLetterA: Character = "\u{0410}" + +if latinCapitalLetterA != cyrillicCapitalLetterA { + print("These two characters are not equivalent") +} +// 打印“These two characters are not equivalent” +``` + +> 注意 +> +> 在 Swift 中,字符串和字符并不区分地域(not locale-sensitive)。 + +### 前缀/后缀相等 + +通过调用字符串的 `hasPrefix(_:)`/`hasSuffix(_:)` 方法来检查字符串是否拥有特定前缀/后缀,两个方法均接收一个 `String` 类型的参数,并返回一个布尔值。 + +下面的例子以一个字符串数组表示莎士比亚话剧《罗密欧与朱丽叶》中前两场的场景位置: + +```swift +let romeoAndJuliet = [ + "Act 1 Scene 1: Verona, A public place", + "Act 1 Scene 2: Capulet's mansion", + "Act 1 Scene 3: A room in Capulet's mansion", + "Act 1 Scene 4: A street outside Capulet's mansion", + "Act 1 Scene 5: The Great Hall in Capulet's mansion", + "Act 2 Scene 1: Outside Capulet's mansion", + "Act 2 Scene 2: Capulet's orchard", + "Act 2 Scene 3: Outside Friar Lawrence's cell", + "Act 2 Scene 4: A street in Verona", + "Act 2 Scene 5: Capulet's mansion", + "Act 2 Scene 6: Friar Lawrence's cell" +] +``` + +你可以调用 `hasPrefix(_:)` 方法来计算话剧中第一幕的场景数: + +```swift +var act1SceneCount = 0 +for scene in romeoAndJuliet { + if scene.hasPrefix("Act 1 ") { + act1SceneCount += 1 + } +} +print("There are \(act1SceneCount) scenes in Act 1") +// 打印输出“There are 5 scenes in Act 1” +``` + +相似地,你可以用 `hasSuffix(_:)` 方法来计算发生在不同地方的场景数: + +```swift +var mansionCount = 0 +var cellCount = 0 +for scene in romeoAndJuliet { + if scene.hasSuffix("Capulet's mansion") { + mansionCount += 1 + } else if scene.hasSuffix("Friar Lawrence's cell") { + cellCount += 1 + } +} +print("\(mansionCount) mansion scenes; \(cellCount) cell scenes") +// 打印输出“6 mansion scenes; 2 cell scenes” +``` + +> 注意 +> +> `hasPrefix(_:)` 和 `hasSuffix(_:)` 方法都是在每个字符串中逐字符比较其可扩展的字符群集是否标准相等,详细描述在 [字符串/字符相等](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/03_Strings_and_Characters.html#string_and_character_equality)。 + +## 字符串的 Unicode 表示形式 + +当一个 Unicode 字符串被写进文本文件或者其他储存时,字符串中的 Unicode 标量会用 Unicode 定义的几种 `编码格式`(encoding forms)编码。每一个字符串中的小块编码都被称 `代码单元`(code units)。这些包括 UTF-8 编码格式(编码字符串为 8 位的代码单元), UTF-16 编码格式(编码字符串位 16 位的代码单元),以及 UTF-32 编码格式(编码字符串32位的代码单元)。 + +Swift 提供了几种不同的方式来访问字符串的 Unicode 表示形式。你可以利用 `for-in` 来对字符串进行遍历,从而以 Unicode 可扩展的字符群集的方式访问每一个 `Character` 值。该过程在 [使用字符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/03_Strings_and_Characters.html#working_with_characters) 中进行了描述。 + +另外,能够以其他三种 Unicode 兼容的方式访问字符串的值: + +- UTF-8 代码单元集合(利用字符串的 `utf8` 属性进行访问) +- UTF-16 代码单元集合(利用字符串的 `utf16` 属性进行访问) +- 21 位的 Unicode 标量值集合,也就是字符串的 UTF-32 编码格式(利用字符串的 `unicodeScalars`属性进行访问) + +下面由 `D`,`o`,`g`,`‼`(`DOUBLE EXCLAMATION MARK`, Unicode 标量 `U+203C`)和 `🐶`(`DOG FACE`,Unicode 标量为 `U+1F436`)组成的字符串中的每一个字符代表着一种不同的表示: + +```swift +let dogString = "Dog‼🐶" +``` + +### UTF-8 表示 + +你可以通过遍历 `String` 的 `utf8` 属性来访问它的 `UTF-8` 表示。其为 `String.UTF8View` 类型的属性,`UTF8View` 是无符号 8 位(`UInt8`)值的集合,每一个 `UInt8` 值都是一个字符的 UTF-8 表示: + +| Character | D U+0044 | o U+006F | g U+0067 | ‼ U+203C | 🐶 U+1F436 | | | | | | +| --------------- | -------- | -------- | -------- | -------- | --------- | ---- | ---- | ---- | ---- | ---- | +| UTF-8 Code Unit | 68 | 111 | 103 | 226 | 128 | 188 | 240 | 159 | 144 | 182 | +| Position | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | + +```swift +for codeUnit in dogString.utf8 { + print("\(codeUnit) ", terminator: "") +} +print("") +// 68 111 103 226 128 188 240 159 144 182 +``` + +上面的例子中,前三个 10 进制 `codeUnit` 值(`68`、`111`、`103`)代表了字符 `D`、`o` 和 `g`,它们的 UTF-8 表示与 ASCII 表示相同。接下来的三个 10 进制 `codeUnit` 值(`226`、`128`、`188`)是 `DOUBLE EXCLAMATION MARK` 的3字节 UTF-8 表示。最后的四个 `codeUnit` 值(`240`、`159`、`144`、`182`)是 `DOG FACE` 的4字节 UTF-8 表示。 + +### UTF-16 表示 + +你可以通过遍历 `String` 的 `utf16` 属性来访问它的 `UTF-16` 表示。其为 `String.UTF16View` 类型的属性,`UTF16View` 是无符号16位(`UInt16`)值的集合,每一个 `UInt16` 都是一个字符的 UTF-16 表示: + +| Character | D U+0044 | o U+006F | g U+0067 | ‼ U+203C | 🐶 U+1F436 | | +| ---------------- | -------- | -------- | -------- | -------- | --------- | ----- | +| UTF-16 Code Unit | 68 | 111 | 103 | 8252 | 55357 | 56374 | +| Position | 0 | 1 | 2 | 3 | 4 | 5 | + +```swift +for codeUnit in dogString.utf16 { + print("\(codeUnit) ", terminator: "") +} +print("") +// 68 111 103 8252 55357 56374 +``` + +同样,前三个 `codeUnit` 值(`68`、`111`、`103`)代表了字符 `D`、`o` 和 `g`,它们的 UTF-16 代码单元和 UTF-8 完全相同(因为这些 Unicode 标量表示 ASCII 字符)。 + +第四个 `codeUnit` 值(`8252`)是一个等于十六进制 `203C` 的的十进制值。这个代表了 `DOUBLE EXCLAMATION MARK` 字符的 Unicode 标量值 `U+203C`。这个字符在 UTF-16 中可以用一个代码单元表示。 + +第五和第六个 `codeUnit` 值(`55357` 和 `56374`)是 `DOG FACE` 字符的 UTF-16 表示。第一个值为 `U+D83D`(十进制值为 `55357`),第二个值为 `U+DC36`(十进制值为 `56374`)。 + +### Unicode 标量表示 + +你可以通过遍历 `String` 值的 `unicodeScalars` 属性来访问它的 Unicode 标量表示。其为 `UnicodeScalarView` 类型的属性,`UnicodeScalarView` 是 `UnicodeScalar` 类型的值的集合。 + +每一个 `UnicodeScalar` 拥有一个 `value` 属性,可以返回对应的 21 位数值,用 `UInt32` 来表示: + +| Character | D U+0044 | o U+006F | g U+0067 | ‼ U+203C | 🐶 U+1F436 | +| ------------------------ | -------- | -------- | -------- | -------- | --------- | +| Unicode Scalar Code Unit | 68 | 111 | 103 | 8252 | 128054 | +| Position | 0 | 1 | 2 | 3 | 4 | + +```swift +for scalar in dogString.unicodeScalars { + print("\(scalar.value) ", terminator: "") +} +print("") +// 68 111 103 8252 128054 +``` + +前三个 `UnicodeScalar` 值(`68`、`111`、`103`)的 `value` 属性仍然代表字符 `D`、`o` 和 `g`。 + +第四个 `codeUnit` 值(`8252`)仍然是一个等于十六进制 `203C` 的十进制值。这个代表了 `DOUBLE EXCLAMATION MARK` 字符的 Unicode 标量 `U+203C`。 + +第五个 `UnicodeScalar` 值的 `value` 属性,`128054`,是一个十六进制 `1F436` 的十进制表示。其等同于 `DOG FACE` 的 Unicode 标量 `U+1F436`。 + +作为查询它们的 `value` 属性的一种替代方法,每个 `UnicodeScalar` 值也可以用来构建一个新的 `String` 值,比如在字符串插值中使用: + +```swift +for scalar in dogString.unicodeScalars { + print("\(scalar) ") +} +// D +// o +// g +// ‼ +// 🐶 +``` + diff --git a/IOS/Task00:Swift基础语法学习/4.集合类型.md b/IOS/Task00:Swift基础语法学习/4.集合类型.md new file mode 100644 index 0000000..cc8756d --- /dev/null +++ b/IOS/Task00:Swift基础语法学习/4.集合类型.md @@ -0,0 +1,644 @@ +# 集合类型 +[TOC] +Swift 语言提供 `Arrays`、`Sets` 和 `Dictionaries` 三种基本的*集合类型*用来存储集合数据。数组(Arrays)是有序数据的集。集合(Sets)是无序无重复数据的集。字典(Dictionaries)是无序的键值对的集。 + +![img](https://docs.swift.org/swift-book/_images/CollectionTypes_intro_2x.png) + +Swift 语言中的 `Arrays`、`Sets` 和 `Dictionaries` 中存储的数据值类型必须明确。这意味着我们不能把错误的数据类型插入其中。同时这也说明你完全可以对取回值的类型非常放心。 + +> 注意 +> +> Swift 的 `Arrays`、`Sets` 和 `Dictionaries` 类型被实现为*泛型集合*。更多关于泛型类型和集合,参见 [泛型](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/23_Generics.md) 章节。 + +## 集合的可变性 + +如果创建一个 `Arrays`、`Sets` 或 `Dictionaries` 并且把它分配成一个变量,这个集合将会是*可变的*。这意味着你可以在创建之后添加更多或移除已存在的数据项,或者改变集合中的数据项。如果我们把 `Arrays`、`Sets` 或 `Dictionaries` 分配成常量,那么它就是*不可变的*,它的大小和内容都不能被改变。 + +> 注意 +> +> 在我们不需要改变集合的时候创建不可变集合是很好的实践。如此 Swift 编译器可以优化我们创建的集合。 + +## 数组(Arrays) + +*数组*使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。 + +> 注意 +> +> Swift 的 `Array` 类型被桥接到 `Foundation` 中的 `NSArray` 类。更多关于在 `Foundation` 和 `Cocoa` 中使用 `Array` 的信息,参见 [*Using Swift with Cocoa and Obejective-C(Swift 4.1)*](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/index.html#//apple_ref/doc/uid/TP40014216) 中 [使用 Cocoa 数据类型](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/WorkingWithCocoaDataTypes.html#//apple_ref/doc/uid/TP40014216-CH6) 部分。 + +### 数组的简单语法 + +写 Swift 数组应该遵循像 `Array` 这样的形式,其中 `Element` 是这个数组中唯一允许存在的数据类型。我们也可以使用像 `[Element]` 这样的简单语法。尽管两种形式在功能上是一样的,但是推荐较短的那种,而且在本文中都会使用这种形式来使用数组。 + +### 创建一个空数组 + +我们可以使用构造语法来创建一个由特定数据类型构成的空数组: + +```swift +var someInts = [Int]() +print("someInts is of type [Int] with \(someInts.count) items.") +// 打印“someInts is of type [Int] with 0 items.” +``` + +注意,通过构造函数的类型,`someInts` 的值类型被推断为 `[Int]`。 + +或者,如果代码上下文中已经提供了类型信息,例如一个函数参数或者一个已经定义好类型的常量或者变量,我们可以使用空数组语句创建一个空数组,它的写法很简单:`[]`(一对空方括号): + +```swift +someInts.append(3) +// someInts 现在包含一个 Int 值 +someInts = [] +// someInts 现在是空数组,但是仍然是 [Int] 类型的。 +``` + +### 创建一个带有默认值的数组 + +Swift 中的 `Array` 类型还提供一个可以创建特定大小并且所有数据都被默认的构造方法。我们可以把准备加入新数组的数据项数量(`count`)和适当类型的初始值(`repeating`)传入数组构造函数: + +```swift +var threeDoubles = Array(repeating: 0.0, count: 3) +// threeDoubles 是一种 [Double] 数组,等价于 [0.0, 0.0, 0.0] +``` + +### 通过两个数组相加创建一个数组 + +我们可以使用加法操作符(`+`)来组合两种已存在的相同类型数组。新数组的数据类型会被从两个数组的数据类型中推断出来: + +```swift +var anotherThreeDoubles = Array(repeating: 2.5, count: 3) +// anotherThreeDoubles 被推断为 [Double],等价于 [2.5, 2.5, 2.5] + +var sixDoubles = threeDoubles + anotherThreeDoubles +// sixDoubles 被推断为 [Double],等价于 [0.0, 0.0, 0.0, 2.5, 2.5, 2.5] +``` + +### 用数组字面量构造数组 + +我们可以使用*数组字面量*来进行数组构造,这是一种用一个或者多个数值构造数组的简单方法。数组字面量是一系列由逗号分割并由方括号包含的数值: + +`[value 1, value 2, value 3]`。 + +下面这个例子创建了一个叫做 `shoppingList` 并且存储 `String` 的数组: + +```swift +var shoppingList: [String] = ["Eggs", "Milk"] +// shoppingList 已经被构造并且拥有两个初始项。 +``` + +`shoppingList` 变量被声明为“字符串值类型的数组“,记作 `[String]`。 因为这个数组被规定只有 `String` 一种数据结构,所以只有 `String` 类型可以在其中被存取。 在这里,`shoppingList` 数组由两个 `String` 值(`"Eggs"` 和 `"Milk"`)构造,并且由数组字面量定义。 + +> 注意 +> +> `shoppingList` 数组被声明为变量(`var` 关键字创建)而不是常量(`let` 创建)是因为以后可能会有更多的数据项被插入其中。 + +在这个例子中,字面量仅仅包含两个 `String` 值。匹配了该数组的变量声明(只能包含 `String` 的数组),所以这个字面量的分配过程可以作为用两个初始项来构造 `shoppingList` 的一种方式。 + +由于 Swift 的类型推断机制,当我们用字面量构造只拥有相同类型值数组的时候,我们不必把数组的类型定义清楚。`shoppingList` 的构造也可以这样写: + +```swift +var shoppingList = ["Eggs", "Milk"] +``` + +因为所有数组字面量中的值都是相同的类型,Swift 可以推断出 `[String]` 是 `shoppingList` 中变量的正确类型。 + +### 访问和修改数组 + +我们可以通过数组的方法和属性来访问和修改数组,或者使用下标语法。 + +可以使用数组的只读属性 `count` 来获取数组中的数据项数量: + +```swift +print("The shopping list contains \(shoppingList.count) items.") +// 输出“The shopping list contains 2 items.”(这个数组有2个项) +``` + +使用布尔属性 `isEmpty` 作为一个缩写形式去检查 `count` 属性是否为 `0`: + +```swift +if shoppingList.isEmpty { + print("The shopping list is empty.") +} else { + print("The shopping list is not empty.") +} +// 打印“The shopping list is not empty.”(shoppinglist 不是空的) +``` + +也可以使用 `append(_:)` 方法在数组后面添加新的数据项: + +```swift +shoppingList.append("Flour") +// shoppingList 现在有3个数据项,有人在摊煎饼 +``` + +除此之外,使用加法赋值运算符(`+=`)也可以直接在数组后面添加一个或多个拥有相同类型的数据项: + +```swift +shoppingList += ["Baking Powder"] +// shoppingList 现在有四项了 +shoppingList += ["Chocolate Spread", "Cheese", "Butter"] +// shoppingList 现在有七项了 +``` + +可以直接使用下标语法来获取数组中的数据项,把我们需要的数据项的索引值放在直接放在数组名称的方括号中: + +```swift +var firstItem = shoppingList[0] +// 第一项是“Eggs” +``` + +> 注意 +> +> 第一项在数组中的索引值是 `0` 而不是 `1`。 Swift 中的数组索引总是从零开始。 + +我们也可以用下标来改变某个已有索引值对应的数据值: + +```swift +shoppingList[0] = "Six eggs" +// 其中的第一项现在是“Six eggs”而不是“Eggs” +``` + +还可以利用下标来一次改变一系列数据值,即使新数据和原有数据的数量是不一样的。下面的例子把 `"Chocolate Spread"`、`"Cheese"` 和 `"Butter"` 替换为 `"Bananas"` 和 `"Apples"`: + +```swift +shoppingList[4...6] = ["Bananas", "Apples"] +// shoppingList 现在有6项 +``` + +> 注意 +> +> 不可以用下标访问的形式去在数组尾部添加新项。 + +调用数组的 `insert(_:at:)` 方法来在某个具体索引值之前添加数据项: + +```swift +shoppingList.insert("Maple Syrup", at: 0) +// shoppingList 现在有7项 +// 现在是这个列表中的第一项是“Maple Syrup” +``` + +这次 `insert(_:at:)` 方法调用把值为 `"Maple Syrup"` 的新数据项插入列表的最开始位置,并且使用 `0` 作为索引值。 + +类似的我们可以使用 `remove(at:)` 方法来移除数组中的某一项。这个方法把数组在特定索引值中存储的数据项移除并且返回这个被移除的数据项(我们不需要的时候就可以无视它): + +```swift +let mapleSyrup = shoppingList.remove(at: 0) +// 索引值为0的数据项被移除 +// shoppingList 现在只有6项,而且不包括 Maple Syrup +// mapleSyrup 常量的值等于被移除数据项“Maple Syrup”的值 +``` + +> 注意 +> +> 如果我们试着对索引越界的数据进行检索或者设置新值的操作,会引发一个运行期错误。我们可以使用索引值和数组的 `count` 属性进行比较来在使用某个索引之前先检验是否有效。除了当 `count` 等于 0 时(说明这是个空数组),最大索引值一直是 `count - 1`,因为数组都是零起索引。 + +数据项被移除后数组中的空出项会被自动填补,所以现在索引值为 `0` 的数据项的值再次等于 `"Six eggs"`: + +```swift +firstItem = shoppingList[0] +// firstItem 现在等于“Six eggs” +``` + +如果我们只想把数组中的最后一项移除,可以使用 `removeLast()` 方法而不是 `remove(at:)` 方法来避免我们需要获取数组的 `count` 属性。就像后者一样,前者也会返回被移除的数据项: + +```swift +let apples = shoppingList.removeLast() +// 数组的最后一项被移除了 +// shoppingList 现在只有5项,不包括 Apples +// apples 常量的值现在等于“Apples”字符串 +``` + +### 数组的遍历 + +我们可以使用 `for-in` 循环来遍历所有数组中的数据项: + +```swift +for item in shoppingList { + print(item) +} +// Six eggs +// Milk +// Flour +// Baking Powder +// Bananas +``` + +如果我们同时需要每个数据项的值和索引值,可以使用 `enumerated()` 方法来进行数组遍历。`enumerated()` 返回一个由每一个数据项索引值和数据值组成的元组。我们可以把这个元组分解成临时常量或者变量来进行遍历: + +```swift +for (index, value) in shoppingList.enumerated() { + print("Item \(String(index + 1)): \(value)") +} +// Item 1: Six eggs +// Item 2: Milk +// Item 3: Flour +// Item 4: Baking Powder +// Item 5: Bananas +``` + +更多关于 `for-in` 循环的介绍请参见 [For 循环](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#for_loops)。 + +## 集合(Sets) + +*集合(Set)*用来存储相同类型并且没有确定顺序的值。当集合元素顺序不重要时或者希望确保每个元素只出现一次时可以使用集合而不是数组。 + +> 注意 Swift 的 `Set` 类型被桥接到 `Foundation` 中的 `NSSet` 类。 +> +> 关于使用 `Foundation` 和 `Cocoa` 中 `Set` 的知识,参见 [*Using Swift with Cocoa and Obejective-C(Swift 4.1)*](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/index.html#//apple_ref/doc/uid/TP40014216) 中[使用 Cocoa 数据类型](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/WorkingWithCocoaDataTypes.html#//apple_ref/doc/uid/TP40014216-CH6)部分。 + +### 集合类型的哈希值 + +一个类型为了存储在集合中,该类型必须是*可哈希化*的——也就是说,该类型必须提供一个方法来计算它的*哈希值*。一个哈希值是 `Int` 类型的,相等的对象哈希值必须相同,比如 `a==b`,因此必须 `a.hashValue == b.hashValue`。 + +Swift 的所有基本类型(比如 `String`、`Int`、`Double` 和 `Bool`)默认都是可哈希化的,可以作为集合的值的类型或者字典的键的类型。没有关联值的枚举成员值(在 [枚举](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/08_Enumerations.html) 有讲述)默认也是可哈希化的。 + +> 注意 +> +> 你可以使用你自定义的类型作为集合的值的类型或者是字典的键的类型,但你需要使你的自定义类型遵循 Swift 标准库中的 `Hashable` 协议。遵循 `Hashable` 协议的类型需要提供一个类型为 `Int` 的可读属性 `hashValue`。由类型的 `hashValue` 属性返回的值不需要在同一程序的不同执行周期或者不同程序之间保持相同。 +> +> 因为 `Hashable` 协议遵循 `Equatable` 协议,所以遵循该协议的类型也必须提供一个“是否相等”运算符(`==`)的实现。这个 `Equatable` 协议要求任何遵循 `==` 实现的实例间都是一种相等的关系。也就是说,对于 `a,b,c` 三个值来说,`==` 的实现必须满足下面三种情况: +> +> - `a == a`(自反性) +> - `a == b` 意味着 `b == a`(对称性) +> - `a == b && b == c` 意味着 `a == c`(传递性) + +关于遵循协议的更多信息,请看 [协议](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/22_Protocols.md)。 + +### 集合类型语法 + +Swift 中的 `Set` 类型被写为 `Set`,这里的 `Element` 表示 `Set` 中允许存储的类型,和数组不同的是,集合没有等价的简化形式。 + +### 创建和构造一个空的集合 + +你可以通过构造器语法创建一个特定类型的空集合: + +```swift +var letters = Set() +print("letters is of type Set with \(letters.count) items.") +// 打印“letters is of type Set with 0 items.” +``` + +> 注意 +> +> 通过构造器,这里的 `letters` 变量的类型被推断为 `Set`。 + +此外,如果上下文提供了类型信息,比如作为函数的参数或者已知类型的变量或常量,我们可以通过一个空的数组字面量创建一个空的 `Set`: + +```swift +letters.insert("a") +// letters 现在含有1个 Character 类型的值 +letters = [] +// letters 现在是一个空的 Set,但是它依然是 Set 类型 +``` + +### 用数组字面量创建集合 + +你可以使用数组字面量来构造集合,并且可以使用简化形式写一个或者多个值作为集合元素。 + +下面的例子创建一个称之为 `favoriteGenres` 的集合来存储 `String` 类型的值: + +```swift +var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"] +// favoriteGenres 被构造成含有三个初始值的集合 +``` + +这个 `favoriteGenres` 变量被声明为“一个 `String` 值的集合”,写为 `Set`。由于这个特定的集合含有指定 `String` 类型的值,所以它只允许存储 `String` 类型值。这里的 `favoriteGenres` 变量有三个 `String` 类型的初始值(`"Rock"`,`"Classical"` 和 `"Hip hop"`),并以数组字面量的方式出现。 + +> 注意 +> +> `favoriteGenres` 被声明为一个变量(拥有 `var` 标示符)而不是一个常量(拥有 `let` 标示符),因为它里面的元素将会在下面的例子中被增加或者移除。 + +一个 `Set` 类型不能从数组字面量中被单独推断出来,因此 `Set` 类型必须显式声明。然而,由于 Swift 的类型推断功能,如果你想使用一个数组字面量构造一个 `Set` 并且该数组字面量中的所有元素类型相同,那么你无须写出 `Set` 的具体类型。`favoriteGenres` 的构造形式可以采用简化的方式代替: + +```swift +var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"] +``` + +由于数组字面量中的所有元素类型相同,Swift 可以推断出 `Set` 作为 `favoriteGenres` 变量的正确类型。 + +### 访问和修改一个集合 + +你可以通过 `Set` 的属性和方法来访问和修改一个 `Set`。 + +为了找出一个 `Set` 中元素的数量,可以使用其只读属性 `count`: + +```swift +print("I have \(favoriteGenres.count) favorite music genres.") +// 打印“I have 3 favorite music genres.” +``` + +使用布尔属性 `isEmpty` 作为一个缩写形式去检查 `count` 属性是否为 `0`: + +```swift +if favoriteGenres.isEmpty { + print("As far as music goes, I'm not picky.") +} else { + print("I have particular music preferences.") +} +// 打印“I have particular music preferences.” +``` + +你可以通过调用 `Set` 的 `insert(_:)` 方法来添加一个新元素: + +```swift +favoriteGenres.insert("Jazz") +// favoriteGenres 现在包含4个元素 +``` + +你可以通过调用 `Set` 的 `remove(_:)` 方法去删除一个元素,如果该值是该 `Set` 的一个元素则删除该元素并且返回被删除的元素值,否则如果该 `Set` 不包含该值,则返回 `nil`。另外,`Set` 中的所有元素可以通过它的 `removeAll()` 方法删除。 + +```swift +if let removedGenre = favoriteGenres.remove("Rock") { + print("\(removedGenre)? I'm over it.") +} else { + print("I never much cared for that.") +} +// 打印“Rock? I'm over it.” +``` + +使用 `contains(_:)` 方法去检查 `Set` 中是否包含一个特定的值: + +```swift +if favoriteGenres.contains("Funk") { + print("I get up on the good foot.") +} else { + print("It's too funky in here.") +} +// 打印“It's too funky in here.” +``` + +### 遍历一个集合 + +你可以在一个 `for-in` 循环中遍历一个 `Set` 中的所有值。 + +```swift +for genre in favoriteGenres { + print("\(genre)") +} +// Classical +// Jazz +// Hip hop +``` + +更多关于 `for-in` 循环的信息,参见 [For 循环](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#for_loops)。 + +Swift 的 `Set` 类型没有确定的顺序,为了按照特定顺序来遍历一个 `Set` 中的值可以使用 `sorted()` 方法,它将返回一个有序数组,这个数组的元素排列顺序由操作符'<'对元素进行比较的结果来确定。 + +```swift +for genre in favoriteGenres.sorted() { + print("\(genre)") +} +// Classical +// Hip hop +// Jazz +``` + +## 集合操作 + +你可以高效地完成 `Set` 的一些基本操作,比如把两个集合组合到一起,判断两个集合共有元素,或者判断两个集合是否全包含,部分包含或者不相交。 + +### 基本集合操作 + +下面的插图描述了两个集合 `a` 和 `b`,以及通过阴影部分的区域显示集合各种操作的结果。 + +![img](https://docs.swift.org/swift-book/_images/setVennDiagram_2x.png) + +- 使用 `intersection(_:)` 方法根据两个集合中都包含的值创建的一个新的集合。 +- 使用 `symmetricDifference(_:)` 方法根据在一个集合中但不在两个集合中的值创建一个新的集合。 +- 使用 `union(_:)` 方法根据两个集合的值创建一个新的集合。 +- 使用 `subtracting(_:)` 方法根据不在该集合中的值创建一个新的集合。 + +```swift +let oddDigits: Set = [1, 3, 5, 7, 9] +let evenDigits: Set = [0, 2, 4, 6, 8] +let singleDigitPrimeNumbers: Set = [2, 3, 5, 7] + +oddDigits.union(evenDigits).sorted() +// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +oddDigits.intersection(evenDigits).sorted() +// [] +oddDigits.subtracting(singleDigitPrimeNumbers).sorted() +// [1, 9] +oddDigits.symmetricDifference(singleDigitPrimeNumbers).sorted() +// [1, 2, 9] +``` + +### 集合成员关系和相等 + +下面的插图描述了三个集合 `a`、`b` 和 `c`,以及通过重叠区域表述集合间共享的元素。集合 `a` 是集合 `b` 的父集合,因为 `a` 包含了 `b` 中所有的元素,相反的,集合 `b` 是集合 `a` 的子集合,因为属于 `b` 的元素也被 `a` 包含。集合 `b` 和集合 `c` 彼此不关联,因为它们之间没有共同的元素。 + +![img](https://docs.swift.org/swift-book/_images/setEulerDiagram_2x.png) + +- 使用“是否相等”运算符(`==`)来判断两个集合是否包含全部相同的值。 +- 使用 `isSubset(of:)` 方法来判断一个集合中的值是否也被包含在另外一个集合中。 +- 使用 `isSuperset(of:)` 方法来判断一个集合中包含另一个集合中所有的值。 +- 使用 `isStrictSubset(of:)` 或者 `isStrictSuperset(of:)` 方法来判断一个集合是否是另外一个集合的子集合或者父集合并且两个集合并不相等。 +- 使用 `isDisjoint(with:)` 方法来判断两个集合是否不含有相同的值(是否没有交集)。 + +```swift +let houseAnimals: Set = ["🐶", "🐱"] +let farmAnimals: Set = ["🐮", "🐔", "🐑", "🐶", "🐱"] +let cityAnimals: Set = ["🐦", "🐭"] + +houseAnimals.isSubset(of: farmAnimals) +// true +farmAnimals.isSuperset(of: houseAnimals) +// true +farmAnimals.isDisjoint(with: cityAnimals) +// true +``` + +## 字典 + +*字典*是一种存储多个相同类型的值的容器。每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。和数组中的数据项不同,字典中的数据项并没有具体顺序。我们在需要通过标识符(键)访问数据的时候使用字典,这种方法很大程度上和我们在现实世界中使用字典查字义的方法一样。 + +> 注意 +> +> Swift 的 `Dictionary` 类型被桥接到 `Foundation` 的 `NSDictionary` 类。 +> +> 更多关于在 `Foundation` 和 `Cocoa` 中使用 `Dictionary` 类型的信息,参见 [*Using Swift with Cocoa and Obejective-C(Swift 4.1)*](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/index.html#//apple_ref/doc/uid/TP40014216) 中 [使用 Cocoa 数据类型](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/WorkingWithCocoaDataTypes.html#//apple_ref/doc/uid/TP40014216-CH6) 部分。 + +### 字典类型简化语法 + +Swift 的字典使用 `Dictionary` 定义,其中 `Key` 是字典中键的数据类型,`Value` 是字典中对应于这些键所存储值的数据类型。 + +> 注意 +> +> 一个字典的 `Key` 类型必须遵循 `Hashable` 协议,就像 `Set` 的值类型。 + +我们也可以用 `[Key: Value]` 这样简化的形式去创建一个字典类型。虽然这两种形式功能上相同,但是后者是首选,并且这本指导书涉及到字典类型时通篇采用后者。 + +### 创建一个空字典 + +我们可以像数组一样使用构造语法创建一个拥有确定类型的空字典: + +```swift +var namesOfIntegers = [Int: String]() +// namesOfIntegers 是一个空的 [Int: String] 字典 +``` + +这个例子创建了一个 `[Int: String]` 类型的空字典来储存整数的英语命名。它的键是 `Int` 型,值是 `String` 型。 + +如果上下文已经提供了类型信息,我们可以使用空字典字面量来创建一个空字典,记作 `[:]`(中括号中放一个冒号): + +```swift +namesOfIntegers[16] = "sixteen" +// namesOfIntegers 现在包含一个键值对 +namesOfIntegers = [:] +// namesOfIntegers 又成为了一个 [Int: String] 类型的空字典 +``` + +### 用字典字面量创建字典 + +我们可以使用*字典字面量*来构造字典,这和我们刚才介绍过的数组字面量拥有相似语法。字典字面量是一种将一个或多个键值对写作 `Dictionary` 集合的快捷途径。 + +一个键值对是一个 `key` 和一个 `value` 的结合体。在字典字面量中,每一个键值对的键和值都由冒号分割。这些键值对构成一个列表,其中这些键值对由方括号包含、由逗号分割: + +```swift +[key 1: value 1, key 2: value 2, key 3: value 3] +``` + +下面的例子创建了一个存储国际机场名称的字典。在这个字典中键是三个字母的国际航空运输相关代码,值是机场名称: + +```swift +var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"] +``` + +`airports` 字典被声明为一种 `[String: String]` 类型,这意味着这个字典的键和值都是 `String` 类型。 + +> 注意 +> +> `airports` 字典被声明为变量(用 `var` 关键字)而不是常量(`let` 关键字)因为后来更多的机场信息会被添加到这个示例字典中。 + +`airports` 字典使用字典字面量初始化,包含两个键值对。第一对的键是 `YYZ`,值是 `Toronto Pearson`。第二对的键是 `DUB`,值是 `Dublin`。 + +这个字典语句包含了两个 `String: String` 类型的键值对。它们对应 `airports` 变量声明的类型(一个只有 `String` 键和 `String` 值的字典)所以这个字典字面量的任务是构造拥有两个初始数据项的 `airport` 字典。 + +和数组一样,我们在用字典字面量构造字典时,如果它的键和值都有各自一致的类型,那么就不必写出字典的类型。 `airports` 字典也可以用这种简短方式定义: + +```swift +var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"] +``` + +因为这个语句中所有的键和值都各自拥有相同的数据类型,Swift 可以推断出 `Dictionary` 是 `airports` 字典的正确类型。 + +### 访问和修改字典 + +我们可以通过字典的方法和属性来访问和修改字典,或者通过使用下标语法。 + +和数组一样,我们可以通过字典的只读属性 `count` 来获取某个字典的数据项数量: + +```swift +print("The dictionary of airports contains \(airports.count) items.") +// 打印“The dictionary of airports contains 2 items.”(这个字典有两个数据项) +``` + +使用布尔属性 `isEmpty` 作为一个缩写形式去检查 `count` 属性是否为 `0`: + +```swift +if airports.isEmpty { + print("The airports dictionary is empty.") +} else { + print("The airports dictionary is not empty.") +} +// 打印“The airports dictionary is not empty.” +``` + +我们也可以在字典中使用下标语法来添加新的数据项。可以使用一个恰当类型的键作为下标索引,并且分配恰当类型的新值: + +```swift +airports["LHR"] = "London" +// airports 字典现在有三个数据项 +``` + +我们也可以使用下标语法来改变特定键对应的值: + +```swift +airports["LHR"] = "London Heathrow" +// “LHR”对应的值被改为“London Heathrow” +``` + +作为另一种下标方法,字典的 `updateValue(_:forKey:)` 方法可以设置或者更新特定键对应的值。就像上面所示的下标示例,`updateValue(_:forKey:)` 方法在这个键不存在对应值的时候会设置新值或者在存在时更新已存在的值。和上面的下标方法不同的,`updateValue(_:forKey:)` 这个方法返回更新值之前的原值。这样使得我们可以检查更新是否成功。 + +`updateValue(_:forKey:)` 方法会返回对应值的类型的可选值。举例来说:对于存储 `String` 值的字典,这个函数会返回一个 `String?` 或者“可选 `String`”类型的值。 + +如果有值存在于更新前,则这个可选值包含了旧值,否则它将会是 `nil`。 + +```swift +if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") { + print("The old value for DUB was \(oldValue).") +} +// 输出“The old value for DUB was Dublin.” +``` + +我们也可以使用下标语法来在字典中检索特定键对应的值。因为有可能请求的键没有对应的值存在,字典的下标访问会返回对应值的类型的可选值。如果这个字典包含请求键所对应的值,下标会返回一个包含这个存在值的可选值,否则将返回 `nil`: + +```swift +if let airportName = airports["DUB"] { + print("The name of the airport is \(airportName).") +} else { + print("That airport is not in the airports dictionary.") +} +// 打印“The name of the airport is Dublin Airport.” +``` + +我们还可以使用下标语法来通过给某个键的对应值赋值为 `nil` 来从字典里移除一个键值对: + +```swift +airports["APL"] = "Apple Internation" +// “Apple Internation”不是真的 APL 机场,删除它 +airports["APL"] = nil +// APL 现在被移除了 +``` + +此外,`removeValue(forKey:)` 方法也可以用来在字典中移除键值对。这个方法在键值对存在的情况下会移除该键值对并且返回被移除的值或者在没有值的情况下返回 `nil`: + +```swift +if let removedValue = airports.removeValue(forKey: "DUB") { + print("The removed airport's name is \(removedValue).") +} else { + print("The airports dictionary does not contain a value for DUB.") +} +// 打印“The removed airport's name is Dublin Airport.” +``` + +### 字典遍历 + +我们可以使用 `for-in` 循环来遍历某个字典中的键值对。每一个字典中的数据项都以 `(key, value)` 元组形式返回,并且我们可以使用临时常量或者变量来分解这些元组: + +```swift +for (airportCode, airportName) in airports { + print("\(airportCode): \(airportName)") +} +// YYZ: Toronto Pearson +// LHR: London Heathrow +``` + +更多关于 `for-in` 循环的信息,参见 [For 循环](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#for_loops)。 + +通过访问 `keys` 或者 `values` 属性,我们也可以遍历字典的键或者值: + +```swift +for airportCode in airports.keys { + print("Airport code: \(airportCode)") +} +// Airport code: YYZ +// Airport code: LHR + +for airportName in airports.values { + print("Airport name: \(airportName)") +} +// Airport name: Toronto Pearson +// Airport name: London Heathrow +``` + +如果我们只是需要使用某个字典的键集合或者值集合来作为某个接受 `Array` 实例的 API 的参数,可以直接使用 `keys` 或者 `values` 属性构造一个新数组: + +```swift +let airportCodes = [String](airports.keys) +// airportCodes 是 ["YYZ", "LHR"] + +let airportNames = [String](airports.values) +// airportNames 是 ["Toronto Pearson", "London Heathrow"] +``` + +Swift 的字典类型是无序集合类型。为了以特定的顺序遍历字典的键或值,可以对字典的 `keys` 或 `values` 属性使用 `sorted()` 方法。 diff --git a/IOS/Task00:Swift基础语法学习/5.控制流.md b/IOS/Task00:Swift基础语法学习/5.控制流.md new file mode 100644 index 0000000..f1af21e --- /dev/null +++ b/IOS/Task00:Swift基础语法学习/5.控制流.md @@ -0,0 +1,766 @@ +# 控制流 +[TOC] +Swift 提供了多种流程控制结构,包括可以多次执行任务的 `while` 循环,基于特定条件选择执行不同代码分支的 `if`、`guard` 和 `switch` 语句,还有控制流程跳转到其他代码位置的 `break` 和 `continue` 语句。 + +Swift 还提供了 `for-in` 循环,用来更简单地遍历数组(Array),字典(Dictionary),区间(Range),字符串(String)和其他序列类型。 + +Swift 的 `switch` 语句比许多类 C 语言要更加强大。case 还可以匹配很多不同的模式,包括范围匹配,元组(tuple)和特定类型匹配。`switch` 语句的 case 中匹配的值可以声明为临时常量或变量,在 case 作用域内使用,也可以配合 `where` 来描述更复杂的匹配条件。 + +## For-In 循环 + +你可以使用 `for-in` 循环来遍历一个集合中的所有元素,例如数组中的元素、范围内的数字或者字符串中的字符。 + +以下例子使用 `for-in` 遍历一个数组所有元素: + +```swift +let names = ["Anna", "Alex", "Brian", "Jack"] +for name in names { + print("Hello, \(name)!") +} +// Hello, Anna! +// Hello, Alex! +// Hello, Brian! +// Hello, Jack! +``` + +你也可以通过遍历一个字典来访问它的键值对。遍历字典时,字典的每项元素会以 `(key, value)` 元组的形式返回,你可以在 `for-in` 循环中使用显式的常量名称来解读 `(key, value)` 元组。下面的例子中,字典的键声明会为 `animalName` 常量,字典的值会声明为 `legCount` 常量: + +```swift +let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4] +for (animalName, legCount) in numberOfLegs { + print("\(animalName)s have \(legCount) legs") +} +// cats have 4 legs +// ants have 6 legs +// spiders have 8 legs +``` + +字典的内容理论上是无序的,遍历元素时的顺序是无法确定的。将元素插入字典的顺序并不会决定它们被遍历的顺序。关于数组和字典的细节,参见 [集合类型](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/04_Collection_Types.html)。 + +`for-in` 循环还可以使用数字范围。下面的例子用来输出乘法表的一部分内容: + +```swift +for index in 1...5 { + print("\(index) times 5 is \(index * 5)") +} +// 1 times 5 is 5 +// 2 times 5 is 10 +// 3 times 5 is 15 +// 4 times 5 is 20 +// 5 times 5 is 25 +``` + +例子中用来进行遍历的元素是使用闭区间操作符(`...`)表示的从 `1` 到 `5` 的数字区间。`index` 被赋值为闭区间中的第一个数字(`1`),然后循环中的语句被执行一次。在本例中,这个循环只包含一个语句,用来输出当前 `index` 值所对应的乘 5 乘法表的结果。该语句执行后,`index` 的值被更新为闭区间中的第二个数字(`2`),之后 `print(_:separator:terminator:)` 函数会再执行一次。整个过程会进行到闭区间结尾为止。 + +上面的例子中,`index` 是一个每次循环遍历开始时被自动赋值的常量。这种情况下,`index` 在使用前不需要声明,只需要将它包含在循环的声明中,就可以对其进行隐式声明,而无需使用 `let` 关键字声明。 + +如果你不需要区间序列内每一项的值,你可以使用下划线(`_`)替代变量名来忽略这个值: + +```swift +let base = 3 +let power = 10 +var answer = 1 +for _ in 1...power { + answer *= base +} +print("\(base) to the power of \(power) is \(answer)") +// 输出“3 to the power of 10 is 59049” +``` + +这个例子计算 base 这个数的 power 次幂(本例中,是 `3` 的 `10` 次幂),从 `1`(`3` 的 `0` 次幂)开始做 `3` 的乘法, 进行 `10` 次,使用 `1` 到 `10` 的闭区间循环。这个计算并不需要知道每一次循环中计数器具体的值,只需要执行了正确的循环次数即可。下划线符号 `_` (替代循环中的变量)能够忽略当前值,并且不提供循环遍历时对值的访问。 + +在某些情况下,你可能不想使用包括两个端点的闭区间。想象一下,你在一个手表上绘制分钟的刻度线。总共 `60` 个刻度,从 `0` 分开始。使用半开区间运算符(`..<`)来表示一个左闭右开的区间。有关区间的更多信息,请参阅 [区间运算符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/02_Basic_Operators.html#range_operators)。 + +```swift +let minutes = 60 +for tickMark in 0.. 注意 +> +> 如果没有这个检测(`square < board.count`),`board[square]` 可能会越界访问 `board` 数组,导致运行时错误。 + +当本轮 `while` 循环运行完毕,会再检测循环条件是否需要再运行一次循环。如果玩家移动到或者超过第 25 个方格,循环条件结果为 `false`,此时游戏结束。 + +`while` 循环比较适合本例中的这种情况,因为在 `while` 循环开始时,我们并不知道游戏要跑多久,只有在达成指定条件时循环才会结束。 + +### Repeat-While + +`while` 循环的另外一种形式是 `repeat-while`,它和 `while` 的区别是在判断循环条件之前,先执行一次循环的代码块。然后重复循环直到条件为 `false`。 + +> 注意 +> +> Swift 语言的 `repeat-while` 循环和其他语言中的 `do-while` 循环是类似的。 + +下面是 `repeat-while` 循环的一般格式: + +```swift +repeat { + statements +} while condition +``` + +还是*蛇和梯子*的游戏,使用 `repeat-while` 循环来替代 `while` 循环。`finalSquare`、`board`、`square` 和 `diceRoll` 的值初始化同 `while` 循环时一样: + +```swift +let finalSquare = 25 +var board = [Int](repeating: 0, count: finalSquare + 1) +board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02 +board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08 +var square = 0 +var diceRoll = 0 +``` + +`repeat-while` 的循环版本,循环中*第一步*就需要去检测是否在梯子或者蛇的方块上。没有梯子会让玩家直接上到第 25 个方格,所以玩家不会通过梯子直接赢得游戏。这样在循环开始时先检测是否踩在梯子或者蛇上是安全的。 + +游戏开始时,玩家在第 0 个方格上,`board[0]` 一直等于 0, 不会有什么影响: + +```swift +repeat { + // 顺着梯子爬上去或者顺着蛇滑下去 + square += board[square] + // 掷骰子 + diceRoll += 1 + if diceRoll == 7 { diceRoll = 1 } + // 根据点数移动 + square += diceRoll +} while square < finalSquare +print("Game over!") +``` + +检测完玩家是否踩在梯子或者蛇上之后,开始掷骰子,然后玩家向前移动 `diceRoll` 个方格,本轮循环结束。 + +循环条件(`while square < finalSquare`)和 `while` 方式相同,但是只会在循环结束后进行计算。在这个游戏中,`repeat-while` 表现得比 `while` 循环更好。`repeat-while` 方式会在条件判断 `square` 没有超出后直接运行 `square += board[square]`,这种方式可以比起前面 `while` 循环的版本,可以省去数组越界的检查。 + +## 条件语句 + +根据特定的条件执行特定的代码通常是十分有用的。当错误发生时,你可能想运行额外的代码;或者,当值太大或太小时,向用户显示一条消息。要实现这些功能,你就需要使用*条件语句*。 + +Swift 提供两种类型的条件语句:`if` 语句和 `switch` 语句。通常,当条件较为简单且可能的情况很少时,使用 `if` 语句。而 `switch` 语句更适用于条件较复杂、有更多排列组合的时候。并且 `switch` 在需要用到模式匹配(pattern-matching)的情况下会更有用。 + +### If + +`if` 语句最简单的形式就是只包含一个条件,只有该条件为 `true` 时,才执行相关代码: + +```swift +var temperatureInFahrenheit = 30 +if temperatureInFahrenheit <= 32 { + print("It's very cold. Consider wearing a scarf.") +} +// 输出“It's very cold. Consider wearing a scarf.” +``` + +上面的例子会判断温度是否小于等于 32 华氏度(水的冰点)。如果是,则打印一条消息;否则,不打印任何消息,继续执行 `if` 块后面的代码。 + +当然,`if` 语句允许二选一执行,叫做 `else` 从句。也就是当条件为 `false` 时,执行 *else 语句*: + +```swift +temperatureInFahrenheit = 40 +if temperatureInFahrenheit <= 32 { + print("It's very cold. Consider wearing a scarf.") +} else { + print("It's not that cold. Wear a t-shirt.") +} +// 输出“It's not that cold. Wear a t-shirt.” +``` + +显然,这两条分支中总有一条会被执行。由于温度已升至 40 华氏度,不算太冷,没必要再围围巾。因此,`else` 分支就被触发了。 + +你可以把多个 `if` 语句链接在一起,来实现更多分支: + +```swift +temperatureInFahrenheit = 90 +if temperatureInFahrenheit <= 32 { + print("It's very cold. Consider wearing a scarf.") +} else if temperatureInFahrenheit >= 86 { + print("It's really warm. Don't forget to wear sunscreen.") +} else { + print("It's not that cold. Wear a t-shirt.") +} +// 输出“It's really warm. Don't forget to wear sunscreen.” +``` + +在上面的例子中,额外的 `if` 语句用于判断是不是特别热。而最后的 `else` 语句被保留了下来,用于打印既不冷也不热时的消息。 + +实际上,当不需要完整判断情况的时候,最后的 `else` 语句是可选的: + +```swift +temperatureInFahrenheit = 72 +if temperatureInFahrenheit <= 32 { + print("It's very cold. Consider wearing a scarf.") +} else if temperatureInFahrenheit >= 86 { + print("It's really warm. Don't forget to wear sunscreen.") +} +``` + +在这个例子中,由于既不冷也不热,所以不会触发 `if` 或 `else if` 分支,也就不会打印任何消息。 + +### Switch + +`switch` 语句会尝试把某个值与若干个模式(pattern)进行匹配。根据第一个匹配成功的模式,`switch` 语句会执行对应的代码。当有可能的情况较多时,通常用 `switch` 语句替换 `if` 语句。 + +`switch` 语句最简单的形式就是把某个值与一个或若干个相同类型的值作比较: + +```swift +switch some value to consider { +case value 1: + respond to value 1 +case value 2, + value 3: + respond to value 2 or 3 +default: + otherwise, do something else +} +``` + +`switch` 语句由*多个 case* 构成,每个由 `case` 关键字开始。为了匹配某些更特定的值,Swift 提供了几种方法来进行更复杂的模式匹配,这些模式将在本节的稍后部分提到。 + +与 `if` 语句类似,每一个 case 都是代码执行的一条分支。`switch` 语句会决定哪一条分支应该被执行,这个流程被称作根据给定的值*切换(switching)*。 + +`switch` 语句必须是完备的。这就是说,每一个可能的值都必须至少有一个 case 分支与之对应。在某些不可能涵盖所有值的情况下,你可以使用默认(`default`)分支来涵盖其它所有没有对应的值,这个默认分支必须在 `switch` 语句的最后面。 + +下面的例子使用 `switch` 语句来匹配一个名为 `someCharacter` 的小写字符: + +```swift +let someCharacter: Character = "z" +switch someCharacter { +case "a": + print("The first letter of the alphabet") +case "z": + print("The last letter of the alphabet") +default: + print("Some other character") +} +// 输出“The last letter of the alphabet” +``` + +在这个例子中,第一个 case 分支用于匹配第一个英文字母 `a`,第二个 case 分支用于匹配最后一个字母 `z`。因为 `switch` 语句必须有一个 case 分支用于覆盖所有可能的字符,而不仅仅是所有的英文字母,所以 switch 语句使用 `default` 分支来匹配除了 `a` 和 `z` 外的所有值,这个分支保证了 swith 语句的完备性。 + +#### 不存在隐式的贯穿 + +与 C 和 Objective-C 中的 `switch` 语句不同,在 Swift 中,当匹配的 case 分支中的代码执行完毕后,程序会终止 `switch` 语句,而不会继续执行下一个 case 分支。这也就是说,不需要在 case 分支中显式地使用 `break` 语句。这使得 `switch` 语句更安全、更易用,也避免了漏写 `break` 语句导致多个语言被执行的错误。 + +> 注意 +> +> 虽然在 Swift 中 `break` 不是必须的,但你依然可以在 case 分支中的代码执行完毕前使用 `break`跳出,详情请参见 [Switch 语句中的 break](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#break_in_a_switch_statement)。 + +每一个 case 分支都*必须*包含至少一条语句。像下面这样书写代码是无效的,因为第一个 case 分支是空的: + +```swift +let anotherCharacter: Character = "a" +switch anotherCharacter { +case "a": // 无效,这个分支下面没有语句 +case "A": + print("The letter A") +default: + print("Not the letter A") +} +// 这段代码会报编译错误 +``` + +不像 C 语言里的 `switch` 语句,在 Swift 中,`switch` 语句不会一起匹配 `"a"` 和 `"A"`。相反的,上面的代码会引起编译期错误:`case "a": 不包含任何可执行语句`——这就避免了意外地从一个 case 分支贯穿到另外一个,使得代码更安全、也更直观。 + +为了让单个 case 同时匹配 `a` 和 `A`,可以将这个两个值组合成一个复合匹配,并且用逗号分开: + +```swift +let anotherCharacter: Character = "a" +switch anotherCharacter { +case "a", "A": + print("The letter A") +default: + print("Not the letter A") +} +// 输出“The letter A” +``` + +为了可读性,符合匹配可以写成多行形式,详情请参考 [复合匹配](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#compound_cases) + +> 注意 +> +> 如果想要显式贯穿 case 分支,请使用 `fallthrough` 语句,详情请参考 [贯穿](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html#fallthrough)。 + +#### 区间匹配 + +case 分支的模式也可以是一个值的区间。下面的例子展示了如何使用区间匹配来输出任意数字对应的自然语言格式: + +```swift +let approximateCount = 62 +let countedThings = "moons orbiting Saturn" +let naturalCount: String +switch approximateCount { +case 0: + naturalCount = "no" +case 1..<5: + naturalCount = "a few" +case 5..<12: + naturalCount = "several" +case 12..<100: + naturalCount = "dozens of" +case 100..<1000: + naturalCount = "hundreds of" +default: + naturalCount = "many" +} +print("There are \(naturalCount) \(countedThings).") +// 输出“There are dozens of moons orbiting Saturn.” +``` + +在上例中,`approximateCount` 在一个 `switch` 声明中被评估。每一个 `case` 都与之进行比较。因为 `approximateCount` 落在了 12 到 100 的区间,所以 `naturalCount` 等于 `"dozens of"` 值,并且此后的执行跳出了 `switch` 语句。 + +#### 元组 + +我们可以使用元组在同一个 `switch` 语句中测试多个值。元组中的元素可以是值,也可以是区间。另外,使用下划线(`_`)来匹配所有可能的值。 + +下面的例子展示了如何使用一个 `(Int, Int)` 类型的元组来分类下图中的点 (x, y): + +```swift +let somePoint = (1, 1) +switch somePoint { +case (0, 0): + print("\(somePoint) is at the origin") +case (_, 0): + print("\(somePoint) is on the x-axis") +case (0, _): + print("\(somePoint) is on the y-axis") +case (-2...2, -2...2): + print("\(somePoint) is inside the box") +default: + print("\(somePoint) is outside of the box") +} +// 输出“(1, 1) is inside the box” +``` + +![image](https://docs.swift.org/swift-book/_images/coordinateGraphSimple_2x.png) + +在上面的例子中,`switch` 语句会判断某个点是否是原点 (0, 0),是否在红色的 x 轴上,是否在橘黄色的 y 轴上,是否在一个以原点为中心的4x4的蓝色矩形里,或者在这个矩形外面。 + +不像 C 语言,Swift 允许多个 case 匹配同一个值。实际上,在这个例子中,点 (0, 0)可以匹配所有*四个 case*。但是,如果存在多个匹配,那么只会执行第一个被匹配到的 case 分支。考虑点 (0, 0)会首先匹配 `case (0, 0)`,因此剩下的能够匹配的分支都会被忽视掉。 + +#### 值绑定(Value Bindings) + +case 分支允许将匹配的值声明为临时常量或变量,并且在 case 分支体内使用 —— 这种行为被称为*值绑定*(value binding),因为匹配的值在 case 分支体内,与临时的常量或变量绑定。 + +下面的例子将下图中的点 (x, y),使用 `(Int, Int)` 类型的元组表示,然后分类表示: + +```swift +let anotherPoint = (2, 0) +switch anotherPoint { +case (let x, 0): + print("on the x-axis with an x value of \(x)") +case (0, let y): + print("on the y-axis with a y value of \(y)") +case let (x, y): + print("somewhere else at (\(x), \(y))") +} +// 输出“on the x-axis with an x value of 2” +``` + +![image](https://docs.swift.org/swift-book/_images/coordinateGraphMedium_2x.png) + +在上面的例子中,`switch` 语句会判断某个点是否在红色的 x 轴上,是否在橘黄色的 y 轴上,或者不在坐标轴上。 + +这三个 case 都声明了常量 `x` 和 `y` 的占位符,用于临时获取元组 `anotherPoint` 的一个或两个值。第一个 case ——`case (let x, 0)` 将匹配一个纵坐标为 `0` 的点,并把这个点的横坐标赋给临时的常量 `x`。类似的,第二个 case ——`case (0, let y)` 将匹配一个横坐标为 `0` 的点,并把这个点的纵坐标赋给临时的常量 `y`。 + +一旦声明了这些临时的常量,它们就可以在其对应的 case 分支里使用。在这个例子中,它们用于打印给定点的类型。 + +请注意,这个 `switch` 语句不包含默认分支。这是因为最后一个 case ——`case let(x, y)` 声明了一个可以匹配余下所有值的元组。这使得 `switch` 语句已经完备了,因此不需要再书写默认分支。 + +#### Where + +case 分支的模式可以使用 `where` 语句来判断额外的条件。 + +下面的例子把下图中的点 (x, y)进行了分类: + +```swift +let yetAnotherPoint = (1, -1) +switch yetAnotherPoint { +case let (x, y) where x == y: + print("(\(x), \(y)) is on the line x == y") +case let (x, y) where x == -y: + print("(\(x), \(y)) is on the line x == -y") +case let (x, y): + print("(\(x), \(y)) is just some arbitrary point") +} +// 输出“(1, -1) is on the line x == -y” +``` + +![image](https://docs.swift.org/swift-book/_images/coordinateGraphComplex_2x.png) + +在上面的例子中,`switch` 语句会判断某个点是否在绿色的对角线 `x == y` 上,是否在紫色的对角线 `x == -y` 上,或者不在对角线上。 + +这三个 case 都声明了常量 `x` 和 `y` 的占位符,用于临时获取元组 `yetAnotherPoint` 的两个值。这两个常量被用作 `where` 语句的一部分,从而创建一个动态的过滤器(filter)。当且仅当 `where` 语句的条件为 `true` 时,匹配到的 case 分支才会被执行。 + +就像是值绑定中的例子,由于最后一个 case 分支匹配了余下所有可能的值,`switch` 语句就已经完备了,因此不需要再书写默认分支。 + +#### 复合型 Cases + +当多个条件可以使用同一种方法来处理时,可以将这几种可能放在同一个 `case` 后面,并且用逗号隔开。当 case 后面的任意一种模式匹配的时候,这条分支就会被匹配。并且,如果匹配列表过长,还可以分行书写: + +```swift +let someCharacter: Character = "e" +switch someCharacter { +case "a", "e", "i", "o", "u": + print("\(someCharacter) is a vowel") +case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m", + "n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z": + print("\(someCharacter) is a consonant") +default: + print("\(someCharacter) is not a vowel or a consonant") +} +// 输出“e is a vowel” +``` + +这个 `switch` 语句中的第一个 case,匹配了英语中的五个小写元音字母。相似的,第二个 case 匹配了英语中所有的小写辅音字母。最终,`default` 分支匹配了其它所有字符。 + +复合匹配同样可以包含值绑定。复合匹配里所有的匹配模式,都必须包含相同的值绑定。并且每一个绑定都必须获取到相同类型的值。这保证了,无论复合匹配中的哪个模式发生了匹配,分支体内的代码,都能获取到绑定的值,并且绑定的值都有一样的类型。 + +```swift +let stillAnotherPoint = (9, 0) +switch stillAnotherPoint { +case (let distance, 0), (0, let distance): + print("On an axis, \(distance) from the origin") +default: + print("Not on an axis") +} +// 输出“On an axis, 9 from the origin” +``` + +上面的 case 有两个模式:`(let distance, 0)` 匹配了在 x 轴上的值,`(0, let distance)` 匹配了在 y 轴上的值。两个模式都绑定了 `distance`,并且 `distance` 在两种模式下,都是整型——这意味着分支体内的代码,只要 case 匹配,都可以获取到 `distance` 值。 + +## 控制转移语句 + +控制转移语句改变你代码的执行顺序,通过它可以实现代码的跳转。Swift 有五种控制转移语句: + +- `continue` +- `break` +- `fallthrough` +- `return` +- `throw` + +我们将会在下面讨论 `continue`、`break` 和 `fallthrough` 语句。`return` 语句将会在 [函数](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/06_Functions.html) 章节讨论,`throw` 语句会在 [错误抛出](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/18_Error_Handling.md#throwing_errors) 章节讨论。 + +### Continue + +`continue` 语句告诉一个循环体立刻停止本次循环,重新开始下次循环。就好像在说“本次循环我已经执行完了”,但是并不会离开整个循环体。 + +下面的例子把一个小写字符串中的元音字母和空格字符移除,生成了一个含义模糊的短句: + +```swift +let puzzleInput = "great minds think alike" +var puzzleOutput = "" +for character in puzzleInput { + switch character { + case "a", "e", "i", "o", "u", " ": + continue + default: + puzzleOutput.append(character) + } +} +print(puzzleOutput) + // 输出“grtmndsthnklk” +``` + +在上面的代码中,只要匹配到元音字母或者空格字符,就调用 `continue` 语句,使本次循环结束,重新开始下次循环。这种行为使 `switch` 匹配到元音字母和空格字符时不做处理,而不是让每一个匹配到的字符都被打印。 + +### Break + +`break` 语句会立刻结束整个控制流的执行。`break` 可以在 `switch` 或循环语句中使用,用来提前结束 `switch` 或循环语句。 + +#### 循环语句中的 break + +当在一个循环体中使用 `break` 时,会立刻中断该循环体的执行,然后跳转到表示循环体结束的大括号(`}`)后的第一行代码。不会再有本次循环的代码被执行,也不会再有下次的循环产生。 + +#### Switch 语句中的 break + +当在一个 `switch` 代码块中使用 `break` 时,会立即中断该 `switch` 代码块的执行,并且跳转到表示 `switch` 代码块结束的大括号(`}`)后的第一行代码。 + +这种特性可以被用来匹配或者忽略一个或多个分支。因为 Swift 的 `switch` 需要包含所有的分支而且不允许有为空的分支,有时为了使你的意图更明显,需要特意匹配或者忽略某个分支。那么当你想忽略某个分支时,可以在该分支内写上 `break` 语句。当那个分支被匹配到时,分支内的 `break` 语句立即结束 `switch` 代码块。 + +> 注意 +> +> 当一个 `switch` 分支仅仅包含注释时,会被报编译时错误。注释不是代码语句而且也不能让 `switch` 分支达到被忽略的效果。你应该使用 `break` 来忽略某个分支。 + +下面的例子通过 `switch` 来判断一个 `Character` 值是否代表下面四种语言之一。为了简洁,多个值被包含在了同一个分支情况中。 + +```swift +let numberSymbol: Character = "三" // 简体中文里的数字 3 +var possibleIntegerValue: Int? +switch numberSymbol { +case "1", "١", "一", "๑": + possibleIntegerValue = 1 +case "2", "٢", "二", "๒": + possibleIntegerValue = 2 +case "3", "٣", "三", "๓": + possibleIntegerValue = 3 +case "4", "٤", "四", "๔": + possibleIntegerValue = 4 +default: + break +} +if let integerValue = possibleIntegerValue { + print("The integer value of \(numberSymbol) is \(integerValue).") +} else { + print("An integer value could not be found for \(numberSymbol).") +} +// 输出“The integer value of 三 is 3.” +``` + +这个例子检查 `numberSymbol` 是否是拉丁,阿拉伯,中文或者泰语中的 `1` 到 `4` 之一。如果被匹配到,该 `switch` 分支语句给 `Int?` 类型变量 `possibleIntegerValue` 设置一个整数值。 + +当 `switch` 代码块执行完后,接下来的代码通过使用可选绑定来判断 `possibleIntegerValue` 是否曾经被设置过值。因为是可选类型的缘故,`possibleIntegerValue` 有一个隐式的初始值 `nil`,所以仅仅当 `possibleIntegerValue` 曾被 `switch` 代码块的前四个分支中的某个设置过一个值时,可选的绑定才会被判定为成功。 + +在上面的例子中,想要把 `Character` 所有的的可能性都枚举出来是不现实的,所以使用 `default` 分支来包含所有上面没有匹配到字符的情况。由于这个 `default` 分支不需要执行任何动作,所以它只写了一条 `break` 语句。一旦落入到 `default` 分支中后,`break` 语句就完成了该分支的所有代码操作,代码继续向下,开始执行 `if let` 语句。 + +### 贯穿(Fallthrough) + +在 Swift 里,`switch` 语句不会从上一个 case 分支跳转到下一个 case 分支中。相反,只要第一个匹配到的 case 分支完成了它需要执行的语句,整个 `switch` 代码块完成了它的执行。相比之下,C 语言要求你显式地插入 `break` 语句到每个 case 分支的末尾来阻止自动落入到下一个 case 分支中。Swift 的这种避免默认落入到下一个分支中的特性意味着它的 `switch` 功能要比 C 语言的更加清晰和可预测,可以避免无意识地执行多个 case 分支从而引发的错误。 + +如果你确实需要 C 风格的贯穿的特性,你可以在每个需要该特性的 case 分支中使用 `fallthrough` 关键字。下面的例子使用 `fallthrough` 来创建一个数字的描述语句。 + +```swift +let integerToDescribe = 5 +var description = "The number \(integerToDescribe) is" +switch integerToDescribe { +case 2, 3, 5, 7, 11, 13, 17, 19: + description += " a prime number, and also" + fallthrough +default: + description += " an integer." +} +print(description) +// 输出“The number 5 is a prime number, and also an integer.” +``` + +这个例子定义了一个 `String` 类型的变量 `description` 并且给它设置了一个初始值。函数使用 `switch` 逻辑来判断 `integerToDescribe` 变量的值。当 `integerToDescribe` 的值属于列表中的质数之一时,该函数在 `description` 后添加一段文字,来表明这个数字是一个质数。然后它使用 `fallthrough` 关键字来“贯穿”到 `default` 分支中。`default` 分支在 `description` 的最后添加一段额外的文字,至此 `switch` 代码块执行完了。 + +如果 `integerToDescribe` 的值不属于列表中的任何质数,那么它不会匹配到第一个 `switch` 分支。而这里没有其他特别的分支情况,所以 `integerToDescribe` 匹配到 `default` 分支中。 + +当 `switch` 代码块执行完后,使用 `print(_:separator:terminator:)` 函数打印该数字的描述。在这个例子中,数字 `5` 被准确的识别为了一个质数。 + +> 注意 +> +> `fallthrough` 关键字不会检查它下一个将会落入执行的 case 中的匹配条件。`fallthrough` 简单地使代码继续连接到下一个 case 中的代码,这和 C 语言标准中的 `switch` 语句特性是一样的。 + +### 带标签的语句 + +在 Swift 中,你可以在循环体和条件语句中嵌套循环体和条件语句来创造复杂的控制流结构。并且,循环体和条件语句都可以使用 `break` 语句来提前结束整个代码块。因此,显式地指明 `break` 语句想要终止的是哪个循环体或者条件语句,会很有用。类似地,如果你有许多嵌套的循环体,显式指明 `continue`语句想要影响哪一个循环体也会非常有用。 + +为了实现这个目的,你可以使用标签(*statement label*)来标记一个循环体或者条件语句,对于一个条件语句,你可以使用 `break` 加标签的方式,来结束这个被标记的语句。对于一个循环语句,你可以使用 `break` 或者 `continue` 加标签,来结束或者继续这条被标记语句的执行。 + +声明一个带标签的语句是通过在该语句的关键词的同一行前面放置一个标签,作为这个语句的前导关键字(introducor keyword),并且该标签后面跟随一个冒号。下面是一个针对 `while` 循环体的标签语法,同样的规则适用于所有的循环体和条件语句。 + +```swift + label name: while condition { + statements + } +``` + +下面的例子是前面章节中*蛇和梯子*的适配版本,在此版本中,我们将使用一个带有标签的 `while` 循环体中调用 `break` 和 `continue` 语句。这次,游戏增加了一条额外的规则: + +- 为了获胜,你必须*刚好*落在第 25 个方块中。 + +如果某次掷骰子使你的移动超出第 25 个方块,你必须重新掷骰子,直到你掷出的骰子数刚好使你能落在第 25 个方块中。 + +游戏的棋盘和之前一样: + +![image](https://docs.swift.org/swift-book/_images/snakesAndLadders_2x.png) + +`finalSquare`、`board`、`square` 和 `diceRoll` 值被和之前一样的方式初始化: + +```swift +let finalSquare = 25 +var board = [Int](repeating: 0, count: finalSquare + 1) +board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02 +board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08 +var square = 0 +var diceRoll = 0 +``` + +这个版本的游戏使用 `while` 循环和 `switch` 语句来实现游戏的逻辑。`while` 循环有一个标签名 `gameLoop`,来表明它是游戏的主循环。 + +该 `while` 循环体的条件判断语句是 `while square !=finalSquare`,这表明你必须刚好落在方格25中。 + +```swift +gameLoop: while square != finalSquare { + diceRoll += 1 + if diceRoll == 7 { diceRoll = 1 } + switch square + diceRoll { + case finalSquare: + // 骰子数刚好使玩家移动到最终的方格里,游戏结束。 + break gameLoop + case let newSquare where newSquare > finalSquare: + // 骰子数将会使玩家的移动超出最后的方格,那么这种移动是不合法的,玩家需要重新掷骰子 + continue gameLoop + default: + // 合法移动,做正常的处理 + square += diceRoll + square += board[square] + } +} +print("Game over!") +``` + +每次循环迭代开始时掷骰子。与之前玩家掷完骰子就立即移动不同,这里使用了 `switch` 语句来考虑每次移动可能产生的结果,从而决定玩家本次是否能够移动。 + +- 如果骰子数刚好使玩家移动到最终的方格里,游戏结束。`break gameLoop` 语句跳转控制去执行 `while` 循环体后的第一行代码,意味着游戏结束。 +- 如果骰子数将会使玩家的移动超出最后的方格,那么这种移动是不合法的,玩家需要重新掷骰子。`continue gameLoop` 语句结束本次 `while` 循环,开始下一次循环。 +- 在剩余的所有情况中,骰子数产生的都是合法的移动。玩家向前移动 `diceRoll` 个方格,然后游戏逻辑再处理玩家当前是否处于蛇头或者梯子的底部。接着本次循环结束,控制跳转到 `while` 循环体的条件判断语句处,再决定是否需要继续执行下次循环。 + +> 注意 +> +> 如果上述的 `break` 语句没有使用 `gameLoop` 标签,那么它将会中断 `switch` 语句而不是 `while` 循环。使用 `gameLoop` 标签清晰的表明了 `break` 想要中断的是哪个代码块。 +> +> 同时请注意,当调用 `continue gameLoop` 去跳转到下一次循环迭代时,这里使用 `gameLoop` 标签并不是严格必须的。因为在这个游戏中,只有一个循环体,所以 `continue` 语句会影响到哪个循环体是没有歧义的。然而,`continue` 语句使用 `gameLoop` 标签也是没有危害的。这样做符合标签的使用规则,同时参照旁边的 `break gameLoop`,能够使游戏的逻辑更加清晰和易于理解。 + +## 提前退出 + +像 `if` 语句一样,`guard` 的执行取决于一个表达式的布尔值。我们可以使用 `guard` 语句来要求条件必须为真时,以执行 `guard` 语句后的代码。不同于 `if` 语句,一个 `guard` 语句总是有一个 `else`从句,如果条件不为真则执行 `else` 从句中的代码。 + +```swift +func greet(person: [String: String]) { + guard let name = person["name"] else { + return + } + + print("Hello \(name)!") + + guard let location = person["location"] else { + print("I hope the weather is nice near you.") + return + } + + print("I hope the weather is nice in \(location).") +} + +greet(person: ["name": "John"]) +// 输出“Hello John!” +// 输出“I hope the weather is nice near you.” +greet(person: ["name": "Jane", "location": "Cupertino"]) +// 输出“Hello Jane!” +// 输出“I hope the weather is nice in Cupertino.” +``` + +如果 `guard` 语句的条件被满足,则继续执行 `guard` 语句大括号后的代码。将变量或者常量的可选绑定作为 `guard` 语句的条件,都可以保护 `guard` 语句后面的代码。 + +如果条件不被满足,在 `else` 分支上的代码就会被执行。这个分支必须转移控制以退出 `guard` 语句出现的代码段。它可以用控制转移语句如 `return`、`break`、`continue` 或者 `throw` 做这件事,或者调用一个不返回的方法或函数,例如 `fatalError()`。 + +相比于可以实现同样功能的 `if` 语句,按需使用 `guard` 语句会提升我们代码的可读性。它可以使你的代码连贯的被执行而不需要将它包在 `else` 块中,它可以使你在紧邻条件判断的地方,处理违规的情况。 + +## 检测 API 可用性 + +Swift 内置支持检查 API 可用性,这可以确保我们不会在当前部署机器上,不小心地使用了不可用的 API。 + +编译器使用 SDK 中的可用信息来验证我们的代码中使用的所有 API 在项目指定的部署目标上是否可用。如果我们尝试使用一个不可用的 API,Swift 会在编译时报错。 + +我们在 `if` 或 `guard` 语句中使用 `可用性条件(availability condition)`去有条件的执行一段代码,来在运行时判断调用的 API 是否可用。编译器使用从可用性条件语句中获取的信息去验证,在这个代码块中调用的 API 是否可用。 + +```swift +if #available(iOS 10, macOS 10.12, *) { + // 在 iOS 使用 iOS 10 的 API, 在 macOS 使用 macOS 10.12 的 API +} else { + // 使用先前版本的 iOS 和 macOS 的 API +} +``` + +以上可用性条件指定,`if` 语句的代码块仅仅在 iOS 10 或 macOS 10.12 及更高版本才运行。最后一个参数,`*`,是必须的,用于指定在所有其它平台中,如果版本号高于你的设备指定的最低版本,if 语句的代码块将会运行。 + +在它一般的形式中,可用性条件使用了一个平台名字和版本的列表。平台名字可以是 `iOS`,`macOS`,`watchOS` 和 `tvOS`——请访问 [声明属性](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter3/06_Attributes.html) 来获取完整列表。除了指定像 iOS 8 或 macOS 10.10 的大版本号,也可以指定像 iOS 11.2.6 以及 macOS 10.13.3 的小版本号。 + +```swift +if #available(平台名称 版本号, ..., *) { + APIs 可用,语句将执行 +} else { + APIs 不可用,语句将不执行 +} +``` + diff --git a/IOS/Task00:Swift基础语法学习/6.函数.md b/IOS/Task00:Swift基础语法学习/6.函数.md new file mode 100644 index 0000000..7d8de2b --- /dev/null +++ b/IOS/Task00:Swift基础语法学习/6.函数.md @@ -0,0 +1,519 @@ +# 函数 +[TOC] +*函数*是一段完成特定任务的独立代码片段。你可以通过给函数命名来标识某个函数的功能,这个名字可以被用来在需要的时候“调用”这个函数来完成它的任务。 + +Swift 统一的函数语法非常的灵活,可以用来表示任何函数,包括从最简单的没有参数名字的 C 风格函数,到复杂的带局部和外部参数名的 Objective-C 风格函数。参数可以提供默认值,以简化函数调用。参数也可以既当做传入参数,也当做传出参数,也就是说,一旦函数执行结束,传入的参数值将被修改。 + +在 Swift 中,每个函数都有一个由函数的参数值类型和返回值类型组成的类型。你可以把函数类型当做任何其他普通变量类型一样处理,这样就可以更简单地把函数当做别的函数的参数,也可以从其他函数中返回函数。函数的定义可以写在其他函数定义中,这样可以在嵌套函数范围内实现功能封装。 + +## 函数的定义与调用 + +当你定义一个函数时,你可以定义一个或多个有名字和类型的值,作为函数的输入,称为*参数*,也可以定义某种类型的值作为函数执行结束时的输出,称为*返回类型*。 + +每个函数有个*函数名*,用来描述函数执行的任务。要使用一个函数时,用函数名来“调用”这个函数,并传给它匹配的输入值(称作*实参*)。函数的实参必须与函数参数表里参数的顺序一致。 + +下面例子中的函数的名字是 `greet(person:)`,之所以叫这个名字,是因为这个函数用一个人的名字当做输入,并返回向这个人问候的语句。为了完成这个任务,你需要定义一个输入参数——一个叫做 `person` 的 `String` 值,和一个包含给这个人问候语的 `String` 类型的返回值: + +```swift +func greet(person: String) -> String { + let greeting = "Hello, " + person + "!" + return greeting +} +``` + +所有的这些信息汇总起来成为函数的*定义*,并以 `func` 作为前缀。指定函数返回类型时,用返回箭头 `->`(一个连字符后跟一个右尖括号)后跟返回类型的名称的方式来表示。 + +该定义描述了函数的功能,它期望接收什么作为参数和执行结束时它返回的结果是什么类型。这样的定义使得函数可以在别的地方以一种清晰的方式被调用: + +```swift +print(greet(person: "Anna")) +// 打印“Hello, Anna!” +print(greet(person: "Brian")) +// 打印“Hello, Brian!” +``` + +调用 `greet(person:)` 函数时,在圆括号中传给它一个 `String` 类型的实参,例如 `greet(person: "Anna")`。正如上面所示,因为这个函数返回一个 `String` 类型的值,所以 `greet` 可以被包含在 `print(_:separator:terminator:)` 的调用中,用来输出这个函数的返回值。 + +> 注意 +> +> `print(_:separator:terminator:)` 函数的第一个参数并没有设置一个标签,而其他的参数因为已经有了默认值,因此是可选的。关于这些函数语法上的变化详见下方关于 函数参数标签和参数名以及默认参数值。 + +在 `greet(person:)` 的函数体中,先定义了一个新的名为 `greeting` 的 `String` 常量,同时,把对 `personName` 的问候消息赋值给了 `greeting` 。然后用 `return` 关键字把这个问候返回出去。一旦 `return greeting` 被调用,该函数结束它的执行并返回 `greeting` 的当前值。 + +你可以用不同的输入值多次调用 `greet(person:)`。上面的例子展示的是用 `"Anna"` 和 `"Brian"` 调用的结果,该函数分别返回了不同的结果。 + +为了简化这个函数的定义,可以将问候消息的创建和返回写成一句: + +```swift +func greetAgain(person: String) -> String { + return "Hello again, " + person + "!" +} +print(greetAgain(person: "Anna")) +// 打印“Hello again, Anna!” +``` + +## 函数参数与返回值 + +函数参数与返回值在 Swift 中非常的灵活。你可以定义任何类型的函数,包括从只带一个未名参数的简单函数到复杂的带有表达性参数名和不同参数选项的复杂函数。 + +### 无参数函数 + +函数可以没有参数。下面这个函数就是一个无参数函数,当被调用时,它返回固定的 `String` 消息: + +```swift +func sayHelloWorld() -> String { + return "hello, world" +} +print(sayHelloWorld()) +// 打印“hello, world” +``` + +尽管这个函数没有参数,但是定义中在函数名后还是需要一对圆括号。当被调用时,也需要在函数名后写一对圆括号。 + +### 多参数函数 + +函数可以有多种输入参数,这些参数被包含在函数的括号之中,以逗号分隔。 + +下面这个函数用一个人名和是否已经打过招呼作为输入,并返回对这个人的适当问候语: + +```swift +func greet(person: String, alreadyGreeted: Bool) -> String { + if alreadyGreeted { + return greetAgain(person: person) + } else { + return greet(person: person) + } +} +print(greet(person: "Tim", alreadyGreeted: true)) +// 打印“Hello again, Tim!” +``` + +你可以通过在括号内使用逗号分隔来传递一个 `String` 参数值和一个标识为 `alreadyGreeted` 的 `Bool` 值,来调用 `greet(person:alreadyGreeted:)` 函数。注意这个函数和上面 `greet(person:)`是不同的。虽然它们都有着同样的名字 `greet`,但是 `greet(person:alreadyGreeted:)` 函数需要两个参数,而 `greet(person:)` 只需要一个参数。 + +### 无返回值函数 + +函数可以没有返回值。下面是 `greet(person:)` 函数的另一个版本,这个函数直接打印一个 `String`值,而不是返回它: + +```swift +func greet(person: String) { + print("Hello, \(person)!") +} +greet(person: "Dave") +// 打印“Hello, Dave!” +``` + +因为这个函数不需要返回值,所以这个函数的定义中没有返回箭头(->)和返回类型。 + +> 注意 +> +> 严格地说,即使没有明确定义返回值,该 `greet(Person:)` 函数仍然返回一个值。没有明确定义返回类型的函数的返回一个 `Void` 类型特殊值,该值为一个空元组,写成 ()。 + +调用函数时,可以忽略该函数的返回值: + +```swift +func printAndCount(string: String) -> Int { + print(string) + return string.count +} +func printWithoutCounting(string: String) { + let _ = printAndCount(string: string) +} +printAndCount(string: "hello, world") +// 打印“hello, world”,并且返回值 12 +printWithoutCounting(string: "hello, world") +// 打印“hello, world”,但是没有返回任何值 +``` + +第一个函数 `printAndCount(string:)`,输出一个字符串并返回 `Int` 类型的字符数。第二个函数 `printWithoutCounting(string:)` 调用了第一个函数,但是忽略了它的返回值。当第二个函数被调用时,消息依然会由第一个函数输出,但是返回值不会被用到。 + +> 注意 +> +> 返回值可以被忽略,但定义了有返回值的函数必须返回一个值,如果在函数定义底部没有返回任何值,将导致编译时错误。 + +### 多重返回值函数 + +你可以用元组(tuple)类型让多个值作为一个复合值从函数中返回。 + +下例中定义了一个名为 `minMax(array:)` 的函数,作用是在一个 `Int` 类型的数组中找出最小值与最大值。 + +```swift +func minMax(array: [Int]) -> (min: Int, max: Int) { + var currentMin = array[0] + var currentMax = array[0] + for value in array[1.. currentMax { + currentMax = value + } + } + return (currentMin, currentMax) +} +``` + +`minMax(array:)` 函数返回一个包含两个 `Int` 值的元组,这些值被标记为 `min` 和 `max` ,以便查询函数的返回值时可以通过名字访问它们。 + +在 `minMax(array:)` 的函数体中,在开始的时候设置两个工作变量 `currentMin` 和 `currentMax` 的值为数组中的第一个数。然后函数会遍历数组中剩余的值并检查该值是否比 `currentMin` 和 `currentMax`更小或更大。最后数组中的最小值与最大值作为一个包含两个 `Int` 值的元组返回。 + +因为元组的成员值已被命名,因此可以通过 `.` 语法来检索找到的最小值与最大值: + +```swift +let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) +print("min is \(bounds.min) and max is \(bounds.max)") +// 打印“min is -6 and max is 109” +``` + +需要注意的是,元组的成员不需要在元组从函数中返回时命名,因为它们的名字已经在函数返回类型中指定了。 + +### 可选元组返回类型 + +如果函数返回的元组类型有可能整个元组都“没有值”,你可以使用*可选的* 元组返回类型反映整个元组可以是 `nil` 的事实。你可以通过在元组类型的右括号后放置一个问号来定义一个可选元组,例如 `(Int, Int)?` 或 `(String, Int, Bool)?` + +> 注意 +> +> 可选元组类型如 `(Int, Int)?` 与元组包含可选类型如 `(Int?, Int?)` 是不同的。可选的元组类型,整个元组是可选的,而不只是元组中的每个元素值。 + +前面的 `minMax(array:)` 函数返回了一个包含两个 `Int` 值的元组。但是函数不会对传入的数组执行任何安全检查,如果 `array` 参数是一个空数组,如上定义的 `minMax(array:)` 在试图访问 `array[0]`时会触发一个运行时错误。 + +为了安全地处理这个“空数组”问题,将 `minMax(array:)` 函数改写为使用可选元组返回类型,并且当数组为空时返回 `nil`: + +```swift +func minMax(array: [Int]) -> (min: Int, max: Int)? { + if array.isEmpty { return nil } + var currentMin = array[0] + var currentMax = array[0] + for value in array[1.. currentMax { + currentMax = value + } + } + return (currentMin, currentMax) +} +``` + +你可以使用可选绑定来检查 `minMax(array:)` 函数返回的是一个存在的元组值还是 `nil`: + +```swift +if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) { + print("min is \(bounds.min) and max is \(bounds.max)") +} +// 打印“min is -6 and max is 109” +``` + +### 隐式返回的函数 + +如果一个函数的整个函数体是一个单行表达式,这个函数可以隐式地返回这个表达式。举个例子,以下的函数有着同样的作用: + +``` +func greeting(for person: String) -> String { + "Hello, " + person + "!" +} +print(greeting(for: "Dave")) +// 打印 "Hello, Dave!" + +func anotherGreeting(for person: String) -> String { + return "Hello, " + person + "!" +} +print(anotherGreeting(for: "Dave")) +// 打印 "Hello, Dave!" +``` + +`greeting(for:)` 函数的完整定义是打招呼内容的返回,这就意味着它能使用隐式返回这样更简短的形式。`anothergreeting(for:)` 函数返回同样的内容,却因为 `return` 关键字显得函数更长。任何一个可以被写成一行 `return` 语句的函数都可以忽略 `return`。 + +正如你将会在 [简略的 Getter 声明](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/10_Properties.html) 里看到的, 一个属性的 getter 也可以使用隐式返回的形式。 + +## 函数参数标签和参数名称 + +每个函数参数都有一个*参数标签(argument label)*以及一个*参数名称(parameter name)*。参数标签在调用函数的时候使用;调用的时候需要将函数的参数标签写在对应的参数前面。参数名称在函数的实现中使用。默认情况下,函数参数使用参数名称来作为它们的参数标签。 + +```swift +func someFunction(firstParameterName: Int, secondParameterName: Int) { + // 在函数体内,firstParameterName 和 secondParameterName 代表参数中的第一个和第二个参数值 +} +someFunction(firstParameterName: 1, secondParameterName: 2) +``` + +所有的参数都必须有一个独一无二的名字。虽然多个参数拥有同样的参数标签是可能的,但是一个唯一的函数标签能够使你的代码更具可读性。 + +### 指定参数标签 + +你可以在参数名称前指定它的参数标签,中间以空格分隔: + +```swift +func someFunction(argumentLabel parameterName: Int) { + // 在函数体内,parameterName 代表参数值 +} +``` + +这个版本的 `greet(person:)` 函数,接收一个人的名字和他的家乡,并且返回一句问候: + +```swift +func greet(person: String, from hometown: String) -> String { + return "Hello \(person)! Glad you could visit from \(hometown)." +} +print(greet(person: "Bill", from: "Cupertino")) +// 打印“Hello Bill! Glad you could visit from Cupertino.” +``` + +参数标签的使用能够让一个函数在调用时更有表达力,更类似自然语言,并且仍保持了函数内部的可读性以及清晰的意图。 + +### 忽略参数标签 + +如果你不希望为某个参数添加一个标签,可以使用一个下划线(`_`)来代替一个明确的参数标签。 + +```swift +func someFunction(_ firstParameterName: Int, secondParameterName: Int) { + // 在函数体内,firstParameterName 和 secondParameterName 代表参数中的第一个和第二个参数值 +} +someFunction(1, secondParameterName: 2) +``` + +如果一个参数有一个标签,那么在调用的时候必须使用标签来标记这个参数。 + +### 默认参数值 + +你可以在函数体中通过给参数赋值来为任意一个参数定义*默认值(Deafult Value)*。当默认值被定义后,调用这个函数时可以忽略这个参数。 + +```swift +func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) { + // 如果你在调用时候不传第二个参数,parameterWithDefault 会值为 12 传入到函数体中。 +} +someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) // parameterWithDefault = 6 +someFunction(parameterWithoutDefault: 4) // parameterWithDefault = 12 +``` + +将不带有默认值的参数放在函数参数列表的最前。一般来说,没有默认值的参数更加的重要,将不带默认值的参数放在最前保证在函数调用时,非默认参数的顺序是一致的,同时也使得相同的函数在不同情况下调用时显得更为清晰。 + +### 可变参数 + +一个*可变参数(variadic parameter)*可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数可以被传入不确定数量的输入值。通过在变量类型名后面加入(`...`)的方式来定义可变参数。 + +可变参数的传入值在函数体中变为此类型的一个数组。例如,一个叫做 `numbers` 的 `Double...` 型可变参数,在函数体内可以当做一个叫 `numbers` 的 `[Double]` 型的数组常量。 + +下面的这个函数用来计算一组任意长度数字的 *算术平均数(arithmetic mean)*: + +```swift +func arithmeticMean(_ numbers: Double...) -> Double { + var total: Double = 0 + for number in numbers { + total += number + } + return total / Double(numbers.count) +} +arithmeticMean(1, 2, 3, 4, 5) +// 返回 3.0, 是这 5 个数的平均数。 +arithmeticMean(3, 8.25, 18.75) +// 返回 10.0, 是这 3 个数的平均数。 +``` + +> 注意 +> +> 一个函数最多只能拥有一个可变参数。 + +### 输入输出参数 + +函数参数默认是常量。试图在函数体中更改参数值将会导致编译错误。这意味着你不能错误地更改参数值。如果你想要一个函数可以修改参数的值,并且想要在这些修改在函数调用结束后仍然存在,那么就应该把这个参数定义为*输入输出参数(In-Out Parameters)*。 + +定义一个输入输出参数时,在参数定义前加 `inout` 关键字。一个 `输入输出参数`有传入函数的值,这个值被函数修改,然后被传出函数,替换原来的值。想获取更多的关于输入输出参数的细节和相关的编译器优化,请查看 [输入输出参数](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Declarations.html#//apple_ref/doc/uid/TP40014097-CH34-ID545) 一节。 + +你只能传递变量给输入输出参数。你不能传入常量或者字面量,因为这些量是不能被修改的。当传入的参数作为输入输出参数时,需要在参数名前加 `&` 符,表示这个值可以被函数修改。 + +> 注意 +> +> 输入输出参数不能有默认值,而且可变参数不能用 `inout` 标记。 + +下例中,`swapTwoInts(_:_:)` 函数有两个分别叫做 `a` 和 `b` 的输入输出参数: + +```swift +func swapTwoInts(_ a: inout Int, _ b: inout Int) { + let temporaryA = a + a = b + b = temporaryA +} +``` + +`swapTwoInts(_:_:)` 函数简单地交换 `a` 与 `b` 的值。该函数先将 `a` 的值存到一个临时常量 `temporaryA` 中,然后将 `b` 的值赋给 `a`,最后将 `temporaryA` 赋值给 `b`。 + +你可以用两个 `Int` 型的变量来调用 `swapTwoInts(_:_:)`。需要注意的是,`someInt` 和 `anotherInt` 在传入 `swapTwoInts(_:_:)` 函数前,都加了 `&` 的前缀: + +```swift +var someInt = 3 +var anotherInt = 107 +swapTwoInts(&someInt, &anotherInt) +print("someInt is now \(someInt), and anotherInt is now \(anotherInt)") +// 打印“someInt is now 107, and anotherInt is now 3” +``` + +从上面这个例子中,我们可以看到 `someInt` 和 `anotherInt` 的原始值在 `swapTwoInts(_:_:)` 函数中被修改,尽管它们的定义在函数体外。 + +> 注意 +> +> 输入输出参数和返回值是不一样的。上面的 `swapTwoInts` 函数并没有定义任何返回值,但仍然修改了 `someInt` 和 `anotherInt` 的值。输入输出参数是函数对函数体外产生影响的另一种方式。 + +## 函数类型 + +每个函数都有种特定的*函数类型*,函数的类型由函数的参数类型和返回类型组成。 + +例如: + +```swift +func addTwoInts(_ a: Int, _ b: Int) -> Int { + return a + b +} +func multiplyTwoInts(_ a: Int, _ b: Int) -> Int { + return a * b +} +``` + +这个例子中定义了两个简单的数学函数:`addTwoInts` 和 `multiplyTwoInts`。这两个函数都接受两个 `Int` 值, 返回一个 `Int` 值。 + +这两个函数的类型是 `(Int, Int) -> Int`,可以解读为: + +“这个函数类型有两个 `Int` 型的参数并返回一个 `Int` 型的值”。 + +下面是另一个例子,一个没有参数,也没有返回值的函数: + +```swift +func printHelloWorld() { + print("hello, world") +} +``` + +这个函数的类型是:`() -> Void`,或者叫“没有参数,并返回 `Void` 类型的函数”。 + +### 使用函数类型 + +在 Swift 中,使用函数类型就像使用其他类型一样。例如,你可以定义一个类型为函数的常量或变量,并将适当的函数赋值给它: + +```swift +var mathFunction: (Int, Int) -> Int = addTwoInts +``` + +这段代码可以被解读为: + +”定义一个叫做 `mathFunction` 的变量,类型是‘一个有两个 `Int` 型的参数并返回一个 `Int` 型的值的函数’,并让这个新变量指向 `addTwoInts` 函数”。 + +`addTwoInts` 和 `mathFunction` 有同样的类型,所以这个赋值过程在 Swift 类型检查(type-check)中是允许的。 + +现在,你可以用 `mathFunction` 来调用被赋值的函数了: + +```swift +print("Result: \(mathFunction(2, 3))") +// Prints "Result: 5" +``` + +有相同匹配类型的不同函数可以被赋值给同一个变量,就像非函数类型的变量一样: + +```swift +mathFunction = multiplyTwoInts +print("Result: \(mathFunction(2, 3))") +// Prints "Result: 6" +``` + +就像其他类型一样,当赋值一个函数给常量或变量时,你可以让 Swift 来推断其函数类型: + +```swift +let anotherMathFunction = addTwoInts +// anotherMathFunction 被推断为 (Int, Int) -> Int 类型 +``` + +### 函数类型作为参数类型 + +你可以用 `(Int, Int) -> Int` 这样的函数类型作为另一个函数的参数类型。这样你可以将函数的一部分实现留给函数的调用者来提供。 + +下面是另一个例子,正如上面的函数一样,同样是输出某种数学运算结果: + +```swift +func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) { + print("Result: \(mathFunction(a, b))") +} +printMathResult(addTwoInts, 3, 5) +// 打印“Result: 8” +``` + +这个例子定义了 `printMathResult(_:_:_:)` 函数,它有三个参数:第一个参数叫 `mathFunction`,类型是 `(Int, Int) -> Int`,你可以传入任何这种类型的函数;第二个和第三个参数叫 `a` 和 `b`,它们的类型都是 `Int`,这两个值作为已给出的函数的输入值。 + +当 `printMathResult(_:_:_:)` 被调用时,它被传入 `addTwoInts` 函数和整数 `3` 和 `5`。它用传入 `3` 和 `5` 调用 `addTwoInts`,并输出结果:`8`。 + +`printMathResult(_:_:_:)` 函数的作用就是输出另一个适当类型的数学函数的调用结果。它不关心传入函数是如何实现的,只关心传入的函数是不是一个正确的类型。这使得 `printMathResult(_:_:_:)` 能以一种类型安全(type-safe)的方式将一部分功能转给调用者实现。 + +### 函数类型作为返回类型 + +你可以用函数类型作为另一个函数的返回类型。你需要做的是在返回箭头(->)后写一个完整的函数类型。 + +下面的这个例子中定义了两个简单函数,分别是 `stepForward(_:)` 和 `stepBackward(_:)`。`stepForward(_:)` 函数返回一个比输入值大 `1` 的值。`stepBackward(_:)` 函数返回一个比输入值小 `1` 的值。这两个函数的类型都是 `(Int) -> Int`: + +```swift +func stepForward(_ input: Int) -> Int { + return input + 1 +} +func stepBackward(_ input: Int) -> Int { + return input - 1 +} +``` + +如下名为 `chooseStepFunction(backward:)` 的函数,它的返回类型是 `(Int) -> Int` 类型的函数。`chooseStepFunction(backward:)` 根据布尔值 `backwards` 来返回 `stepForward(_:)` 函数或 `stepBackward(_:)` 函数: + +```swift +func chooseStepFunction(backward: Bool) -> (Int) -> Int { + return backward ? stepBackward : stepForward +} +``` + +你现在可以用 `chooseStepFunction(backward:)` 来获得两个函数其中的一个: + +```swift +var currentValue = 3 +let moveNearerToZero = chooseStepFunction(backward: currentValue > 0) +// moveNearerToZero 现在指向 stepBackward() 函数。 +``` + +上面这个例子中计算出从 `currentValue` 逐渐接近到0是需要向正数走还是向负数走。`currentValue`的初始值是 `3`,这意味着 `currentValue > 0` 为真(true),这将使得 `chooseStepFunction(_:)` 返回 `stepBackward(_:)` 函数。一个指向返回的函数的引用保存在了 `moveNearerToZero` 常量中。 + +现在,`moveNearerToZero` 指向了正确的函数,它可以被用来数到零: + +```swift +print("Counting to zero:") +// Counting to zero: +while currentValue != 0 { + print("\(currentValue)... ") + currentValue = moveNearerToZero(currentValue) +} +print("zero!") +// 3... +// 2... +// 1... +// zero! +``` + +## 嵌套函数 + +到目前为止本章中你所见到的所有函数都叫*全局函数(global functions)*,它们定义在全局域中。你也可以把函数定义在别的函数体中,称作 *嵌套函数(nested functions)*。 + +默认情况下,嵌套函数是对外界不可见的,但是可以被它们的外围函数(enclosing function)调用。一个外围函数也可以返回它的某一个嵌套函数,使得这个函数可以在其他域中被使用。 + +你可以用返回嵌套函数的方式重写 `chooseStepFunction(backward:)` 函数: + +```swift +func chooseStepFunction(backward: Bool) -> (Int) -> Int { + func stepForward(input: Int) -> Int { return input + 1 } + func stepBackward(input: Int) -> Int { return input - 1 } + return backward ? stepBackward : stepForward +} +var currentValue = -4 +let moveNearerToZero = chooseStepFunction(backward: currentValue > 0) +// moveNearerToZero now refers to the nested stepForward() function +while currentValue != 0 { + print("\(currentValue)... ") + currentValue = moveNearerToZero(currentValue) +} +print("zero!") +// -4... +// -3... +// -2... +// -1... +// zero! +``` + diff --git a/IOS/Task00:Swift基础语法学习/7.闭包.md b/IOS/Task00:Swift基础语法学习/7.闭包.md new file mode 100644 index 0000000..5f9310e --- /dev/null +++ b/IOS/Task00:Swift基础语法学习/7.闭包.md @@ -0,0 +1,420 @@ +# 闭包 +[TOC] +*闭包*是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数(Lambdas)比较相似。 + +闭包可以捕获和存储其所在上下文中任意常量和变量的引用。被称为*包裹*常量和变量。 Swift 会为你管理在捕获过程中涉及到的所有内存操作。 + +> 注意 +> +> 如果你不熟悉捕获(capturing)这个概念也不用担心,在 [值捕获](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/07_Closures.html#capturing_values) 章节有它更详细的介绍。 + +在 [函数](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/06_Functions.html) 章节中介绍的全局和嵌套函数实际上也是特殊的闭包,闭包采用如下三种形式之一: + +- 全局函数是一个有名字但不会捕获任何值的闭包 +- 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包 +- 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包 + +Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下: + +- 利用上下文推断参数和返回值类型 +- 隐式返回单表达式闭包,即单表达式闭包可以省略 `return` 关键字 +- 参数名称缩写 +- 尾随闭包语法 + +## 闭包表达式 + +[嵌套函数](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/06_Functions.html#Nested_Functions) 作为复杂函数的一部分时,它自包含代码块式的定义和命名形式在使用上带来了方便。当然,编写未完整声明和没有函数名的类函数结构代码是很有用的,尤其是在编码中涉及到函数作为参数的那些方法时。 + +*闭包表达式*是一种构建内联闭包的方式,它的语法简洁。在保证不丢失它语法清晰明了的同时,闭包表达式提供了几种优化的语法简写形式。下面通过对 `sorted(by:)` 这一个案例的多次迭代改进来展示这个过程,每次迭代都使用了更加简明的方式描述了相同功能。。 + +### 排序方法 + +Swift 标准库提供了名为 `sorted(by:)` 的方法,它会基于你提供的排序闭包表达式的判断结果对数组中的值(类型确定)进行排序。一旦它完成排序过程,`sorted(by:)` 方法会返回一个与旧数组类型大小相同类型的新数组,该数组的元素有着正确的排序顺序。原数组不会被 `sorted(by:)` 方法修改。 + +下面的闭包表达式示例使用 `sorted(by:)` 方法对一个 `String` 类型的数组进行字母逆序排序。以下是初始数组: + +```swift +let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] +``` + +`sorted(by:)` 方法接受一个闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值*前面*,排序闭包函数需要返回 `true`,反之返回 `false`。 + +该例子对一个 `String` 类型的数组进行排序,因此排序闭包函数类型需为 `(String, String) -> Bool`。 + +提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数,并将其作为 `sorted(by:)` 方法的参数传入: + +```swift +func backward(_ s1: String, _ s2: String) -> Bool { + return s1 > s2 +} +var reversedNames = names.sorted(by: backward) +// reversedNames 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"] +``` + +如果第一个字符串(`s1`)大于第二个字符串(`s2`),`backward(_:_:)` 函数会返回 `true`,表示在新的数组中 `s1` 应该出现在 `s2` 前。对于字符串中的字符来说,“大于”表示“按照字母顺序较晚出现”。这意味着字母 `"B"` 大于字母 `"A"` ,字符串 `"Tom"` 大于字符串 `"Tim"`。该闭包将进行字母逆序排序,`"Barry"` 将会排在 `"Alex"` 之前。 + +然而,以这种方式来编写一个实际上很简单的表达式(`a > b`),确实太过繁琐了。对于这个例子来说,利用闭包表达式语法可以更好地构造一个内联排序闭包。 + +### 闭包表达式语法 + +闭包表达式语法有如下的一般形式: + +```swift +{ (parameters) -> return type in + statements +} +``` + +*闭包表达式参数* 可以是 in-out 参数,但不能设定默认值。如果你命名了可变参数,也可以使用此可变参数。元组也可以作为参数和返回值。 + +下面的例子展示了之前 `backward(_:_:)` 函数对应的闭包表达式版本的代码: + +```swift +reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in + return s1 > s2 +}) +``` + +需要注意的是内联闭包参数和返回值类型声明与 `backward(_:_:)` 函数类型声明相同。在这两种方式中,都写成了 `(s1: String, s2: String) -> Bool`。然而在内联闭包表达式中,函数和返回值类型都写在*大括号内*,而不是大括号外。 + +闭包的函数体部分由关键字 `in` 引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。 + +由于这个闭包的函数体部分如此短,以至于可以将其改写成一行代码: + +```swift +reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } ) +``` + +该例中 `sorted(by:)` 方法的整体调用保持不变,一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了内联闭包。 + +### 根据上下文推断类型 + +因为排序闭包函数是作为 `sorted(by:)` 方法的参数传入的,Swift 可以推断其参数和返回值的类型。`sorted(by:)` 方法被一个字符串数组调用,因此其参数必须是 `(String, String) -> Bool` 类型的函数。这意味着 `(String, String)` 和 `Bool` 类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(`->`)和围绕在参数周围的括号也可以被省略: + +```swift +reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } ) +``` + +实际上,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,总是能够推断出闭包的参数和返回值类型。这意味着闭包作为函数或者方法的参数时,你几乎不需要利用完整格式构造内联闭包。 + +尽管如此,你仍然可以明确写出有着完整格式的闭包。如果完整格式的闭包能够提高代码的可读性,则我们更鼓励采用完整格式的闭包。而在 `sorted(by:)` 方法这个例子里,显然闭包的目的就是排序。由于这个闭包是为了处理字符串数组的排序,因此读者能够推测出这个闭包是用于字符串处理的。 + +### 单表达式闭包的隐式返回 + +单行表达式闭包可以通过省略 `return` 关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为: + +```swift +reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } ) +``` + +在这个例子中,`sorted(by:)` 方法的参数类型明确了闭包必须返回一个 `Bool` 类型值。因为闭包函数体只包含了一个单一表达式(`s1 > s2`),该表达式返回 `Bool` 类型值,因此这里没有歧义,`return`关键字可以省略。 + +### 参数名称缩写 + +Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 `$0`,`$1`,`$2` 来顺序调用闭包的参数,以此类推。 + +如果你在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。`in` 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成: + +```swift +reversedNames = names.sorted(by: { $0 > $1 } ) +``` + +在这个例子中,`$0` 和 `$1` 表示闭包中第一个和第二个 `String` 类型的参数。 + +### 运算符方法 + +实际上还有一种更*简短的*方式来编写上面例子中的闭包表达式。Swift 的 `String` 类型定义了关于大于号(`>`)的字符串实现,其作为一个函数接受两个 `String` 类型的参数并返回 `Bool` 类型的值。而这正好与 `sorted(by:)` 方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断找到系统自带的那个字符串函数的实现: + +```swift +reversedNames = names.sorted(by: >) +``` + +更多关于运算符方法的内容请查看 [运算符方法](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/26_Advanced_Operators.html#operator_methods)。 + +## 尾随闭包 + +如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,将这个闭包替换成为尾随闭包的形式很有用。尾随闭包是一个书写在函数圆括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签: + +```swift +func someFunctionThatTakesAClosure(closure: () -> Void) { + // 函数体部分 +} + +// 以下是不使用尾随闭包进行函数调用 +someFunctionThatTakesAClosure(closure: { + // 闭包主体部分 +}) + +// 以下是使用尾随闭包进行函数调用 +someFunctionThatTakesAClosure() { + // 闭包主体部分 +} +``` + +在 [闭包表达式语法](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/07_Closures.html#closure_expression_syntax) 上章节中的字符串排序闭包可以作为尾随包的形式改写在 `sorted(by:)` 方法圆括号的外面: + +```swift +reversedNames = names.sorted() { $0 > $1 } +``` + +如果闭包表达式是函数或方法的唯一参数,则当你使用尾随闭包时,你甚至可以把 `()` 省略掉: + +```swift +reversedNames = names.sorted { $0 > $1 } +``` + +当闭包非常长以至于不能在一行中进行书写时,尾随闭包变得非常有用。举例来说,Swift 的 `Array` 类型有一个 `map(_:)` 方法,这个方法获取一个闭包表达式作为其唯一参数。该闭包函数会为数组中的每一个元素调用一次,并返回该元素所映射的值。具体的映射方式和返回值类型由闭包来指定。 + +当提供给数组的闭包应用于每个数组元素后,`map(_:)` 方法将返回一个新的数组,数组中包含了与原数组中的元素一一对应的映射后的值。 + +下例介绍了如何在 `map(_:)` 方法中使用尾随闭包将 `Int` 类型数组 `[16, 58, 510]` 转换为包含对应 `String` 类型的值的数组 `["OneSix", "FiveEight", "FiveOneZero"]`: + +```swift +let digitNames = [ + 0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four", + 5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine" +] +let numbers = [16, 58, 510] +``` + +如上代码创建了一个整型数位和它们英文版本名字相映射的字典。同时还定义了一个准备转换为字符串数组的整型数组。 + +你现在可以通过传递一个尾随闭包给 `numbers` 数组的 `map(_:)` 方法来创建对应的字符串版本数组: + +```swift +let strings = numbers.map { + (number) -> String in + var number = number + var output = "" + repeat { + output = digitNames[number % 10]! + output + number /= 10 + } while number > 0 + return output +} +// strings 常量被推断为字符串类型数组,即 [String] +// 其值为 ["OneSix", "FiveEight", "FiveOneZero"] +``` + +`map(_:)` 为数组中每一个元素调用了一次闭包表达式。你不需要指定闭包的输入参数 `number` 的类型,因为可以通过要映射的数组类型进行推断。 + +在该例中,局部变量 `number` 的值由闭包中的 `number` 参数获得,因此可以在闭包函数体内对其进行修改,(闭包或者函数的参数总是常量),闭包表达式指定了返回类型为 `String`,以表明存储映射值的新数组类型为 `String`。 + +闭包表达式在每次被调用的时候创建了一个叫做 `output` 的字符串并返回。其使用求余运算符(`number % 10`)计算最后一位数字并利用 `digitNames` 字典获取所映射的字符串。这个闭包能够用于创建任意正整数的字符串表示。 + +> 注意 +> +> 字典 `digitNames` 下标后跟着一个叹号(`!`),因为字典下标返回一个可选值(optional value),表明该键不存在时会查找失败。在上例中,由于可以确定 `number % 10` 总是 `digitNames` 字典的有效下标,因此叹号可以用于强制解包(force-unwrap)存储在下标的可选类型的返回值中的 `String` 类型的值。 + +从 `digitNames` 字典中获取的字符串被添加到 `output` 的*前部*,逆序建立了一个字符串版本的数字。(在表达式 `number % 10` 中,如果 `number` 为 `16`,则返回 `6`,`58` 返回 `8`,`510` 返回 `0`。) + +`number` 变量之后除以 `10`。因为其是整数,在计算过程中未除尽部分被忽略。因此 `16` 变成了 `1`,`58` 变成了 `5`,`510` 变成了 `51`。 + +整个过程重复进行,直到 `number /= 10` 为 `0`,这时闭包会将字符串 `output` 返回,而 `map(_:)`方法则会将字符串添加到映射数组中。 + +在上面的例子中,通过尾随闭包语法,优雅地在函数后封装了闭包的具体功能,而不再需要将整个闭包包裹在 `map(_:)` 方法的括号内。 + +## 值捕获 + +闭包可以在其被定义的上下文中*捕获*常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。 + +Swift 中,可以捕获值的闭包的最简单形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。 + +举个例子,这有一个叫做 `makeIncrementer` 的函数,其包含了一个叫做 `incrementer` 的嵌套函数。嵌套函数 `incrementer()` 从上下文中捕获了两个值,`runningTotal` 和 `amount`。捕获这些值之后,`makeIncrementer` 将 `incrementer` 作为闭包返回。每次调用 `incrementer` 时,其会以 `amount` 作为增量增加 `runningTotal` 的值。 + +```swift +func makeIncrementer(forIncrement amount: Int) -> () -> Int { + var runningTotal = 0 + func incrementer() -> Int { + runningTotal += amount + return runningTotal + } + return incrementer +} +``` + +`makeIncrementer` 返回类型为 `() -> Int`。这意味着其返回的是一个*函数*,而非一个简单类型的值。该函数在每次调用时不接受参数,只返回一个 `Int` 类型的值。关于函数返回其他函数的内容,请查看 [函数类型作为返回类型](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/06_Functions.html#function_types_as_return_types)。 + +`makeIncrementer(forIncrement:)` 函数定义了一个初始值为 `0` 的整型变量 `runningTotal`,用来存储当前总计数值。该值为 `incrementer` 的返回值。 + +`makeIncrementer(forIncrement:)` 有一个 `Int` 类型的参数,其外部参数名为 `forIncrement`,内部参数名为 `amount`,该参数表示每次 `incrementer` 被调用时 `runningTotal` 将要增加的量。`makeIncrementer` 函数还定义了一个嵌套函数 `incrementer`,用来执行实际的增加操作。该函数简单地使 `runningTotal` 增加 `amount`,并将其返回。 + +如果我们单独考虑嵌套函数 `incrementer()`,会发现它有些不同寻常: + +```swift +func incrementer() -> Int { + runningTotal += amount + return runningTotal +} +``` + +`incrementer()` 函数并没有任何参数,但是在函数体内访问了 `runningTotal` 和 `amount` 变量。这是因为它从外围函数捕获了 `runningTotal` 和 `amount` 变量的*引用*。捕获引用保证了 `runningTotal`和 `amount` 变量在调用完 `makeIncrementer` 后不会消失,并且保证了在下一次执行 `incrementer` 函数时,`runningTotal` 依旧存在。 + +> 注意 +> +> 为了优化,如果一个值不会被闭包改变,或者在闭包创建后不会改变,Swift 可能会改为捕获并保存一份对值的拷贝。 +> +> Swift 也会负责被捕获变量的所有内存管理工作,包括释放不再需要的变量。 + +下面是一个使用 `makeIncrementer` 的例子: + +```swift +let incrementByTen = makeIncrementer(forIncrement: 10) +``` + +该例子定义了一个叫做 `incrementByTen` 的常量,该常量指向一个每次调用会将其 `runningTotal` 变量增加 `10` 的 `incrementer` 函数。调用这个函数多次可以得到以下结果: + +```swift +incrementByTen() +// 返回的值为10 +incrementByTen() +// 返回的值为20 +incrementByTen() +// 返回的值为30 +``` + +如果你创建了另一个 `incrementer`,它会有属于自己的引用,指向一个全新、独立的 `runningTotal`变量: + +```swift +let incrementBySeven = makeIncrementer(forIncrement: 7) +incrementBySeven() +// 返回的值为7 +``` + +再次调用原来的 `incrementByTen` 会继续增加它自己的 `runningTotal` 变量,该变量和 `incrementBySeven` 中捕获的变量没有任何联系: + +```swift +incrementByTen() +// 返回的值为40 +``` + +> 注意 +> +> 如果你将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员而捕获了该实例,你将在闭包和该实例间创建一个循环强引用。Swift 使用捕获列表来打破这种循环强引用。更多信息,请参考 [闭包引起的循环强引用](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/23_Automatic_Reference_Counting.html#strong_reference_cycles_for_closures)。 + +## 闭包是引用类型 + +上面的例子中,`incrementBySeven` 和 `incrementByTen` 都是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是*引用类型*。 + +无论你将函数或闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的*引用*。上面的例子中,指向闭包的引用 `incrementByTen` 是一个常量,而并非闭包内容本身。 + +这也意味着如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包: + +```swift +let alsoIncrementByTen = incrementByTen +alsoIncrementByTen() +// 返回的值为50 +``` + +## 逃逸闭包 + +当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中*逃逸*。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 `@escaping`,用来指明这个闭包是允许“逃逸”出这个函数的。 + +一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如: + +```swift +var completionHandlers: [() -> Void] = [] +func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) { + completionHandlers.append(completionHandler) +} +``` + +`someFunctionWithEscapingClosure(_:)` 函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为 `@escaping`,就会得到一个编译错误。 + +将一个闭包标记为 `@escaping` 意味着你必须在闭包中显式地引用 `self`。比如说,在下面的代码中,传递到 `someFunctionWithEscapingClosure(_:)` 中的闭包是一个逃逸闭包,这意味着它需要显式地引用 `self`。相对的,传递到 `someFunctionWithNonescapingClosure(_:)` 中的闭包是一个非逃逸闭包,这意味着它可以隐式引用 `self`。 + +```swift +func someFunctionWithNonescapingClosure(closure: () -> Void) { + closure() +} + +class SomeClass { + var x = 10 + func doSomething() { + someFunctionWithEscapingClosure { self.x = 100 } + someFunctionWithNonescapingClosure { x = 200 } + } +} + +let instance = SomeClass() +instance.doSomething() +print(instance.x) +// 打印出“200” + +completionHandlers.first?() +print(instance.x) +// 打印出“100” +``` + +## 自动闭包 + +*自动闭包*是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。 + +我们经常会*调用*采用自动闭包的函数,但是很少去*实现*这样的函数。举个例子来说,`assert(condition:message:file:line:)` 函数接受自动闭包作为它的 `condition` 参数和 `message` 参数;它的 `condition` 参数仅会在 debug 模式下被求值,它的 `message` 参数仅当 `condition` 参数为 `false` 时被计算求值。 + +自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。下面的代码展示了闭包如何延时求值。 + +```swift +var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] +print(customersInLine.count) +// 打印出“5” + +let customerProvider = { customersInLine.remove(at: 0) } +print(customersInLine.count) +// 打印出“5” + +print("Now serving \(customerProvider())!") +// Prints "Now serving Chris!" +print(customersInLine.count) +// 打印出“4” +``` + +尽管在闭包的代码中,`customersInLine` 的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。请注意,`customerProvider` 的类型不是 `String`,而是 `() -> String`,一个没有参数且返回值为 `String` 的函数。 + +将闭包作为参数传递给函数时,你能获得同样的延时求值行为。 + +```swift +// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"] +func serve(customer customerProvider: () -> String) { + print("Now serving \(customerProvider())!") +} +serve(customer: { customersInLine.remove(at: 0) } ) +// 打印出“Now serving Alex!” +``` + +上面的 `serve(customer:)` 函数接受一个返回顾客名字的显式的闭包。下面这个版本的 `serve(customer:)` 完成了相同的操作,不过它并没有接受一个显式的闭包,而是通过将参数标记为 `@autoclosure` 来接收一个自动闭包。现在你可以将该函数当作接受 `String` 类型参数(而非闭包)的函数来调用。`customerProvider` 参数将自动转化为一个闭包,因为该参数被标记了 `@autoclosure` 特性。 + +```swift +// customersInLine is ["Ewa", "Barry", "Daniella"] +func serve(customer customerProvider: @autoclosure () -> String) { + print("Now serving \(customerProvider())!") +} +serve(customer: customersInLine.remove(at: 0)) +// 打印“Now serving Ewa!” +``` + +> 注意 +> +> 过度使用 `autoclosures` 会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的。 + +如果你想让一个自动闭包可以“逃逸”,则应该同时使用 `@autoclosure` 和 `@escaping` 属性。`@escaping` 属性的讲解见上面的`逃逸闭包`。 + +```swift +// customersInLine i= ["Barry", "Daniella"] +var customerProviders: [() -> String] = [] +func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) { + customerProviders.append(customerProvider) +} +collectCustomerProviders(customersInLine.remove(at: 0)) +collectCustomerProviders(customersInLine.remove(at: 0)) + +print("Collected \(customerProviders.count) closures.") +// 打印“Collected 2 closures.” +for customerProvider in customerProviders { + print("Now serving \(customerProvider())!") +} +// 打印“Now serving Barry!” +// 打印“Now serving Daniella!” +``` + +在上面的代码中,`collectCustomerProviders(_:)` 函数并没有调用传入的 `customerProvider` 闭包,而是将闭包追加到了 `customerProviders` 数组中。这个数组定义在函数作用域范围外,这意味着数组内的闭包能够在函数返回之后被调用。因此,`customerProvider` 参数必须允许“逃逸”出函数作用域。 diff --git a/IOS/Task00:Swift基础语法学习/8.枚举.md b/IOS/Task00:Swift基础语法学习/8.枚举.md new file mode 100644 index 0000000..bfda7c1 --- /dev/null +++ b/IOS/Task00:Swift基础语法学习/8.枚举.md @@ -0,0 +1,348 @@ +# 枚举 +[TOC] +*枚举*为一组相关的值定义了一个共同的类型,使你可以在你的代码中以类型安全的方式来使用这些值。 + +如果你熟悉 C 语言,你会知道在 C 语言中,枚举会为一组整型值分配相关联的名称。Swift 中的枚举更加灵活,不必给每一个枚举成员提供一个值。如果给枚举成员提供一个值(称为原始值),则该值的类型可以是字符串、字符,或是一个整型值或浮点数。 + +此外,枚举成员可以指定*任意*类型的关联值存储到枚举成员中,就像其他语言中的联合体(unions)和变体(variants)。你可以在一个枚举中定义一组相关的枚举成员,每一个枚举成员都可以有适当类型的关联值。 + +在 Swift 中,枚举类型是一等(first-class)类型。它们采用了很多在传统上只被类(class)所支持的特性,例如计算属性(computed properties),用于提供枚举值的附加信息,实例方法(instance methods),用于提供和枚举值相关联的功能。枚举也可以定义构造函数(initializers)来提供一个初始值;可以在原始实现的基础上扩展它们的功能;还可以遵循协议(protocols)来提供标准的功能。 + +想了解更多相关信息,请参见 [属性](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/10_Properties.html),[方法](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/11_Methods.html),[构造过程](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/14_Initialization.html),[扩展](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/20_Extensions.html) 和 [协议](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/21_Protocols.html)。 + +## 枚举语法 + +使用 `enum` 关键词来创建枚举并且把它们的整个定义放在一对大括号内: + +```swift +enum SomeEnumeration { + // 枚举定义放在这里 +} +``` + +下面是用枚举表示指南针四个方向的例子: + +```swift +enum CompassPoint { + case north + case south + case east + case west +} +``` + +枚举中定义的值(如 `north`,`south`,`east` 和 `west`)是这个枚举的*成员值*(或*成员*)。你可以使用 `case` 关键字来定义一个新的枚举成员值。 + +> 注意 +> +> 与 C 和 Objective-C 不同,Swift 的枚举成员在被创建时不会被赋予一个默认的整型值。在上面的 `CompassPoint` 例子中,`north`,`south`,`east` 和 `west` 不会被隐式地赋值为 `0`,`1`,`2` 和 `3`。相反,这些枚举成员本身就是完备的值,这些值的类型是已经明确定义好的 `CompassPoint` 类型。 + +多个成员值可以出现在同一行上,用逗号隔开: + +```swift +enum Planet { + case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune +} +``` + +每个枚举定义了一个全新的类型。像 Swift 中其他类型一样,它们的名字(例如 `CompassPoint` 和 `Planet`)以一个大写字母开头。给枚举类型起一个单数名字而不是复数名字,以便于: + +```swift +var directionToHead = CompassPoint.west +``` + +`directionToHead` 的类型可以在它被 `CompassPoint` 的某个值初始化时推断出来。一旦 `directionToHead` 被声明为 `CompassPoint` 类型,你可以使用更简短的点语法将其设置为另一个 `CompassPoint` 的值: + +```swift +directionToHead = .east +``` + +当 `directionToHead` 的类型已知时,再次为其赋值可以省略枚举类型名。在使用具有显式类型的枚举值时,这种写法让代码具有更好的可读性。 + +## 使用 Switch 语句匹配枚举值 + +你可以使用 `switch` 语句匹配单个枚举值: + +```swift +directionToHead = .south +switch directionToHead { +case .north: + print("Lots of planets have a north") +case .south: + print("Watch out for penguins") +case .east: + print("Where the sun rises") +case .west: + print("Where the skies are blue") +} +// 打印“Watch out for penguins” +``` + +你可以这样理解这段代码: + +“判断 `directionToHead` 的值。当它等于 `.north`,打印 `“Lots of planets have a north”`。当它等于 `.south`,打印 `“Watch out for penguins”`。” + +……以此类推。 + +正如在 [控制流](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/05_Control_Flow.html) 中介绍的那样,在判断一个枚举类型的值时,`switch` 语句必须穷举所有情况。如果忽略了 `.west` 这种情况,上面那段代码将无法通过编译,因为它没有考虑到 `CompassPoint` 的全部成员。强制穷举确保了枚举成员不会被意外遗漏。 + +当不需要匹配每个枚举成员的时候,你可以提供一个 `default` 分支来涵盖所有未明确处理的枚举成员: + +```swift +let somePlanet = Planet.earth +switch somePlanet { +case .earth: + print("Mostly harmless") +default: + print("Not a safe place for humans") +} +// 打印“Mostly harmless” +``` + +## 枚举成员的遍历 + +在一些情况下,你会需要得到一个包含枚举所有成员的集合。可以通过如下代码实现: + +令枚举遵循 `CaseIterable` 协议。Swift 会生成一个 `allCases` 属性,用于表示一个包含枚举所有成员的集合。下面是一个例子: + +```swift +enum Beverage: CaseIterable { + case coffee, tea, juice +} +let numberOfChoices = Beverage.allCases.count +print("\(numberOfChoices) beverages available") +// 打印“3 beverages available” +``` + +在前面的例子中,通过 `Beverage.allCases` 可以访问到包含 `Beverage` 枚举所有成员的集合。`allCases` 的使用方法和其它一般集合一样——集合中的元素是枚举类型的实例,所以在上面的情况中,这些元素是 `Beverage` 值。在前面的例子中,统计了总共有多少个枚举成员。而在下面的例子中,则使用 `for` 循环来遍历所有枚举成员。 + +```swift +for beverage in Beverage.allCases { + print(beverage) +} +// coffee +// tea +// juice +``` + +在前面的例子中,使用的语法表明这个枚举遵循 [CaseIterable](https://developer.apple.com/documentation/swift/caseiterable) 协议。想了解 protocols 相关信息,请参见 [协议](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/21_Protocols.html)。 + +## 关联值 + +枚举语法那一小节的例子演示了如何定义和分类枚举的成员。你可以为 `Planet.earth` 设置一个常量或者变量,并在赋值之后查看这个值。然而,有时候把其他类型的值和成员值一起存储起来会很有用。这额外的信息称为*关联值*,并且你每次在代码中使用该枚举成员时,还可以修改这个关联值。 + +你可以定义 Swift 枚举来存储任意类型的关联值,如果需要的话,每个枚举成员的关联值类型可以各不相同。枚举的这种特性跟其他语言中的可识别联合(discriminated unions),标签联合(tagged unions),或者变体(variants)相似。 + +例如,假设一个库存跟踪系统需要利用两种不同类型的条形码来跟踪商品。有些商品上标有使用 `0` 到 `9` 的数字的 UPC 格式的一维条形码。每一个条形码都有一个代表数字系统的数字,该数字后接五位代表厂商代码的数字,接下来是五位代表“产品代码”的数字。最后一个数字是检查位,用来验证代码是否被正确扫描: + +![img](https://docs.swift.org/swift-book/_images/barcode_UPC_2x.png) + +其他商品上标有 QR 码格式的二维码,它可以使用任何 ISO 8859-1 字符,并且可以编码一个最多拥有 2,953 个字符的字符串: + +![img](https://docs.swift.org/swift-book/_images/barcode_QR_2x.png) + +这便于库存跟踪系统用包含四个整型值的元组存储 UPC 码,以及用任意长度的字符串储存 QR 码。 + +在 Swift 中,使用如下方式定义表示两种商品条形码的枚举: + +```swift +enum Barcode { + case upc(Int, Int, Int, Int) + case qrCode(String) +} +``` + +以上代码可以这么理解: + +“定义一个名为 `Barcode` 的枚举类型,它的一个成员值是具有 `(Int,Int,Int,Int)` 类型关联值的 `upc`,另一个成员值是具有 `String` 类型关联值的 `qrCode`。” + +这个定义不提供任何 `Int` 或 `String` 类型的关联值,它只是定义了,当 `Barcode` 常量和变量等于 `Barcode.upc` 或 `Barcode.qrCode` 时,可以存储的关联值的类型。 + +然后你可以使用任意一种条形码类型创建新的条形码,例如: + +```swift +var productBarcode = Barcode.upc(8, 85909, 51226, 3) +``` + +上面的例子创建了一个名为 `productBarcode` 的变量,并将 `Barcode.upc` 赋值给它,关联的元组值为 `(8, 85909, 51226, 3)`。 + +同一个商品可以被分配一个不同类型的条形码,例如: + +```swift +productBarcode = .qrCode("ABCDEFGHIJKLMNOP") +``` + +这时,原始的 `Barcode.upc` 和其整数关联值被新的 `Barcode.qrCode` 和其字符串关联值所替代。`Barcode` 类型的常量和变量可以存储一个 `.upc` 或者一个 `.qrCode`(连同它们的关联值),但是在同一时间只能存储这两个值中的一个。 + +你可以使用一个 switch 语句来检查不同的条形码类型,和之前使用 Switch 语句来匹配枚举值的例子一样。然而,这一次,关联值可以被提取出来作为 switch 语句的一部分。你可以在 `switch` 的 case 分支代码中提取每个关联值作为一个常量(用 `let` 前缀)或者作为一个变量(用 `var` 前缀)来使用: + +```swift +switch productBarcode { +case .upc(let numberSystem, let manufacturer, let product, let check): + print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).") +case .qrCode(let productCode): + print("QR code: \(productCode).") +} +// 打印“QR code: ABCDEFGHIJKLMNOP.” +``` + +如果一个枚举成员的所有关联值都被提取为常量,或者都被提取为变量,为了简洁,你可以只在成员名称前标注一个 `let` 或者 `var`: + +```swift +switch productBarcode { +case let .upc(numberSystem, manufacturer, product, check): + print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).") +case let .qrCode(productCode): + print("QR code: \(productCode).") +} +// 打印“QR code: ABCDEFGHIJKLMNOP.” +``` + +## 原始值 + +在 [关联值](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/08_Enumerations.html#associated_values) 小节的条形码例子中,演示了如何声明存储不同类型关联值的枚举成员。作为关联值的替代选择,枚举成员可以被默认值(称为*原始值*)预填充,这些原始值的类型必须相同。 + +这是一个使用 ASCII 码作为原始值的枚举: + +```swift +enum ASCIIControlCharacter: Character { + case tab = "\t" + case lineFeed = "\n" + case carriageReturn = "\r" +} +``` + +枚举类型 `ASCIIControlCharacter` 的原始值类型被定义为 `Character`,并设置了一些比较常见的 ASCII 控制字符。`Character` 的描述详见 [字符串和字符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/03_Strings_and_Characters.html) 部分。 + +原始值可以是字符串、字符,或者任意整型值或浮点型值。每个原始值在枚举声明中必须是唯一的。 + +> 注意 +> +> 原始值和关联值是不同的。原始值是在定义枚举时被预先填充的值,像上述三个 ASCII 码。对于一个特定的枚举成员,它的原始值始终不变。关联值是创建一个基于枚举成员的常量或变量时才设置的值,枚举成员的关联值可以变化。 + +### 原始值的隐式赋值 + +在使用原始值为整数或者字符串类型的枚举时,不需要显式地为每一个枚举成员设置原始值,Swift 将会自动为你赋值。 + +例如,当使用整数作为原始值时,隐式赋值的值依次递增 `1`。如果第一个枚举成员没有设置原始值,其原始值将为 `0`。 + +下面的枚举是对之前 `Planet` 这个枚举的一个细化,利用整型的原始值来表示每个行星在太阳系中的顺序: + +```swift +enum Planet: Int { + case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune +} +``` + +在上面的例子中,`Plant.mercury` 的显式原始值为 `1`,`Planet.venus` 的隐式原始值为 `2`,依次类推。 + +当使用字符串作为枚举类型的原始值时,每个枚举成员的隐式原始值为该枚举成员的名称。 + +下面的例子是 `CompassPoint` 枚举的细化,使用字符串类型的原始值来表示各个方向的名称: + +```swift +enum CompassPoint: String { + case north, south, east, west +} +``` + +上面例子中,`CompassPoint.south` 拥有隐式原始值 `south`,依次类推。 + +使用枚举成员的 `rawValue` 属性可以访问该枚举成员的原始值: + +```swift +let earthsOrder = Planet.earth.rawValue +// earthsOrder 值为 3 + +let sunsetDirection = CompassPoint.west.rawValue +// sunsetDirection 值为 "west" +``` + +### 使用原始值初始化枚举实例 + +如果在定义枚举类型的时候使用了原始值,那么将会自动获得一个初始化方法,这个方法接收一个叫做 `rawValue` 的参数,参数类型即为原始值类型,返回值则是枚举成员或 `nil`。你可以使用这个初始化方法来创建一个新的枚举实例。 + +这个例子利用原始值 `7` 创建了枚举成员 `Uranus`: + +```swift +let possiblePlanet = Planet(rawValue: 7) +// possiblePlanet 类型为 Planet? 值为 Planet.uranus +``` + +然而,并非所有 `Int` 值都可以找到一个匹配的行星。因此,原始值构造器总是返回一个*可选*的枚举成员。在上面的例子中,`possiblePlanet` 是 `Planet?` 类型,或者说“可选的 `Planet`”。 + +> 注意 +> +> 原始值构造器是一个可失败构造器,因为并不是每一个原始值都有与之对应的枚举成员。更多信息请参见 [可失败构造器](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter3/05_Declarations.html#failable_initializers) + +如果你试图寻找一个位置为 `11` 的行星,通过原始值构造器返回的可选 `Planet` 值将是 `nil`: + +```swift +let positionToFind = 11 +if let somePlanet = Planet(rawValue: positionToFind) { + switch somePlanet { + case .earth: + print("Mostly harmless") + default: + print("Not a safe place for humans") + } +} else { + print("There isn't a planet at position \(positionToFind)") +} +// 打印“There isn't a planet at position 11” +``` + +这个例子使用了可选绑定(optional binding),试图通过原始值 `11` 来访问一个行星。`if let somePlanet = Planet(rawValue: 11)` 语句创建了一个可选 `Planet`,如果可选 `Planet` 的值存在,就会赋值给 `somePlanet`。在这个例子中,无法检索到位置为 `11` 的行星,所以 `else` 分支被执行。 + +## 递归枚举 + +*递归枚举*是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上 `indirect` 来表示该成员可递归。 + +例如,下面的例子中,枚举类型存储了简单的算术表达式: + +```swift +enum ArithmeticExpression { + case number(Int) + indirect case addition(ArithmeticExpression, ArithmeticExpression) + indirect case multiplication(ArithmeticExpression, ArithmeticExpression) +} +``` + +你也可以在枚举类型开头加上 `indirect` 关键字来表明它的所有成员都是可递归的: + +```swift +indirect enum ArithmeticExpression { + case number(Int) + case addition(ArithmeticExpression, ArithmeticExpression) + case multiplication(ArithmeticExpression, ArithmeticExpression) +} +``` + +上面定义的枚举类型可以存储三种算术表达式:纯数字、两个表达式相加、两个表达式相乘。枚举成员 `addition` 和 `multiplication` 的关联值也是算术表达式——这些关联值使得嵌套表达式成为可能。例如,表达式 `(5 + 4) * 2`,乘号右边是一个数字,左边则是另一个表达式。因为数据是嵌套的,因而用来存储数据的枚举类型也需要支持这种嵌套——这意味着枚举类型需要支持递归。下面的代码展示了使用 `ArithmeticExpression` 这个递归枚举创建表达式 `(5 + 4) * 2` + +```swift +let five = ArithmeticExpression.number(5) +let four = ArithmeticExpression.number(4) +let sum = ArithmeticExpression.addition(five, four) +let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2)) +``` + +要操作具有递归性质的数据结构,使用递归函数是一种直截了当的方式。例如,下面是一个对算术表达式求值的函数: + +```swift +func evaluate(_ expression: ArithmeticExpression) -> Int { + switch expression { + case let .number(value): + return value + case let .addition(left, right): + return evaluate(left) + evaluate(right) + case let .multiplication(left, right): + return evaluate(left) * evaluate(right) + } +} + +print(evaluate(product)) +// 打印“18” +``` + +该函数如果遇到纯数字,就直接返回该数字的值。如果遇到的是加法或乘法运算,则分别计算左边表达式和右边表达式的值,然后相加或相乘。 diff --git a/IOS/Task00:Swift基础语法学习/9.结构体和类.md b/IOS/Task00:Swift基础语法学习/9.结构体和类.md new file mode 100644 index 0000000..40548e5 --- /dev/null +++ b/IOS/Task00:Swift基础语法学习/9.结构体和类.md @@ -0,0 +1,252 @@ +# 结构体和类 +[TOC] +*结构体*和*类*作为一种通用而又灵活的结构,成为了人们构建代码的基础。你可以使用定义常量、变量和函数的语法,为你的结构体和类定义属性、添加方法。 + +与其他编程语言所不同的是,Swift 并不要求你为自定义的结构体和类的接口与实现代码分别创建文件。你只需在单一的文件中定义一个结构体或者类,系统将会自动生成面向其它代码的外部接口。 + +> 注意 +> +> 通常一个*类*的实例被称为*对象*。然而相比其他语言,Swift 中结构体和类的功能更加相近,本章中所讨论的大部分功能都可以用在结构体或者类上。因此,这里会使用*实例*这个更通用的术语。 + +## 结构体和类对比 + +Swift 中结构体和类有很多共同点。两者都可以: + +- 定义属性用于存储值 +- 定义方法用于提供功能 +- 定义下标操作用于通过下标语法访问它们的值 +- 定义构造器用于设置初始值 +- 通过扩展以增加默认实现之外的功能 +- 遵循协议以提供某种标准功能 + +更多信息请参见 [属性](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/10_Properties.html)、[方法](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/11_Methods.html)、[下标](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/12_Subscripts.html)、[构造过程](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/14_Initialization.html)、[扩展](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/20_Extensions.html) 和 [协议](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/21_Protocols.html)。 + +与结构体相比,类还有如下的附加功能: + +- 继承允许一个类继承另一个类的特征 +- 类型转换允许在运行时检查和解释一个类实例的类型 +- 析构器允许一个类实例释放任何其所被分配的资源 +- 引用计数允许对一个类的多次引用 + +更多信息请参见 [继承](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/13_Inheritance.html)、[类型转换](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/18_Type_Casting.html)、[析构过程](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/15_Deinitialization.html) 和 [自动引用计数](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/23_Automatic_Reference_Counting.html)。 + +类支持的附加功能是以增加复杂性为代价的。作为一般准则,优先使用结构体,因为它们更容易理解,仅在适当或必要时才使用类。实际上,这意味着你的大多数自定义数据类型都会是结构体和枚举。更多详细的比较参见 [在结构和类之间进行选择](https://developer.apple.com/documentation/swift/choosing_between_structures_and_classes)。 + +### 类型定义的语法 + +结构体和类有着相似的定义方式。你通过 `struct` 关键字引入结构体,通过 `class` 关键字引入类,并将它们的具体定义放在一对大括号中: + +```swift +struct SomeStructure { + // 在这里定义结构体 +} +class SomeClass { + // 在这里定义类 +} +``` + +> 注意 +> +> 每当你定义一个新的结构体或者类时,你都是定义了一个新的 Swift 类型。请使用 `UpperCamelCase`这种方式来命名类型(如这里的 `SomeClass` 和 `SomeStructure`),以便符合标准 Swift 类型的大写命名风格(如 `String`,`Int` 和 `Bool`)。请使用 `lowerCamelCase` 这种方式来命名属性和方法(如 `framerate` 和 `incrementCount`),以便和类型名区分。 + +以下是定义结构体和定义类的示例: + +```swift +struct Resolution { + var width = 0 + var height = 0 +} +class VideoMode { + var resolution = Resolution() + var interlaced = false + var frameRate = 0.0 + var name: String? +} +``` + +在上面的示例中定义了一个名为 `Resolution` 的结构体,用来描述基于像素的分辨率。这个结构体包含了名为 `width` 和 `height` 的两个存储属性。存储属性是与结构体或者类绑定的,并存储在其中的常量或变量。当这两个属性被初始化为整数 `0` 的时候,它们会被推断为 `Int` 类型。 + +在上面的示例还定义了一个名为 `VideoMode` 的类,用来描述视频显示器的某个特定视频模式。这个类包含了四个可变的存储属性。第一个, `resolution`,被初始化为一个新的 `Resolution` 结构体的实例,属性类型被推断为 `Resolution`。新 `VideoMode` 实例同时还会初始化其它三个属性,它们分别是初始值为 `false` 的 `interlaced`(意为“非隔行视频”),初始值为 `0.0` 的 `frameRate`,以及值为可选 `String` 的 `name`。因为 `name` 是一个可选类型,它会被自动赋予一个默认值 `nil`,意为“没有 `name` 值”。 + +### 结构体和类的实例 + +`Resolution` 结构体和 `VideoMode` 类的定义仅描述了什么是 `Resolution` 和 `VideoMode`。它们并没有描述一个特定的分辨率(resolution)或者视频模式(video mode)。为此,你需要创建结构体或者类的一个实例。 + +创建结构体和类实例的语法非常相似: + +```swift +let someResolution = Resolution() +let someVideoMode = VideoMode() +``` + +结构体和类都使用构造器语法来创建新的实例。构造器语法的最简单形式是在结构体或者类的类型名称后跟随一对空括号,如 `Resolution()` 或 `VideoMode()`。通过这种方式所创建的类或者结构体实例,其属性均会被初始化为默认值。[构造过程](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/14_Initialization.html) 章节会对类和结构体的初始化进行更详细的讨论。 + +### 属性访问 + +你可以通过使用*点语法*访问实例的属性。其语法规则是,实例名后面紧跟属性名,两者以点号(`.`)分隔,不带空格: + +```swift +print("The width of someResolution is \(someResolution.width)") +// 打印 "The width of someResolution is 0" +``` + +在上面的例子中,`someResolution.width` 引用 `someResolution` 的 `width` 属性,返回 `width` 的初始值 `0`。 + +你也可以访问子属性,如 `VideoMode` 中 `resolution` 属性的 `width` 属性: + +```swift +print("The width of someVideoMode is \(someVideoMode.resolution.width)") +// 打印 "The width of someVideoMode is 0" +``` + +你也可以使用点语法为可变属性赋值: + +```swift +someVideoMode.resolution.width = 1280 +print("The width of someVideoMode is now \(someVideoMode.resolution.width)") +// 打印 "The width of someVideoMode is now 1280" +``` + +### 结构体类型的成员逐一构造器 + +所有结构体都有一个自动生成的*成员逐一构造器*,用于初始化新结构体实例中成员的属性。新实例中各个属性的初始值可以通过属性的名称传递到成员逐一构造器之中: + +```swift +let vga = Resolution(width: 640, height: 480) +``` + +与结构体不同,类实例没有默认的成员逐一构造器。[构造过程](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/14_Initialization.html) 章节会对构造器进行更详细的讨论。 + +## 结构体和枚举是值类型 + +*值类型*是这样一种类型,当它被赋值给一个变量、常量或者被传递给一个函数的时候,其值会被*拷贝*。 + +在之前的章节中,你已经大量使用了值类型。实际上,Swift 中所有的基本类型:整数(integer)、浮点数(floating-point number)、布尔值(boolean)、字符串(string)、数组(array)和字典(dictionary),都是值类型,其底层也是使用结构体实现的。 + +Swift 中所有的结构体和枚举类型都是值类型。这意味着它们的实例,以及实例中所包含的任何值类型的属性,在代码中传递的时候都会被复制。 + +> 注意 +> +> 标准库定义的集合,例如数组,字典和字符串,都对复制进行了优化以降低性能成本。新集合不会立即复制,而是跟原集合共享同一份内存,共享同样的元素。在集合的某个副本要被修改前,才会复制它的元素。而你在代码中看起来就像是立即发生了复制。 + +请看下面这个示例,其使用了上一个示例中的 `Resolution` 结构体: + +```swift +let hd = Resolution(width: 1920, height: 1080) +var cinema = hd +``` + +在以上示例中,声明了一个名为 `hd` 的常量,其值为一个初始化为全高清视频分辨率(`1920` 像素宽,`1080` 像素高)的 `Resolution` 实例。 + +然后示例中又声明了一个名为 `cinema` 的变量,并将 `hd` 赋值给它。因为 `Resolution` 是一个结构体,所以会先创建一个现有实例的副本,然后将副本赋值给 `cinema` 。尽管 `hd` 和 `cinema` 有着相同的宽(width)和高(height),但是在幕后它们是两个完全不同的实例。 + +下面,为了符合数码影院放映的需求(`2048` 像素宽,`1080` 像素高),`cinema` 的 `width` 属性被修改为稍微宽一点的 2K 标准: + +```swift +cinema.width = 2048 +``` + +查看 `cinema` 的 `width` 属性,它的值确实改为了 `2048`: + +```swift +print("cinema is now \(cinema.width) pixels wide") +// 打印 "cinema is now 2048 pixels wide" +``` + +然而,初始的 `hd` 实例中 `width` 属性还是 `1920`: + +```swift +print("hd is still \(hd.width) pixels wide") +// 打印 "hd is still 1920 pixels wide" +``` + +将 `hd` 赋值给 `cinema` 时,`hd` 中所存储的*值*会拷贝到新的 `cinema` 实例中。结果就是两个完全独立的实例包含了相同的数值。由于两者相互独立,因此将 `cinema` 的 `width` 修改为 `2048` 并不会影响 `hd` 中的 `width` 的值,如下图所示: + +![sharedStateStruct_2x](https://docs.swift.org/swift-book/_images/sharedStateStruct_2x.png) + +枚举也遵循相同的行为准则: + +```swift +enum CompassPoint { + case north, south, east, west + mutating func turnNorth() { + self = .north + } +} +var currentDirection = CompassPoint.west +let rememberedDirection = currentDirection +currentDirection.turnNorth() + +print("The current direction is \(currentDirection)") +print("The remembered direction is \(rememberedDirection)") +// 打印 "The current direction is north" +// 打印 "The remembered direction is west" +``` + +当 `rememberedDirection` 被赋予了 `currentDirection` 的值,实际上它被赋予的是值的一个拷贝。赋值过程结束后再修改 `currentDirection` 的值并不影响 `rememberedDirection` 所储存的原始值的拷贝。 + +## 类是引用类型 + +与值类型不同,*引用类型*在被赋予到一个变量、常量或者被传递到一个函数时,其值不会被拷贝。因此,使用的是已存在实例的引用,而不是其拷贝。 + +请看下面这个示例,其使用了之前定义的 `VideoMode` 类: + +```swift +let tenEighty = VideoMode() +tenEighty.resolution = hd +tenEighty.interlaced = true +tenEighty.name = "1080i" +tenEighty.frameRate = 25.0 +``` + +以上示例中,声明了一个名为 `tenEighty` 的常量,并让其引用一个 `VideoMode` 类的新实例。它的视频模式(video mode)被赋值为之前创建的 HD 分辨率(`1920`*`1080`)的一个拷贝。然后将它设置为隔行视频,名字设为 `“1080i”`,并将帧率设置为 `25.0` 帧每秒。 + +接下来,将 `tenEighty` 赋值给一个名为 `alsoTenEighty` 的新常量,并修改 `alsoTenEighty` 的帧率: + +```swift +let alsoTenEighty = tenEighty +alsoTenEighty.frameRate = 30.0 +``` + +因为类是引用类型,所以 `tenEight` 和 `alsoTenEight` 实际上引用的是同一个 `VideoMode` 实例。换句话说,它们是同一个实例的两种叫法,如下图所示: + +![sharedStateClass_2x](https://docs.swift.org/swift-book/_images/sharedStateClass_2x.png) + +通过查看 `tenEighty` 的 `frameRate` 属性,可以看到它正确地显示了底层的 `VideoMode` 实例的新帧率 `30.0`: + +```swift +print("The frameRate property of tenEighty is now \(tenEighty.frameRate)") +// 打印 "The frameRate property of theEighty is now 30.0" +``` + +这个例子也显示了为何引用类型更加难以理解。如果 `tenEighty` 和 `alsoTenEighty` 在你代码中的位置相距很远,那么就很难找到所有修改视频模式的地方。无论在哪使用 `tenEighty`,你都要考虑使用 `alsoTenEighty` 的代码,反之亦然。相反,值类型就更容易理解了,因为你的源码中与同一个值交互的代码都很近。 + +需要注意的是 `tenEighty` 和 `alsoTenEighty` 被声明为常量而不是变量。然而你依然可以改变 `tenEighty.frameRate` 和 `alsoTenEighty.frameRate`,这是因为 `tenEighty` 和 `alsoTenEighty`这两个常量的值并未改变。它们并不“存储”这个 `VideoMode` 实例,而仅仅是对 `VideoMode` 实例的引用。所以,改变的是底层 `VideoMode` 实例的 `frameRate` 属性,而不是指向 `VideoMode` 的常量引用的值。 + +### 恒等运算符 + +因为类是引用类型,所以多个常量和变量可能在幕后同时引用同一个类实例。(对于结构体和枚举来说,这并不成立。因为它们作为值类型,在被赋予到常量、变量或者传递到函数时,其值总是会被拷贝。) + +判定两个常量或者变量是否引用同一个类实例有时很有用。为了达到这个目的,Swift 提供了两个恒等运算符: + +- 相同(`===`) +- 不相同(`!==`) + +使用这两个运算符检测两个常量或者变量是否引用了同一个实例: + +```swift +if tenEighty === alsoTenEighty { + print("tenEighty and alsoTenEighty refer to the same VideoMode instance.") +} +// 打印 "tenEighty and alsoTenEighty refer to the same VideoMode instance." +``` + +请注意,“相同”(用三个等号表示,`===`)与“等于”(用两个等号表示,`==`)的不同。“相同”表示两个类类型(class type)的常量或者变量引用同一个类实例。“等于”表示两个实例的值“相等”或“等价”,判定时要遵照设计者定义的评判标准。 + +当在定义你的自定义结构体和类的时候,你有义务来决定判定两个实例“相等”的标准。在章节 [等价操作符](https://www.runoob.com/manual/gitbook/swift5/source/_book/chapter2/26_Advanced_Operators.html#equivalence_operators)中将会详细介绍实现自定义 == 和 !== 运算符的流程。 + +### 指针 + +如果你有 C,C++ 或者 Objective-C 语言的经验,那么你也许会知道这些语言使用*指针*来引用内存中的地址。Swift 中引用了某个引用类型实例的常量或变量,与 C 语言中的指针类似,不过它并不直接指向某个内存地址,也不要求你使用星号(`*`)来表明你在创建一个引用。相反,Swift 中引用的定义方式与其它的常量或变量的一样。如果需要直接与指针交互,你可以使用标准库提供的指针和缓冲区类型 —— 参见 [手动管理内存](https://developer.apple.com/documentation/swift/swift_standard_library/manual_memory_management)。 + +## iOS内存管理 diff --git a/IOS/Task00:Swift基础语法学习/figures/accept.png b/IOS/Task00:Swift基础语法学习/figures/accept.png new file mode 100644 index 0000000..a1c8e70 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/accept.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/chooseinstall.png b/IOS/Task00:Swift基础语法学习/figures/chooseinstall.png new file mode 100644 index 0000000..92d1026 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/chooseinstall.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/chooseiso.png b/IOS/Task00:Swift基础语法学习/figures/chooseiso.png new file mode 100644 index 0000000..31aee83 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/chooseiso.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/chooseiso2.png b/IOS/Task00:Swift基础语法学习/figures/chooseiso2.png new file mode 100644 index 0000000..b06d86c Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/chooseiso2.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/chooselang.png b/IOS/Task00:Swift基础语法学习/figures/chooselang.png new file mode 100644 index 0000000..ad12a8c Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/chooselang.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/choosemacos.png b/IOS/Task00:Swift基础语法学习/figures/choosemacos.png new file mode 100644 index 0000000..e120519 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/choosemacos.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/cleardisk.png b/IOS/Task00:Swift基础语法学习/figures/cleardisk.png new file mode 100644 index 0000000..0ca6405 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/cleardisk.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/darwin15.png b/IOS/Task00:Swift基础语法学习/figures/darwin15.png new file mode 100644 index 0000000..27bfcfb Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/darwin15.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/disktool.png b/IOS/Task00:Swift基础语法学习/figures/disktool.png new file mode 100644 index 0000000..c432019 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/disktool.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/exitiso.png b/IOS/Task00:Swift基础语法学习/figures/exitiso.png new file mode 100644 index 0000000..2142f4a Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/exitiso.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/installed.png b/IOS/Task00:Swift基础语法学习/figures/installed.png new file mode 100644 index 0000000..69f23ce Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/installed.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/installwmwaretools.png b/IOS/Task00:Swift基础语法学习/figures/installwmwaretools.png new file mode 100644 index 0000000..e61cd29 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/installwmwaretools.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/installxcode.png b/IOS/Task00:Swift基础语法学习/figures/installxcode.png new file mode 100644 index 0000000..1c8d9c1 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/installxcode.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/newvm.png b/IOS/Task00:Swift基础语法学习/figures/newvm.png new file mode 100644 index 0000000..e97dffa Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/newvm.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/server.png b/IOS/Task00:Swift基础语法学习/figures/server.png new file mode 100644 index 0000000..c113d05 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/server.png differ diff --git a/IOS/Task00:Swift基础语法学习/figures/unlocker.png b/IOS/Task00:Swift基础语法学习/figures/unlocker.png new file mode 100644 index 0000000..7ac44c4 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/figures/unlocker.png differ diff --git a/IOS/Task00:Swift基础语法学习/images/1.png b/IOS/Task00:Swift基础语法学习/images/1.png new file mode 100644 index 0000000..0cd209d Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/images/1.png differ diff --git a/IOS/Task00:Swift基础语法学习/images/1637228746939.png b/IOS/Task00:Swift基础语法学习/images/1637228746939.png new file mode 100644 index 0000000..d32b5ca Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/images/1637228746939.png differ diff --git a/IOS/Task00:Swift基础语法学习/images/1637228788344.png b/IOS/Task00:Swift基础语法学习/images/1637228788344.png new file mode 100644 index 0000000..f4e1300 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/images/1637228788344.png differ diff --git a/IOS/Task00:Swift基础语法学习/images/1637230229308.png b/IOS/Task00:Swift基础语法学习/images/1637230229308.png new file mode 100644 index 0000000..2957bb3 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/images/1637230229308.png differ diff --git a/IOS/Task00:Swift基础语法学习/images/2.png b/IOS/Task00:Swift基础语法学习/images/2.png new file mode 100644 index 0000000..b83d96d Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/images/2.png differ diff --git a/IOS/Task00:Swift基础语法学习/images/3.png b/IOS/Task00:Swift基础语法学习/images/3.png new file mode 100644 index 0000000..0ef7722 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/images/3.png differ diff --git a/IOS/Task00:Swift基础语法学习/images/4.png b/IOS/Task00:Swift基础语法学习/images/4.png new file mode 100644 index 0000000..86c33e1 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/images/4.png differ diff --git a/IOS/Task00:Swift基础语法学习/images/5.png b/IOS/Task00:Swift基础语法学习/images/5.png new file mode 100644 index 0000000..79f4742 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/images/5.png differ diff --git a/IOS/Task00:Swift基础语法学习/images/6.png b/IOS/Task00:Swift基础语法学习/images/6.png new file mode 100644 index 0000000..6605564 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/images/6.png differ diff --git a/IOS/Task00:Swift基础语法学习/images/7.png b/IOS/Task00:Swift基础语法学习/images/7.png new file mode 100644 index 0000000..5dcbf39 Binary files /dev/null and b/IOS/Task00:Swift基础语法学习/images/7.png differ diff --git a/IOS/Task00:Swift基础语法学习/安装macOS.md b/IOS/Task00:Swift基础语法学习/安装macOS.md new file mode 100644 index 0000000..12d2efb --- /dev/null +++ b/IOS/Task00:Swift基础语法学习/安装macOS.md @@ -0,0 +1,105 @@ +# Windows 系统下安装 macOS +[TOC] +由于 iOS 开发必须使用 XCode,而 XCode 又必须运行在 macOS 上,因此为了让使用 Windows 系统的朋友们也能进行 iOS 开发,我们将在 Windows 上通过虚拟机安装 macOS。 + +## 安装 VMware + +操作系统版本:Windows 11 家庭中文版 21H2 22000.194 + +VMware 版本:16.0.0 build-16894299 + +macOS 版本:MacOS Catalina-10.15.6 + + + +我们将选用 VMware 来安装 macOS,首先下载并安装 VMware Workstation,建议前往官网下载: + +https://www.vmware.com/cn/products/workstation-pro/workstation-pro-evaluation.html + +安装完毕后需要输入许可密钥,在此提供三个许可密钥: + +``` +ZF3R0-FHED2-M80TY-8QYGC-NPKYF +YF390-0HF8P-M81RQ-2DXQE-M2UT6 +ZF71R-DMX85-08DQY-8YMNC-PPHV8 +``` + +## 安装 VMware macOS 解锁补丁 + +由于在 Windows 和 Linux 上的 VMware 对安装 macOS 进行了屏蔽,因此需要通过对 VMware 进行“插桩”来解除屏蔽。首先下载解锁补丁: + +https://github.com/paolo-projects/unlocker/releases + +在运行补丁前需要关闭 VMware 相关的服务: + +![server](.\figures\server.png) + +将几个 VMware 开头的服务停止。 + +然后解压下载的 `unlocker.zip` ,以管理员身份运行 `win-install.cmd`,等待补丁自动下载安装完成: + +![unlocker](.\figures\unlocker.png) + +## 安装 macOS + +打开 VMware,新建虚拟机: + +![newvm](.\figures\newvm.png) + +选择稍后安装操作系统: + +![chooseiso](.\figures\chooseiso.png) + +如果上一步破解成功,这里会出现 macOS 的选项,选择 macOS 10.15: + +![choosemacos](.\figures\choosemacos.png) + +剩下的名称、安装位置和磁盘容量根据个人需求选择。 + +创建完毕后,右击创建的虚拟机 - 设置 - CD/DVD - 连接 - 使用ISO映像文件,选择下载的 cdr 文件: + +![chooseiso2](.\figures\chooseiso2.png) + +选择完毕后即可开启虚拟机启动安装程序: + +![chooselang](.\figures\chooselang.png) + +选择磁盘工具: + +![disktool](.\figures\disktool.png) + +选择并抹掉盘符: + +![cleardisk](.\figures\cleardisk.png) + +完成之后关闭,并选择安装 macOS: + +![chooseinstall](.\figures\chooseinstall.png) + +剩下的步骤根据提示完成即可: + +![image-20211008200903134](.\figures\installed.png) + + + +直接安装完毕后,会发现存在很多问题,如分辨率较低、鼠标移动缓慢等,此时需要安装 VMware Tool。 + +首先,将 Install macOS Catalina 推出: + +![exitiso](.\figures\exitiso.png) + +修改使用的映像文件,改成 VMware 安装路径下的 darwin15.iso: + +![darwin15](.\figures\darwin15.png) + +然后选择 VMware Tools: + +![installwmwaretools](.\figures\installwmwaretools.png) + +随后根据提示依次选择继续 -> 安装,并在安全性与隐私窗口选择允许: + +![accept](.\accept.png) + +随后等待安装完毕,重新启动即可正常使用。随后,在 App Store 中下载 Xcode 即可进行开发: + +![installxcode](.\figures\installxcode.png) diff --git a/IOS/Task01:基础插件与功能/1. Cocopods的安装与使用.md b/IOS/Task01:基础插件与功能/1. Cocopods的安装与使用.md new file mode 100644 index 0000000..ac3cd07 --- /dev/null +++ b/IOS/Task01:基础插件与功能/1. Cocopods的安装与使用.md @@ -0,0 +1,182 @@ +# Cocopods的安装与使用 + +## Cocopods介绍 + +CocoaPods是管理iOS项目所依赖的第三方开源库的工具,其项目源码在Github上管理。若项目开发不使用Cocopods,我们引入第三方开源库要做的步骤可能有: + +- 把开源库的源代码复制到项目中 +- 添加一些依赖框架和动态库(可能还需要引入其他的第三方开源库) +- 设置-ObjC,-fno,-objc,-arc 等参数 +- 管理他们的更新 + +CocoaPods 是管理第三方插件的合集,其出现帮助节省了配置和更新第三方开源库的时间。 CocoaPods 将所有依赖的库都放在一个名为 Pods 的项目下,然后让主项目依赖 Pods 项目。然后, 我们编码工作都从主项目转移到 Pods 项目。Pods 项目最终会编译为一个 libPod-项目名.a 静态库, 主项目依赖于这个静态库。对于资源文件,CocoaPods 提供了一个名为 Pods-resources.sh 的 bash 脚本,该脚本在每次项目编译的时候都会执行,将第三方库的各种资源文件复制到目标目录中。 + +CocoaPods 通过一个名为 Pods.xcconfig 的文件来在编译时设置所有的依赖和参数。 + +CocoaPods 是用 Ruby 写的,并由若干个 Ruby 包 (gems) 构成的。在解析整合过程中,最重 要的几个 gems 分别是:CocoaPods/CocoaPods, CocoaPods/Core, 和 CocoaPods/Xcodeproj。 + + + +## Cocopods安装 + +在使用 Cocopods 之前,我们需要在`Terminal`进行安装: + +1. 安装需要用到 Ruby,虽然 Mac 自带了 Ruby,但版本需要更新: + + ```bash + sudo gem update −−system + ``` + +2. 输入密码后安装 Cocopods: + + ```bash + sudo gem install cocoapods + ``` + +3. 如果安装过程过慢导致失败,可以更换国内下载源,安装完成后,在 `Terminal` 中: + + - cd 到目标工程文件路径下 + - pod init 会看到 Podfile 文件 + - vim Podfile 即可向其中插入想引用的第三方库 + + + +## 示例:安装第三方库 + +本部分我们将安装 + + + +新建Xcode项目myFirstApp后关闭Xcode(过程可参考Task00中的第一部分教程),打开终端(command+ "空格" ) + +1. cd到目标目录下: + + ```bash + cd Downloads/code/myFirstApp/ + ``` + +2. 进行Cocopods初始化 + + ```bash + pod init + ``` + +3. 打开Podfile文件 + + ```bash + open Podfile + ``` + + ![1](img/1.png) + +4. 插入如下命令后关闭Podfile + + ```bash + pod 'Charts' + pod 'TinyConstraints' + ``` + + ![2](img/2.png) + +5. 返回`终端`(Terminal),进行Pod更新,等待片刻即可 + + ```bash + pod update + ``` + +6. 在目录下双击运行 `myFirstApp.xcworkspace` + + ![25](img/25.png) + +7. 在`myFirstApp`-`myFirstApp`-`ViewController.swift`导入加载的两个第三方库 + + ```swift + import Charts + import TinyConstraints + ``` + + ![3](img/3.png) + +8. 即可完成加载,完整代码如下(暂时无需弄懂代码含义,复制即可): + +```swift +import UIKit +import Charts +import TinyConstraints + +class ViewController: UIViewController,ChartViewDelegate { + lazy var lineChartView:LineChartView={ + + let chartView = LineChartView() + chartView.backgroundColor = .systemGray6 + chartView.rightAxis.enabled = false + let yAxis = chartView.leftAxis + yAxis.labelFont = .boldSystemFont(ofSize: 12) + yAxis.setLabelCount(6, force: false) + yAxis.labelTextColor = .black + yAxis.labelPosition = .outsideChart + + chartView.xAxis.labelPosition = .bottom + chartView.xAxis.labelFont = .boldSystemFont(ofSize: 12) + chartView.xAxis.setLabelCount(6, force: false) + chartView.xAxis.labelTextColor = .black + chartView.xAxis.axisLineColor = .systemGray + chartView.animate(xAxisDuration: 4.5) + + return chartView + }() + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(lineChartView) + lineChartView.centerInSuperview() + lineChartView.width(to:view) + lineChartView.heightToWidth(of:view) + setData() + } + + func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) { + print(entry) + } + + func setData(){ + let set1 = LineChartDataSet(entries: yValues, label: "Subscribers") + let data = LineChartData(dataSet:set1) + lineChartView.data = data + + } + + let yValues:[ChartDataEntry] = [ + ChartDataEntry(x:0.0, y:10.0), + ChartDataEntry(x:1.0, y:9.0), + ChartDataEntry(x:2.0, y:8.0), + ChartDataEntry(x:3.0, y:7.0), + ChartDataEntry(x:4.0, y:6.0), + ChartDataEntry(x:5.0, y:5.0), + ChartDataEntry(x:6.0, y:4.0), + ChartDataEntry(x:7.0, y:3.0), + ChartDataEntry(x:8.0, y:2.0), + ChartDataEntry(x:9.0, y:1.0) + ] +} +``` + +9. 点击运行,预览状态如下: + + 4 + +## 如何寻找自己心仪的第三方库? + +如何找到`Charts`与`TinyConstraints`等优秀的第三方库?如何知晓不同的第三方库的安装命令?你可以访问Github的开源项目: + +[awesome-ios](https://github.com/vsouza/awesome-ios) + +这里面罗列了iOS开发流程中可能用到的第三方库,以`Charts`为例,我们点击相应label进行跳转 + +![5](img/5.png) + +就会得到安装该库的命令: + +![6](img/6.png) + +至此,我们已经学会了基本的第三方库引入,下面我们要学习基本控件的应用,以更好地运用到我们的app上 diff --git a/IOS/Task01:基础插件与功能/2. ScrollView的应用.md b/IOS/Task01:基础插件与功能/2. ScrollView的应用.md new file mode 100644 index 0000000..8f3c8ab --- /dev/null +++ b/IOS/Task01:基础插件与功能/2. ScrollView的应用.md @@ -0,0 +1,217 @@ +# ScrollView的应用 + +## 什么是ScrollView? + +`ScrollView`(即滚动视图)可被应用到多种场景,例如:纵向滚动视图可以用来上下滑动以查看超出屏幕尺寸的内容,横向滚动视图可以用来实现翻页、轮播图等功能。事实上, 对某个图片的缩放、移动等功能也是由ScrollView 来实现的。在本章中,你将学会如何开启计时器,如何纵向、横向滚动查看页面,如何滑动查看图片以及实现轮播图等。 + +## Storyboard中添加Scroll View + +在storyboard控件库中搜索`Scroll View`,并拖拽至面板中,放大到与屏幕同尺寸。 + +![scroll View](img/scroll View.png) + +右侧属性栏中Indicators部分罗列了两种滚动条:`纵向滚动`与`横向滚动`,如果我们不想在滑动过程中显示这两种滚动条,取消勾选即可。 + + + +## ScrollView 的代理及基本属性 + +在 `Main.storyboard` 中添加了 ScrollView 之后,我们需要手动在 `UIViewController` 中代理`UIScrollViewDelegate`,实现步骤如下: + +1. 打开`Main.Storyboard`,鼠标右键将`ScrollView`控件与`ViewController`面板相连,点击`delegate` + + ![sc1](img/sc1.png) + + sc delegate + +2. 打开 `ViewController.swift`,在 `class UIViewController` 后添加 `UISCrollViewDelegate` + +代理仅仅允许滚动视图可以进行滚动,但并没有明确何时才进行滚动。因此我们需要监听 ScrollView(捕捉用户动作),从而使当用户在上面做出滑动动作时,ScrollView 开始进行滚动。函数如下: + +```swift +func scrollViewDidScroll(_ scrollView: UIScrollView){} +``` + +上述内容完毕后,我们即可在 `viewDidLoad()` 中对 ScrollView 进行各项参数初始化设置: + +```swift +self.scrollView.contentSize = self.imageView.frame.size //(去掉自动布局) +//在图片内进行滚动,滚动范围为图片的尺寸 + +self.scrollView.contentSize = CGSize(width: lastImage.bounds.size.width, height:1000) +//在页面中滚动,滚动范围为代码所设置的页面尺寸 + +//在页面中滚动,手动设置移动坐标 +var point:CGPoint = scrollView.contentOffset +point.x += 150 +point.y += 150 +self.scrollView.contentOffset = point + +self.scrollView.setContentOffset(contentOffset:CGPoint, animated:Bool) +//加动画模式 + +self.scrollView.showsVerticalScrollIndicator = true self.scrollView.showsHorizontalScrollIndicator = false +//显示/隐藏水平、垂直滚动器 + +self.scrollView.contentInset = UIEdgeInsets(top:100, left: CGFloat,bottom: CGFloat, right:CGFloat) +//图片内边距 +``` + + + +## 坐标参数 + +通常,我们以屏幕左上角作为坐标系的原点,垂直向下为y轴正半轴,水平向右为x轴正半轴。坐标需要专用的类型来定义,即 `CGPoint`: + +```swift +CGPoint:坐标参数 +CGSize:设置大小,如 ScrollView,TableView 大小等 +CGFloat:CG 浮点数,用于计算坐标 +CGRect:设置尺寸,框架范围 +``` + + + +## 实现竖直、水平滚动 + +1. 竖直滚动:竖直滚动既可以通过 storyboard 实现,也可以通过代码实现,在 `viewDidLoad()` 中添加代码部分如下: + + ```swift + self.scrollView.contentSize = CGSize(width: lastImage.bounds.size.wid th, height:1000) + ``` + + 其中,`width`、`height` 可以通过手动计算得出,也可以通过图片宽和高的对应乘积让编译器自己设置数值。值得注意的是,此处仅接受 `CGFloat` 格式。 + +2. 水平滚动:在纵向滚动处,我们可以通过 storyboard 来设置 `Image` 或者 `Button` 尺寸,但水平滚动则不同,我们无法通过一块屏幕来设置超出屏幕尺寸的多个横向图,因此需 要手动设置代码。在 `viewDidLoad()` 中: + + ```swift + var imageView1 = UIImageView() + imageView1.frame = CGRect(x: 0, y: 0, width: 414, height: 180) + imageView1.image = UIImage(named:"one.jepg") + scrollView.addSubview(imageView1) + self.scrollView.contentSize = CGSize(width: 2050, height: 0) self.scrollView.showsHorizontalScrollIndicator = false滚动播放逻辑 + ``` + + + +## 滚动播放逻辑 + +我们经常会在一些 app 上看到置顶的一些循环播放的广告图,这些广告被称之为轮播图。事实上,轮播图的本质也是一种滚动视图,接下来的两个部分你将学着自己构建出一个轮播图。 + +想要实现图片的横向滚动播放,需要我们在预定区域内横向同时放置 5 张图片,通过 `ScrollView` 控件实现 5 张图片的滚动查看。本章中,我们已经学习了通过一个 `Button` 来实现图片滚动的方法,此部分我们将把图片滚动和计时器(NSTimer)绑定到一起从而达到自动播放的效果。由于计时器应在程序初始时加入到内存中,因此我们将代码写入 `viewDidLoad()` 内。初始化计时器代码如下: + +```swift +let timer:Timer? = Timer.scheduledTimer(timeInterval: 3.0, target: sel f, selector:#selector(myTimer), userInfo: nil, repeats: true) +timer?.fire() +``` + +请注意,在 Timer 函数中我们擅自使用了#selector(myTimer)方法,此类方法需要 + +Objective-C 类型函数才能执行。事实上,随着学习内容的深入你会发现:在 Swift 中不仅仅是计时器才会用到`#selector` 方法,其他情况下如音频播放等基础功能也同样需要自定义 `selector` 的方法。事实上,所有的监听事件都需要`@objc` 的方法来实现,不过随着版本的不断更新,Apple 逐渐在试着将这些 `objc` 的方法淡出 Swift 舞台,但这并不代表着你现在可以完全放弃使用 OC 的方法,本笔记中也会延续这一习惯。回到计时器问题上,此刻我们需要定义一个 OC 类型且并列于 `viewDidLoad()` 的函数: + +```swift +@objc func myTimer(){} +``` + +至此,初始化计时器已经完成。我们先不急着将坐标偏移量与计时器函数绑定到一起,先试着让 `ScrollView` 页面可实现手动在几个图片之间滚动,以及将 `pageControl` 的 `currentPage` 与当前 `ScrollView` 页面同步。 + +如何才能实现两者同步滚动?由于 `scrollView` 与 `pageControl` 是两个独立的控件,我们没办法将两者进行绑定,二者的同步似乎成了一个难题。然而,换个思路看这个问题, 从结果导向和用户体验角度来说,我们仅仅希望看到当 `scrollView` 滚动时,`pageControl` 也随之滚动,至于两者是否是真的同步我们并不关心,也不用去确认他们两个是否真的被 绑定到了一起,只要让它看起来是一体化就达到了我们的目的。对于这个问题,一种比较常用的解决思路是计算当前图片在 `scrollView` 中偏移的 x 值,并用 x 值和当前页面图片 (以左上角为原点,横向为 x 正半轴,纵向为 y 轴正半轴)初始的 x 坐标作商,结果取整来定位 `scrollView` 目前的页面。由于 `pageControl `是索引值,因此初始为 0,在实际操作中我们需要根据不同情况来进行作商取整后的一些列加减数值实现同步。 + +在其他默认参数通过 storyboard 中设置完成后,我们可以通过监听滚动视图的值实现 两者同步,在 `scrollViewDidScroll()` 函数: + +```swift +var page:Int +page = Int(self.scrollView.contentOffset.x / 414) +self.pageControl.currentPage = page +``` + +上述代码中,我们通过对滑动动作的监听,实现了当前页面与 pageControl 的同步 效果,接下来我们将这两者一并写入计时器方法中: + +```swift +@objc func myTimer(){ + if page==0{ + self.scrollView.contentOffset.x = 0 + page += 1 + self.scrollView.setContentOffset(CGPoint(x:self.scrollView.contentOffset.x, y:0), animated: true) } + elseif page<5 && page>0 { + self.scrollView.contentOffset.x += point + page += 1 + self.scrollView.setContentOffset(CGPoint(x:self.scrollView.contentOffset.x, y:0), animated: true) } + elseif page==5{ + page=0 + self.scrollView.setContentOffset(CGPoint(x: self.scrollView.contentOffset.x, y:0), animated: true) + } +} +``` + +至此,我们已经成功搭建了自己的第一个轮播图。事实上,计时器的使用对内存来说是一种负担,尤其是多个计时器同时启用时,它们会占据大量的内存空间,因此我们必须学会如何在程序结束的时候销毁计时器,后面我们会逐步了解。 + + + +## 轮播图参考代码 + +```swift +//定义部分 +var page:Int = 0 +let point:CGFloat = 414 + +//页面同步 +func scrollViewDidScroll(_ scrollView: UIScrollView) { + page = Int(self.scrollView.contentOffset.x / 414) + self.pageControl.currentPage = page + self.scrollView.setContentOffset(CGPoint(x:self.scrollView.contentOffset.x, y:0), animated: true) +} + +//手动添加五张图片 +var imageView1 = UIImageView() +var imageView2 = UIImageView() +var imageView3 = UIImageView() +var imageView4 = UIImageView() +var imageView5 = UIImageView() + +//在 viewDidLoad 里添加 image +imageView1.frame = CGRect(x: 20, y: 80, width: 374, height: 180) +imageView1.image = UIImage(named:"one.jepg") +scrollView.addSubview(imageView1) + +imageView2.frame = CGRect(x: 434, y: 80, width: 374, height: 180) +imageView2.image = UIImage(named:"two.jepg") +scrollView.addSubview(imageView2) + +imageView3.frame = CGRect(x: 848, y: 80, width: 374, height: 180) +imageView3.image = UIImage(named:"three.jepg") +scrollView.addSubview(imageView3) + +imageView4.frame = CGRect(x: 1262, y: 80, width: 374, height: 180) +imageView4.image = UIImage(named:"four.jepg") +scrollView.addSubview(imageView4) + +imageView5.frame = CGRect(x: 1676, y: 80, width: 374, height: 180) +imageView5.image = UIImage(named:"five.jepg") +scrollView.addSubview(imageView5) + +//scrollView 总尺寸 +self.scrollView.contentSize = CGSize(width: 2050, height: 0) self.scrollView.showsHorizontalScrollIndicator = false + +//定义、开始计时器,在 override 内 +let timer:Timer? = Timer.scheduledTimer(timeInterval: 3.0, target:self, selector: #selector(myTimer), userInfo: nil, repeats: true) +timer?.fire() + +//定时器启用方法函数 @objc func myTimer(){ +if page==0 { + self.scrollView.contentOffset.x = 0 + page += 1 + self.scrollView.setContentOffset(CGPoint(x:self.scrollView.contentOffset.x, y:0), animated: true) } +elseif page<5 && page>0 { + self.scrollView.contentOffset.x += point + page += 1 + self.scrollView.setContentOffset(CGPoint(x:self.scrollView.contentOffset.x, y:0), animated: true) } +elseif page==5 { + page=0 + self.scrollView.setContentOffset(CGPoint(x: self.scrollView.contentOffset.x, y:0), animated: true) +} +} +``` + + + diff --git a/IOS/Task01:基础插件与功能/3. TableView的应用.md b/IOS/Task01:基础插件与功能/3. TableView的应用.md new file mode 100644 index 0000000..f83d38d --- /dev/null +++ b/IOS/Task01:基础插件与功能/3. TableView的应用.md @@ -0,0 +1,198 @@ +# TableView的应用 + +## 什么是 TableView? + +`TableView` 直译为表格视图,如今,我们手机上的各种 app 都存在着表格视图:当我们打开知乎时,首先看到的就是一个 `TableView`,首页上的每一个回答都是 `TableView` 中的一个 `cell`,只不它是一种自定义的表格视图;当我们在淘宝搜索栏中搜索某一关键词后,屏幕上出现的也是`TableView`,每一个商品都是`TableView`上的一个`cell`;当我们打开微信想找某个对话框时,对话框也是 `TableView` 上的 `cell`。通过本章以及下一章的学习,你将学习如何做出联系人列表,以及如何构建基本的表格视图界面。当然,仅有`TableView` 是不够的,在下一章结束后,你将掌握更多的技能。 + +## Storyboard中添加Table View + +在storyboard控件库中搜索`Table View`,并拖拽至面板中,放大到与屏幕同尺寸。 + +![7](img/7.png) + +页面的右侧罗列了Table View的一些基本属性,第一个部分为单元格模式,分为两种:`Dynamic Prototypes`与`Static Cells` + +`动态单元格`的原型在我们日常应用中十分常见,例如:iPhone上的`信息`应用就是一种动态表格,其特性为支持动态添加和删除。 + +`静态单元格`的数据内容相对固定,很少会用到动态添加和删除,但支持对表格的分组,通常可以用作个人界面的信息显示。Storyboard中提供了静态单元格的简易搭建(提示:在实际的开发中很少这样使用)本处仅作为演示使用。(下图为静态单元格) + +8 + + + +## 数据源代理 + +`UITableView` 继承自 `UIScrollView`,在进行 `ViewController` 编辑时,有如下两种方法对其进行代理: + +- 第一种 + + 1. 鼠标`右键`点击 `Table View`面板的空白处,拖拽至 `View Controller` 处与之连接,点击`dataSource`与`delegate` + + ![delegate](img/delegate.png) + + datasource + + 2. 在`ViewController.swift`中添加如下代码: + + ```swift + import UIKit + class ViewController: UIViewController, UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + <#code#> + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + <#code#> + } + + override func viewDidLoad() { + super.viewDidLoad() + } + } + ``` + + 这就完成了TableView的初始化,接下来我们仅需要在函数中添加相应代码即可运行。 + +- 第二种 + + 1. 拖拽 `UITableView` 至代码中 + + 2. 在 `ViewController` 中添加如下代码(或利用智能补全): + + ```swift + import UIKit + class ViewController: UIViewController, UITableViewDataSource{ + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + <#code#> + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + <#code#> + } + + @IBOutlet weak var tableView: UITableView! + + override func viewDidLoad() { + super.viewDidLoad() + self.tableView.dataSource = (self as UITableViewDataSource) + } + } + ``` + +![10](img/10.png) + +11 + +## TableView 的基本属性 + +完成代理后,代码部分需要设置 `UITableView` 的各项参数,与 `ScrollView `不同的是,`TableView` 存在两个必须继承的方法: + +1. ```swift + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } + ``` + +2. ```swift + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + code + } + ``` + +上述内容中,前者为每一个 section 有多少个 rows,后者为对每个 rows 进行属性设置, + +下一章中我会对 rows 中的属性进行详细说明,本章仅探讨 `TableView` 的一些性质。除了这两个必须继承的方法外,`TableView` 还有一个十分重要的内容:设置 section 数量。若想显示不同分组,需要我们手动添加一个方法,否则只显示第一组内的单元格(注:分组显示需要在 storyboard 中设置Grouped): + +```swift +func numberOfSections(in tableView: UITableView) -> Int { + return 1 +} +``` + +若不设置此方法,return 默认值为 1,`TableView` 中除了上述的三个方法外,还有如下的几个属性: + +```swift +rowHeight//统一设置所有行高度 +separatorColor//分割线颜色 +separatorStyle//分割线样式 +tableHeaderView//可在分组单元格上方放置 +title tableFooterView//可以用来显示加载更多页面 +``` + + + +## 国家分组 demo + +接下来以一个国家分组 demo 练习表格视图的分组: + +```swift +func numberOfSections(in tableView: UITableView) -> Int { + //一共分几组,若没有此则默认为 1 section,不会显示出了第一个以外的 section + return 3 +} + +func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + //组标题,即页眉 + if section == 0{ + return "亚洲" + }else if section == 1{ + return "非洲" + }else { + return "欧洲" + } +} + +/* func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + //组注释,即页尾 + if section == 0{ + return "Asia" + }else if section == 1{ + return "Africa" + }else { + return "Euro" + } +} +*/ + +func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0{ + //亚洲 + return 3 + }else if section == 1{ + //非洲 + return 2 + }else { + //欧洲 + return 1 + } +} + +func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = UITableViewCell.init(style: UITableViewCell.CellStyle.default, reuseIdentifier: nil) + if indexPath.section == 0 { //亚洲 + if indexPath.row == 0 { + cell.textLabel?.text = "中国" + }else if indexPath.row == 1{ + cell.textLabel?.text = "日本" + }else if indexPath.row == 2{ + cell.textLabel?.text = "韩国" + } + + }else if indexPath.section == 1 { + //非洲 + if indexPath.row == 0{ + cell.textLabel?.text = "南非" + }else { + cell.textLabel?.text = "坦桑尼亚" + } + + }else { + cell.textLabel?.text = "英国" + } + return cell +} +``` + +值得注意的是,如果我们在工程文件中大量套用if else函数会导致性能大大下降,该部分仅为演示时使用,在下一章中我们会集中探讨如何通过数组来定义 `cell`。 + +12 diff --git a/IOS/Task01:基础插件与功能/4. Cell的应用.md b/IOS/Task01:基础插件与功能/4. Cell的应用.md new file mode 100644 index 0000000..477b288 --- /dev/null +++ b/IOS/Task01:基础插件与功能/4. Cell的应用.md @@ -0,0 +1,111 @@ +# Cell + +## Cell 的基本属性 + +在前一章的学习中我们初步学习了 `cell`,但事实上,在国家分组 demo 中我们仅仅是构建了一个动态单元格(Dynamic Prototypes),`TableView`中的`Cell`共有两种类型,分别是动态单元格(Dynamic Cell)和静态单元格(Static Cell),前者往往用于联系人列表、商品信息表、新闻软件的信息栏等图片和文字需要经常变化的地方,而后者往往被用于系统设置、个人页面的设置等不轻易改变单元格本身属性内容的位置。 + +通常来说,定义一个 `cell` 通常有以下两种方式: + +```swift +let cell = UITableViewCell.init(style: UITableViewCell.CellStyle.default, re useIdentifier: nil) + +let cell = tableView.dequeueReusableCell(withIdentifier: "RegistrationCell", +for: indexPath) +``` + +在简单的页面设置中,我们通常可以用第一类定义cell,值得注意的是,例子中给出的 `CelStyle.default`是`cell`的一种style,该格式不会显示除主标题以外的任何内容,对于本章的Emoji案例来说,这显然是不合适的,因此我们在具体设置时将其改为`CellStyle.subtitle`。以下罗列了一些cell的属性: + +```swift +imageView//主视图 +textlabel//大标题 +detailTextLabel//小标题 +accessoryType//更多信息 +accessoryView//更多信息 +backgroundColor//单元格背景颜色 +backgroundView//单元格背景图片,制定一个 UIImageView 就可以 +selectedBackgroundView//当某行被选中时候的背景 +``` + + + +## Emoji 的 TableView 显示 + +此前我们通过 if-else 函数定义了一些单元格。在实际工程文件中,我们往往需要通过数组来确定单元格数量,在后面的章节你会了解到,开发工程师们会在互联网中引用 JSON 文件来直接调用服务器中的数组,这就赋予了 app 生命力,让其不断更新数据。本部分我们通过自定义一个 Emoji 数组来模拟这一过程,需要注意的是,我们将在此使用**动态单元格**(Dynamic Prototypes)。 + +接下来我们将通过一个构建demo案例:Emoji的`tableView`显示来学习数组与cell定义之间的联系,在以后的学习过程中,我们通常要用到这种方法。 + +1. 并列于 `UIViewController` 定义一个类: + + ```swift + class Emoji { + var symbol: String + var name: String + var description: String + var usage: String + init(symbol: String, name: String, description: String, usage: String) { + self.symbol = symbol + self.name = name + self.description = description + self.usage = usage + } + } + ``` + +2. 代理、继承 `UITableViewDataSource`: + + ```swift + class ViewController: UIViewController,UITableViewDataSource {} + ``` + +3. 定义 `UITableViewCell` 属性: + + ```swift + func tableView(_ tableView: UITableView, numberOfRowsInSection section: + Int) -> Int { + if section == 0 { + return emojis.count + }else{ + return 0 + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let Emoji:String = "Emoji" + let cell:UITableViewCell = UITableViewCell.init(style: UITableViewCell.CellStyle.subtitle, reuseIdentifier: Emoji) + let emoji = emojis[indexPath.row] + cell.textLabel?.text = "\(emoji.symbol) - \(emoji.name)" + cell.detailTextLabel?.text = emoji.description + return cell + } + ``` + +4. 定义子类: + + ```swift + var emojis: [Emoji] = + [Emoji(symbol: "😀", name: "Grinning Face", description: "A typical smiley face.", usage: "happiness"), + + Emoji(symbol: "😕", name: "Confused Face",description: "A confused, puzzled face.", usage: "unsure what to think; displeasure"), + + Emoji(symbol: "😍", name: "Heart Eyes", description: "A smiley face with hearts for eyes.", usage: "love of something; attractive"), + + Emoji(symbol: "👮", name: "Police Officer", description: "A police officer wearing a blue cap with a gold badge.", usage: "person of authority"), + + Emoji(symbol: "🐢", name: "Turtle", description:"A cute turtle.", usage: "Something slow"), + + Emoji(symbol: "🐘", name: "Elephant", description: "A gray elephant.", usage: "good memory"), + + Emoji(symbol: "🍝", name: "Spaghetti",description: "A plate of spaghetti.", usage: "spaghetti"), + + Emoji(symbol: "🎲", name: "Die", description: "A single die.", usage:"taking a risk, chance; game"), + + Emoji(symbol:"⛺", name: "Tent", description: "A small tent.", usage: "camping"), + + Emoji(symbol: "📚", name: "Stack of Books", description: "Three colored books stacked on each other.", usage: "homework, studying")] + + ``` + + + + 至此,你已经认识了一些基本控件。在接下来的两章中,我将通过酒店管理系统与贪吃蛇来帮助你熟悉更多的基本控件并强化你的基本功,学习完毕后你将具备最基本的一些开发能力,并且能够搭建自己的一些简单 app,继续加油! + diff --git a/IOS/Task01:基础插件与功能/5. 常见的Controller概览.md b/IOS/Task01:基础插件与功能/5. 常见的Controller概览.md new file mode 100644 index 0000000..03f21fc --- /dev/null +++ b/IOS/Task01:基础插件与功能/5. 常见的Controller概览.md @@ -0,0 +1,62 @@ +# 常见的Controller概览 + + + +## 主视图控制器 View Controller + +在前面的任务中,我们打开Storyboard即可看到主视图控制器 View Controller,这是一种最基本、最常见的视图控制器。例如:当我们需要实现Table View的功能时,我们可以在控件库中添加 Table View插件于 View Controller中;也可以添加Scroll View于 View Controller中以实现页面滚动。View Controller更像是一张空白的A4纸,我们可以在上面书写任何想要的内容。 + +在本章的学习过程中,请确保您的视图控制器中的一个处于`Is Initial View Controller`状态,这样您的程序在运行时才不会崩溃。 + +ViewController + + + +## Table View Controller + +前文我们已经通过Table View控件实现了表格单元。事实上,控件库中提供了一种简易的实现方式:Table View Controller,直接添加相应控制器即可,该Controller与Table View完全一致,你可以直接在右侧信息栏中设置相关属性。 + +如果把View Controller比作空白的A4纸,那么在View Controller上添加Table View控件相当于在A4纸上画满一条一条的格线——这当然是一种办法,但我们常常还有另外的一种简易方法:直接使用带格线的笔记纸。Table View Controller正是这种带格线的笔记纸。 + +Table View Controller + + + +## Page View Controller + +某乎的首页界面就是一个Page View Controller,Page View并不是在底部的控制栏中进行切换,你可以看到,即使在相同的Tab Bar Item中,我们也可以对页面进行左右切换,这正是Page View Controller的用处。 + +page1 page2 + +在Xcode中,我们可以使用Page控件进行实现,当然也可以使用Page View Controller来“一劳永逸”地解决Page切换问题。 + +Page View Controller + +## Navigation Controller + +你可能并不清楚Navigation Controller究竟指的是什么,但是你一定用过且见过Navigation Controller。例如,在手机的设置中任何一个界面都是一个Navigation,它常常与带Cell的Table View一起出现 + +navigation bar + +上图中,`主屏幕`与`设置` 回退button都是Navigation Controller上的一部分。 + +![Navigation Controller](img/Navigation Controller.png) + +## Tab Bar Controller + +上面的Page View Controller提供了一种在页面内进行切换的方式。然而,以微信为例,如果我们要实现不同页面的切换,我们需要点击屏幕下端的item进行切换。Tab Bar Controller正是实现该功能的控制器。 + +tab bar item + +在Xcode中添加Tab Bar Controller,会直接出现三个视图控制器(当然我们可以手动添加更多),分别是Tab Bar Controller主控制器与两个Item实现我们可以在右侧的属性栏中手动选择每个Item对应的图标、文字,当然也可以设置更多的属性,你可以自行探索。 + +Tab Bar Controller + +## Collection Controller + +除了上述视图控制器外,还有一种常见的视图控制器:Collection Controller,这是一种展览、罗列内容相关的Controller。例如,一些电子杂志app会将近几期发行本以矩阵形式罗列;一些音乐app也会以这种方式罗列自己的唱片集合。iPhone中的`图书` 功能就是Collection Controller的一种 + +Collection Collection View Controller + +熟悉这些基本的视图控制器可以帮助我们在实际开发过程中设计出更符合逻辑的app,后面,我们会在这些控制器上进行操作,以实现各种不同的内容。 + diff --git a/IOS/Task01:基础插件与功能/6. 动画、音频与视频.md b/IOS/Task01:基础插件与功能/6. 动画、音频与视频.md new file mode 100644 index 0000000..d04b160 --- /dev/null +++ b/IOS/Task01:基础插件与功能/6. 动画、音频与视频.md @@ -0,0 +1,166 @@ +# 动画、音频与视频 + +## 动画 + +流畅的动画是 iOS 的灵魂所在。对于开发者而言,合理地使用动画,让用户体验得到提升是攻城狮们的必修课程。在前面的课程中我们 已经熟悉了对 Swift 的基础控件,接下来我们将把学习内容放到设计结构上。 + +对于动画的应用一般在 View 控件上,动画的种类大体上分为仿射变换、旋转变换以及平移三 种,对于动画过程中的速度时间我们都可以进行设置,这三种变换是所有动画的基础,例如:我们可以通过平移来实现 app 的加载和退场,可以使用仿射变换实现音乐播放时封面的动画,从而让用户有真实感。此外,我们还可以通过旋转来使整个程序增添一份趣味。下面的代码罗列了一些基本的动画使用方法,你需要熟练掌握并且试着学会如何应用。在 playgrounds 中: + +```swift +import UIKit +import PlaygroundSupport + +//初始化 View +let liveViewFrame = CGRect(x: 0, y: 0, width: 500, height: 500) let liveView = UIView(frame : liveViewFrame) liveView . +backgroundColor = . white + +PlaygroundPage . current . liveView = liveView +let smallFrame = CGRect(x: 0, y: 0, width: 100, height: 100) +let square = UIView(frame: smallFrame) +square.backgroundColor = .purple +liveView.addSubview(square) + +//添加动画 +UIView.animate(withDuration: 2.0) { + square . backgroundColor = . orange + let scaleTransform = CGAffineTransform(scaleX : 2.0 , y: 2.0) //添加仿射变换 + let rotateTransform = CGAffineTransform(rotationAngle: .pi)//添加旋转变换 + let translateTransform = CGAffineTransform(translationX:200, y: 200) //添加移动变换 + let comboTransform = scaleTransform.concatenating(rotateTransform ).concatenating (translateTransform ) square .transform = comboTransform //将三种变换合成 +} +``` + + + +## 短效果音播放 + +一个有趣的 app 离不开各种令人印象深刻的音效。在 Swift 中,我们通常使用 AudioToolbox 与 AVFoundation 来播放音频和视频。假设导入一个 30s 内的 wav 格式音频,并将其命名为 myVoice, 接下来,我们需要将该音频的所在路径设置为虚拟的 url 地址,并将音频播放的地址设置为该地址: + +```swift +import UIKit +import AudioToolbox + +class ViewController : UIViewController{ + override func viewDidLoad () { + super.viewDidLoad () + var _soundId : SystemSoundID = 0 + let path = Bundle.main.path(forResource: ”myVioce”,ofType: ”wav”) + let soundUrl = URL( fileURLWithPath : path ! ) + AudioServicesCreateSystemSoundID ( soundUrl as CFURL,&_soundId ) + AudioServicesAddSystemSoundCompletion(_soundId , nil , nil ,{(soundID , clientData ) −> Void in + print(”音频播放后重复之前的播放”) + AudioServicesPlaySystemSound ( soundID ) + }, nil) + AudioServicesPlaySystemSound ( _soundId ) + } +} +``` + + + +## 长音乐音乐播放 + +在一款 app 中,仅仅有 30s 的效果音可能还不够,有时候我们需要导入长音乐。Swift 中内置了一款音乐播放器,对于基础开发者来说足够应用。该播放器的设置过程和短效果音的思路相同,我们也需要设置 url 路径来导入我们的本地音乐。请注意,Swift 内置的音频播放器中并没有暂停和继续播放的功能,我们需要手动设置一个 Button 来实现该效果: + +```swift +import UIKit +import AVFoundation + +class ViewController: UIViewController,AVAudioPlayerDelegate { + var audioPlayer:AVAudioPlayer = AVAudioPlayer() + //初始化音乐播放器为 AVAudioPlayer 格式 + + override func viewDidLoad() { + super.viewDidLoad() + //令路径为项目文件下的音乐文件 + let path = Bundle.main.path(forResource: "兰亭序", ofType: "mp3") //将路径转化为 URL 路径 + let soundUrl = URL(fileURLWithPath: path!) + do{ + try audioPlayer = AVAudioPlayer(contentsOf: soundUrl) audioPlayer.volume = 1.0 + //音频音量为 1.0 + audioPlayer.numberOfLoops = -1 //-1 为无限循环播放 + audioPlayer.delegate = self audioPlayer.play() +} catch { + print(error) +} + //定义一个暂停/恢复播放按钮 + let stopMusic = UIButton(frame: CGRect(x: 20, y: 80, width: 280, height: 44)) + stopMusic.backgroundColor = UIColor.blue + stopMusic.setTitle("暂停/恢复音乐", for:UIControl.State.init(rawValue:0)) + + //为该按钮添加点击事件 + stopMusic.addTarget(self, action: #selector(ViewController.pauseOrResumeMusic), for: .touchUpInside) + self.view.addSubview(stopMusic) + } + + //填写 objc 方法 + @objc func pauseOrResumeMusic(){ + if self.audioPlayer.isPlaying{ + //isPlaying 判断音乐是否在播放,若播放则暂停,反之亦然 + self.audioPlayer.pause() + }else{ + self.audioPlayer.play() + } + } + + func audioPlayerBeginInterruption(_ player: AVAudioPlayer) { + print("被打断") + } + + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + print("播放完成") + } +} +``` + +附件中携带了一份我自己制作的兰亭序伴奏demo.mp3,你可以用这个音频文件进行功能实现尝试。 + + + +## 视频播放 + +实现了音频播放,该如何实现视频播放呢?Swift 中内置了视频播放的功能,总体上来说,本地视频播放的思路和音频一样,都需要我们设置视频的 url 地址,将本地视频导入其中。在此之前我们需要导入 AVFoundation 头文件,在 `viewDidLoad()`中: + +```swift +let moviePath = Bundle.main.path(forResource: "movie", ofType: "mp4") +let movieURL = URL(fileURLWithPath: moviePath!) + +let avPlayer = AVPlayer(url: movieURL as URL) +let avPlayerLayer = AVPlayerLayer(player:avPlayer) +avPlayerLayer.frame = self.view.bounds +avPlayerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill self.view.layer.addSublayer(avPlayerLayer) +avPlayer.play() +``` + + + +### 借助播放器 + +在上面的代码中,我们实现了视频的播放,不过你可能发现在该 项目中我们无法进行暂停与快进,更不用提画中画和声音等功能,要想实现这些内容,我们需要借助 Swift 内置的播放器: + +```swift +import UIKit +import AVFoundation +import AVKit + +class ViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + //设置视频路径,令 URL 为预设地址 + let moviePath = Bundle.main.path(forResource: "movie", ofType: "mp4") + let movieURL = URL(fileURLWithPath: moviePath!) + //设置播放器 + let avPlayer = AVPlayer(url:movieURL as URL) + let playerVC = AVPlayerViewController() + playerVC.player = avPlayer + playerVC.videoGravity = AVLayerVideoGravity.resizeAspect + /*.resizeAspectFill 等比缩小,填满整个范围 + .resizeAspect 等比播放,中间播放 */ + playerVC.allowsPictureInPicturePlayback = true + playerVC.showsPlaybackControls = true playerVC.view.frame = self.view.bounds + playerVC.player!.play() + self.view.addSubview(playerVC.view) + } +} +``` + diff --git a/IOS/Task01:基础插件与功能/7. 基本权限与url访问.md b/IOS/Task01:基础插件与功能/7. 基本权限与url访问.md new file mode 100644 index 0000000..518572d --- /dev/null +++ b/IOS/Task01:基础插件与功能/7. 基本权限与url访问.md @@ -0,0 +1,92 @@ +# 基本权限与url访问 + +## 分享图片 + +我们的 app 肯定会用到各种权限,权限的申请也是开发者需要学习的一门课程,本章中提供了一些基本的权限请求方式,在后面的基础功能部分你将学习更多的内容。 + +此部分提供了分享照片的基本权限,我们可以通过一个 Button 来分享照片: + +```swift +@IBOutlet var imageView: UIView! +@IBAction func shareButton(_ sender: Any) { + guard let image = imageView else { return } + let activityController = UIActivityViewController(activityItems:[image],applicationActivities: nil) + activityController.popoverPresentationController?.sourceView = sender as? UIView present(activityController, animated: true, completion: nil) +} +``` + + + +## 网页链接 + +如果想要 `Button` 连接到浏览器,则需要对网页做出请求,在此前需要导入 `Safari Services` 头文件: + +```swift +import SafariServices + +@IBAction func Safari(_ sender: Any) { + if let url = URL(string: "http://www.bilibili.com") { + let safariViewController = SFSafariViewController(url:url) + present(safariViewController, animated: true,completion: nil) + } +} +``` + + + +## 提示与弹窗 + +如果想要获得照片权限,你需要一个弹窗,不仅如此,在选择头像的功能中,你也需要弹窗提示:从照片中选择、拍摄照片和取消三个部分: + +```swift +let alertController = UIAlertController(title: "Choose Image Source", message: nil, preferredStyle: .actionSheet) + +let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) +let cameraAction = UIAlertAction(title: "Camera", style: .default, handler: {action in print("User selected Camera action") }) + +let photoLibraryAction = UIAlertAction(title: "Photo Library", style: .default, handler: { action in print("User selected Photo Library action") }) + +alertController.addAction(cancelAction) +alertController.addAction(cameraAction) alertController.addAction(photoLibraryAction) + +alertController.popoverPresentationController?.sourceView = sender as? UIView present(alertController, animated: true, completion: nil) + +let alertController = UIAlertController(title: "Choose Image Source", message: nil, preferredStyle: .actionSheet) + +let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) +let cameraAction = UIAlertAction(title: "Camera", style: .default, handler: { action in print("User selected Camera action") }) +let photoLibraryAction = UIAlertAction(title: "Photo Library", style: .default, handler: { action in print("User selected Photo Library action") }) + +alertController.addAction(cancelAction) +alertController.addAction(cameraAction) alertController.addAction(photoLibraryAction) alertController.popoverPresentationController?.sourceView = sender as? UIView present(alertController, animated: true, completion: nil) +``` + + + +## 请求相机权限 + +iOS对相机以及其他各种权限的设置较为复杂,目前通用有两种手段获取相机权限:第一种通过 `ImagePickerController` 来实现,智能识别部分用到的就是这个,但是设置起来十分繁琐。不过好在还有第二种,通过 `Info` 中设置相关参数,直接访问即可,在 `Info.plist` 文件中添加: + +```swift +Privacy - Photo Library Additions Usage Description +``` + +赋值 `value`: + +```swift +To share photos from camera or photo library +``` + +相机请求: + +```swift +@IBAction func photosButtonTapped(_ sender: UIButton) {...} + +func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { + if let selectedImage = info[UIImagePickerControllerOriginalImage] as? UIImage { + imageView.image = selectedImage + dismiss(animated: true, completion: nil) + } +} +``` + diff --git a/IOS/Task01:基础插件与功能/8. 代码启动与页面image.md b/IOS/Task01:基础插件与功能/8. 代码启动与页面image.md new file mode 100644 index 0000000..64105e5 --- /dev/null +++ b/IOS/Task01:基础插件与功能/8. 代码启动与页面image.md @@ -0,0 +1,61 @@ +# 代码启动与登录界面 + +## 代码启动 + +在大型工程文件中,我们常常要避免使用storyboard,因为大量的storyboard会造成卡顿,内存占用过多。下面我们将介绍如何使用代码启动我们的app。 + +**Step 1:打开工程文件,在左侧导航栏中删除storyboard** + +![21](img/21.png) + +**Step 2:**在`Info.plist`中删除`Main storyboard file base name`一行 + +![22](img/22.png) + +**Step 3:** 在左侧导航栏----`Info.plist`----`Application Scene Manifest`----`Scene Configuration`----`Application Session role`----`Item -(Default Configuration)`----`Storyboard Name`右侧Main中,删除该行 + +![23](img/23.png) + +**Step 4:在左侧SceneDelegate.swift中,添加如下代码:** + +```swift + guard let windowScene = (scene as? UIWindowScene) else { return } + window = UIWindow(frame: windowScene.coordinateSpace.bounds) + window?.windowScene = windowScene + window?.rootViewController = ViewController() + window?.makeKeyAndVisible() +``` + +![setup](img/setup.png) + +即可完成! + +## 设置app的Icon + +你可以将喜欢的图片用作自己app的Icon,这是一个在线免费转换尺寸的网址: + +https://icon.wuruihong.com + +![figure](img/figure.png) + +Xcode的Assets.xcassets中罗列了该应用可能与用到的所有尺寸的Icon,你可以将上述网站下载得到的文件夹拖拽至Appicon处,系统会自动进行填充。 + +## 设置启动页面 + +打开各种app后,映入眼帘的往往不是空白屏幕,而是伴有启动页面的等待加载部分,如何实现用自己的图片进行加载?下面我们就来一起设置! + +iOS 中设置启动页有两种方式 Launch Image 和 LaunchScreen,本部分我们以Launch Image为例 + +**Step 1:**在工程 targets--Build Settings 搜索 Asset Catalog Launch Image Set Name 然后设置创建的启动页名字LaunchImage。 + +![13](img/13.png) + +**Step 2:**再在Info.plist中删除 `Launch screen interface file base name`并添加 `Launch image`并设置 `LaunchImage` + +![14](img/14.png) + +**Step 3:**资源文件中添加LaunchImage放入不同尺寸的图片。 + +**Step 4:**点击运行 + +15 diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Contents.json b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000..025deea --- /dev/null +++ b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Contents.json @@ -0,0 +1,210 @@ +{ + "images" : [ + { + "extent" : "full-screen", + "filename" : "Default-2688h.png", + "idiom" : "iphone", + "minimum-system-version" : "12.0", + "orientation" : "portrait", + "scale" : "3x", + "subtype" : "2688h" + }, + { + "extent" : "full-screen", + "filename" : "Default-Landscape-2688h.png", + "idiom" : "iphone", + "minimum-system-version" : "12.0", + "orientation" : "landscape", + "scale" : "3x", + "subtype" : "2688h" + }, + { + "extent" : "full-screen", + "filename" : "Default-1792h.png", + "idiom" : "iphone", + "minimum-system-version" : "12.0", + "orientation" : "portrait", + "scale" : "2x", + "subtype" : "1792h" + }, + { + "extent" : "full-screen", + "filename" : "Default-Landscape-1792h.png", + "idiom" : "iphone", + "minimum-system-version" : "12.0", + "orientation" : "landscape", + "scale" : "2x", + "subtype" : "1792h" + }, + { + "extent" : "full-screen", + "filename" : "Default-2436h.png", + "idiom" : "iphone", + "minimum-system-version" : "11.0", + "orientation" : "portrait", + "scale" : "3x", + "subtype" : "2436h" + }, + { + "extent" : "full-screen", + "filename" : "Default-Landscape-2436h.png", + "idiom" : "iphone", + "minimum-system-version" : "11.0", + "orientation" : "landscape", + "scale" : "3x", + "subtype" : "2436h" + }, + { + "extent" : "full-screen", + "filename" : "Default-736h.png", + "idiom" : "iphone", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "3x", + "subtype" : "736h" + }, + { + "extent" : "full-screen", + "filename" : "Default-Landscape-736h.png", + "idiom" : "iphone", + "minimum-system-version" : "8.0", + "orientation" : "landscape", + "scale" : "3x", + "subtype" : "736h" + }, + { + "extent" : "full-screen", + "filename" : "Default-667h.png", + "idiom" : "iphone", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "2x", + "subtype" : "667h" + }, + { + "extent" : "full-screen", + "filename" : "Default@2x~iphone.png", + "idiom" : "iphone", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x", + "subtype" : "retina4" + }, + { + "extent" : "full-screen", + "filename" : "Default-Portrait~ipad.png", + "idiom" : "ipad", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "1x" + }, + { + "extent" : "full-screen", + "filename" : "Default-Landscape~ipad.png", + "idiom" : "ipad", + "minimum-system-version" : "7.0", + "orientation" : "landscape", + "scale" : "1x" + }, + { + "extent" : "full-screen", + "filename" : "Default-Portrait@2x~ipad.png", + "idiom" : "ipad", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "filename" : "Default-Landscape@2x~ipad.png", + "idiom" : "ipad", + "minimum-system-version" : "7.0", + "orientation" : "landscape", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "filename" : "Default~iphone.png", + "idiom" : "iphone", + "orientation" : "portrait", + "scale" : "1x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "filename" : "Default-568h@2x~iphone.png", + "idiom" : "iphone", + "orientation" : "portrait", + "scale" : "2x", + "subtype" : "retina4" + }, + { + "extent" : "to-status-bar", + "filename" : "Default-No-StatusBar~ipad.png", + "idiom" : "ipad", + "orientation" : "portrait", + "scale" : "1x" + }, + { + "extent" : "full-screen", + "idiom" : "ipad", + "orientation" : "portrait", + "scale" : "1x" + }, + { + "extent" : "to-status-bar", + "filename" : "Default-Landscape-No-StatusBar~ipad.png", + "idiom" : "ipad", + "orientation" : "landscape", + "scale" : "1x" + }, + { + "extent" : "full-screen", + "idiom" : "ipad", + "orientation" : "landscape", + "scale" : "1x" + }, + { + "extent" : "to-status-bar", + "filename" : "Default-No-StatusBar@2x~ipad.png", + "idiom" : "ipad", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "ipad", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "extent" : "to-status-bar", + "filename" : "Default-Landscape-No-StatusBar@2x~ipad.png", + "idiom" : "ipad", + "orientation" : "landscape", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "ipad", + "orientation" : "landscape", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-1792h.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-1792h.png new file mode 100644 index 0000000..86cc0f4 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-1792h.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-2436h.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-2436h.png new file mode 100644 index 0000000..3a2ba53 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-2436h.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-2688h.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-2688h.png new file mode 100644 index 0000000..fa0d927 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-2688h.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-568h@2x~iphone.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-568h@2x~iphone.png new file mode 100644 index 0000000..dcc3ab1 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-568h@2x~iphone.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-667h.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-667h.png new file mode 100644 index 0000000..e8c7ec9 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-667h.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-736h.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-736h.png new file mode 100644 index 0000000..8be40b4 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-736h.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-1792h.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-1792h.png new file mode 100644 index 0000000..6873ed5 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-1792h.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-2436h.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-2436h.png new file mode 100644 index 0000000..943c557 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-2436h.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-2688h.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-2688h.png new file mode 100644 index 0000000..7ab5bcb Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-2688h.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-736h.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-736h.png new file mode 100644 index 0000000..5a62c45 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-736h.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-No-StatusBar@2x~ipad.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-No-StatusBar@2x~ipad.png new file mode 100644 index 0000000..d59c390 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-No-StatusBar@2x~ipad.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-No-StatusBar~ipad.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-No-StatusBar~ipad.png new file mode 100644 index 0000000..233aa99 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape-No-StatusBar~ipad.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape@2x~ipad.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape@2x~ipad.png new file mode 100644 index 0000000..a13f885 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape@2x~ipad.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape~ipad.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape~ipad.png new file mode 100644 index 0000000..925a60a Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Landscape~ipad.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-No-StatusBar@2x~ipad.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-No-StatusBar@2x~ipad.png new file mode 100644 index 0000000..d2c5381 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-No-StatusBar@2x~ipad.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-No-StatusBar~ipad.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-No-StatusBar~ipad.png new file mode 100644 index 0000000..7b79503 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-No-StatusBar~ipad.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Portrait@2x~ipad.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Portrait@2x~ipad.png new file mode 100644 index 0000000..fe5b565 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Portrait@2x~ipad.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Portrait~ipad.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Portrait~ipad.png new file mode 100644 index 0000000..439a5a2 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default-Portrait~ipad.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default@2x~iphone.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default@2x~iphone.png new file mode 100644 index 0000000..25579f1 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default@2x~iphone.png differ diff --git a/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default~iphone.png b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default~iphone.png new file mode 100644 index 0000000..5272ba8 Binary files /dev/null and b/IOS/Task01:基础插件与功能/LaunchImage.launchimage/Default~iphone.png differ diff --git a/IOS/Task01:基础插件与功能/img/1.png b/IOS/Task01:基础插件与功能/img/1.png new file mode 100644 index 0000000..872b8f4 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/1.png differ diff --git a/IOS/Task01:基础插件与功能/img/10.png b/IOS/Task01:基础插件与功能/img/10.png new file mode 100644 index 0000000..a666a5e Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/10.png differ diff --git a/IOS/Task01:基础插件与功能/img/11.png b/IOS/Task01:基础插件与功能/img/11.png new file mode 100644 index 0000000..e0b38f7 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/11.png differ diff --git a/IOS/Task01:基础插件与功能/img/12.png b/IOS/Task01:基础插件与功能/img/12.png new file mode 100644 index 0000000..1d5ee7d Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/12.png differ diff --git a/IOS/Task01:基础插件与功能/img/13.png b/IOS/Task01:基础插件与功能/img/13.png new file mode 100644 index 0000000..835625f Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/13.png differ diff --git a/IOS/Task01:基础插件与功能/img/14.png b/IOS/Task01:基础插件与功能/img/14.png new file mode 100644 index 0000000..aae37f4 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/14.png differ diff --git a/IOS/Task01:基础插件与功能/img/15.png b/IOS/Task01:基础插件与功能/img/15.png new file mode 100644 index 0000000..fcc29f1 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/15.png differ diff --git a/IOS/Task01:基础插件与功能/img/1637248297486.png b/IOS/Task01:基础插件与功能/img/1637248297486.png new file mode 100644 index 0000000..a1e8b87 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/1637248297486.png differ diff --git a/IOS/Task01:基础插件与功能/img/1637257405373.png b/IOS/Task01:基础插件与功能/img/1637257405373.png new file mode 100644 index 0000000..e209bc5 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/1637257405373.png differ diff --git a/IOS/Task01:基础插件与功能/img/1637394497232.png b/IOS/Task01:基础插件与功能/img/1637394497232.png new file mode 100644 index 0000000..6fc0306 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/1637394497232.png differ diff --git a/IOS/Task01:基础插件与功能/img/1637394775302.png b/IOS/Task01:基础插件与功能/img/1637394775302.png new file mode 100644 index 0000000..93fb9cf Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/1637394775302.png differ diff --git a/IOS/Task01:基础插件与功能/img/1637399754057.png b/IOS/Task01:基础插件与功能/img/1637399754057.png new file mode 100644 index 0000000..675d4dd Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/1637399754057.png differ diff --git a/IOS/Task01:基础插件与功能/img/1637400601299.png b/IOS/Task01:基础插件与功能/img/1637400601299.png new file mode 100644 index 0000000..304ead3 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/1637400601299.png differ diff --git a/IOS/Task01:基础插件与功能/img/1637400988232.png b/IOS/Task01:基础插件与功能/img/1637400988232.png new file mode 100644 index 0000000..2639626 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/1637400988232.png differ diff --git a/IOS/Task01:基础插件与功能/img/1637401210478.png b/IOS/Task01:基础插件与功能/img/1637401210478.png new file mode 100644 index 0000000..960f40c Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/1637401210478.png differ diff --git a/IOS/Task01:基础插件与功能/img/2.png b/IOS/Task01:基础插件与功能/img/2.png new file mode 100644 index 0000000..8ea0d7c Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/2.png differ diff --git a/IOS/Task01:基础插件与功能/img/21.png b/IOS/Task01:基础插件与功能/img/21.png new file mode 100644 index 0000000..0c78612 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/21.png differ diff --git a/IOS/Task01:基础插件与功能/img/22.png b/IOS/Task01:基础插件与功能/img/22.png new file mode 100644 index 0000000..54c3535 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/22.png differ diff --git a/IOS/Task01:基础插件与功能/img/23.png b/IOS/Task01:基础插件与功能/img/23.png new file mode 100644 index 0000000..e91075d Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/23.png differ diff --git a/IOS/Task01:基础插件与功能/img/24.png b/IOS/Task01:基础插件与功能/img/24.png new file mode 100644 index 0000000..6c825d3 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/24.png differ diff --git a/IOS/Task01:基础插件与功能/img/25.png b/IOS/Task01:基础插件与功能/img/25.png new file mode 100644 index 0000000..99e9215 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/25.png differ diff --git a/IOS/Task01:基础插件与功能/img/3.png b/IOS/Task01:基础插件与功能/img/3.png new file mode 100644 index 0000000..0d74c79 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/3.png differ diff --git a/IOS/Task01:基础插件与功能/img/4.png b/IOS/Task01:基础插件与功能/img/4.png new file mode 100644 index 0000000..429f295 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/4.png differ diff --git a/IOS/Task01:基础插件与功能/img/5.png b/IOS/Task01:基础插件与功能/img/5.png new file mode 100644 index 0000000..49c013c Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/5.png differ diff --git a/IOS/Task01:基础插件与功能/img/6.png b/IOS/Task01:基础插件与功能/img/6.png new file mode 100644 index 0000000..39b5b12 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/6.png differ diff --git a/IOS/Task01:基础插件与功能/img/7.png b/IOS/Task01:基础插件与功能/img/7.png new file mode 100644 index 0000000..860b9f5 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/7.png differ diff --git a/IOS/Task01:基础插件与功能/img/8.png b/IOS/Task01:基础插件与功能/img/8.png new file mode 100644 index 0000000..331937a Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/8.png differ diff --git a/IOS/Task01:基础插件与功能/img/9.png b/IOS/Task01:基础插件与功能/img/9.png new file mode 100644 index 0000000..03084e1 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/9.png differ diff --git a/IOS/Task01:基础插件与功能/img/Collection View Controller.png b/IOS/Task01:基础插件与功能/img/Collection View Controller.png new file mode 100644 index 0000000..f946b42 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/Collection View Controller.png differ diff --git a/IOS/Task01:基础插件与功能/img/Collection.PNG b/IOS/Task01:基础插件与功能/img/Collection.PNG new file mode 100644 index 0000000..7831ff6 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/Collection.PNG differ diff --git a/IOS/Task01:基础插件与功能/img/IMG_1324.PNG b/IOS/Task01:基础插件与功能/img/IMG_1324.PNG new file mode 100644 index 0000000..6ac0409 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/IMG_1324.PNG differ diff --git a/IOS/Task01:基础插件与功能/img/IMG_1326.PNG b/IOS/Task01:基础插件与功能/img/IMG_1326.PNG new file mode 100644 index 0000000..1a743ff Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/IMG_1326.PNG differ diff --git a/IOS/Task01:基础插件与功能/img/IMG_1331.PNG b/IOS/Task01:基础插件与功能/img/IMG_1331.PNG new file mode 100644 index 0000000..4178605 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/IMG_1331.PNG differ diff --git a/IOS/Task01:基础插件与功能/img/Launch 3.png b/IOS/Task01:基础插件与功能/img/Launch 3.png new file mode 100644 index 0000000..834b092 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/Launch 3.png differ diff --git a/IOS/Task01:基础插件与功能/img/Navigation Controller.png b/IOS/Task01:基础插件与功能/img/Navigation Controller.png new file mode 100644 index 0000000..ade545d Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/Navigation Controller.png differ diff --git a/IOS/Task01:基础插件与功能/img/Page View Controller.png b/IOS/Task01:基础插件与功能/img/Page View Controller.png new file mode 100644 index 0000000..dfb1bcf Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/Page View Controller.png differ diff --git a/IOS/Task01:基础插件与功能/img/Tab Bar Controller.png b/IOS/Task01:基础插件与功能/img/Tab Bar Controller.png new file mode 100644 index 0000000..1bfae62 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/Tab Bar Controller.png differ diff --git a/IOS/Task01:基础插件与功能/img/Table View Controller.png b/IOS/Task01:基础插件与功能/img/Table View Controller.png new file mode 100644 index 0000000..57ec2ee Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/Table View Controller.png differ diff --git a/IOS/Task01:基础插件与功能/img/ViewController.png b/IOS/Task01:基础插件与功能/img/ViewController.png new file mode 100644 index 0000000..f5b020c Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/ViewController.png differ diff --git a/IOS/Task01:基础插件与功能/img/add Navigation Controller.png b/IOS/Task01:基础插件与功能/img/add Navigation Controller.png new file mode 100644 index 0000000..8e82fa9 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/add Navigation Controller.png differ diff --git a/IOS/Task01:基础插件与功能/img/back_lunach.png b/IOS/Task01:基础插件与功能/img/back_lunach.png new file mode 100644 index 0000000..e372578 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/back_lunach.png differ diff --git a/IOS/Task01:基础插件与功能/img/coding Setup3.png b/IOS/Task01:基础插件与功能/img/coding Setup3.png new file mode 100644 index 0000000..c40b756 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/coding Setup3.png differ diff --git a/IOS/Task01:基础插件与功能/img/coding setup.png b/IOS/Task01:基础插件与功能/img/coding setup.png new file mode 100644 index 0000000..8afb13e Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/coding setup.png differ diff --git a/IOS/Task01:基础插件与功能/img/coding setup2.png b/IOS/Task01:基础插件与功能/img/coding setup2.png new file mode 100644 index 0000000..9cb9d00 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/coding setup2.png differ diff --git a/IOS/Task01:基础插件与功能/img/datasource.png b/IOS/Task01:基础插件与功能/img/datasource.png new file mode 100644 index 0000000..3d4bce6 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/datasource.png differ diff --git a/IOS/Task01:基础插件与功能/img/delegate ViewController.png b/IOS/Task01:基础插件与功能/img/delegate ViewController.png new file mode 100644 index 0000000..d46874d Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/delegate ViewController.png differ diff --git a/IOS/Task01:基础插件与功能/img/delegate.png b/IOS/Task01:基础插件与功能/img/delegate.png new file mode 100644 index 0000000..e7a7297 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/delegate.png differ diff --git a/IOS/Task01:基础插件与功能/img/figure.png b/IOS/Task01:基础插件与功能/img/figure.png new file mode 100644 index 0000000..4f1b8c8 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/figure.png differ diff --git a/IOS/Task01:基础插件与功能/img/launch 2.png b/IOS/Task01:基础插件与功能/img/launch 2.png new file mode 100644 index 0000000..0f152f0 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/launch 2.png differ diff --git a/IOS/Task01:基础插件与功能/img/launch screen.png b/IOS/Task01:基础插件与功能/img/launch screen.png new file mode 100644 index 0000000..14dd673 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/launch screen.png differ diff --git a/IOS/Task01:基础插件与功能/img/navigation bar.PNG b/IOS/Task01:基础插件与功能/img/navigation bar.PNG new file mode 100644 index 0000000..c32a26a Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/navigation bar.PNG differ diff --git a/IOS/Task01:基础插件与功能/img/page1.PNG b/IOS/Task01:基础插件与功能/img/page1.PNG new file mode 100644 index 0000000..ed799f7 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/page1.PNG differ diff --git a/IOS/Task01:基础插件与功能/img/page2.PNG b/IOS/Task01:基础插件与功能/img/page2.PNG new file mode 100644 index 0000000..918375f Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/page2.PNG differ diff --git a/IOS/Task01:基础插件与功能/img/sc delegate.png b/IOS/Task01:基础插件与功能/img/sc delegate.png new file mode 100644 index 0000000..6e55ade Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/sc delegate.png differ diff --git a/IOS/Task01:基础插件与功能/img/sc1.png b/IOS/Task01:基础插件与功能/img/sc1.png new file mode 100644 index 0000000..31f8c42 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/sc1.png differ diff --git a/IOS/Task01:基础插件与功能/img/scroll View.png b/IOS/Task01:基础插件与功能/img/scroll View.png new file mode 100644 index 0000000..ad643c5 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/scroll View.png differ diff --git a/IOS/Task01:基础插件与功能/img/setup.png b/IOS/Task01:基础插件与功能/img/setup.png new file mode 100644 index 0000000..9269760 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/setup.png differ diff --git a/IOS/Task01:基础插件与功能/img/tab bar item.jpg b/IOS/Task01:基础插件与功能/img/tab bar item.jpg new file mode 100644 index 0000000..9a86580 Binary files /dev/null and b/IOS/Task01:基础插件与功能/img/tab bar item.jpg differ diff --git a/IOS/Task01:基础插件与功能/兰亭序.mp3 b/IOS/Task01:基础插件与功能/兰亭序.mp3 new file mode 100644 index 0000000..40d5c1b Binary files /dev/null and b/IOS/Task01:基础插件与功能/兰亭序.mp3 differ diff --git a/IOS/Task01:基础插件与功能/登录功能实战篇(上).md b/IOS/Task01:基础插件与功能/登录功能实战篇(上).md new file mode 100644 index 0000000..12201e3 --- /dev/null +++ b/IOS/Task01:基础插件与功能/登录功能实战篇(上).md @@ -0,0 +1,363 @@ +# UIKit框架 +> iOS以UIKit框架的形式提供了很多控件用来构建视图展示,这些控件组合成了用户界面工具包也就是UIKit框架,此外UIKit当中还有视图控制器也就是ViewController。在控件当中他们都是以UI开头的一些对象如UIButton、UIView、UILabel、UIImageView、UIProgressView、UISlider、UIImage、UITextFiled、UITextView等等,下面我们简单介绍一下日常使用的基础控件。 + +## UIView +UIView是一个视图容器,也可以看作一个空白视图 +``` swift + private lazy var headView : UIView = { + var headView = UIView.init(frame: CGRect.init(x: ()UIScreen.main.bounds.size.width - 200) / 2, y: ()UIScreen.main.bounds.size.height - 200) / 2, width: 200, height: 200)) // 1 + headView.backgroundColor = .red // 2 + return headView // 3 + } () +``` +> **解释一下上面代码关键字的含义**: +> **private:**当前对象的访问权限,Swift当中权限访问总共有四种,安全性从高到低分别是的是private>fileprivate>Interal>public>open +> **lazy**lazy表示当前对象懒加载的含义,只有当对象真正被调用时实例才会被加入内存当中 +> **UIView**表示创建一个视图 +> **UIScreen**屏幕对象,UIScreen.main.bounds代表获取主屏幕对象的bounds,bounds与frame类似,区别在于frame是x、y是针对父视图的x、y相对距离,bounds的x、y是针对视图本身的x、y距离,两者的height、width含义相同都是代表当前视图的宽高。由此UIScreen.main.bounds.size.width代表当前屏幕的宽,UIScreen.main.bounds.size.height代表当前屏幕的高。 +> **backgroundColor:**代表背景颜色,上面表示headView这个视图的背景颜色为红色 +> **return headView**当前是声明一个闭包,1.我们在闭包里面创建一个UIView,x,y值表示屏幕中间,展示一个宽200,高200的视图,2.视图背景为红色,3.返回值是headView + +接下来我们把声明的视图展示到屏幕上去 +编写如下代码到viewDidLoad当中去 +``` swift + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(headView) + } +``` +- 可以看到当前红色的UIView展示到了屏幕中间4 + +- ## UIKit坐标系 +在UIKit坐标系当中原点在(0,0)左上角,x值向右正向延伸,y值向下正向延伸 +4 +### 如何修改控件的状态 +> 每一个UI控件都是一个对象 +修改UI控件的状态,其实就是修改控件对象的属性 +比如修改UILabel显示的文字,就修改UILabel对象的**text**属性即可 +比如修改UIImageView显示的图片,就修改UIImageView对象的**image**属性即可 +不难想到,每一个UI控件肯定都有很多属性,比如: +UIProgressView进度条控件有**progress**属性(进度值) +UILabel和UITextField都有**text**属性(显示文字) +…… +虽然,每一个UI控件都有自己的独特属性,但是有些属性是每个UI控件都具备的,比如每一个UI控件都有自己的位置和尺寸、都有自己的父控件、子控件。于是,所有的UI控件最终都继承自UIView,UI控件的公共属性都定义在UIView中,比如: +frame :位置和尺寸 +center :中心点位置 +### UIView常见属性 + +``` + open var superview: UIView? { get } \\ 获得自己的父控件对象,只读属性 + + open var subviews: [UIView] { get } \\ 获取自己所有的子类对象,get 代表读 set代表写,如果只有get那么代表当前对象时只读属性 + open var isUserInteractionEnabled: Bool \\ isUserInteractionEnabled代表当前视图是否接受点击事件 + + open var tag: Int \\控件的ID(标识),父控件可以通过tag来找到对应的子控件 + + open var layer: CALayer { get } \\ 当前视图的Layer层(我们后续会详细介绍Layer与View的关系),目前同学们只需要了解Layer是视图的管理者,如控制View的宽高、x,y值等等 + open var frame: CGRect + + open var bounds: CGRect \\控件所在矩形框的位置和尺寸(以自己左上角为坐标原点,所以bounds的x、y一般为0) +\\ 注意:如果没有设置位置的情况下,把自己的左上角放在控制器的中心点 +\\ 可以定义控件的大小W H(size) + + open var center: CGPoint \\控件中点的位置(以父控件的左上角为坐标原点),可以定义控件的位置X Y(center) + + open var transform: CGAffineTransform \\控件的形变属性(可以设置旋转角度、比例缩放、平移等属性) + + @available(iOS 13.0, *) + open var transform3D: CATransform3D \\控件3D仿射变换的控制属性 +``` +### UIView常用方法 +``` +view.addSubview(headView) //给View添加一个子视图headView +view.removeFromSuperview() // 将View从他的父视图上删除 +``` +### UILabel +> 1. text 设置label的文字 +> 2. textcolor 设置label的文字颜色 +> 3. textAlignment 对齐方式 +> 4. numberOfLines 换行个数 0 "自动换行" +> 5. linesBreakMode 缩进方式 ...abc abc... a..bc +> 6. font 字体大小和样式 (系统默认样式, 斜体样式,粗体样式) +> 我们经常使用的字如下: +![Alt text](./img/1637394775302.png) +## 登录界面实战 +> 下面我带大家画一个登录界面,在画的过程当中和大家讲解一下使用到的基础控件 +## 重点:AutoLayout自动布局 +> **适配**:在移动端开发当中,我们最需要注意的几个问题1.用户体验 2.手机性能消耗 3.包体积。适配是用户体验当中最基础也是最关键的一个问题点,移动端工程师需要针对不同的机型保证UI的还原度,防止不同机型的用户体验出现差异。我们都知道iPhone有很多种不同大小的屏幕,屏幕有不同大小的尺寸,不同的尺寸对应着不同的像素点,不同的像素点代表不同机型的手机宽高不同,有最小3.5寸最大手机目前有6.5寸,如iphone6 屏幕宽高就是375X667,iphone11promax屏宽高就是414 x 896。如果我们使用固定的宽高来进行UI视图布局会有一个问题,固定一个位置如frame = CGRect.init(x : 0 ,y:200,width : 300,height: 400);在小屏手机这个UI控件展示在中间,而大屏手机展示在左边。所以AutoLayout布局技术可以解决这种问题. +> ![Alt text](./img/1637248297486.png) + +Autolayout是一种“自动布局”技术,专门用来布局UI界面的,苹果官方也推荐开发者尽量使用Autolayout来布局UI界面。我们这里直接最流行的AutoLayout三方框架SnapKit来解决布局问题。 +> 详细使用方法会专门出一节课介绍,这里简单说说如何使用 +``` swift +top \\ 当前视图的顶部 +bottom \\ 当前视图的底部 +right \\ 当前视图右边 +left \\ 当前视图左边 +size \\ 当前视图的大小 +edges \\ 当前视图的边缘 +centerX \\ 当前视图X轴中心 +centerY \\ 当前视图Y轴中心 +equalTo \\ 等于 +equalToSuperview \\ 等于父视图 +offset \\ 偏移量 +snp \\ 关键字表示当前视图使用snp框架布局 +``` +### 如何使用 +``` + view.addSubview(headView) // 将顶部视图添加到View当中 + // headView.snp.makeConstrains是使用SnapKit布局的基本格式 + //在闭包内{ + //$0.top.right.left.equalToSuperview().offset(0) + //} + // 代表当前headView的顶部、左边、右边距离与父视图对齐,偏移量为0 + // $0.height.equalTo(324)代表当前视图的高度为324 + // 这样我们就可以确定一个视图在父视图的位置信息, + // 具体代码如下 + headView.snp.makeConstraints{ + $0.top.right.left.equalToSuperview().offset(0) + $0.height.equalTo(324) + } +``` +### 效果 + 4 + +- 用户点击登录按钮时didTapLogin会监听到点击事件,并运行里面的方法运行方法如下,里面的方法会跳转到登录界面去。 +``` +@objc func didTapLogin() { + let loginViewController = DWLoginViewController.init() + /// self.navigationController当前视图的导航栏推入一个登录界面的视图控制器,animated=true表示开启动画 + self.navigationController?.pushViewController(loginViewController, animated: true) + } +``` + +我们在DWMeViewController当中写一个按钮,这个按钮的点击事件是跳转到DWLoginViewController,具体代码如下 +``` swift +// +// DWMeViewController.swift +// DatawhaleApp +// +// Created by 杨皓博 on 2021/11/14. +// +import UIKit +class DWMeViewController: DWBaseViewController { + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(headView) + view.addSubview(loginButton) + loginButton.snp.makeConstraints{ + $0.top.equalTo(headView.snp.bottom).offset(64) + $0.left.right.equalTo(headView) + $0.height.equalTo(44) + } + } + + // MARK: - + @objc func didTapLogin() { + let loginViewController = DWLoginViewController.init() + /// self.navigationController当前视图的导航栏推入一个登录界面的视图控制器,animated=true表示开启动画 + self.navigationController?.pushViewController(loginViewController, animated: true) + } + + private lazy var headView : UIView = { + var headView = UIView.init(frame: CGRect.init(x: (UIScreen.main.bounds.size.width - 200) / 2, y: (UIScreen.main.bounds.size.height - 200) / 2, width: 200, height: 200)) + headView.backgroundColor = .red + return headView + } () + + private let loginButton : UIButton = { + let button = UIButton() + button.setTitle("进入登录界面", for: .normal) + button.backgroundColor = UIColor.brandBlueColor() + button.setTitleColor(.white, for: .normal) + button.layer.cornerRadius = 22 + button.layer.masksToBounds = true + button.titleLabel?.font = UIFont.meduimFont(16) + button.addTarget(self, action: #selector(didTapLogin), for: .touchUpInside) + return button + } () +} +``` +> 这是我们接下来要做的界面 +![Alt text](./img/back_lunach.png) +> 我们首先来分析一下当前界面 +![Alt text](./img/1637400988232.png) +> 从上面可以看出我们需要用到如下几个控件UIView、UIImageView、UILabel、UIButton, +## UIImageView +> 图片展示控件 +首先在工程的Assets当中,我们创建一个文件夹用来存储我们需要使用到的图片,我们把对应的图片文件拖入创建的Image_set当中,之后将Image_set改名为login_bg_image +![Alt text](./img/1637400601299.png) +在登录界面中我们先添加背景图片 +``` + // 背景图片 + private lazy var bgImageView: UIImageView = { + var imageView = UIImageView.init(image: UIImage.init(named: "login_bg_image")) + imageView.contentMode = .scaleAspectFill //表示当前视图的模式是等比例拉伸至填充 + imageView.clipsToBounds = true + return imageView + }() + 之后把背景图片加入到视图控制器的view上去 +``` +![Alt text](./img/1637257405373.png) +``` +// 将背景视图加入到视图控制器的view +view.addSubview(bgImageView) +``` +> 可以看看效果 +![Alt text](./img/IMG_1326.PNG) +> 以此类推我们把视图上面的控件都加到对应的视图控制器上面去 +``` +// +// DWLoginViewController.swift +// DatawhaleApp +// +// Created by 杨皓博 on 2021/11/18. +// + +import UIKit + +class DWLoginViewController: DWBaseViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // 将背景视图加入到视图控制器的view + view.addSubview(bgImageView) + // 将背景蒙层加入到View + view.addSubview(bgView) + // 将logo加入到当前视图控制器当中 + view.addSubview(logoImage) + // 将当前文本加入视图控制器 + view.addSubview(datawhaleLabel) + view.addSubview(phoneNumberLabel) + view.addSubview(loginButton) + view.addSubview(otherNumberButton) + view.addSubview(problemLabel) + self.bgImageView.snp.makeConstraints{(make) in + make.edges.equalToSuperview().inset(UIEdgeInsets.zero) + } + self.bgView.snp.makeConstraints{(make) in + make.edges.equalToSuperview().inset(UIEdgeInsets.zero) + } + self.logoImage.snp.makeConstraints{(make) in + make.top.equalToSuperview().offset(DWNavgationHeight+56) + make.centerX.equalToSuperview() // centerX表示相对于父视图的X轴中心 + } + self.datawhaleLabel.snp.makeConstraints{(make) in + make.centerX.equalToSuperview() + make.top.equalTo(self.logoImage.snp.bottom).offset(-8) + } + self.phoneNumberLabel.snp.makeConstraints{(make) in + make.centerX.equalToSuperview() + make.top.equalTo(self.datawhaleLabel.snp.bottom).offset(24) + } + loginButton.snp.makeConstraints{(make) in + make.top.equalTo(self.phoneNumberLabel.snp.bottom).offset(12) + make.centerX.equalToSuperview() + make.size.equalTo(CGSize.init(width: DWScreenWidth - 64, height: 48)) + } + otherNumberButton.snp.makeConstraints{(make) in + make.top.equalTo(self.loginButton.snp.bottom).offset(12) + make.centerX.equalToSuperview() + make.size.equalTo(CGSize.init(width: DWScreenWidth - 64, height: 48)) + } + problemLabel.snp.makeConstraints{(make) in + make.right.equalTo(otherNumberButton) + make.top.equalTo(otherNumberButton.snp.bottom).offset(14) + } + } + + + // MARK: - Life Cycle + // MARK: - InitView + // MARK: - Public + + // MARK: - Private + + // MARK: - Action + + // MARK: - privateUI + // 手机号区域 + private lazy var problemLabel : UILabel = { + var label = UILabel.init(frame: CGRect.zero) + label.text = "遇到问题" + label.textColor = UIColor.blackGray1() + label.numberOfLines = 1 + label.textAlignment = .center + label.font = UIFont.meduimFont(16) + return label + } () + // 使用其他手机登录按钮 + private lazy var otherNumberButton: UIButton = { + var button = UIButton.init(type: .custom) + button.setTitle("使用其他手机登录", for: .normal) + button.backgroundColor = .black + button.setTitleColor(UIColor.white, for: .normal) + button.layer.cornerRadius = 24 + button.layer.masksToBounds = true + button.titleLabel?.font = UIFont.meduimFont(16) + return button + }() + + // 一键登录按钮 + private lazy var loginButton: UIButton = { + var button = UIButton.init(type: .custom) + button.setTitle("本机号码一键登录", for: .normal) + button.backgroundColor = .white + button.layer.cornerRadius = 24 + button.layer.masksToBounds = true + button.setTitleColor(UIColor.black, for: .normal) + button.titleLabel?.font = UIFont.meduimFont(16) + return button + }() + + // 手机号区域 + private lazy var phoneNumberLabel : UILabel = { + var label = UILabel.init(frame: CGRect.zero) + label.text = "138****1680" + label.textColor = UIColor.white + label.numberOfLines = 1 + label.textAlignment = .center + label.font = UIFont.boldFont(32) + return label + } () + + // 当前文字的Label + private lazy var datawhaleLabel : UILabel = { + var label = UILabel.init(frame: CGRect.zero) + label.text = "Datawhale" + label.textColor = UIColor.white + label.numberOfLines = 1 + label.textAlignment = .center + label.font = UIFont.boldFont(32) + return label + } () + + // 添加Datawhale的logo + private lazy var logoImage: UIImageView = { + var imageView = UIImageView.init(image: UIImage.init(named: "datawhale_logo")) + imageView.contentMode = .scaleToFill //表示当前视图的模式是 + return imageView + }() + + //添加透明度层 + private lazy var bgView: UIView = { + var view = UIView.init(frame: CGRect.zero) + view.backgroundColor = UIColor.black.withAlphaComponent(0.7) // 背景色为黑色透明度为0.7 + return view + }() + + // 背景图片 + private lazy var bgImageView: UIImageView = { + var imageView = UIImageView.init(image: UIImage.init(named: "login_bg_image")) + imageView.contentMode = .scaleAspectFill //表示当前视图的模式是 + imageView.clipsToBounds = true + return imageView + }() +} +``` +> 最终界面效果 +![Alt text](./img/IMG_1331.PNG) + +具体项目地址:[github](https://github.com/Dulpyanghaobo/DatawhaleApp) +下载之后点击xcworkspace,之后run跑项目 +![Alt text](./img/1637401210478.png) diff --git a/IOS/Task02:算法实现/1.三种递归问题求解.md b/IOS/Task02:算法实现/1.三种递归问题求解.md new file mode 100644 index 0000000..f4b0424 --- /dev/null +++ b/IOS/Task02:算法实现/1.三种递归问题求解.md @@ -0,0 +1,172 @@ +# 三种递归问题求解 + +## 斐波那契数列 + +斐波那契数列是一系列数字,除了第一个和第二个数字外,任何数字都是前两个数字之和。 +$$ +0、1、2、3、5、8、13、21... +$$ +于是我们可以写出斐波那契数列的伪代码: + +```swift +fib(n) = fib(n - 1) + fib(n - 2) +``` + +这种方式十分适合递归求解,对于斐波那契数列而言,我们可以指定数列最前面两个元素为:0与1。于是我们可以将伪代码翻译成Swift中源码: + +```swift +func fib2(n: UInt) -> UInt { + if (n < 2) { + // base cases + return n + } + return fib2(n: n - 2) + fib2(n: n - 1) + // recursive cases +} +``` + +这种方式虽然可以运行,但是时间复杂度过大,随着调用数值的不断增加,这种算法的时间复杂度达到了指数级别!每次的计算都会存在大量的重复计算: + +- 当我们计算fib(4)时,算法会计算fib(3)与fib(2) +- 在计算fib(3)时,算法会计算fib(2)与fib(1),这样fib(2)被计算了两次,当计算fib(100)时,算法会存在大量计算重复的内容 + +如何修复这种问题呢?既然fib(2)被计算了两次,那么我们可以开启一份`备忘录`,每当算法计算到fib(2)时,如果这个值已经存在,那么直接从备忘录中调用即可! + +```swift +var fibMemo: [UInt: UInt] = [0: 0, 1: 1] // our old base cases +func fib3(n: UInt) -> UInt { + if let result = fibMemo[n] { // our new base case + return result + } else { + fibMemo[n] = fib3(n: n - 1) + fib3(n: n - 2) // memoization + } + return fibMemo[n]! +} +``` + +保持斐波那契数列还有一种性能更高的办法:迭代 + +```swift +func fib4(n: UInt) -> UInt { + if (n == 0) { // special case + return n + } + var last: UInt = 0, next: UInt = 1 // initially set to fib(0) & fib(1) + for _ in 1.. Double { + let numerator: Double = 4 + var denominator: Double = 1 + var operation: Double = -1 + var pi: Double = 0 + for _ in 0..: CustomStringConvertible { + private var container: [T] = [T]() + public func push(_ thing: T) { container.append(thing) } + public func pop() -> T { return container.removeLast() } + public var description: String { return container.description } +} +``` + +下面我们将塔定义为Stack,并初始化汉诺塔 + +```swift +var numDiscs = 3 +var towerA = Stack() +var towerB = Stack() +var towerC = Stack() +for i in 1...numDiscs { // initialize the first tower + towerA.push(i) +} +``` + +如何求解汉诺塔问题?我们只需要处理两种情况: + +- 移动一个盘子(base case) +- 移动多个盘子(递归处理) + +我们可以将汉诺塔问题的递归解决方案分为3个步骤: + +1. 以塔C为中介,蒋上面的n-1个盘子从塔A移动到塔B(临时塔) +2. 将底部的盘子从塔A移动到塔C +3. 将n-1个盘子从塔B移动到塔C + +这种算法不仅用于3个盘子的情况,甚至可以递归解决任意数量的盘子。我们只需要完成base case的搭建,剩下的任务交给递归处理即可: + +```swift +func hanoi(from: Stack, to: Stack, temp: Stack, n: Int) { + if n == 1 { // base case + to.push(from.pop()) // move 1 disk + } else { // recursive case + hanoi(from: from, to: temp, temp: to, n: n-1) + hanoi(from: from, to: to, temp: temp, n: 1) + hanoi(from: temp, to: to, temp: from, n: n-1) + } +} +``` + +我们不必理解将多个盘子从塔A移动到塔C所需的每一步。这就是采用递归方法的魅力所在——只需要考虑抽象的解决方案,而不是罗列出每一种单独的动作。不过值得注意的是,上述算法的时间复杂度处在指数级别。 + +## 练习作业 + +1. 自己设计一种斐波那契数列的计算算法,编写测试单元并对比上述算法的性能差异 +2. 寻找相应数学公式,计算无理数e +3. 编写汉诺塔问题的解决程序,以解决任意数量的汉诺塔问题 + + + +[1] 百度百科-汉诺塔 + +*参考代码开源版权说明: + +```swift +// Copyright 2017 David Kopec +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +``` + + + diff --git a/IOS/Task02:算法实现/2.搜索问题.md b/IOS/Task02:算法实现/2.搜索问题.md new file mode 100644 index 0000000..74621f4 --- /dev/null +++ b/IOS/Task02:算法实现/2.搜索问题.md @@ -0,0 +1,214 @@ +# 搜索问题 + +## 二分搜索 + +搜索问题通常有较多的解决方法。一般而言,**线性搜索**按照原始的数据结构顺序,遍历搜索空间中的每一个元素。线性搜索是最简单、最自然、最直观的搜索方法。该算法的时间复杂度最坏情况下为 $O(n)$ ,其中 $n$ 为待搜索结构中的元素总个数。 + +线性搜索的代码也十分简单: + +```swift +func linearContains(_ array: Array, Target: ourTarget ) -> Bool { + for element in Array where Target == ourTarget { + return true + } + return false +} +``` + + + +然而,如果我们已经知道了数据结构的顺序,即每一种元素都是排列好的,并且可以通过索引立即访问数据结构内部的任何项,那么我们就可以执行**二分搜索**(Binary Search)。 + +假设存在一个按照字母序排列的数组Array,我们的目标是搜索f: + +```swift +Array = [a, b, c, d, e, f, g] +``` + +这7个字母的中间值为 $d$,我们可以确定要搜索的目标位于 $d$ 之后,因此我们再于 $e, f, g$ 中搜索中间元素 $f$,即可完成目标。 + +二分搜索与线性搜索不同,它不需要遍历结构中的每一个元素。二分搜索能不断将搜索空间减半,因此最坏情况下的时间复杂度为 $O(\text{lg } n)$ 。二分搜索的缺点也显而易见:我们需要对数组进行排序,排序的最佳时间复杂度也需要 $O(n \text{lg }n)$ ,实际上,如果只运行一次,且原始数据为未排序数组,线性搜索的效果要好于二分搜索。 + +以基因和密码子的二分搜索为例,其中Gene类型为Array,而Codon可以与其他Codon进行比较,代码如下 + +```swift +func binaryContains(_ array: Gene, item: Codon) -> Bool { + var low = 0 + var high = array.count - 1 + while low <= high { + let mid = (low + high) / 2 + if array[mid] < item { + low = mid + 1 + } else if array[mid] > item { + high = mid - 1 + } else { + return true + } + } + return false +} +``` + +## 深度优先搜索(DFS) + +**深度优先搜索**(Depth-First Search, DFS)为首先尽可能深入地搜索,如果到达终点,则回溯到最后一个决策点。此处我们以一款小游戏为例,假设右下角的绿色顶点为起始点,橙色方块为障碍物,最左上角的白色方块为终点,DFS的算法过程如下: + +1 + +深度优先搜索给出的答案: + +2 + + + + + +深度优搜索依赖于**栈**(Stack)这种数据结构,前文的递归中我们已经提到过:栈有两种最基本的操作 + +1. push() 压栈,将元素送入栈顶 +2. pop() 移除栈顶元素并返回该元素 + +在Swift中栈的实现也十分简单: + +```swift +public class Stack { + private var container: [T] = [T]() + public var isEmpty: Bool { return container.isEmpty } + public func push(_ thing: T) { container.append(thing) } + public func pop() -> T { return container.removeLast() } +} +``` + +DFS的精髓在于可以回溯,要实现这个功能,我们需要添加一个类Node,用于记录当前搜索的状态(或从一个状态到另一个状态的方式)。Node可以看成包围状态的包装器。我们将状态来源的Node称为parent。此外,我们还需定义Node的类拥有cost和heuristic属性,并且能够比较Comparable和散列化Hashable + +```swift +class Node: Comparable, Hashable { + let state: T + let parent: Node? + let cost: Float + let heuristic: Float + init(state: T, parent: Node?, cost: Float = 0.0, heuristic: Float = 0.0) { + self.state = state + self.parent = parent + self.cost = cost + self.heuristic = heuristic + } + + var hashValue: Int { return Int(cost + heuristic) } +} + +func < (lhs: Node, rhs: Node) -> Bool { + return (lhs.cost + lhs.heuristic) < (rhs.cost + rhs.heuristic) +} + +func == (lhs: Node, rhs: Node) -> Bool { + return lhs === rhs +} +``` + +深度优先搜索中需要记录两种结构:正准备搜索的状态和已经搜索的状态,分别用frontier和explored表示。只要frontier中还有更多的状态要访问,DFS就会继续检查他们是否为目标值,并且将这些状态的后者加入frontier中。DFS还会把搜索过的点标记为explored,从而避免DFS陷入死循环,到达那些作为后继者先前已经访问过的状态。如果frontier是空的,则意味着没有地方可以继续搜索。 + +```swift +func dfs(initialState: StateType, goalTestFn: (StateType) -> Bool, successorFn: (StateType) -> [StateType]) -> Node? { + // frontier is where we've yet to go + let frontier: Stack> = Stack>() + frontier.push(Node(state: initialState, parent: nil)) + // explored is where we've been + var explored: Set = Set() + explored.insert(initialState) + + // keep going while there is more to explore + while !frontier.isEmpty { + let currentNode = frontier.pop() + let currentState = currentNode.state + // if we found the goal, we're done + if goalTestFn(currentState) { return currentNode } + // check where we can go next and haven't explored + for child in successorFn(currentState) where !explored.contains(child) { + explored.insert(child) + frontier.push(Node(state: child, parent: currentNode)) + } + } + return nil // never found the goal +} +``` + +## 广度优先搜索(BFS) + +深度优先搜索尽管提出了一种较为可靠的搜索方式,但是时间复杂度较大。通过深度优先遍历的路径通常不是最短路径。与之对应的**广度优先算法**(Breadth-First Search,BFS)总是查找到最短的路径,因为广度优先搜索的策略为优先遍历距离当前节点较近的点,找到的路径一定是最短的。但是,广度优先搜索并不总是比深度优先搜索性能优异,有些情况下深度优先搜索可能在广度优先搜索之前找到解决办法。运用DFS还是BFS,取决于我们在快速找到解决方法的可能性与查找到目标的最短路径的确定性之间的权衡。 + +![3](img/3.png) + +值得注意的是,BFS给出的路径是最短的,但并不一定唯一。 + +与深度优先搜索不同,广度优先搜索依赖于**队列**(queue)结构,栈与队列的区别在于: + +- 栈为后进先出(LIFO) +- 队列为先进先出(FIFO,First-In-First-Out) + +队列也对应着至少两种操作: + +1. push() 将元素添加到队列中 +2. pop() 将先添加的元素从队列中删除 + +实际上,在Swift中Array的队列实现与栈的实现几乎完全相同,区别就是从Array的左侧而不是右侧移除元素。Array最左侧元素是Array中存在最久的一个元素。队列的参考实现如下: + +```swift +public class Queue { + private var container: [T] = [T]() + public var isEmpty: Bool { return container.isEmpty } + public func push(_ thing: T) { container.append(thing) } + public func pop() -> T { return container.removeFirst() } +} +``` + +BFS的实现很大程度上与DFS类似甚至相同,只是frontier从栈变成了队列。将frontier从栈更改为队列就会更改状态被搜索的顺序,确保首先搜索最长接近初始状态的状态。 + +```swift +func bfs(initialState: StateType, goalTestFn: (StateType) -> Bool, successorFn: (StateType) -> [StateType]) -> Node? { + // frontier is where we've yet to go + let frontier: Queue> = Queue>() + frontier.push(Node(state: initialState, parent: nil)) + // explored is where we've been + var explored: Set = Set() + explored.insert(initialState) + // keep going while there is more to explore + while !frontier.isEmpty { + let currentNode = frontier.pop() + let currentState = currentNode.state + // if we found the goal, we're done + if goalTestFn(currentState) { return currentNode } + // check where we can go next and haven't explored + for child in successorFn(currentState) where !explored.contains(child) { + explored.insert(child) + frontier.push(Node(state: child, parent: currentNode)) + } + } + return nil // never found the goal +} +``` + +## 练习 + +1. 如果二分搜索过程中数组长度为偶数该怎么办?代码如何实现? +2. 为dfs()、bfs()函数各添加一个计数器,以查看每个函数在搜索相同迷宫时所遍历的状态数量。并在上图中画出dfs和bfs的实现过程。 +3. 运用dfs与bfs实现“八皇后”问题。 + +*参考代码开源版权说明: + +```swift +// Copyright 2017 David Kopec +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +``` + diff --git a/IOS/Task02:算法实现/3.图问题.md b/IOS/Task02:算法实现/3.图问题.md new file mode 100644 index 0000000..a973950 --- /dev/null +++ b/IOS/Task02:算法实现/3.图问题.md @@ -0,0 +1,372 @@ +# 图问题 + +## 构建图框架 + +Swift语言的定义为面向协议的编程范式(区别于传统的面相对象或函数范式)。尽管这种新范式的正统性仍然在充实阶段,但可以明确的是:这种语义将接口和复合放在继承之前。相对于类是面向对象语言的基本构建块,函数是函数式编程语言的基本构建块,所以在面向协议的编程语言中,协议是基本的构建块。下面,我们将以协议的方式构建块。 + +```swift +public protocol Edge: CustomStringConvertible { + var u: Int { get set } // index of the "from" vertex + var v: Int { get set } // index of the "to" vertex + var reversed: Edge { get } +} +``` + +我们对Edge定义为两个顶点之间的连接。用一个整数索引表示。按照习惯,我们通常用 $u$ 表示第一个顶点,用 $v$ 表示第二个顶点。本章中,我们只处理双向边,而在图论中,有向边也常常是讨论的对象。 + +我们使用Swfit语言关键字associatedtype来定义Graph采用者(adopters)可以配置的类型。 + +```swift +protocol Graph: class, CustomStringConvertible { + associatedtype VertexType: Equatable + associatedtype EdgeType: Edge + var vertices: [VertexType] { get set } + var edges: [[EdgeType]] { get set } +} +``` + +实现图数据结构的方法有很多种,最常用的两种就是顶点矩阵和邻接矩阵。在顶点矩阵方法中,每个矩阵单元表示两个个顶点的交接,下面是对Graph的一种完整协议拓展,为协议拓展添加了基本函数: + +```swift +extension Graph { + /// How many vertices are in the graph? + public var vertexCount: Int { return vertices.count } + + /// How many edges are in the graph? + public var edgeCount: Int { return edges.joined().count } + + /// Get a vertex by its index. + /// + /// - parameter index: The index of the vertex. + /// - returns: The vertex at i. + public func vertexAtIndex(_ index: Int) -> VertexType { + return vertices[index] + } + + /// Find the first occurence of a vertex if it exists. + /// + /// - parameter vertex: The vertex you are looking for. + /// - returns: The index of the vertex. Return nil if it can't find it. + public func indexOfVertex(_ vertex: VertexType) -> Int? { + if let i = vertices.index(of: vertex) { + return i + } + return nil + } + + /// Find all of the neighbors of a vertex at a given index. + /// + /// - parameter index: The index for the vertex to find the neighbors of. + /// - returns: An array of the neighbor vertices. + public func neighborsForIndex(_ index: Int) -> [VertexType] { + return edges[index].map({self.vertices[$0.v]}) + } + + /// Find all of the neighbors of a given Vertex. + /// + /// - parameter vertex: The vertex to find the neighbors of. + /// - returns: An optional array of the neighbor vertices. + public func neighborsForVertex(_ vertex: VertexType) -> [VertexType]? { + if let i = indexOfVertex(vertex) { + return neighborsForIndex(i) + } + return nil + } + + /// Find all of the edges of a vertex at a given index. + /// + /// - parameter index: The index for the vertex to find the children of. + public func edgesForIndex(_ index: Int) -> [EdgeType] { + return edges[index] + } + + /// Find all of the edges of a given vertex. + /// + /// - parameter vertex: The vertex to find the edges of. + public func edgesForVertex(_ vertex: VertexType) -> [EdgeType]? { + if let i = indexOfVertex(vertex) { + return edgesForIndex(i) + } + return nil + } + + /// Add a vertex to the graph. + /// + /// - parameter v: The vertex to be added. + /// - returns: The index where the vertex was added. + public func addVertex(_ v: VertexType) -> Int { + vertices.append(v) + edges.append([EdgeType]()) + return vertices.count - 1 + } + + /// Add an edge to the graph. + /// + /// - parameter e: The edge to add. + public func addEdge(_ e: EdgeType) { + edges[e.u].append(e) + edges[e.v].append(e.reversed as! EdgeType) + } +} +``` + +正如前面所述,本章我们只讨论双向边。除了分为双向边和无向边外,边还可以赋予权重。下面我们实现一种不带权重的边UnweightedEdge,当然也会实现Edge协议。Edge协议必须定义“from”顶点u “to” 顶点v,以及一种反转Edge的方式。Edge协议还要按照Edge的要求必须实现CustomStringConvertible,这意味着定义一个description属性。 + +```swift +open class UnweightedEdge: Edge { + public var u: Int // "from" vertex + public var v: Int // "to" vertex + public var reversed: Edge { + return UnweightedEdge(u: v, v: u) + } + + public init(u: Int, v: Int) { + self.u = u + self.v = v + } + + //MARK: CustomStringConvertable + public var description: String { + return "\(u) <-> \(v)" + } +} +``` + +Graph的具体实现十分简单,UnweightedGraph就是一个顶点可以是任意Equatable类型、边是UnweightedEdge类型的Graph。通过定义vertices和edges数组的类型,我们在Graph协议中隐式地填充关联类型VertexType和EdgeType + +```swift +open class UnweightedGraph: Graph { + var vertices: [V] = [V]() + var edges: [[UnweightedEdge]] = [[UnweightedEdge]]() //adjacency lists + + public init() { + } + + public init(vertices: [V]) { + for vertex in vertices { + _ = self.addVertex(vertex) + } + } + + /// This is a convenience method that adds an unweighted edge. + /// + /// - parameter from: The starting vertex's index. + /// - parameter to: The ending vertex's index. + public func addEdge(from: Int, to: Int) { + addEdge(UnweightedEdge(u: from, v: to)) + } + + /// This is a convenience method that adds an unweighted, undirected edge between the first occurence of two vertices. + /// + /// - parameter from: The starting vertex. + /// - parameter to: The ending vertex. + public func addEdge(from: V, to: V) { + if let u = indexOfVertex(from) { + if let v = indexOfVertex(to) { + addEdge(UnweightedEdge(u: u, v: v)) + } + } + } + + /// MARK: Implement CustomStringConvertible + public var description: String { + var d: String = "" + for i in 0.. \(vertexAtIndex(edge.v))") + } + } +} +``` + +根据上面的原理,我们可以定带权重的图结构: + +```swift +/// A subclass of Graph that has convenience methods for adding and removing WeightedEdges. All added Edges should have the same generic Comparable type W as the WeightedGraph itself. +open class WeightedGraph: Graph { + var vertices: [V] = [V]() + var edges: [[WeightedEdge]] = [[WeightedEdge]]() //adjacency lists + + public init() { + } + + public init(vertices: [V]) { + for vertex in vertices { + _ = self.addVertex(vertex) + } + } + + /// Find all of the neighbors of a vertex at a given index. + /// + /// - parameter index: The index for the vertex to find the neighbors of. + /// - returns: An array of tuples including the vertices as the first element and the weights as the second element. + public func neighborsForIndexWithWeights(_ index: Int) -> [(V, W)] { + var distanceTuples: [(V, W)] = [(V, W)]() + for edge in edges[index] { + distanceTuples += [(vertices[edge.v], edge.weight)] + } + return distanceTuples + } + + /// This is a convenience method that adds a weighted edge. + /// + /// - parameter from: The starting vertex's index. + /// - parameter to: The ending vertex's index. + /// - parameter weight: the Weight of the edge to add. + public func addEdge(from: Int, to: Int, weight:W) { + addEdge(WeightedEdge(u: from, v: to, weight: weight)) + } + + /// This is a convenience method that adds a weighted edge between the first occurence of two vertices. It takes O(n) time. + /// + /// - parameter from: The starting vertex. + /// - parameter to: The ending vertex. + /// - parameter weight: the Weight of the edge to add. + public func addEdge(from: V, to: V, weight: W) { + if let u = indexOfVertex(from) { + if let v = indexOfVertex(to) { + addEdge(WeightedEdge(u: u, v: v, weight:weight)) + } + } + } + + //Implement Printable protocol + public var description: String { + var d: String = "" + for i in 0.. Bool { + return lhs.distance < rhs.distance + } + + public static func == (lhs: DijkstraNode, rhs: DijkstraNode) -> Bool { + return lhs.distance == rhs.distance + } + } + + /// Finds the shortest paths from some route vertex to every other vertex in the graph. + /// + /// - parameter graph: The WeightedGraph to look within. + /// - parameter root: The index of the root node to build the shortest paths from. + /// - parameter startDistance: The distance to get to the root node (typically 0). + /// - returns: Returns a tuple of two things: the first, an array containing the distances, the second, a dictionary containing the edge to reach each vertex. Use the function pathDictToPath() to convert the dictionary into something useful for a specific point. + public func dijkstra(root: Int, startDistance: W) -> ([W?], [Int: WeightedEdge]) { + var distances: [W?] = [W?](repeating: nil, count: vertexCount) // how far each vertex is from start + distances[root] = startDistance // the start vertex is startDistance away + var pq: PriorityQueue = PriorityQueue(ascending: true) + var pathDict: [Int: WeightedEdge] = [Int: WeightedEdge]() // how we got to each vertex + pq.push(DijkstraNode(vertex: root, distance: startDistance)) + + while let u = pq.pop()?.vertex { // explore the next closest vertex + guard let distU = distances[u] else { continue } // should already have seen it + for we in edgesForIndex(u) { // look at every edge/vertex from the vertex in question + let distV = distances[we.v] // the old distance to this vertex + if distV == nil || distV! > we.weight + distU { // if we have no old distance or we found a shorter path + distances[we.v] = we.weight + distU // update the distance to this vertex + pathDict[we.v] = we // update the edge on the shortest path to this vertex + pq.push(DijkstraNode(vertex: we.v, distance: we.weight + distU)) // explore it soon + } + } + } + + return (distances, pathDict) + } + + + /// A convenience version of dijkstra() that allows the supply of the root + /// vertex instead of the index of the root vertex. + public func dijkstra(root: V, startDistance: W) -> ([W?], [Int: WeightedEdge]) { + if let u = indexOfVertex(root) { + return dijkstra(root: u, startDistance: startDistance) + } + return ([], [:]) + } + + /// Helper function to get easier access to Dijkstra results. + public func distanceArrayToVertexDict(distances: [W?]) -> [V : W?] { + var distanceDict: [V: W?] = [V: W?]() + for i in 0.. [Item] { + //build up dynamic programming table + var table: [[Float]] = [[Float]](repeating: [Float](repeating: 0.0, count: maxCapacity + 1), count: items.count + 1) //initialize table - overshooting in size + for (i, item) in items.enumerated() { + for capacity in 1...maxCapacity { + let previousItemsValue = table[i][capacity] + if capacity >= item.weight { // item fits in knapsack + let valueFreeingWeightForItem = table[i][capacity - item.weight] + table[i + 1][capacity] = max(valueFreeingWeightForItem + item.value, previousItemsValue) // only take if more valuable than previous combo + } else { // no room for this item + table[i + 1][capacity] = previousItemsValue //use prior combo + } + } + } + // figure out solution from table + var solution: [Item] = [Item]() + var capacity = maxCapacity + for i in stride(from: items.count, to: 0, by: -1) { // work backwards + if table[i - 1][capacity] != table[i][capacity] { // did we use this item? + solution.append(items[i - 1]) + capacity -= items[i - 1].weight // if we used an item, remove its weight + } + } + return solution +} +``` + +我们将各种物品填入表单中,进行最优求解: + +```swift +let items = [Item(name: "television", weight: 50, value: 500), + Item(name: "candlesticks", weight: 2, value: 300), + Item(name: "stereo", weight: 35, value: 400), + Item(name: "laptop", weight: 3, value: 1000), + Item(name: "food", weight: 15, value: 50), + Item(name: "clothing", weight: 20, value: 800), + Item(name: "jewelry", weight: 1, value: 4000), + Item(name: "books", weight: 100, value: 300), + Item(name: "printer", weight: 18, value: 30), + Item(name: "refrigerator", weight: 200, value: 700), + Item(name: "painting", weight: 10, value: 1000)] +knapsack(items: items, maxCapacity: 75) +``` + +即可解决背包问题。 + +## 旅行商问题 + +旅行商问题是最经典、最常被讨论的内容之一。一名推销员必须对地图上的所有城市访问一次,最终回到起点城市。每座城市之间都有一条路直连路径,推销员可以按任何顺序访问这些城市。那么对于推销员而言的最短路径是什么? + +这个问题看起来简单,但是还没有能够针对任意数量的城市快速解决该问题的算法。“快速”的含义在此处指的是:这个问题是一个NP难题。NP难题是不存在多项式时间算法的。随着推销员需要访问的城市数量的增加,解决这个问题的难度急剧提高。20个城市的旅行推销员问题相比10个城市的旅行推销员问题要难得多。当城市数量达到上百万个,在合理的时间内要完美解决这个问题是不可能的。 + +```swift +let vtCities = ["Rutland", "Burlington", "White River Junction", "Bennington", "Brattleboro"] + +let vtDistances = [ + "Rutland": + ["Burlington": 67, "White River Junction": 46, "Bennington": 55, "Brattleboro": 75], + "Burlington": + ["Rutland": 67, "White River Junction": 91, "Bennington": 122, "Brattleboro": 153], + "White River Junction": + ["Rutland": 46, "Burlington": 91, "Bennington": 98, "Brattleboro": 65], + "Bennington": + ["Rutland": 55, "Burlington": 122, "White River Junction": 98, "Brattleboro": 40], + "Brattleboro": + ["Rutland": 75, "Burlington": 153, "White River Junction": 65, "Bennington": 40] +] +``` + +上面的表格输入了旅行商问题需要到达的城市与对应距离,对应的解决办法完全代码如下: + +```swift +// backtracking permutations algorithm +func allPermutationsHelper(contents: [T], permutations: inout [[T]], n: Int) { + guard n > 0 else { permutations.append(contents); return } + var tempContents = contents + for i in 0..(_ original: [T]) -> [[T]] { + var permutations = [[T]]() + allPermutationsHelper(contents: original, permutations: &permutations, n: original.count) + return permutations +} + +// test allPermutations +let abc = ["a","b","c"] +let testPerms = allPermutations(abc) +print(testPerms) +print(testPerms.count) + +// make complete paths for tsp +func tspPaths(_ permutations: [[T]]) -> [[T]] { + return permutations.map { + if let first = $0.first { + return ($0 + [first]) // append first to end + } else { + return [] // empty is just itself + } + } +} + +print(tspPaths(testPerms)) + +func solveTSP(cities: [T], distances: [T: [T: Int]]) -> (solution: [T], distance: Int) { + let possiblePaths = tspPaths(allPermutations(cities)) // all potential paths + var bestPath: [T] = [] // shortest path by distance + var minDistance: Int = Int.max // distance of the shortest path + for path in possiblePaths { + if path.count < 2 { continue } // must be at least one city pair to calculate + var distance = 0 + var last = path.first! // we know there is one becuase of above line + for next in path[1.. 本文原链接:[https://www.iosdevlog.com](https://www.iosdevlog.com/),特别鸣谢:贾献华 的开源贡献 + +## 维基介绍 + +在[模式识别](https://zh.wikipedia.org/wiki/模式识别)领域中,**最近邻居法**(**KNN**算法,又译**K-近邻算法**)是一种用于[分类](https://zh.wikipedia.org/wiki/分类问题)和[回归](https://zh.wikipedia.org/wiki/迴歸分析)的[非参数统计](https://zh.wikipedia.org/wiki/無母數統計)方法[[1\]](https://zh.wikipedia.org/wiki/最近鄰居法#cite_note-1)。在这两种情况下,输入包含[特征空间(Feature Space)](https://zh.wikipedia.org/w/index.php?title=特徵空間(機器學習)&action=edit&redlink=1)中的***k\***个最接近的训练样本。 + +- 在*k-NN分类*中,输出是一个分类族群。一个对象的分类是由其邻居的“多数表决”确定的,*k*个最近邻居(*k*为正[整数](https://zh.wikipedia.org/wiki/整数),通常较小)中最常见的分类决定了赋予该对象的类别。若*k* = 1,则该对象的类别直接由最近的一个节点赋予。 +- 在*k-NN回归*中,输出是该对象的属性值。该值是其*k*个最近邻居的值的平均值。 + +最近邻居法采用向量空间模型来分类,概念为相同类别的案例,彼此的相似度高,而可以借由计算与已知类别案例之相似度,来评估未知类别案例可能的分类。 + +K-NN是一种[基于实例的学习](https://zh.wikipedia.org/w/index.php?title=基于实例的学习&action=edit&redlink=1),或者是局部近似和将所有计算推迟到分类之后的[惰性学习](https://zh.wikipedia.org/w/index.php?title=惰性学习&action=edit&redlink=1)。k-近邻算法是所有的[机器学习](https://zh.wikipedia.org/wiki/机器学习)算法中最简单的之一。 + +无论是分类还是回归,衡量邻居的权重都非常有用,使较近邻居的权重比较远邻居的权重大。例如,一种常见的加权方案是给每个邻居权重赋值为1/ d,其中d是到邻居的距离。[[注 1\]](https://zh.wikipedia.org/wiki/最近鄰居法#cite_note-2) + +邻居都取自一组已经正确分类(在回归的情况下,指属性值正确)的对象。虽然没要求明确的训练步骤,但这也可以当作是此算法的一个训练样本集。 + +k-近邻算法的缺点是对数据的局部结构非常敏感。本算法与[K-平均算法](https://zh.wikipedia.org/wiki/K-平均算法)(另一流行的机器学习技术)没有任何关系,请勿与之混淆。 + +## ARKit + Swift + k-NN 实现 + +创建 KNN 类(结构体 `struct` 也行,我是为了 与 `sklearn` 尽量一致)。 + +``` +/// KNN +public struct KNN { +} +``` + +属性 + +``` + /// Number of neighbors to use by default for :meth:`kneighbors` queries + private var k: Int + /// Data set + private var X = [Feature]() + /// Target values + private var y = [Label]() + + + /// distance + private let distanceMetric: (_ x1: Feature, _ x2: Feature) -> Double + /// draw radius for debug + public var debugRadiusCallback: (([Double]) -> ())? = nil +``` + +数据: + +- `k`: 指定取 k 个最接近的训练样本 +- `X`: 样本特征 (数组)一般要传数组的数组 +- `y`: 样本标签 (数组) + +辅助: + +- `distanceMetric`: 用来计算距离的函数 +- `debugRadiusCallback`: 调度时候用,主要是画出最近的 k 个样本范围 + +## 方法 + +``` + /// constructorLabels for xTest + /// + /// - Parameters: + /// - k: k + /// - distanceMetric: distance + public init (k: Int, distanceMetric: @escaping (_ x1: Feature, _ x2: Feature) -> Double) + + /// train + /// + /// - Parameters: + /// - X: Training set + /// - y: Target values + public mutating func fit(X: [Feature], y: [Label]) + + + /// Labels for xTest + /// + /// - Parameter XTest: Test set + /// - Returns: Target values + public func predict(XTest: [Feature]) -> [Label] +``` + +- `init()`: 构造函数 需要预先决定 `k` 和距离计算方法 +- `fit()`: 拟合目标函数,kNN 不需要拟合,只要记下数据即可 +- `predict()`: 预测给定的特征,返回对应的标签 + +## 计算距离 + +``` +public struct Distance { + + /// Helper function to calculate euclidian distance + /// + /// - Parameters: + /// - x0: source coordinate + /// - x1: target coordinate + /// - Returns: euclidian distance + public static func euclideanDistance(_ x0: [Double], _ x1: [Double]) -> Double + + // Convenience + public static func euclideanDistance() -> (([Double], [Double]) -> Double) { + return { self.euclideanDistance($0, $1) } + } +``` + +这里使用 欧式距离(Euclidean Distance) + +KNN 使用: + +``` + func testKNN() { + let kNN = KNN(k: 3) + let X = [[1.0], [2], [3], [4]] + let y = [0, 0, 1, 1] + kNN.fit(X, y) + + let label = kNN.predict([[1.2], [3]]) + + XCTAssertEqual([0, 1], label) + } +``` + +## 显示2维 + +``` +enum MLStep: Int { + case train + case predict +} + +enum GeometryType: Int { + case box + case pyramid + case sphere + case cylinder + case cone + case tube + case torus +... +} +``` + +- `MLStep`: 分成 训练 和 预测 ,训练一次,可以一直预测。 +- `GeometryType`: 定义几种形状,这里定义给 `ARKIT` 使用的 + +## KNNViewController + +``` +class KNNViewController: UIViewController { + + let radius: CGFloat = 5 + + public var X: [[Double]] = [] + public var y: [GeometryType] = [] + public var XTest: [[Double]] = [] + public var yTest: [GeometryType] = [] + public var radiuses: [Double] = [] { + didSet { + for (center, r) in zip(XTest, radiuses) { + drawCircle(center: CGPoint(x: center[0], y: center[1]), radius: CGFloat(r)) + } + } + } + public var predictLayers: [CALayer] = [] + + var model = KNN<[Double], GeometryType>(k: 1, distanceMetric: Distance.euclideanDistance()) + + @IBOutlet weak var kNNPickerView: UIPickerView! + @IBOutlet weak var panelView: UIView! + @IBOutlet weak var trainBarButtonItem: UIBarButtonItem! + + var mlStep = MLStep.train { + didSet { + switch mlStep { + case .train: + trainBarButtonItem.title = "train" + default: + trainBarButtonItem.title = "predict" + } + } + } +... +} +``` + +添加 `Layer` 到 `panelView` 上实现类别,2D 只分了三个类别,分别用 方形(红),三角形(蓝),花形(绿)表示。 + +使用 `alpha` 表示预测类别,以预测样本为中心画一个圈,圈内为最近的 `k` 个样本。 + +``` + func drawCircle(center: CGPoint, radius: CGFloat, alpha: CGFloat = 0.1) { + let r = self.radius + radius + let kNNCircleLayer = CAShapeLayer() + kNNCircleLayer.path = UIBezierPath(roundedRect: CGRect(x: center.x - r, y: center.y - r, width: r * 2, height: r * 2), cornerRadius: r).cgPath + kNNCircleLayer.fillColor = UIColor(red: 0.1, green: 0.1, blue: 0.1, alpha: alpha).cgColor + kNNCircleLayer.borderColor = UIColor(red: 0.8, green: 0.8, blue: 0.8, alpha: 1).cgColor + kNNCircleLayer.borderWidth = 1 + panelView.layer.addSublayer(kNNCircleLayer) + } +``` + +圆内为 `k` 个样本。 + +## ARKit 实现 + +能 3D 展示多好,别急,下面就是用 `ARKit` 实现的 3D 版本。 + +``` +class ARKNNViewController: UIViewController +``` + +基本实现和 `ARKNNViewController` 类似。 + +``` + func drawSphere(center: SCNVector3, radius: Float) { + let geometry = SCNSphere(radius: CGFloat(radius) + self.radius) + + geometry.firstMaterial?.diffuse.contents = UIColor(red: 0.1, green: 0.1, blue: 0.8, alpha: 0.7) + + let node = SCNNode() + node.geometry = geometry + node.position = center + sceneView.scene.rootNode.addChildNode(node) + } +``` + +这是画最外面的范围球体,球体内为 `k` 个样本。 + +## 视频 + +b站视频:https://www.bilibili.com/video/av48647681/ \ No newline at end of file diff --git a/IOS/Task02:项目练习/智能识别水果/iOS端智能识别水果.md b/IOS/Task02:项目练习/智能识别水果/iOS端智能识别水果.md new file mode 100644 index 0000000..09941f5 --- /dev/null +++ b/IOS/Task02:项目练习/智能识别水果/iOS端智能识别水果.md @@ -0,0 +1,71 @@ +# iOS端智能识别水果 + +## 参考教程 + +本部分参考自浙江大学Swift创新导论: + +https://www.icourse163.org/course/ZJU-1450024180?from=searchPage + +你可以查看相应章节,对应代码如下: + +```swift +import UIKit +class ViewController: UIViewController{ + var pickerController = UIImagePickerController() + @IBOutlet weak var nameLabel: UILabel! + @IBOutlet weak var confidenceLabel: UILabel! + @IBOutlet weak var imageView: UIImageView! + @IBAction func recognaizeButton(_ sender: Any) { + pickerController.sourceType = .photoLibrary + self.present(pickerController, animated:true, completion:nil) + } + let model = fruitRecognizer_1() + override func viewDidLoad() { + super.viewDidLoad() + pickerController.delegate = self + } +} + +extension ViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate { + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]){ + guard let image = info[.originalImage]as? UIImage else{ + fatalError("Expected a dictionary containing an image, but was provided the following:\(info)") + } + imageView.image = image + + guard let fruitRecognizer_1Output = try? model.prediction(image: buffer(from:image)!)else{ + fatalError("Unexpected runtime error") + } + + nameLabel.text = "Fruit Name:\(fruitRecognizer_1Output.classLabel)" + confidenceLabel.text = "Possibility:\(fruitRecognizer_1Output.classLabelProbs[fruitRecognizer_1Output. classLabel]!)" + pickerController.dismiss(animated:true, completion: nil) + } + + func buffer(from image: UIImage) ->CVPixelBuffer?{ + let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue]as CFDictionary + var pixelBuffer : CVPixelBuffer? + let status = CVPixelBufferCreate(kCFAllocatorDefault, +kCVPixelFormatType_32ARGB,attrs,&pixelBuffer) + guard(status == kCVReturnSuccess)else { + return nil + } + + CVPixelBufferLockBaseAddress(pixelBuffer!,CVPixelBufferLockFlags(rawValue:0)) + let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer!) + let rgbColorSpace = CGColorSpaceCreateDeviceRGB() + let context = CGContext(data: pixelData, width:Int (image.size.width), height: Int(image.size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!),space: rgbColorSpace,bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) + context?.translateBy(x: 0, y: image.size.height) + context?.scaleBy(x: 1.0, y: -1.0) + UIGraphicsPushContext(context!) + image.draw(in:CGRect(x:0,y:0, width:image.size.width, +height:image.size.height)) + UIGraphicsPushContext(context!) + image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) + UIGraphicsPopContext() + CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0)) + return pixelBuffer + } +} +``` + diff --git a/IOS/Task02:项目练习/贪吃蛇/贪吃蛇主体.md b/IOS/Task02:项目练习/贪吃蛇/贪吃蛇主体.md new file mode 100644 index 0000000..741f3a3 --- /dev/null +++ b/IOS/Task02:项目练习/贪吃蛇/贪吃蛇主体.md @@ -0,0 +1,355 @@ +# 贪吃蛇主体 + +## 贪吃蛇基本逻辑 + +想要对Swift有进一步了解,就不能忽略如何用Swift编写贪吃蛇。在上面我们已经学习了各种控件,接下来思考这样一些问题:贪吃蛇中蛇是什么控件?食物又是什么控件?如何才能让蛇身体随着吃食物而不断变长?如何让蛇在超出屏幕范围后回到另一边?如果你还没有思路,别着急,我们会一步一步解决。 + +对于贪吃蛇而言,最基本的功能包括行走、改变移动方向、吃食物等事件。我们需要从蛇、食物、整体游戏规则三个角度来进行构建。 + + + +## 关于蛇 + +一条完整的蛇应该包括蛇头和蛇的身体。当按上下左右键时,用户可以通过控制蛇头进而控制方向。当蛇头位置超出屏幕范围时,我们也可以重新定义蛇的位置让它出现在另一边。当蛇头的坐标与食物坐标相同时,食物消失,蛇身体的数组值加一,游戏积分增加。以下为初始化代码: + +```swift +override var frame: CGRect { + set { + if let next = nextItem { + next.frame.origin = frame.origin + } + super.frame.origin = newValue.origin + } + get { + return super.frame + } +} + +var nextItem: ListBodyItem? +override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = UIColor.red +} + +required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") +} +``` + +初始完成后,我们需要对蛇进行定义。首先从蛇身入手,定义一个 SnakeBody 类,初始长度为 1,代码如下: + +```swift +class SnakeBody { + var bodys = [ListBodyItem]() + //初始化身体时只有一段 + init() { + bodys.append(conBody(frame: CGRect(x: 60, y: 90, width: 30, height: 3 0))) + } +} +``` + +很好,接下来我们接着在 `SnakeBody` 中定义增加身体长度的函数 `addOne`,代码如下: + +```swift +func addOne(view: UIView) { + let curLast = bodys.last! + bodys.append(conBody(frame: curLast.frame)) + curLast.nextItem = bodys.last + view.addSubview(bodys.last!) +} + +//构造一个 bodyItem +func conBody(frame: CGRect) -> ListBodyItem { + let body = ListBodyItem(frame: frame) + return body +} +``` + +至此,我们实现了蛇身长度增加的函数,接下来我们定义蛇头,通过一个错误处理保证程序不会崩溃,下面的代码实现当蛇超出屏幕尺寸时,会在屏幕另一端出现: + +```swift +class SnakeHead: ListBodyItem { + func touchEdge(){ + if (frame.origin.x < 12) { + frame.origin.x = 390 + } else if (frame.origin.x > 390) { + frame.origin.x = 12 + } else if (frame.origin.y < 50) { + frame.origin.y = 890 + } else if (frame.origin.y > 890) { + frame.origin.y = 50 + } + } init() { + super.init(frame: CGRect(x: 90, y: 90, width: 30, height: 30)) } + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} +``` + +接下来将蛇头与蛇身拼接,并定义移动函数、改变移动方向函数与吃食物函数: + +```swift +class Snake { + let head = SnakeHead() + let body = SnakeBody() + var direction = Direction.RIGHT + weak var mainView: UIView! + init(view: UIView) { //初始化定义蛇身长度数组 + mainView = view + head.nextItem = body.bodys[0] + view.addSubview(head) + view.addSubview(body.bodys[0]) + } + + @objc func walk() { + direction.walk(point: &head.frame.origin) + head.touchEdge() + } + + func changeDirection(point: CGPoint) { + direction.changeDirection(cur: head.frame.origin, target: point) + } + + func eat() { + body.addOne(view: mainView) + } +} +``` + +请注意,我们在行走函数时用了 `Objective-C` 方法。此外,在 `eat()`函数中,我们通过 `addOne` 来实现自身长度的增加,通过对蛇头坐标方向的改变来实现整条蛇的方向改变,上面的代码并不难理解,如果你没太明白,不要着急,请先继续读下去。 + + + +## 食物、动作捕捉与主继承器 + +上面我们完成了蛇的定义,整体上就完成了百分之九十的内容。如何构造食物与接下来的部分?我们通过引入随机函数,将食物的出现坐标交给随机函数并进行强行取整,从而达到食物随机出现的效果,值得注意的是,我们要充分考虑食物的坐标,从而让蛇能够正确吃到它。对于主继承器而言,我们需要启用一个计时器,并在计时器方法中添加蛇行走函数, 此时需要用到 `Objective-C` 的方法。我们还需要添加手势动作捕捉,当我们做出改变方向命令时能够使蛇做出相应改变。由于剩余部分为一个整体,在此就不进行代码罗列,具体内容请阅读参考代码。 + + + +## 参考代码(食物) + +```swift +import UIKit +class Food: UIButton { + let foodX = 30 + let foodY = 30 + let screenX = 330 + let screenY = 630 + init(view: UIView) { + //初始化在屏幕中央 + super.init(frame: CGRect(x: 180, y: 330, width: foodX, height: foodY)) + //设置食物的颜色为蓝色 + backgroundColor = UIColor.blue + //将 Food 添加到主 view 中显示 + view.addSubview(self) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func changeFood(button: UIButton) { + button.frame.origin = randomPos() + } + + func randomNum(num: Int) -> Int { + return Int(arc4random_uniform(UInt32(num))) + } + + func randomPos() -> CGPoint { + //对产生的随机数按食物大小取整,方便后面蛇的吃食操作 + func round(value: Int) -> Int { + return (value/foodX) * foodX + } + + let curX = round(value: randomNum(num: screenX)) + let curY = round(value: 60 + randomNum(num: screenY)) + //60 为屏幕的导航栏 + + return CGPoint(x: curX, y: curY) + } +} +``` + + + +## 参考代码(方向) + +```swift +import UIKit +enum Direction { + case UP case DOWN + case LEFT + case RIGHT +} + +extension Direction { + func walk( point: inout CGPoint) { + switch self { + case .UP: point.y -= CGFloat(30) + case .DOWN: point.y += CGFloat(30) + case .LEFT: point.x -= CGFloat(30) + case .RIGHT: point.x += CGFloat(30) } + } + + mutating func changeDirection(cur: CGPoint, target: CGPoint) { + switch self { + case .UP: self = cur.x > target.x ? .LEFT : .RIGHT + case .DOWN: self = cur.x > target.x ? .LEFT : .RIGHT + case .LEFT: self = cur.y > target.y ? .UP : .DOWN + case .RIGHT: self = cur.y > target.y ? .UP : .DOWN + } + } +} +``` + + + +## 参考代码(SnakeViewController) + +```swift +import UIKit +class SnakeViewController: UIViewController { + var food:Food! + var snake: Snake! + var time: Timer! + var isPause = false + override func viewDidLoad() { + super.viewDidLoad() + food = Food(view: view) + snake = Snake(view: view) + let timer:Timer? = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(walk), userInfo: nil, repeats: true) + timer?.fire() + } + + @IBAction func pause(_ sender: UIBarButtonItem) { + isPause = !isPause + if (isPause) { + sender.title = "开始" + time.fireDate = NSDate.distantFuture + }else{ + sender.title = "暂停" + time.fireDate = NSDate.distantPast + } + } + + var score = 0 //记录分数 + @objc func walk() { + if food.frame.origin == snake.head.frame.origin { + food.changeFood(button: food) snake.eat() + score += 10 + navigationItem.title = String(score) + } + snake.walk() + } + + @IBAction func changeDirection(_ sender: UITapGestureRecognizer) { + let tapPoint = sender.location(in: self.view) + snake.changeDirection(point: tapPoint) + } +} +``` + + + +## 参考代码(蛇) + +```swift +import UIKit +class ListBodyItem: UIButton { + override var frame: CGRect { + set { + if let next = nextItem { + next.frame.origin = frame.origin + } + super.frame.origin = newValue.origin + } + get { + return super.frame + } + } + var nextItem: ListBodyItem? + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = UIColor.red + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class SnakeHead: ListBodyItem { + func touchEdge(){ + if (frame.origin.x < 12) { + frame.origin.x = 390 + } else if (frame.origin.x > 390) { + frame.origin.x = 12 + } else if (frame.origin.y < 50) { + frame.origin.y = 890 + } else if (frame.origin.y > 890) { + frame.origin.y = 50 + } + } + + init() { + super.init(frame: CGRect(x: 90, y: 90, width: 30, height: 30)) + } + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class SnakeBody { + var bodys = [ListBodyItem]() + //初始化身体时只有一段 + init() { + bodys.append(conBody(frame: CGRect(x: 60, y: 90, width: 30, height: 3 0))) + } + //为蛇添加一段(完成一次吃食后调用) + func addOne(view: UIView) { + let curLast = bodys.last! + bodys.append(conBody(frame: curLast.frame)) + curLast.nextItem = bodys.last + view.addSubview(bodys.last!) + } + + //构造一个 bodyItem + func conBody(frame: CGRect) -> ListBodyItem { + let body = ListBodyItem(frame: frame) + return body + } +} + +class Snake { + let head = SnakeHead() + let body = SnakeBody() + var direction = Direction.RIGHT + weak var mainView: UIView! + init(view: UIView) { + mainView = view + head.nextItem = body.bodys[0] + view.addSubview(head) + view.addSubview(body.bodys[0]) } + + @objc func walk() { + direction.walk(point: &head.frame.origin) + head.touchEdge() + } + + func changeDirection(point: CGPoint) { + direction.changeDirection(cur: head.frame.origin, target: point) + } + + func eat() { + body.addOne(view: mainView) + } +} +``` + +恭喜,你已经完成了对 Swift 中控件的基本把握。上述内容涵盖了绝大部分控件的使用,需要你熟练运用并且试着掌握他们。此时,你可以试着构建自己的一些简单 app了!不过,想要成为一名合格的工程师仅仅靠上述内容是远远不够的,你需要更多的知识拓宽你的视野。 + +## 思考与练习 + +上述贪吃蛇代码有什么漏洞?如何修改这些漏洞?如何美化我们的界面和图形?请你根据相关资料,对上述代码进行修改,并完善自己的贪吃蛇项目。 diff --git a/IOS/Task02:项目练习/酒店管理系统/代码:OverviewTableViewController.md b/IOS/Task02:项目练习/酒店管理系统/代码:OverviewTableViewController.md new file mode 100644 index 0000000..c50a142 --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/代码:OverviewTableViewController.md @@ -0,0 +1,55 @@ +# 代码:OverviewTableViewController + +```swift +struct Registration { + var name: String + var telNumber: String + var IDNumber: String + var checkInDate: Date + var checkOutDate: Date + var numberOfAdults: Int + var numberOfChildren: Int + var roomType: RoomType + var wifi: Bool +} + +class OverviewTableViewController: UITableViewController { + var registrations: [Registration] = [] + //registration 是继承自 1 页面 + override func viewDidLoad() { + super.viewDidLoad() + } + + //新单元只有一个 section + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return registrations.count + } + + //新建 cell,在新页面中想要显示什么格式就怎么定义 cell + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell:UITableViewCell = UITableViewCell.init(style: UITableViewCell.CellStyle.subtitle, reuseIdentifier: nil) + let registration = registrations[indexPath.row] + //dateFormatter 为自带 + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .short + cell.textLabel?.text = registration.name + " " + registration.telNumber + cell.detailTextLabel?.text = dateFormatter.string(from: registration.checkInDate) + " 至 " + dateFormatter.string(from: registration.checkOutDate) + ": " + registration.roomType.name + " 身份证号:" + registration.IDNumber + return cell + } + + @IBAction func unwindFromAddRegistration(unwindSegue: UIStoryboardSegue) { + //添加回退,先定义回退,后进行 button 的对接 + guard let tableViewController = unwindSegue.source as? TableViewController, + let registration = tableViewController.registration else { return } + registrations.append(registration) + tableView.reloadData() + } +} +``` + + + diff --git a/IOS/Task02:项目练习/酒店管理系统/代码:RoomTypeTableViewController.md b/IOS/Task02:项目练习/酒店管理系统/代码:RoomTypeTableViewController.md new file mode 100644 index 0000000..7b2d45e --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/代码:RoomTypeTableViewController.md @@ -0,0 +1,69 @@ +# 代码:RoomTypeTableViewController + +```swift +protocol RoomTypeTableViewControllerDelegate:class { + //协议传值,自定义 RoomType 类型 + func didSelect(roomType: RoomType) +} + +//定义结构体roomtType,static func若放到结构体外会报错 +struct RoomType: Equatable { + var id: Intvar name: String + var shortName: String + var price: Int + static func ==(lhs: RoomType, rhs: RoomType) -> Bool { + return lhs.id == rhs.id + } +} + +class RoomTypeTableViewController: UITableViewController { + weak var delegate: RoomTypeTableViewControllerDelegate? + static var all: [RoomType] { + return + [RoomType(id: 0, name: "单人间", shortName: "1 人住", price: 179), + RoomType(id: 1, name: "双人间", shortName: "2 人住", price: 209), + RoomType(id: 2, name: "总统套房", shortName: "3人住", price:309)] + } + + var currentRoomT:String = "" + override func viewDidLoad() { + super.viewDidLoad() + } + + //新页面只需要一个 section + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + //该 section 共有 all.count 个 row + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return RoomTypeTableViewController.all.count + } + //接下来定义 cell + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + //value1 为 style 中的 Right detail + let cell:UITableViewCell = UITableViewCell.init(style: UITableViewCell.CellStyle.value1, reuseIdentifier: nil) + //将数组导入定义中 + let roomType = RoomTypeTableViewController.all[indexPath.row] + cell.textLabel?.text = roomType.name + cell.detailTextLabel?.text = "$ \(roomType.price)" + + //选中后出现Right + if roomType == self.roomType { + cell.accessoryType = .checkmark + self.currentRoomT = roomType.name + }else { + cell.accessoryType = .none + } + return cell + } + var roomType: RoomType? + //1-2 传值 + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + roomType = RoomTypeTableViewController.all[indexPath.row] + delegate?.didSelect(roomType: roomType!) + tableView.reloadData() + } +} +``` + diff --git a/IOS/Task02:项目练习/酒店管理系统/代码:TableViewController.md b/IOS/Task02:项目练习/酒店管理系统/代码:TableViewController.md new file mode 100644 index 0000000..adc7995 --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/代码:TableViewController.md @@ -0,0 +1,225 @@ +# 代码:TableViewController + +```swift +class TableViewController: UITableViewController,RoomTypeTableViewControllerDelegate{ + //定义基本输入量 + @IBOutlet weak var nameLabel: UITextField! + @IBOutlet weak var telNumberLabel: UITextField! + @IBOutlet weak var IDCardLabel: UITextField! + @IBOutlet weak var checkInDateLabel: UILabel! + @IBOutlet weak var checkOutDateLabel: UILabel! + @IBOutlet weak var checkInDatePicker: UIDatePicker! + @IBOutlet weak var checkOutDatePicker: UIDatePicker! + @IBOutlet weak var roomTypeSelected: UILabel! + var roomType: RoomType? + + //继承后的传值种类 + func didSelect(roomType: RoomType) { + self.roomType = roomType updateRoomType() + } + + //取消当前页面(动画) + @IBAction func cancelOfCurrentPage(_ sender: UIBarButtonItem) { + dismiss(animated: true, completion: nil) + } + + //Done 按钮的设置,按下时对当前页面信息进行保存 + @IBAction func doneBarButtonTapped(_ sender: Any) { + let name = nameLabel.text ?? "" + let telNumber = telNumberLabel.text ?? "" + let IDNumber = IDCardLabel.text ?? "" + let checkInDate = checkInDatePicker.date + let checkOutDate = checkOutDatePicker.date + let numberOfAdults = Int(numberOfAdultsStepper.value) + let numberOfChildren = Int(numberOfKidsStepper.value) + let hasWifi = wifiSwitch.isOn + let roomChoice = roomType?.name + + print("信息录入:") + print("姓名: \(name)") + print("电话: \(telNumber)") + print("身份证号: \(IDNumber)") + print("入住日期: \(checkInDate)") + print("退房日期: \(checkOutDate)") + print("成人: \(numberOfAdults)") + print("儿童: \(numberOfChildren)") + print("wifi: \(hasWifi)") + print("房间类型: \(String(describing: roomChoice))") + } + + //以下对成人、儿童数量进行统计 + @IBOutlet weak var numberOfAdultsLabel: UILabel! + @IBOutlet weak var numberOfKidsLabel: UILabel! + @IBOutlet weak var numberOfKidsStepper: UIStepper! + @IBOutlet weak var numberOfAdultsStepper: UIStepper! + + //更新客人数量 + func updateNumberOfGuests() { + numberOfAdultsLabel.text = "\(Int(numberOfAdultsStepper.value))" + numberOfKidsLabel.text = "\(Int(numberOfKidsStepper.value))" + } + + //更新房间类型 + func updateRoomType() { + if let roomType = roomType { + roomTypeSelected.text = roomType.name + }else{ + roomTypeSelected.text = "请选择" + //tips:若此处值为空,则后面无论如何都不会显示 + } + } + + //检测 stepper 是否变化,若变化则进行实时动态更新 + @IBAction func stepperDidChanged(_ sender: UIStepper) { + updateNumberOfGuests() + } + + //以下为 WI-FI 内容设置 + @IBOutlet weak var wifiSwitch: UISwitch! + @IBAction func WiFiSwitch(_ sender: UISwitch) { + //自定义一些内容 + } + + //在此处进行两个动态更新:1.更新顾客数量,2.更新房间种类 + override func viewDidLoad() { + super.viewDidLoad() + updateNumberOfGuests() + updateRoomType() + } + + //设置单元格 section + override func numberOfSections(in tableView: UITableView) -> Int { + return 5 + } + + //设置每个 section 的 cell 数量 + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0{ + return 3 + }else if section == 1{ + return 4 + }else if section == 2{ + return 2 + }else if section == 3{ + return 1 + }else { + return 1 + } + } + + //代码初始化入住日期、退房日期,此处若与 storyboard 中不同,程序会崩溃 + let checkInDatePickerCellIndexPath = IndexPath(row: 1, section: 1) + let checkOutDatePickerCellIndexPath = IndexPath(row: 3, section: 1) + //入住日期 + var isCheckInDatePickerShown: Bool = false { + didSet { + checkInDatePicker.isHidden = !isCheckInDatePickerShown + } + } + + //退房日期 + var isCheckOutDatePickerShown: Bool = false { + didSet { + checkOutDatePicker.isHidden = !isCheckOutDatePickerShown + } + } + + //设置 cell 高度,此处若与 storyboard 中不同,程序会崩溃 + override func tableView(_ tableView: UITableView,heightForRowAt indexPath: IndexPath) -> CGFloat { + //通过 switch 选择 Bool 值初始化 static cell + switch (indexPath.section, indexPath.row) { + case (checkInDatePickerCellIndexPath.section, checkInDatePickerCellIndexPath.row): + if isCheckInDatePickerShown { + return 216.0 + }else{ + return 0.0 + } + case (checkOutDatePickerCellIndexPath.section, checkOutDatePickerCellIndexPath.row): if isCheckOutDatePickerShown { + return 216.0 + }else{ + return 0.0 + } + default: return 44.0 + } + } + + //选中后隐藏 + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){ + tableView.deselectRow(at: indexPath, animated: true) + //选中后第二个 section 的 row 减 1,对 Bool 值进行赋值 + switch (indexPath.section, indexPath.row) { + case ( + checkInDatePickerCellIndexPath.section, checkInDatePickerCellIndexPath.row - 1): + if isCheckInDatePickerShown { + isCheckInDatePickerShown = false + } else if isCheckOutDatePickerShown { + isCheckOutDatePickerShown = false + isCheckInDatePickerShown = true + }else{ + isCheckInDatePickerShown = true + } + tableView.beginUpdates() + tableView.endUpdates() + + case (checkOutDatePickerCellIndexPath.section, + checkOutDatePickerCellIndexPath.row - 1): + if isCheckOutDatePickerShown { + isCheckOutDatePickerShown = false + } else if isCheckInDatePickerShown { + isCheckInDatePickerShown = false + isCheckOutDatePickerShown = true + }else{ + isCheckOutDatePickerShown = true + } + tableView.beginUpdates() + tableView.endUpdates() + default: + break + } + + //转换 Date 格式,将 Date 变为 String,此处代码放到以上设置前会报错,程序崩溃 + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + /*dateStyle 中: + .none:无 + .short:“11/23/37” + .medium:"Nov 23, 1937" + .long:"Novermber 23, 1937" + .full:"Tuesday,April 12,1952" */ + + /*timeStyle 中: + .none:无 + .short:"3:30 PM" + .medium:"3:30:32 PM" + .long:"3:30:32 PM PST" + .full:"3:30:42 PM Pacific Standard Time"*/ + + checkInDateLabel.text = dateFormatter.string(from: checkInDatePicker.date) + checkOutDateLabel.text = dateFormatter.string(from:checkOutDatePicker.date) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "SelectRoomType" { + let destinationViewController = segue.destination as? RoomTypeTableViewController + destinationViewController?.delegate = self + destinationViewController?.roomType = roomType + } + } + + //1-3 页面传值 + var registration: Registration? { + guard let roomType = roomType else { + return nil } + let name = nameLabel.text ?? "" + let telNumber = telNumberLabel.text ?? "" + let IDCard = IDCardLabel.text ?? "" + let checkInDate = checkInDatePicker.date + let checkOutDate = checkOutDatePicker.date + let numberOfAdults = Int(numberOfAdultsStepper.value) + let numberOfChildren = Int(numberOfKidsStepper.value) + let hasWifi = wifiSwitch.isOn + return Registration(name: name, telNumber: telNumber, IDNumber: IDCard, checkInDate: checkInDate, checkOutDate: checkOutDate, numberOfAdults: numberOfAdults, numberOfChildren: numberOfChildren, roomType: roomType, wifi: hasWifi) + } +} +``` + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.pbxproj b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.pbxproj new file mode 100644 index 0000000..c1bb952 --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.pbxproj @@ -0,0 +1,575 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 04109C7724E131C80017D3B9 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04109C7624E131C80017D3B9 /* TableViewController.swift */; }; + 04109C7924E13AB40017D3B9 /* RoomTypeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04109C7824E13AB40017D3B9 /* RoomTypeTableViewController.swift */; }; + 04178AC024E0012C0073E92F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04178ABF24E0012C0073E92F /* AppDelegate.swift */; }; + 04178AC224E0012C0073E92F /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04178AC124E0012C0073E92F /* SceneDelegate.swift */; }; + 04178AC424E0012C0073E92F /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04178AC324E0012C0073E92F /* ViewController.swift */; }; + 04178AC724E0012C0073E92F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 04178AC524E0012C0073E92F /* Main.storyboard */; }; + 04178AC924E0012D0073E92F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 04178AC824E0012D0073E92F /* Assets.xcassets */; }; + 04178ACC24E0012D0073E92F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 04178ACA24E0012D0073E92F /* LaunchScreen.storyboard */; }; + 04B5E9E524E3638200F1C5A4 /* OverviewTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B5E9E424E3638200F1C5A4 /* OverviewTableViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 04178AD324E0012D0073E92F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 04178AB424E0012C0073E92F /* Project object */; + proxyType = 1; + remoteGlobalIDString = 04178ABB24E0012C0073E92F; + remoteInfo = "进阶功能"; + }; + 04178ADE24E0012D0073E92F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 04178AB424E0012C0073E92F /* Project object */; + proxyType = 1; + remoteGlobalIDString = 04178ABB24E0012C0073E92F; + remoteInfo = "进阶功能"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 04109C7624E131C80017D3B9 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; + 04109C7824E13AB40017D3B9 /* RoomTypeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTypeTableViewController.swift; sourceTree = ""; }; + 04178ABC24E0012C0073E92F /* Stay with me.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Stay with me.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 04178ABF24E0012C0073E92F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 04178AC124E0012C0073E92F /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 04178AC324E0012C0073E92F /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 04178AC624E0012C0073E92F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 04178AC824E0012D0073E92F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 04178ACB24E0012D0073E92F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 04178ACD24E0012D0073E92F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 04178AD224E0012D0073E92F /* 进阶功能Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "进阶功能Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 04178ADD24E0012D0073E92F /* 进阶功能UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "进阶功能UITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 04B5E9E424E3638200F1C5A4 /* OverviewTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverviewTableViewController.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 04178AB924E0012C0073E92F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 04178ACF24E0012D0073E92F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 04178ADA24E0012D0073E92F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 04178AB324E0012C0073E92F = { + isa = PBXGroup; + children = ( + 04178ABE24E0012C0073E92F /* 进阶功能 */, + 04178ABD24E0012C0073E92F /* Products */, + ); + sourceTree = ""; + }; + 04178ABD24E0012C0073E92F /* Products */ = { + isa = PBXGroup; + children = ( + 04178ABC24E0012C0073E92F /* Stay with me.app */, + 04178AD224E0012D0073E92F /* 进阶功能Tests.xctest */, + 04178ADD24E0012D0073E92F /* 进阶功能UITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 04178ABE24E0012C0073E92F /* 进阶功能 */ = { + isa = PBXGroup; + children = ( + 04178ABF24E0012C0073E92F /* AppDelegate.swift */, + 04178AC124E0012C0073E92F /* SceneDelegate.swift */, + 04178AC324E0012C0073E92F /* ViewController.swift */, + 04178AC524E0012C0073E92F /* Main.storyboard */, + 04B5E9E424E3638200F1C5A4 /* OverviewTableViewController.swift */, + 04109C7824E13AB40017D3B9 /* RoomTypeTableViewController.swift */, + 04109C7624E131C80017D3B9 /* TableViewController.swift */, + 04178AC824E0012D0073E92F /* Assets.xcassets */, + 04178ACA24E0012D0073E92F /* LaunchScreen.storyboard */, + 04178ACD24E0012D0073E92F /* Info.plist */, + ); + path = "进阶功能"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 04178ABB24E0012C0073E92F /* 进阶功能 */ = { + isa = PBXNativeTarget; + buildConfigurationList = 04178AE624E0012E0073E92F /* Build configuration list for PBXNativeTarget "进阶功能" */; + buildPhases = ( + 04178AB824E0012C0073E92F /* Sources */, + 04178AB924E0012C0073E92F /* Frameworks */, + 04178ABA24E0012C0073E92F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "进阶功能"; + productName = "进阶功能"; + productReference = 04178ABC24E0012C0073E92F /* Stay with me.app */; + productType = "com.apple.product-type.application"; + }; + 04178AD124E0012D0073E92F /* 进阶功能Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 04178AE924E0012E0073E92F /* Build configuration list for PBXNativeTarget "进阶功能Tests" */; + buildPhases = ( + 04178ACE24E0012D0073E92F /* Sources */, + 04178ACF24E0012D0073E92F /* Frameworks */, + 04178AD024E0012D0073E92F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 04178AD424E0012D0073E92F /* PBXTargetDependency */, + ); + name = "进阶功能Tests"; + productName = "进阶功能Tests"; + productReference = 04178AD224E0012D0073E92F /* 进阶功能Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 04178ADC24E0012D0073E92F /* 进阶功能UITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 04178AEC24E0012E0073E92F /* Build configuration list for PBXNativeTarget "进阶功能UITests" */; + buildPhases = ( + 04178AD924E0012D0073E92F /* Sources */, + 04178ADA24E0012D0073E92F /* Frameworks */, + 04178ADB24E0012D0073E92F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 04178ADF24E0012D0073E92F /* PBXTargetDependency */, + ); + name = "进阶功能UITests"; + productName = "进阶功能UITests"; + productReference = 04178ADD24E0012D0073E92F /* 进阶功能UITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 04178AB424E0012C0073E92F /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1130; + LastUpgradeCheck = 1130; + ORGANIZATIONNAME = Yurk; + TargetAttributes = { + 04178ABB24E0012C0073E92F = { + CreatedOnToolsVersion = 11.3.1; + }; + 04178AD124E0012D0073E92F = { + CreatedOnToolsVersion = 11.3.1; + TestTargetID = 04178ABB24E0012C0073E92F; + }; + 04178ADC24E0012D0073E92F = { + CreatedOnToolsVersion = 11.3.1; + TestTargetID = 04178ABB24E0012C0073E92F; + }; + }; + }; + buildConfigurationList = 04178AB724E0012C0073E92F /* Build configuration list for PBXProject "进阶功能" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 04178AB324E0012C0073E92F; + productRefGroup = 04178ABD24E0012C0073E92F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 04178ABB24E0012C0073E92F /* 进阶功能 */, + 04178AD124E0012D0073E92F /* 进阶功能Tests */, + 04178ADC24E0012D0073E92F /* 进阶功能UITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 04178ABA24E0012C0073E92F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 04178ACC24E0012D0073E92F /* LaunchScreen.storyboard in Resources */, + 04178AC924E0012D0073E92F /* Assets.xcassets in Resources */, + 04178AC724E0012C0073E92F /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 04178AD024E0012D0073E92F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 04178ADB24E0012D0073E92F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 04178AB824E0012C0073E92F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 04B5E9E524E3638200F1C5A4 /* OverviewTableViewController.swift in Sources */, + 04109C7924E13AB40017D3B9 /* RoomTypeTableViewController.swift in Sources */, + 04178AC424E0012C0073E92F /* ViewController.swift in Sources */, + 04178AC024E0012C0073E92F /* AppDelegate.swift in Sources */, + 04178AC224E0012C0073E92F /* SceneDelegate.swift in Sources */, + 04109C7724E131C80017D3B9 /* TableViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 04178ACE24E0012D0073E92F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 04178AD924E0012D0073E92F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 04178AD424E0012D0073E92F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 04178ABB24E0012C0073E92F /* 进阶功能 */; + targetProxy = 04178AD324E0012D0073E92F /* PBXContainerItemProxy */; + }; + 04178ADF24E0012D0073E92F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 04178ABB24E0012C0073E92F /* 进阶功能 */; + targetProxy = 04178ADE24E0012D0073E92F /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 04178AC524E0012C0073E92F /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 04178AC624E0012C0073E92F /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 04178ACA24E0012D0073E92F /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 04178ACB24E0012D0073E92F /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 04178AE424E0012E0073E92F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 04178AE524E0012E0073E92F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 04178AE724E0012E0073E92F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = MP9VANH4ND; + INFOPLIST_FILE = "进阶功能/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "Yurk.----"; + PRODUCT_NAME = "Stay with me"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 04178AE824E0012E0073E92F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = MP9VANH4ND; + INFOPLIST_FILE = "进阶功能/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "Yurk.----"; + PRODUCT_NAME = "Stay with me"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 04178AEA24E0012E0073E92F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "进阶功能Tests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "Yurk.----Tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/进阶功能.app/进阶功能"; + }; + name = Debug; + }; + 04178AEB24E0012E0073E92F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "进阶功能Tests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "Yurk.----Tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/进阶功能.app/进阶功能"; + }; + name = Release; + }; + 04178AED24E0012E0073E92F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "进阶功能UITests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "Yurk.----UITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "进阶功能"; + }; + name = Debug; + }; + 04178AEE24E0012E0073E92F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "进阶功能UITests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "Yurk.----UITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "进阶功能"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 04178AB724E0012C0073E92F /* Build configuration list for PBXProject "进阶功能" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 04178AE424E0012E0073E92F /* Debug */, + 04178AE524E0012E0073E92F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 04178AE624E0012E0073E92F /* Build configuration list for PBXNativeTarget "进阶功能" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 04178AE724E0012E0073E92F /* Debug */, + 04178AE824E0012E0073E92F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 04178AE924E0012E0073E92F /* Build configuration list for PBXNativeTarget "进阶功能Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 04178AEA24E0012E0073E92F /* Debug */, + 04178AEB24E0012E0073E92F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 04178AEC24E0012E0073E92F /* Build configuration list for PBXNativeTarget "进阶功能UITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 04178AED24E0012E0073E92F /* Debug */, + 04178AEE24E0012E0073E92F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 04178AB424E0012C0073E92F /* Project object */; +} diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..b0a242f --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/xcuserdata/mac.xcuserdatad/UserInterfaceState.xcuserstate b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/xcuserdata/mac.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..2d0329b Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/xcuserdata/mac.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/xcuserdata/mac.xcuserdatad/WorkspaceSettings.xcsettings b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/xcuserdata/mac.xcuserdatad/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..dd7403b --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/project.xcworkspace/xcuserdata/mac.xcuserdatad/WorkspaceSettings.xcsettings @@ -0,0 +1,16 @@ + + + + + BuildLocationStyle + UseAppPreferences + CustomBuildLocationType + RelativeToDerivedData + DerivedDataLocationStyle + Default + IssueFilterStyle + ShowActiveSchemeOnly + LiveSourceIssuesEnabled + + + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/xcuserdata/mac.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/xcuserdata/mac.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..4e42b2c --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/xcuserdata/mac.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,494 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/xcuserdata/mac.xcuserdatad/xcschemes/xcschememanagement.plist b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/xcuserdata/mac.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..4a7269c --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能.xcodeproj/xcuserdata/mac.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + 进阶功能.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/AppDelegate.swift b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/AppDelegate.swift new file mode 100644 index 0000000..47b6956 --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/AppDelegate.swift @@ -0,0 +1,37 @@ +// +// AppDelegate.swift +// 进阶功能 +// +// Created by mac on 2020/8/9. +// Copyright © 2020 Yurk. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/Contents.json b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..c20c7e3 --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,168 @@ +{ + "images" : [ + { + "filename" : "icon-20@2x-ipad.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "icon-20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "icon-29-ipad.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "icon-29@2x-ipad.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon-29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "icon-40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "icon-40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "icon-60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "icon-60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "filename" : "icon-83.5@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon-20-ipad.png", + "idiom" : "universal", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "icon-40.png", + "idiom" : "universal", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "icon-76.png", + "idiom" : "universal", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "icon-76@2x.png", + "idiom" : "universal", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "icon-1024.png", + "idiom" : "universal", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "filename" : "icon-20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20", + "unassigned" : true + }, + { + "filename" : "icon-30.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29", + "unassigned" : true + }, + { + "filename" : "icon-29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29", + "unassigned" : true + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-1024.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-1024.png new file mode 100644 index 0000000..06583d0 Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-1024.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png new file mode 100644 index 0000000..11e776b Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png new file mode 100644 index 0000000..923ac74 Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png new file mode 100644 index 0000000..923ac74 Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png new file mode 100644 index 0000000..650c9ce Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png new file mode 100644 index 0000000..502be97 Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png new file mode 100644 index 0000000..f99ae8e Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png new file mode 100644 index 0000000..f99ae8e Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png new file mode 100644 index 0000000..67d20bf Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-30.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-30.png new file mode 100644 index 0000000..502be97 Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-30.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-40.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-40.png new file mode 100644 index 0000000..923ac74 Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-40.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png new file mode 100644 index 0000000..83196d3 Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png new file mode 100644 index 0000000..0773532 Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png new file mode 100644 index 0000000..0773532 Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png new file mode 100644 index 0000000..4832b12 Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-76.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-76.png new file mode 100644 index 0000000..5287264 Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-76.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png new file mode 100644 index 0000000..ca7c082 Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png new file mode 100644 index 0000000..88be5f4 Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/Contents.json b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/myImage.imageset/2BC6C878-FA06-4C8D-BA23-658281D3D4ED.jpeg b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/myImage.imageset/2BC6C878-FA06-4C8D-BA23-658281D3D4ED.jpeg new file mode 100644 index 0000000..856880f Binary files /dev/null and b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/myImage.imageset/2BC6C878-FA06-4C8D-BA23-658281D3D4ED.jpeg differ diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/myImage.imageset/Contents.json b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/myImage.imageset/Contents.json new file mode 100644 index 0000000..ed8be54 --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Assets.xcassets/myImage.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "2BC6C878-FA06-4C8D-BA23-658281D3D4ED.jpeg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Base.lproj/LaunchScreen.storyboard b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Base.lproj/Main.storyboard b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Base.lproj/Main.storyboard new file mode 100644 index 0000000..46ee3c1 --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Base.lproj/Main.storyboard @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Info.plist b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Info.plist new file mode 100644 index 0000000..2a3483c --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/Info.plist @@ -0,0 +1,64 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/OverviewTableViewController.swift b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/OverviewTableViewController.swift new file mode 100644 index 0000000..7443e87 --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/OverviewTableViewController.swift @@ -0,0 +1,70 @@ +// +// OverviewTableViewController.swift +// 进阶功能 +// +// Created by mac on 2020/8/12. +// Copyright © 2020 Yurk. All rights reserved. +// + +import UIKit +struct Registration { + var name: String + var telNumber: String + var IDNumber: String + var checkInDate: Date + var checkOutDate: Date + var numberOfAdults: Int + var numberOfChildren: Int + var roomType: RoomType + var wifi: Bool + } + +class OverviewTableViewController: UITableViewController { + + var registrations: [Registration] = [] + //registration是继承自1页面 + + override func viewDidLoad() { + super.viewDidLoad() + } + + //新单元只有一个section + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return registrations.count + } + + //新建cell,在新d页面中想要显示什么格式就怎么定义cell + override func tableView(_ tableView: UITableView, cellForRowAt + indexPath: IndexPath) -> UITableViewCell { + let cell:UITableViewCell = UITableViewCell.init(style: UITableViewCell.CellStyle.subtitle, reuseIdentifier: nil) + + let registration = registrations[indexPath.row] + + //dateFormatter为自带 + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .short + + cell.textLabel?.text = registration.name + " " + registration.telNumber + cell.detailTextLabel?.text = dateFormatter.string(from: registration.checkInDate) + " 至 " + dateFormatter.string(from: registration.checkOutDate) + ": " + registration.roomType.name + " 身份证号:" + registration.IDNumber + return cell + } + + @IBAction func unwindFromAddRegistration(unwindSegue: UIStoryboardSegue) { + //添加回退,先定义回退,后进行button的对接 + guard let tableViewController = unwindSegue.source as? TableViewController, + let registration = tableViewController.registration else { return } + registrations.append(registration) + tableView.reloadData() + } + + + + + +} + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/RoomTypeTableViewController.swift b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/RoomTypeTableViewController.swift new file mode 100644 index 0000000..60bbbd7 --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/RoomTypeTableViewController.swift @@ -0,0 +1,88 @@ +// +// RoomTypeTableViewController.swift +// 进阶功能 +// +// Created by mac on 2020/8/10. +// Copyright © 2020 Yurk. All rights reserved. +// + +import UIKit + +protocol RoomTypeTableViewControllerDelegate:class { + //协议传值,自定义RoomType类型 + func didSelect(roomType: RoomType) +} + + //定义结构体:roomtType,static func若放到结构体外会报错 + struct RoomType: Equatable { + var id: Int + var name: String + var shortName: String + var price: Int + static func ==(lhs: RoomType, rhs: RoomType) -> Bool { + return lhs.id == rhs.id + } +} + + +class RoomTypeTableViewController: UITableViewController { + weak var delegate: RoomTypeTableViewControllerDelegate? + //弱继承,weak避免循环 + + static var all: [RoomType] { + return [RoomType(id: 0, name: "单人间", shortName: "1人住", price: 179), + RoomType(id: 1, name: "双人间", shortName: "2人住", price: 209), + RoomType(id: 2, name: "总统套房", shortName: "几人都行", price: 309999)] + } + + + var currentRoomT:String = "" + + override func viewDidLoad() { + super.viewDidLoad() + } + + //新页面只需要一个section + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + //该section共有all.count个row + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return RoomTypeTableViewController.all.count + } + + //接下来定义cell + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + //value1为style中的Right detail + let cell:UITableViewCell = UITableViewCell.init(style: UITableViewCell.CellStyle.value1, reuseIdentifier: nil) + + //将数组导入定义中 + let roomType = RoomTypeTableViewController.all[indexPath.row] + cell.textLabel?.text = roomType.name + cell.detailTextLabel?.text = "$ \(roomType.price)" + + + //选中后出现✅ + if roomType == self.roomType { + cell.accessoryType = .checkmark + self.currentRoomT = roomType.name + }else { + cell.accessoryType = .none + } + return cell + } + + + var roomType: RoomType? + //1-2传值 + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + roomType = RoomTypeTableViewController.all[indexPath.row] + delegate?.didSelect(roomType: roomType!) + tableView.reloadData() + } + + +} diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/SceneDelegate.swift b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/SceneDelegate.swift new file mode 100644 index 0000000..c1c92ce --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/SceneDelegate.swift @@ -0,0 +1,53 @@ +// +// SceneDelegate.swift +// 进阶功能 +// +// Created by mac on 2020/8/9. +// Copyright © 2020 Yurk. All rights reserved. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/TableViewController.swift b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/TableViewController.swift new file mode 100644 index 0000000..954f6dd --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/TableViewController.swift @@ -0,0 +1,256 @@ +// +// TableViewController.swift +// 进阶功能 +// +// Created by mac on 2020/8/10. +// Copyright © 2020 Yurk. All rights reserved. +// + +import UIKit + + +class TableViewController: UITableViewController,RoomTypeTableViewControllerDelegate{ + //定义基本输入量 + @IBOutlet weak var nameLabel: UITextField! + @IBOutlet weak var telNumberLabel: UITextField! + @IBOutlet weak var IDCardLabel: UITextField! + @IBOutlet weak var checkInDateLabel: UILabel! + @IBOutlet weak var checkOutDateLabel: UILabel! + @IBOutlet weak var checkInDatePicker: UIDatePicker! + @IBOutlet weak var checkOutDatePicker: UIDatePicker! + @IBOutlet weak var roomTypeSelected: UILabel! + var roomType: RoomType? + + //继承后的传值种类 + func didSelect(roomType: RoomType) { + self.roomType = roomType + updateRoomType() + } + + + //取消当前页面(动画) + @IBAction func cancelOfCurrentPage(_ sender: UIBarButtonItem) { + dismiss(animated: true, completion: nil) + } + + //Done按钮的设置,按下时对当前页面信息进行保存 + @IBAction func doneBarButtonTapped(_ sender: Any) { + let name = nameLabel.text ?? "" + let telNumber = telNumberLabel.text ?? "" + let IDNumber = IDCardLabel.text ?? "" + let checkInDate = checkInDatePicker.date + let checkOutDate = checkOutDatePicker.date + let numberOfAdults = Int(numberOfAdultsStepper.value) + let numberOfChildren = Int(numberOfKidsStepper.value) + let hasWifi = wifiSwitch.isOn + let roomChoice = roomType?.name + + + + print("信息录入:") + print("姓名: \(name)") + print("电话: \(telNumber)") + print("身份证号: \(IDNumber)") + print("入住日期: \(checkInDate)") + print("退房日期: \(checkOutDate)") + print("成人: \(numberOfAdults)") + print("儿童: \(numberOfChildren)") + print("wifi: \(hasWifi)") + print("房间类型: \(String(describing: roomChoice))") + } + + //以下对成人、儿童数量进行统计从而计算钱数 + @IBOutlet weak var numberOfAdultsLabel: UILabel! + @IBOutlet weak var numberOfAdultsStepper: UIStepper! + @IBOutlet weak var numberOfKidsLabel: UILabel! + @IBOutlet weak var numberOfKidsStepper: UIStepper! + + //更新客人数量 + func updateNumberOfGuests() { + numberOfAdultsLabel.text = "\(Int(numberOfAdultsStepper.value))" + numberOfKidsLabel.text = "\(Int(numberOfKidsStepper.value))" + } + + //更新房间类型 + func updateRoomType() { + if let roomType = roomType { + roomTypeSelected.text = roomType.name + }else{ + roomTypeSelected.text = "请选择" + //tips:若此处值为空,则后面无论如何都不会显示 + } + } + + //检测stepper是否变化,若变化则进行实时动态更新 + @IBAction func stepperDidChanged(_ sender: UIStepper) { + updateNumberOfGuests() + } + + //以下为WI-FI内容设置 + @IBOutlet weak var wifiSwitch: UISwitch! + @IBAction func WiFiSwitch(_ sender: UISwitch) { + //暂时还没有内容 + } + + + + //在此处进行两个动态更新:1.更新顾客数量,2.更新房间种类 + override func viewDidLoad() { + super.viewDidLoad() + updateNumberOfGuests() + updateRoomType() + + } + + //设置单元格section + override func numberOfSections(in tableView: UITableView) -> Int { + return 5 + } + + //设置每个section的cell数量 + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0{ + return 3 + }else if section == 1{ + return 4 + }else if section == 2{ + return 2 + }else if section == 3{ + return 1 + }else { + return 1 + } + } + + //代码初始化入住日期、退房日期,此处若与storyboard中不同,程序会崩溃 + let checkInDatePickerCellIndexPath = IndexPath(row: 1, section: 1) + let checkOutDatePickerCellIndexPath = IndexPath(row: 3, section: 1) + //入住日期 + var isCheckInDatePickerShown: Bool = false { + didSet { + checkInDatePicker.isHidden = !isCheckInDatePickerShown + } + } + + //退房日期 + var isCheckOutDatePickerShown: Bool = false { + didSet { + checkOutDatePicker.isHidden = !isCheckOutDatePickerShown + } + } + + //设置cell高度,此处若与storyboard中不同,程序会崩溃 + override func tableView(_ tableView: UITableView,heightForRowAt indexPath: IndexPath) -> CGFloat { + //通过switch选择Bool值初始化static cell + switch (indexPath.section, indexPath.row) { + case (checkInDatePickerCellIndexPath.section, + checkInDatePickerCellIndexPath.row): + if isCheckInDatePickerShown { + return 216.0 + } else { + return 0.0 + } + case (checkOutDatePickerCellIndexPath.section, + checkOutDatePickerCellIndexPath.row): + if isCheckOutDatePickerShown { + return 216.0 + } else { + return 0.0 + } + default: + return 44.0 + } + } + + //选中后隐藏 + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){ tableView.deselectRow(at: indexPath, animated: true) + //选中后第二个section的row减1,对Bool值进行赋值 + switch (indexPath.section, indexPath.row) { + case (checkInDatePickerCellIndexPath.section, + checkInDatePickerCellIndexPath.row - 1): + if isCheckInDatePickerShown { + isCheckInDatePickerShown = false + } else if isCheckOutDatePickerShown { + isCheckOutDatePickerShown = false + isCheckInDatePickerShown = true + } else { + isCheckInDatePickerShown = true + } + tableView.beginUpdates() + tableView.endUpdates() + + case (checkOutDatePickerCellIndexPath.section, + checkOutDatePickerCellIndexPath.row - 1): + if isCheckOutDatePickerShown { + isCheckOutDatePickerShown = false + } else if isCheckInDatePickerShown { + isCheckInDatePickerShown = false + isCheckOutDatePickerShown = true + } else { + isCheckOutDatePickerShown = true + } + tableView.beginUpdates() + tableView.endUpdates() + default: + break + } + + //转换Date格式,将Date变为String,此处代码放到以上设置前会报错,程序崩溃 + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + /*dateStyle中: + .none:无 + .short:“11/23/37” + .medium:"Nov 23, 1937" + .long:"Novermber 23, 1937" + .full:"Tuesday,April 12,1952" + */ + + /*timeStyle中: + .none:无 + .short:"3:30 PM" + .medium:"3:30:32 PM" + .long:"3:30:32 PM PST" + .full:"3:30:42 PM Pacific Standard Time" + */ + checkInDateLabel.text = dateFormatter.string(from: + checkInDatePicker.date) + checkOutDateLabel.text = dateFormatter.string(from: + checkOutDatePicker.date) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "SelectRoomType" { + let destinationViewController = segue.destination as? + RoomTypeTableViewController + destinationViewController?.delegate = self + destinationViewController?.roomType = roomType + } + + } + + //1-3页面传值 + var registration: Registration? { + + guard let roomType = roomType else { return nil } + + let name = nameLabel.text ?? "" + let telNumber = telNumberLabel.text ?? "" + let IDCard = IDCardLabel.text ?? "" + let checkInDate = checkInDatePicker.date + let checkOutDate = checkOutDatePicker.date + let numberOfAdults = Int(numberOfAdultsStepper.value) + let numberOfChildren = Int(numberOfKidsStepper.value) + let hasWifi = wifiSwitch.isOn + + return Registration(name: name, telNumber: telNumber, IDNumber: IDCard, checkInDate: checkInDate, checkOutDate: checkOutDate, numberOfAdults: numberOfAdults, numberOfChildren: numberOfChildren, roomType: roomType, wifi: hasWifi) + + } + + + + +} + + + diff --git a/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/ViewController.swift b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/ViewController.swift new file mode 100644 index 0000000..410da8f --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/进阶功能/进阶功能/ViewController.swift @@ -0,0 +1,20 @@ +// +// ViewController.swift +// 进阶功能 +// +// Created by mac on 2020/8/9. +// Copyright © 2020 Yurk. All rights reserved. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + + +} + diff --git a/IOS/Task02:项目练习/酒店管理系统/酒店管理系统主体.md b/IOS/Task02:项目练习/酒店管理系统/酒店管理系统主体.md new file mode 100644 index 0000000..ee7406b --- /dev/null +++ b/IOS/Task02:项目练习/酒店管理系统/酒店管理系统主体.md @@ -0,0 +1,316 @@ +# 酒店管理系统 + +## 酒店管理系统基本思路 + +本章中,我们将为一个酒店编写入住管理系统,作为 app 的使用者来说,我们希望能够看到客人的姓名、电话、身份证号码、入住日期、退房日期、成人/儿童数量、是否需要 Wi-Fi、房间种类。根据变量性质的不同,我们将姓名、电话、身份证号设置成一组,通过 `TextField` 输入。将入住日期、退房日期放到一组,通过 `DatePicker` 控件选择日期。将是否需要 Wi-Fi 及房间种类分别放置一组。本节中将对 `DatePicker` 进行设置,从而实现能够通过 `DatePicker` 选择日期,并且当我们点击 `cell` 时能够出现/隐藏 `DatePicker`。本次任务中,你将掌握多页面传值,在不同页面设置回退,以及 `segue` 代理。 + + + +## Static cell 初始设置 + +初始化 `Navigation Controller`(附带 `TableViewController`),在 storyboard 中设置 `Navigation` 的相关 title 以及属性,添加 section 及 cell (注:storyboard 中的设置若与与代码中不相符,编译器会报错,程序崩溃)。在 File 中新建 `TableViewController.swift` 文件,将 storyboard 与之连接,不需要代理 + + + +## 设置 cell 内容 + +```swfit +Button:在 Navigation bar 上设置一个 item,命名为 Done +姓名录入:textField,填充满整个表格,取消边框 +日期显示:staticcell中将style选择RightDetail //可将detail文本写为:“请选择” +日期选择:Picker 控件,将 style 设置成 Date +``` + + + +## 管理系统主体 + +在 `TableViewController.swift` 文件中: + +```swift +@IBOutlet weak var nameLabel: UITextField! +@IBOutlet weak var telNumberLabel: UITextField! +@IBOutlet weak var IDCardLabel: UITextField! +@IBOutlet weak var checkInDateLabel: UILabel! +@IBOutlet weak var checkOutDateLabel: UILabel! +@IBOutlet weak var checkInDatePicker: UIDatePicker! +@IBOutlet weak var checkOutDatePicker: UIDatePicker! + +//Done 按钮的设置,按下时对当前页面信息进行保存 +@IBAction func doneBarButtonTapped(_ sender: Any) { + let name = nameLabel.text ?? "" + let telNumber = telNumberLabel.text ?? "" + let IDNumber = IDCardLabel.text ?? "" + let checkInDate = checkInDatePicker.date + let checkOutDate = checkOutDatePicker.date + print("信息录入:") + print("姓名: \(name)") + print("电话: \(telNumber)") + print("身份证号: \(IDNumber)") + print("入住日期: \(checkInDate)") + print("退房日期: \(checkOutDate)") +} + +override func viewDidLoad() { + super.viewDidLoad() +} + +//设置单元格 section +override func numberOfSections(in tableView: UITableView) -> Int { + return 2 +} + +//设置每个 section 的 cell 数量 +override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0{ + return 3 + }else{ + return 4 + } +} +``` + +以上内容完毕后,我们要考虑一个问题:`DatePicker` 是否和它上面 `CheckInDate` 标签是一个单元格?答案是否定的,我们实现点击上面单元格从而显示 `DatePicker` 的思路是监听点击单元格事件,当此事件发生时通过对布尔值的开启/关闭实现 `DatePicker` 单元格的隐藏/显示。因此我们需要手动将第 2、4 单元格赋给 `DatePicker`,并将监听事件与它们上面紧邻的单元格进行绑定: + +```swift +//代码初始化入住日期、退房日期,此处若与 storyboard 中不同,程序会崩溃 +let checkInDatePickerCellIndexPath = IndexPath(row: 1, section: 1) +let checkOutDatePickerCellIndexPath = IndexPath(row: 3, section: 1) + +//入住日期 +var isCheckInDatePickerShown: Bool = false { + didSet { + checkInDatePicker.isHidden = !isCheckInDatePickerShown + } +} + +//退房日期 +var isCheckOutDatePickerShown: Bool = false { + didSet { + checkOutDatePicker.isHidden = !isCheckOutDatePickerShown + } +} + +//设置 cell 高度,此处若与 storyboard 中不同,程序会崩溃 +override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + //通过 switch 选择 Bool 值初始化 static cell + switch (indexPath.section, indexPath.row) { + case (checkInDatePickerCellIndexPath.section, checkInDatePickerCellIndexPath.row): + if isCheckInDatePickerShown { + return 216.0 + }else{ + return 0.0 + } + case (checkOutDatePickerCellIndexPath.section, checkOutDatePickerCellIndexPath.row): + if isCheckOutDatePickerShown { + return 216.0 + }else{ + return 0.0 + } + default: + return 44.0 + } +} + +//选中后隐藏 +override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){ + tableView.deselectRow(at: indexPath, animated: true) + //选中后第二个 section 的 row 减 1,对 Bool 值进行赋值 + switch (indexPath.section, indexPath.row) { + case (checkInDatePickerCellIndexPath.section, checkInDatePickerCellIndexPath.row - 1): + if isCheckInDatePickerShown { + isCheckInDatePickerShown = false + } else if isCheckOutDatePickerShown { + isCheckOutDatePickerShown = false + isCheckInDatePickerShown = true + }else{ + isCheckInDatePickerShown = true } + tableView.beginUpdates() + tableView.endUpdates() + + case (checkOutDatePickerCellIndexPath.section, checkOutDatePickerCellIndexPath.row - 1): + if isCheckOutDatePickerShown { + isCheckOutDatePickerShown = false + } else if isCheckInDatePickerShown { + isCheckInDatePickerShown = false isCheckOutDatePickerShown = true + }else{ + isCheckOutDatePickerShown = true + } + tableView.beginUpdates() + tableView.endUpdates() default: + break +} +``` + +我们刚刚做了什么? + +我们将入住日期所在的表格部分定义为 `checkInDatePickerCellIndexPath`,并把它的值设置成为第二部分的第二个 row。请注意,section 与 row 都是索引值,因此它们从 0 开始计数。我们如何实现点击使单元格隐藏/显示的?代码中通过 `switch-case` 函数对两个条件进行检索,当被选中的单元格为入住日期/退房日期的上一个 row 时,我们通过控制布尔值来设置单元格高度为 0 或是 `DatePicker` 的显示高度,这样我们就实现了点击控制单元格的显示与隐藏。当然,如果你能想到用动作捕捉来解决这一过程就更棒了。我们解决了点击隐藏的事件,接下来试着转 换日期的格式为 `String`,并可以动态刷新: + +```swift +//转换 Date 格式,将 Date 变为 String,此处代码放到以上设置前会报错,程序崩溃 +let dateFormatter = DateFormatter() +dateFormatter.dateStyle = .medium +/*dateStyle 中: +.none:无 +.short:“11/23/37” .medium:"Nov 23, 1937" .long:"Novermber 23, 1937" .full:"Tuesday,April 12,1952" */ + +/*timeStyle 中: +.none:无 +.short:"3:30 PM" +.medium:"3:30:32 PM" +.long:"3:30:32 PM PST" +.full:"3:30:42 PM Pacific Standard Time" +*/ + +checkInDateLabel.text = dateFormatter.string(from: checkInDatePicker.date) +checkOutDateLabel.text = dateFormatter.string(from: checkOutDatePicker.date) +``` + + + +## 多页面传值 + +我们常常需要在不同页面之间进行数值传递,但是由于不同的 `ViewController` 继承了 不同的类,因此在一个.swift 文件中对一切值进行定义和使用似乎不是那么管用,此刻我 们需要自己拓展新页面的代理/协议。如果我想把此页面的值传递出去,我需要在该页面并 列于 `Class UITableViewController` 自定义一个继承属性:`RoomTypeTableViewController Delegate` 代码如下: + +```swift +protocol RoomTypeTableViewControllerDelegate:class { + //协议传值,自定义 RoomType 类型 + func didSelect(roomType: RoomType) +} +``` + +值得注意的是,该协议中设置了一个必须继承的方法,即为 `didSelect`,接下来我们 +设置要传输的结构体,我们定义为 `RoomType`,具体如下: + +```swift +//定义结构体:roomtType,static func若放到结构体外会报错 +struct RoomType: Equatable { + var id: Int + var name: String + var shortName: String + var price: Int + static func ==(lhs: RoomType, rhs: RoomType) -> Bool { + return lhs.id == rhs.id } +} +``` + +我们完成了协议拓展的内容,接下来需要在 `RoomTypeTableViewController` 中准备传值: + +```swift +weak var delegate: RoomTypeTableViewControllerDelegate? +``` + +我们还需要将 `RoomType` 的可能情况以数组罗列出,在后面我们会以 JSON 的形式完成这一过程,代码如下: + +```swift +static var all: [RoomType] { + return + [RoomType(id: 0, name: "单人间", shortName: "1 人住", price: 179), + RoomType(id: 1, name: "双人间", shortName: "2 人住", price: 209), + RoomType(id: 2, name: "总统套房", shortName: "随意", price: 309999)] +} +``` + +此刻,所要传输的数值被我们成功送出,那么如何才能让接收的文件收到我们的传输的内容呢?这需要我们手动在目的文件中继承自己的代理,`class UITableViewcontroller` 中代码如下: + +```swift +class TableViewController: UITableViewController,RoomTypeTableViewControllerDel egate{} +``` + +此时,我们需要在 `TableViewController` 文件中完成一个必须继承的方法 `didSelect`: + +```swift +func didSelect(roomType: RoomType) { + self.roomType = roomType + updateRoomType() +} +``` + +很好!我们接着定义 `updateRoomType`: + +```swift +//更新房间类型 +func updateRoomType() { + if let roomType = roomType { + roomTypeSelected.text = roomType.name + }else{ + roomTypeSelected.text = "请选择" + //tips:若此处值为空,则后面无论如何都不会显示 +} +``` + +并在 `viewDidLoad()` 中添加该方法: + +```swift +updateRoomType() +``` + +我们已经架好了两个 swift 文件之间的传值协议,接下来回到 `RoomType` 页面,添加如下代码: + +```swift +var roomType: RoomType? +//Roomtype 到主视图的传值 +override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + roomType = RoomTypeTableViewController.all[indexPath.row] + delegate?.didSelect(roomType: roomType!) + tableView.reloadData() +} +``` + +值得注意的是,在上述 `roomType = RoomTypeTableViewController` 中,需要指明控制器全称,否则编译器会报错。至此,我们完成了多页面传值的所有准备工作,接下来将通过设置传出值页面的 `cell` 来导入 `RoomType` 数组,代码如下: + +```swift +override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + //value1 为 style 中的 Right detail + let cell:UITableViewCell = UITableViewCell.init(style: UITableViewCell.CellStyle.value1, reuseIdentifier: nil) + //将数组导入定义中 + let roomType = RoomTypeTableViewController.all[indexPath.row] + cell.textLabel?.text = roomType.name + cell.detailTextLabel?.text = "¥ \(roomType.price)" + + //选中后出现Right符号 + if roomType == self.roomType { + cell.accessoryType = .checkmark + self.currentRoomT = roomType.name + }else { + cell.accessoryType = .none + } + return cell +} +``` + + + +## 代理(Segue) + +完成了上述代码后,你需要在`Main.storyboard`中设置`segue`,在主视图中将`RoomType` 的 `cell` 格式设置为 `Right Detail`,并将指引线连接到传出值页面上,进行 `segue` 连接,完成后在主视图的 swift 文件中输入如下代码: + +```swift +override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "SelectRoomType" { + let destinationViewController = segue.destination as? RoomTypeTableViewController + destinationViewController?.delegate = self + destinationViewController?.roomType = roomType + } +} +``` + +事实上,这个部分指明了代理的初始内容,避免传值出现 `nil` 的情况,直到该部分写完,整个代理传值的内容才算完成。 + + + +## 回退(Exit) + +回退(Exit)即指取消当前页面,也可以理解成跨页面的 `button`,代码如下: + +```swift +@IBAction func unwindFromAddRegistration(unwindSegue: UIStoryboardSegue) { + //添加回退,先定义回退,后进行 button 的对接 + guard let tableViewController = unwindSegue.source as? TableViewController, + let registration = tableViewController.registration else { return } + registrations.append(registration) + tableView.reloadData() +} +``` + diff --git a/IOS/大作业:Statistics in Time统计软件/AppleWWDRCAG3.cer b/IOS/大作业:Statistics in Time统计软件/AppleWWDRCAG3.cer new file mode 100644 index 0000000..32f96f8 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/AppleWWDRCAG3.cer differ diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/Settings.imageset/Contents.json b/IOS/大作业:Statistics in Time统计软件/Icon/Settings.imageset/Contents.json new file mode 100644 index 0000000..fbe47e1 --- /dev/null +++ b/IOS/大作业:Statistics in Time统计软件/Icon/Settings.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Settings-25.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Settings-50.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Settings-100.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/Settings.imageset/Settings-100.png b/IOS/大作业:Statistics in Time统计软件/Icon/Settings.imageset/Settings-100.png new file mode 100644 index 0000000..fb51f55 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/Icon/Settings.imageset/Settings-100.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/Settings.imageset/Settings-25.png b/IOS/大作业:Statistics in Time统计软件/Icon/Settings.imageset/Settings-25.png new file mode 100644 index 0000000..3b9bddd Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/Icon/Settings.imageset/Settings-25.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/Settings.imageset/Settings-50.png b/IOS/大作业:Statistics in Time统计软件/Icon/Settings.imageset/Settings-50.png new file mode 100644 index 0000000..a2f1793 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/Icon/Settings.imageset/Settings-50.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/SettingsFilled.imageset/Contents.json b/IOS/大作业:Statistics in Time统计软件/Icon/SettingsFilled.imageset/Contents.json new file mode 100644 index 0000000..cf83712 --- /dev/null +++ b/IOS/大作业:Statistics in Time统计软件/Icon/SettingsFilled.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Settings Filled-25.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Settings Filled-50.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Settings Filled-100.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/SettingsFilled.imageset/Settings Filled-100.png b/IOS/大作业:Statistics in Time统计软件/Icon/SettingsFilled.imageset/Settings Filled-100.png new file mode 100644 index 0000000..5c03af4 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/Icon/SettingsFilled.imageset/Settings Filled-100.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/SettingsFilled.imageset/Settings Filled-25.png b/IOS/大作业:Statistics in Time统计软件/Icon/SettingsFilled.imageset/Settings Filled-25.png new file mode 100644 index 0000000..5c0732a Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/Icon/SettingsFilled.imageset/Settings Filled-25.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/SettingsFilled.imageset/Settings Filled-50.png b/IOS/大作业:Statistics in Time统计软件/Icon/SettingsFilled.imageset/Settings Filled-50.png new file mode 100644 index 0000000..9632451 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/Icon/SettingsFilled.imageset/Settings Filled-50.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/Trends.imageset/Combo Chart-100.png b/IOS/大作业:Statistics in Time统计软件/Icon/Trends.imageset/Combo Chart-100.png new file mode 100644 index 0000000..baa9732 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/Icon/Trends.imageset/Combo Chart-100.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/Trends.imageset/Combo Chart-25.png b/IOS/大作业:Statistics in Time统计软件/Icon/Trends.imageset/Combo Chart-25.png new file mode 100644 index 0000000..331041a Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/Icon/Trends.imageset/Combo Chart-25.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/Trends.imageset/Combo Chart-50.png b/IOS/大作业:Statistics in Time统计软件/Icon/Trends.imageset/Combo Chart-50.png new file mode 100644 index 0000000..2e7a909 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/Icon/Trends.imageset/Combo Chart-50.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/Trends.imageset/Contents.json b/IOS/大作业:Statistics in Time统计软件/Icon/Trends.imageset/Contents.json new file mode 100644 index 0000000..f34e7ab --- /dev/null +++ b/IOS/大作业:Statistics in Time统计软件/Icon/Trends.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Combo Chart-25.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Combo Chart-50.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Combo Chart-100.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/TrendsFilled.imageset/Combo Chart Filled-100.png b/IOS/大作业:Statistics in Time统计软件/Icon/TrendsFilled.imageset/Combo Chart Filled-100.png new file mode 100644 index 0000000..54baab1 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/Icon/TrendsFilled.imageset/Combo Chart Filled-100.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/TrendsFilled.imageset/Combo Chart Filled-25.png b/IOS/大作业:Statistics in Time统计软件/Icon/TrendsFilled.imageset/Combo Chart Filled-25.png new file mode 100644 index 0000000..00c9e1b Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/Icon/TrendsFilled.imageset/Combo Chart Filled-25.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/TrendsFilled.imageset/Combo Chart Filled-50.png b/IOS/大作业:Statistics in Time统计软件/Icon/TrendsFilled.imageset/Combo Chart Filled-50.png new file mode 100644 index 0000000..360fd96 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/Icon/TrendsFilled.imageset/Combo Chart Filled-50.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/Icon/TrendsFilled.imageset/Contents.json b/IOS/大作业:Statistics in Time统计软件/Icon/TrendsFilled.imageset/Contents.json new file mode 100644 index 0000000..62dd430 --- /dev/null +++ b/IOS/大作业:Statistics in Time统计软件/Icon/TrendsFilled.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Combo Chart Filled-25.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Combo Chart Filled-50.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Combo Chart Filled-100.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IOS/大作业:Statistics in Time统计软件/Statistics in Time.zip b/IOS/大作业:Statistics in Time统计软件/Statistics in Time.zip new file mode 100644 index 0000000..d03f231 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/Statistics in Time.zip differ diff --git a/IOS/大作业:Statistics in Time统计软件/Statistics in Time统计软件.md b/IOS/大作业:Statistics in Time统计软件/Statistics in Time统计软件.md new file mode 100644 index 0000000..e26cb71 --- /dev/null +++ b/IOS/大作业:Statistics in Time统计软件/Statistics in Time统计软件.md @@ -0,0 +1,617 @@ +# Statistics in Time统计软件 + +学习了这么多的基础知识,我们还没有完整的开发一款app!本部分我们将开始Statistics in Time软件的开发,这是一款可以在手机上进行简易数据分析的demo。值得注意的是,这一个Task是开放内容,并没有标准的答案。我们仅将演示完成基本功能的代码,至于更对的样式与搭配则需要你自己来完成。欢迎大家将新奇的想法与精美的UI进行搭配,组合出专属于自己的掌上数据分析app!期待着大家更好的作品! + + + +## 功能分析 + +日常生活中,我们可能常常需要简易观察一下一些数据之间的统计信息,例如:近几周的菜价是否有不寻常的波动;考试成绩是否符合正态分布;近几天的气温数据是否具有相关性等等等等..... + +不管你是数据分析师,是学生/老师,还是财务工作人员,统计都无处不在!由于每个人对统计的需求不尽相同,我们难以做出一款适用于每个人的统计软件,但是我们可以只根据自己的日常需求自己写一个简易的Statistics in Time在手。以我自己为例,我希望能够设计一款随手就能查看一组数据之间的各种信息,其中可以包括这组数据的均值、方差、峰度、偏度等等(如果能可视化分析当然就更赞了!)此外,我还希望能够设计出一款自带日历功能的app,这样就能随时提醒我需要做的事情。因此需求如下: + +- 统计功能 +- 日历功能 + +下面,我们将围绕这两个功能进行实现。 + + + +## 准备工作 + +**Step 1:**创建新工程文件Statistics in Time + +1 + +**Step 2:**删除storyboard,并设置代码启动。 + +**Step3:**`菜单栏`----`file`----`new`----`file`(快捷键command+N) + +2 + +3 + +4 + +**Step4:**同理创建StatisticsViewController.swift与CalendarViewController,继承自UIViewController。 + +5 + +**Step 5:**创建navigationViewController,继承自UINavigationViewController。 + +**Step 6:**删除`ViewController.swift`,并在`SceneDelegate.swfit`中更换如下代码: + +```swift + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let _ = (scene as? UIWindowScene) else { return } + guard let windowScene = (scene as? UIWindowScene) else {return} + window = UIWindow(frame:windowScene.coordinateSpace.bounds) + window?.windowScene = windowScene + window?.backgroundColor = UIColor.white + window?.rootViewController = MainViewController() + window?.makeKeyAndVisible() + } +``` + +**Step 7:**将本课件Icon中四个文件夹拖拽至Assets.xcassets中,并在MainViewController内添加如下代码: + +```swift +import UIKit +class MainViewController: UITabBarController { + + override func viewDidLoad() { + super.viewDidLoad() + self.tabBar.tintColor = UIColor(red: 0/255, green:169/255, blue:169/255, alpha:1) + addNormalTabbar() + } + + func addNormalTabbar() { + //初始化 + let updateVc = StatisticsViewController.init() + updateVc.view.backgroundColor = UIColor.red + setupOneChildViewController(title: "统计", image: "Trends", seletedImage: "TrendsFilled", controller: StatisticsViewController.init()) + setupOneChildViewController(title: "概要", image: "Settings", seletedImage: "SettingsFilled", controller: ProfileViewController.init()) + } + fileprivate func setupOneChildViewController(title: String,image: String,seletedImage: String,controller: UIViewController){ + controller.tabBarItem.title = title + controller.title = title + //这里设置背景色 是每一个vc设置的都一样 + controller.view.backgroundColor = UIColor.white + controller.tabBarItem.image = UIImage.init(named: image) + controller.tabBarItem.selectedImage = UIImage.init(named: seletedImage) + let naviController = navigationViewController.init(rootViewController: controller) + addChild(naviController) + } +} +``` + +**Step 8:**运行代码,即可见下图 + +6 + + + + + +现在的页面仍然是一片空白,我们接下来的任务便是在`统计`页面填充入统计功能,在`概要`界面加入日历功能。 + + + +## Cocopods与第三方库 + +你可以在 [Awesome-swift](https://github.com/matteocrippa/awesome-swift) 查看Swift的一些优秀的第三方库,这些第三方库不仅可以帮我们简化功能实现(加入引导页,实现跑马灯功能等),也可以美化UI(更改Button外观、加入动画过渡),甚至帮助我们高效利用内存/缓存等。 + +本部分我们需要添加两个第三方库: + +- **[ SigmaSwiftStatistics](https://github.com/evgenyneu/SigmaSwiftStatistics)**:数学计算第三方库 +- [Charts](https://github.com/danielgindi/Charts):数据可视化 +- **[ CVCalendar](https://github.com/CVCalendar/CVCalendar)**:实现日历功能 + +添加过程如下: + +1. 关闭Xcode,打开终端(Terminal),切换到Statistics in Time工程文件目录下 + + ```bash + cd Downloads/code/Statistics\ in\ Time/ + ``` + +2. Cocopods初始化: + + ```bash + pod init + ``` + +3. 打开Podfile文件 + + ```swift + open Podfile + ``` + +4. 添加如下命令: + + ```swift + pod 'CVCalendar', '~> 1.7.0' + pod 'Charts' + pod 'TinyConstraints' + pod 'SigmaSwiftStatistics', '~> 9.0' + ``` + +5. 关闭文本编辑器,回到终端,输入 + + ```swift + pod update + ``` + +6. 等待片刻即可完成。 + +## 统计功能实现 + +在Statistics in Time目录下,打开`Statistics in Time.xcworkspace`,并添加如下代码 + +```swift +import SigmaSwiftStatistics +``` + +该计算库提供了一种科学计算的方法,然而这仅仅是通过数组进行计算。常规情况下我们一般用Textfiled来读取用户输入的内容,这是一种String格式。如何将String转化为Array?除了传统的算法外,我们能否想出一种办法利用Swift的特性进行转化?这里给出一种参考方式: + +```swift + @objc func btnClick(_: Any){ + let inputNumber:String = String(textField.text!) + let fullNumberArr = inputNumber.split{$0 == " "}.map(String.init) + let arr = changeArray(textArray: fullNumberArr) + resultLabel.text = String(Sigma.average(arr) ?? 0) + } + + func changeArray(textArray: [String]) -> [Double] { + textArray.compactMap { Double($0) } + } +``` + +这一段代码的含义是什么? + + + +## 统计功能参考代码 + +```swift +import UIKit +import SigmaSwiftStatistics +import TinyConstraints + +class StatisticsViewController: UIViewController,UITextFieldDelegate { + let textField = UITextField(frame: CGRect(x: 120, y: 140, width: 180, height: 40)) + let alertLabel = UILabel(frame: CGRect(x: 20, y: 140, width: 90, height: 40)) + var resultLabel = UILabel(frame: CGRect(x: 175, y: 200, width: 200, height: 40)) + var averLabel = UILabel(frame: CGRect(x: 105, y: 200, width: 90, height: 40)) + var resultLabel2 = UILabel(frame: CGRect(x: 175, y: 240, width: 200, height: 40)) + var centralMomentLabel = UILabel(frame:CGRect(x: 105, y: 240, width: 90, height: 40)) + var resultLabel3 = UILabel(frame: CGRect(x: 175, y: 280, width: 200, height: 40)) + var variancePopulationLabel = UILabel(frame: CGRect(x: 105, y: 280, width: 90, height: 40)) + var resultLabel4 = UILabel(frame:CGRect(x: 175, y: 320, width: 200, height: 40)) + var coefficientOfVariationLabel = UILabel(frame: CGRect(x: 95, y: 320, width: 90, height: 40)) + var resultLabel5 = UILabel(frame:CGRect(x: 175, y: 360, width: 200, height: 40)) + var kurtosisA = UILabel(frame: CGRect(x: 105, y: 360, width: 90, height: 40)) + var resultLabel6 = UILabel(frame:CGRect(x: 175, y: 400, width: 200, height: 40)) + var kurtosisB = UILabel(frame: CGRect(x: 105, y: 400, width: 90, height: 40)) + var resultLabel7 = UILabel(frame:CGRect(x: 175, y: 440, width: 200, height: 40)) + var skewnessLabel = UILabel(frame: CGRect(x: 105, y: 440, width: 90, height: 40)) + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + textField.resignFirstResponder() + } + + override func viewDidLoad() { + super.viewDidLoad() + + textField.borderStyle = .roundedRect + textField.textColor = UIColor.black + alertLabel.text = "数据输入:" + alertLabel.textColor = UIColor.black + resultLabel.text = " " + resultLabel2.text = " " + resultLabel3.text = " " + resultLabel4.text = " " + resultLabel5.text = " " + resultLabel6.text = " " + resultLabel7.text = " " + averLabel.text = "平均值:" + centralMomentLabel.text = "标准误:" + variancePopulationLabel.text = "Var值:" + coefficientOfVariationLabel.text = "变异系数:" + kurtosisA.text = "峰度A:" + kurtosisB.text = "峰度B:" + skewnessLabel.text = "偏度:" + + + + let btn:UIButton=UIButton.init(frame: CGRect(x:310,y:140, width:100,height:40))//创建按钮,并设置位置,宽度、高度 + btn.setTitle("开始统计", for: UIControl.State.normal)//设置按钮上的文字 + btn.backgroundColor = UIColor.systemBlue + btn.addTarget(self, action:#selector(btnClick(_:)), for: UIControl.Event.touchDown)//为按钮添加touchDown事件(按下) + self.view.addSubview(btn)//将标签添加到View中 + self.view.addSubview(textField) + self.view.addSubview(alertLabel) + self.view.addSubview(resultLabel) + self.view.addSubview(averLabel) + self.view.addSubview(centralMomentLabel) + self.view.addSubview(resultLabel2) + self.view.addSubview(variancePopulationLabel) + self.view.addSubview(resultLabel3) + self.view.addSubview(coefficientOfVariationLabel) + self.view.addSubview(resultLabel4) + self.view.addSubview(resultLabel5) + self.view.addSubview(resultLabel6) + self.view.addSubview(kurtosisA) + self.view.addSubview(kurtosisB) + self.view.addSubview(resultLabel7) + self.view.addSubview(skewnessLabel) + //textField.delegate = self + } + + + @objc func btnClick(_: Any){ + let inputNumber:String = String(textField.text!) + let fullNumberArr = inputNumber.split{$0 == " "}.map(String.init) + let arr = changeArray(textArray: fullNumberArr) + print(Sigma.average(arr) ?? 0) + resultLabel.text = String(Sigma.average(arr) ?? 0) + resultLabel2.text = String(Sigma.standardErrorOfTheMean(arr) ?? 0) + resultLabel3.text = String(Sigma.varianceSample(arr) ?? 0) + resultLabel4.text = String(Sigma.coefficientOfVariationSample(arr) ?? 0) + resultLabel5.text = String(Sigma.kurtosisA(arr) ?? 0) + resultLabel6.text = String(Sigma.kurtosisB(arr) ?? 0) + resultLabel7.text = String(Sigma.skewnessA(arr) ?? 0) + } + + func changeArray(textArray: [String]) -> [Double] { + textArray.compactMap { Double($0) } + } +} +``` + +## 日历功能的实现 + +在Statistics in Time目录下,打开`Statistics in Time.xcworkspace`,并添加如下代码 + +```swift +import CVCalendar +``` + +这是Github上一个优秀的第三方库,可以实现日历相关功能。不过,在实现功能之前,我们还需要手动添加一个文件:ColorsConfig,后面我们将在此文件中进行参数设置,参考代码如下: + +```swift +import UIKit + +struct ColorsConfig { + static let selectedText = UIColor.white + static let text = UIColor.black + static let textDisabled = UIColor.purple + static let selectionBackground = UIColor(red: 1, green: 0.647, blue: 0, alpha: 1.0) + static let sundayText = UIColor(red: 1.0, green: 0.2, blue: 0.2, alpha: 1.0) + static let sundayTextDisabled = UIColor(red: 1.0, green: 0.6, blue: 0.6, alpha: 1.0) + static let sundaySelectionBackground = sundayText +} +``` + + + +## 日历功能代码 + +```swift +import UIKit +import CVCalendar + +class ProfileViewController: UIViewController { + private var menuView:CVCalendarMenuView! + private var calendarView:CVCalendarView! + private var monthLabel:UILabel! = UILabel.init(frame: CGRect(x:157,y:80, width:120,height:60)) + private var daysOutSwitch:UISwitch! + private var removeCircleAndDot = UIButton.self + private var randomNumberOfDotMarkersForDay = [Int]() + private var shouldShowDaysOut = true + private var animationFinished = true + private var selectedDay: DayView! + private var currentCalendar: Calendar? + + override func awakeFromNib() { + let timeZoneBias = 480 // (UTC+08:00) + currentCalendar = Calendar(identifier: .gregorian) + currentCalendar?.locale = Locale(identifier: "fr_FR") + if let timeZone = TimeZone(secondsFromGMT: -timeZoneBias * 60) { + currentCalendar?.timeZone = timeZone + } + } + + override func viewDidLoad() { + super.viewDidLoad() + monthLabel = UILabel.init(frame: CGRect(x: 157, y: 100, width: 104, height: 20)) + monthLabel.text = "my Calendar" + if let currentCalendar = currentCalendar { + monthLabel.text = CVDate(date: Date(), calendar: currentCalendar).globalDescription + } + randomizeDotMarkers() + self.menuView = CVCalendarMenuView(frame: CGRect(x: 10, y: 130, width: 394, height: 15)) + + // CVCalendarView initialization with frame + self.calendarView = CVCalendarView(frame: CGRect(x: 10, y: 145, width: 394, height: 310)) + self.calendarView.calendarAppearanceDelegate = self + self.calendarView.animatorDelegate = self + + // Menu delegate [Required] + self.menuView.menuViewDelegate = self + + // Calendar delegate [Required] + self.calendarView.calendarDelegate = self + + calendarView.backgroundColor = UIColor.tertiarySystemGroupedBackground + self.view.addSubview(calendarView) + self.view.addSubview(menuView) + self.view.addSubview(monthLabel) + } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + calendarView.commitCalendarViewUpdate() + menuView.commitMenuViewUpdate() + } + private func randomizeDotMarkers() { + randomNumberOfDotMarkersForDay = [Int]() + for _ in 0...31 { + randomNumberOfDotMarkersForDay.append(Int(arc4random_uniform(3) + 1)) + } + } + +} + +extension ProfileViewController: CVCalendarViewDelegate, CVCalendarMenuViewDelegate { + // MARK: Required methods + func presentationMode() -> CalendarMode { return .monthView } + func firstWeekday() -> Weekday { return .sunday } + // MARK: Optional methods + + func calendar() -> Calendar? { return currentCalendar } + func dayOfWeekTextColor(by weekday: Weekday) -> UIColor { + return weekday == .sunday ? UIColor(red: 1.0, green: 0, blue: 0, alpha: 1.0) : UIColor.white + } + func shouldShowWeekdaysOut() -> Bool { return shouldShowDaysOut } + // Defaults to true + func shouldAnimateResizing() -> Bool { return true } + private func shouldSelectDayView(dayView: DayView) -> Bool { + return arc4random_uniform(3) == 0 ? true : false + } + + func shouldAutoSelectDayOnMonthChange() -> Bool { return false } + func didSelectDayView(_ dayView: CVCalendarDayView, animationDidFinish: Bool) { + selectedDay = dayView + } + + func shouldSelectRange() -> Bool { return true } + func didSelectRange(from startDayView: DayView, to endDayView: DayView) { + print("RANGE SELECTED: \(startDayView.date.commonDescription) to \(endDayView.date.commonDescription)") + } + func presentedDateUpdated(_ date: CVDate) { + if monthLabel.text != date.globalDescription && self.animationFinished { + let updatedMonthLabel = UILabel() + updatedMonthLabel.textColor = monthLabel.textColor + updatedMonthLabel.font = monthLabel.font + updatedMonthLabel.textAlignment = .center + updatedMonthLabel.text = date.globalDescription + updatedMonthLabel.sizeToFit() + updatedMonthLabel.alpha = 0 + updatedMonthLabel.center = self.monthLabel.center + + let offset = CGFloat(48) + updatedMonthLabel.transform = CGAffineTransform(translationX: 0, y: offset) + updatedMonthLabel.transform = CGAffineTransform(scaleX: 1, y: 0.1) + + UIView.animate(withDuration: 0.35, delay: 0, options: UIView.AnimationOptions.curveEaseIn, animations: { + self.animationFinished = false + self.monthLabel.transform = CGAffineTransform(translationX: 0, y: -offset) + self.monthLabel.transform = CGAffineTransform(scaleX: 1, y: 0.1) + self.monthLabel.alpha = 0 + + updatedMonthLabel.alpha = 1 + updatedMonthLabel.transform = CGAffineTransform.identity + + }) { _ in + + self.animationFinished = true + self.monthLabel.frame = updatedMonthLabel.frame + self.monthLabel.text = updatedMonthLabel.text + self.monthLabel.transform = CGAffineTransform.identity + self.monthLabel.alpha = 1 + updatedMonthLabel.removeFromSuperview() + } + + self.view.insertSubview(updatedMonthLabel, aboveSubview: self.monthLabel) + } + } + + func topMarker(shouldDisplayOnDayView dayView: CVCalendarDayView) -> Bool { return true } + + func shouldHideTopMarkerOnPresentedView() -> Bool { + return true + } + + func weekdaySymbolType() -> WeekdaySymbolType { return .short } + + func selectionViewPath() -> ((CGRect) -> (UIBezierPath)) { + return { UIBezierPath(rect: CGRect(x: 0, y: 0, width: $0.width, height: $0.height)) } + } + + func shouldShowCustomSingleSelection() -> Bool { return false } + + func preliminaryView(viewOnDayView dayView: DayView) -> UIView { + let circleView = CVAuxiliaryView(dayView: dayView, rect: dayView.frame, shape: CVShape.circle) + circleView.fillColor = .colorFromCode(0xCCCCCC) + return circleView + } + + func preliminaryView(shouldDisplayOnDayView dayView: DayView) -> Bool { + if (dayView.isCurrentDay) { + return true + } + return false + } + + func supplementaryView(viewOnDayView dayView: DayView) -> UIView { + + dayView.setNeedsLayout() + dayView.layoutIfNeeded() + + let π = Double.pi + + let ringLayer = CAShapeLayer() + let ringLineWidth: CGFloat = 4.0 + let ringLineColour = UIColor.blue + + let newView = UIView(frame: dayView.frame) + + let diameter = (min(newView.bounds.width, newView.bounds.height)) + let radius = diameter / 2.0 - ringLineWidth + + newView.layer.addSublayer(ringLayer) + + ringLayer.fillColor = nil + ringLayer.lineWidth = ringLineWidth + ringLayer.strokeColor = ringLineColour.cgColor + + let centrePoint = CGPoint(x: newView.bounds.width/2.0, y: newView.bounds.height/2.0) + let startAngle = CGFloat(-π/2.0) + let endAngle = CGFloat(π * 2.0) + startAngle + let ringPath = UIBezierPath(arcCenter: centrePoint, + radius: radius, + startAngle: startAngle, + endAngle: endAngle, + clockwise: true) + + ringLayer.path = ringPath.cgPath + ringLayer.frame = newView.layer.bounds + + return newView + } + + func supplementaryView(shouldDisplayOnDayView dayView: DayView) -> Bool { + guard let currentCalendar = currentCalendar else { return false } + + let components = Manager.componentsForDate(Foundation.Date(), calendar: currentCalendar) + + /* For consistency, always show supplementaryView on the 3rd, 13th and 23rd of the current month/year. This is to check that these expected calendar days are "circled". There was a bug that was circling the wrong dates. A fix was put in for #408 #411. + + Other month and years show random days being circled as was done previously in the Demo code. + */ + var shouldDisplay = false + if dayView.date.year == components.year && + dayView.date.month == components.month { + + if (dayView.date.day == 3 || dayView.date.day == 13 || dayView.date.day == 23) { + print("Circle should appear on " + dayView.date.commonDescription) + shouldDisplay = true + } + } else if (Int(arc4random_uniform(3)) == 1) { + shouldDisplay = true + } + + return shouldDisplay + } + + func dayOfWeekTextColor() -> UIColor { return .white } + + func dayOfWeekBackGroundColor() -> UIColor { return .orange } + + func disableScrollingBeforeDate() -> Date { return Date() } + + func maxSelectableRange() -> Int { return 14 } + + func earliestSelectableDate() -> Date { return Date() } + + func latestSelectableDate() -> Date { + var dayComponents = DateComponents() + dayComponents.day = 70 + let calendar = Calendar(identifier: .gregorian) + if let lastDate = calendar.date(byAdding: dayComponents, to: Date()) { + return lastDate + } + + return Date() + } +} + + +// MARK: - CVCalendarViewAppearanceDelegate + +extension ProfileViewController: CVCalendarViewAppearanceDelegate { + + func dayLabelWeekdayDisabledColor() -> UIColor { return .lightGray } + + func dayLabelPresentWeekdayInitallyBold() -> Bool { return false } + + func spaceBetweenDayViews() -> CGFloat { return 0 } + + func dayLabelFont(by weekDay: Weekday, status: CVStatus, present: CVPresent) -> UIFont { return UIFont.systemFont(ofSize: 14) } + + func dayLabelColor(by weekDay: Weekday, status: CVStatus, present: CVPresent) -> UIColor? { + switch (weekDay, status, present) { + case (_, .selected, _), (_, .highlighted, _): return ColorsConfig.selectedText + case (.sunday, .in, _): return ColorsConfig.sundayText + case (.sunday, _, _): return ColorsConfig.sundayTextDisabled + case (_, .in, _): return ColorsConfig.text + default: return ColorsConfig.textDisabled + } + } + + func dayLabelBackgroundColor(by weekDay: Weekday, status: CVStatus, present: CVPresent) -> UIColor? { + switch (weekDay, status, present) { + case (.sunday, .selected, _), (.sunday, .highlighted, _): return ColorsConfig.sundaySelectionBackground + case (_, .selected, _), (_, .highlighted, _): return ColorsConfig.selectionBackground + default: return nil + } + } +} +// MARK: - IB Actions + +// MARK: - Convenience API Demo +extension ProfileViewController { + func toggleMonthViewWithMonthOffset(offset: Int) { + guard let currentCalendar = currentCalendar else { return } + var components = Manager.componentsForDate(Date(), calendar: currentCalendar) // from today + components.month! += offset + let resultDate = currentCalendar.date(from: components)! + self.calendarView.toggleViewWithDate(resultDate) + } + + func didShowNextMonthView(_ date: Date) { + guard let currentCalendar = currentCalendar else { return } + let components = Manager.componentsForDate(date, calendar: currentCalendar) // from today + print("Showing Month: \(components.month!)") + } + + + func didShowPreviousMonthView(_ date: Date) { + guard let currentCalendar = currentCalendar else { return } + let components = Manager.componentsForDate(date, calendar: currentCalendar) // from today + print("Showing Month: \(components.month!)") + } + + func didShowNextWeekView(from startDayView: DayView, to endDayView: DayView) { + print("Showing Week: from \(startDayView.date.day) to \(endDayView.date.day)") + } + + func didShowPreviousWeekView(from startDayView: DayView, to endDayView: DayView) { + print("Showing Week: from \(startDayView.date.day) to \(endDayView.date.day)") + } + +} +``` + + + +## 思考与优化 + +1. 在统计功能部分,我们在内存加载时添加了大量控件,这会造成系统极度卡顿,如何优化上文代码?如何优化整个系统的性能?如何在协议中拓展textFiled的代理功能?(你可以参考`懒加载`相关概念,并试着利用数组来代替大量而重复的控件代码。) +2. 在日历部分,我们并没有将所有的功能都放到主函数中,而是采用了`协议`进行拓展,这是面向协议编程的特性之一。请你试着思考:采取这种方式有什么优点?为什么我们在加载控件时采用private?这对整个程序有什么帮助?如何美化整个Calendar页面?(你可以参考一些精美的UI设计,并在Github相关专题上寻找优化程序的答案) + diff --git a/IOS/大作业:Statistics in Time统计软件/img/1.png b/IOS/大作业:Statistics in Time统计软件/img/1.png new file mode 100644 index 0000000..5dccc2f Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/img/1.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/img/2.png b/IOS/大作业:Statistics in Time统计软件/img/2.png new file mode 100644 index 0000000..b03a2cb Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/img/2.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/img/3.png b/IOS/大作业:Statistics in Time统计软件/img/3.png new file mode 100644 index 0000000..7880083 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/img/3.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/img/4.png b/IOS/大作业:Statistics in Time统计软件/img/4.png new file mode 100644 index 0000000..d4985a9 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/img/4.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/img/5.png b/IOS/大作业:Statistics in Time统计软件/img/5.png new file mode 100644 index 0000000..ca1bf63 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/img/5.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/img/6.png b/IOS/大作业:Statistics in Time统计软件/img/6.png new file mode 100644 index 0000000..5127697 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/img/6.png differ diff --git a/IOS/大作业:Statistics in Time统计软件/img/demo.png b/IOS/大作业:Statistics in Time统计软件/img/demo.png new file mode 100644 index 0000000..b7b9694 Binary files /dev/null and b/IOS/大作业:Statistics in Time统计软件/img/demo.png differ diff --git a/IntroductionToNumpy/task01 数据类型及数组创建/04. 数组的创建.ipynb b/IntroductionToNumpy/task01 数据类型及数组创建/04. 数组的创建.ipynb index 5235bd5..53ed52b 100644 --- a/IntroductionToNumpy/task01 数据类型及数组创建/04. 数组的创建.ipynb +++ b/IntroductionToNumpy/task01 数据类型及数组创建/04. 数组的创建.ipynb @@ -101,7 +101,7 @@ "x = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]])\n", "y = np.array(x)\n", "z = np.asarray(x)\n", - "w = np.asarray(x, dtype=np.int)\n", + "w = np.asarray(x, dtype=np.float64)\n", "x[1][2] = 2\n", "print(x,type(x),x.dtype)\n", "# [[1 1 1]\n", @@ -119,9 +119,9 @@ "# [1 1 1]] int32\n", "\n", "print(w,type(w),w.dtype)\n", - "# [[1 1 1]\n", - "# [1 1 2]\n", - "# [1 1 1]] int32\n", + "# [[1. 1. 1.]\n", + "# [1. 1. 1.]\n", + "# [1. 1. 1.]] float64\n", "```\n", "\n", "\n", @@ -311,6 +311,7 @@ "- `diag()`函数:提取对角线或构造对角数组。\n", "\n", "```python\n", + "k : int, optional 对角线的位置,大于零位于对角线上面,小于零则在下面。\n", "def diag(v, k=0):\n", "```\n", "\n", @@ -325,6 +326,7 @@ "# [6 7 8]]\n", "print(np.diag(x)) # [0 4 8]\n", "print(np.diag(x, k=1)) # [1 5]\n", + "print(np.diag(x, k=2)) # [2]\n", "print(np.diag(x, k=-1)) # [3 7]\n", "\n", "v = [1, 3, 5, 7]\n", diff --git a/OfficeAutomation/Task01 文件自动化与邮件处理.md b/OfficeAutomation/Task01 文件自动化与邮件处理.md index 0db1937..853e711 100644 --- a/OfficeAutomation/Task01 文件自动化与邮件处理.md +++ b/OfficeAutomation/Task01 文件自动化与邮件处理.md @@ -1,40 +1,40 @@ -# 文件自动化处理 +# Task 01 文件自动化处理 -- [1 文件自动化处理](#1-文件自动化处理) - - [1.1 读写文件](#11--读写文件) - - [1.1.1 文件与文件路径](#111--文件与文件路径) - - [1.1.2 当前工作目录](#112--当前工作目录) - - [1.1.3 路径操作](#113--路径操作) - - [1.1.3.1 绝对路径和相对路径](#1131-绝对路径和相对路径) - - [1.1.3.2 路径操作](#1132-路径操作) - - [1.1.3.3 路径有效性检查](#1133-路径有效性检查) - - [1.1.4 文件及文件夹操作](#114--文件及文件夹操作) - - [1.1.4.1 用os.makedirs()创建新文件夹](#1141-用osmakedirs创建新文件夹) - - [1.1.4.2 查看文件大小和文件夹内容](#1142-查看文件大小和文件夹内容) - - [1.1.5 文件读写过程](#115-文件读写过程) - - [1.1.5.1 用open()函数打开文件](#1151-用open函数打开文件) - - [1.1.5.2 读取文件内容](#1152-读取文件内容) - - [1.1.5.3 写入文件](#1153-写入文件) - - [1.1.5.4 保存变量](#1154-保存变量) - - [1.1.6 练习](#116-练习) - - [1.2 组织文件](#12--组织文件) - - [1.2.1 shutil模块](#121-shutil模块) - - [1.2.1.1 复制文件和文件夹](#1211-复制文件和文件夹) - - [1.2.1.2 文件和文件夹的移动与改名](#1212-文件和文件夹的移动与改名) - - [1.2.1.3 永久删除文件和文件夹](#1213-永久删除文件和文件夹) - - [1.2.1.4 用send2trash模块安全地删除](#1214-用send2trash模块安全地删除) - - [1.2.2 遍历目录树](#122-遍历目录树) - - [1.2.3 用zipfile模块压缩文件](#123-用zipfile模块压缩文件) - - [1.2.3.1 创建和添加到zip文件](#1231-创建和添加到zip文件) - - [1.2.3.2 读取zip文件](#1232-读取zip文件) - - [1.2.3.3 从zip文件中解压缩](#1233-从zip文件中解压缩) - - [1.2.4 练习](#124-练习) - - [2 自动发送电子邮件](#2-自动发送电子邮件) +- [Task 01 文件自动化处理](#task-01-文件自动化处理) + - [1.1文件处理](#11文件处理) + - [1.1.1 文件与文件路径](#111--文件与文件路径) + - [1.1.2 当前工作目录](#112--当前工作目录) + - [1.1.3 路径操作](#113--路径操作) + - [1.1.3.1 绝对路径和相对路径](#1131-绝对路径和相对路径) + - [1.1.3.2 路径操作](#1132-路径操作) + - [1.1.3.3 路径有效性检查](#1133-路径有效性检查) + - [1.1.4 文件及文件夹操作](#114--文件及文件夹操作) + - [1.1.4.1 用os.makedirs()创建新文件夹](#1141-用osmakedirs创建新文件夹) + - [1.1.4.2 查看文件大小和文件夹内容](#1142-查看文件大小和文件夹内容) + - [1.1.5 文件读写过程](#115-文件读写过程) + - [1.1.5.1 用open()函数打开文件](#1151-用open函数打开文件) + - [1.1.5.2 读取文件内容](#1152-读取文件内容) + - [1.1.5.3 写入文件](#1153-写入文件) + - [1.1.5.4 保存变量](#1154-保存变量) + - [1.1.6 练习](#116-练习) + - [1.1.7 组织文件](#117--组织文件) + - [1.1.1.7.1 shutil模块](#11171-shutil模块) + - [1.1.1.7.2 复制文件和文件夹](#11172-复制文件和文件夹) + - [1.1.7.3 文件和文件夹的移动与改名](#1173-文件和文件夹的移动与改名) + - [1.1.7.4 永久删除文件和文件夹](#1174-永久删除文件和文件夹) + - [1.1.7.5 用send2trash模块安全地删除](#1175-用send2trash模块安全地删除) + - [1.1.8 遍历目录树](#118-遍历目录树) + - [1.1.9 用zipfile模块压缩文件](#119-用zipfile模块压缩文件) + - [1.1.9.1 创建和添加到zip文件](#1191-创建和添加到zip文件) + - [1.1.9.2 读取zip文件](#1192-读取zip文件) + - [1.1.9.3 从zip文件中解压缩](#1193-从zip文件中解压缩) + - [1.1.10 练习](#1110-练习) + - [1.2 自动发送电子邮件](#12-自动发送电子邮件) -## 1.1 读写文件 我们知道,程序运行时,可以用变量来保存运算结果,但如果希望程序运行关闭后,依然可以查看运行后的结果,就需要将数据保存到文件中。简单点,你可以将文件内容理解为一个字符串值,大小可能有几个GB。本节将学习,如何使用python在硬盘上创建、读取和保存文件。 +## 1.1文件处理 +### 1.1.1 文件与文件路径 -## 1.1.1 文件与文件路径 文件的两个属性:“路径”和“文件名”,路径指明文件在计算机上的位置,文件名是指该位置的文件的名称。比如,我的电脑上,有个名字为Datawhale - 开源发展理论研究.pdf的文件,它的路径在D:\Datawhale。在windows中,路径中的D:\部分是“根文件夹”,Datawhale是文件夹名。注:Windows中文件夹名和文件名不区分大小写的。 在windows上,路径书写是使用倒斜杠'\'作为文件夹之间的分隔符,而在OS X和Linux上,是使用正斜杠'/'作为它们的路径分隔符。通常我们用`os.path.join()`函数来创建文件名称字符串。 @@ -46,7 +46,8 @@ os.path.join('Datawhale','docu') 我们可以看到返回的是('Datawhale\\\docu'),有两个斜杠,这是因为有一个斜杠是用来转义的,在OS X或Linux上调用这个函数,这个字符串就会是'Datawhale/docu'。 -## 1.1.2 当前工作目录 +### 1.1.2 当前工作目录 + 每个运行在计算机上的程序,都有一个“当前工作目录”。利用`os.getcwd()`函数,可以取得当前工作路径的 字符串,并可以利用`os.chdir()`改变它。 @@ -60,13 +61,14 @@ os.chdir('D:\\Datawhale\\python办公自动化') #改变当前工作目 os.getcwd() ``` -## 1.1.3 路径操作 +### 1.1.3 路径操作 + +#### 1.1.3.1 绝对路径和相对路径 -### 1.1.3.1 绝对路径和相对路径 “绝对路径”,总是从根文件夹开始。 “相对路径”,相对于程序的当前工作目录。 相对路径中,单个句点“.”表示当前目录的缩写,两个句点“..”表示父文件夹。 -![image-20210518151332873](.\图片\image-20210518151332873.png) +![image-20210518151332873](./图片/1.1.png) 几个常用的绝对路径和相对路径处理函数 @@ -78,7 +80,8 @@ os.path.isabs('.') #False os.path.isabs(os.path.abspath('.')) #True ``` -### 1.1.3.2 路径操作 +#### 1.1.3.2 路径操作 + `os.path.relpath(path,start)`:返回从start路径到path的相对路径的字符串。如果没提供start,就使用当前工作目录作为开始路径。 `os.path.dirname(path)`: 返回当前路径的目录名称。 `os.path.basename(path)`:返回当前路径的文件名称。 @@ -117,7 +120,8 @@ os.path.split(caFilePath) #('D:\\Datawhale\\python办公自动化', 'python课 caFilePath.split(os.path.sep) #['D:', 'Datawhale', 'python办公自动化', 'python课程画图.pptx'] ``` -### 1.1.3.3 路径有效性检查 +#### 1.1.3.3 路径有效性检查 + 如果提供的路径不存在,很多Python函数就会崩溃并报错。`os.path`模块提供了一些函数,用于检测给定的路径是否存在,以及判定是文件还是文件夹。 `os.path.exists(path)`:如果path参数所指的文件或文件夹存在,则返回True,否则返回False。 @@ -150,9 +154,10 @@ os.path.isdir('D:\\Datawhale\\python办公自动化\\python课程画图.pptx') os.path.isdir('D:\\Datawhale\\python办公自动化') ``` -## 1.1.4 文件及文件夹操作 +### 1.1.4 文件及文件夹操作 + +#### 1.1.4.1 用os.makedirs()创建新文件夹 -### 1.1.4.1 用os.makedirs()创建新文件夹 注:`os.makedirs()`可以创建所有必要的中间文件夹。 ```python @@ -160,7 +165,8 @@ import os os.makedirs('D:\\Datawhale\\practice') #查看目录,已创建,若文件夹已存在,不会覆盖,会报错 ``` -### 1.1.4.2 查看文件大小和文件夹内容 +#### 1.1.4.2 查看文件大小和文件夹内容 + 我们已经可以处理文件路径,这是操作文件及文件夹的基础。接下来,我们可以搜集特定文件和文件夹的信息。`os.path`模块提供了一些函数,用于查看文件的字节数以及给定文件夹中的文件和子文件夹。 `os.path.getsize(path)`:返回path参数中文件的字节数。 `os.listdir(path)`:返回文件名字符串的列表,包含path参数中的每个文件。 @@ -182,7 +188,8 @@ for filename in os.listdir('D:\\Datawhale\\python办公自动化'): print(totalSize) ``` -## 1.1.5 文件读写过程 +### 1.1.5 文件读写过程 + 读写文件3个步骤: 1.调用`open()`函数,返回一个File对象。 @@ -191,7 +198,8 @@ print(totalSize) 3.调用File对象的`close()`方法,关闭该文件。 -### 1.1.5.1 用open()函数打开文件 +#### 1.1.5.1 用open()函数打开文件 + 要用`open()`函数打开一个文件,就要向它传递一个字符串路径,表明希望打开的文件。这既可以是绝对路径,也可以是相对路径。`open()`函数返回一个File对象。 先用TextEdit创建一个文本文件,名为hello.txt。输入Hello World!作为该文本文件的内容,将它保存在你的用户文件夹中。 @@ -202,7 +210,8 @@ print(helloFile) 可以看到,调用`open()`函数将会返回一个File对象。当你需要读取或写入该文件,就可以调用helloFile变量中的File对象的方法。 -### 1.1.5.2 读取文件内容 +#### 1.1.5.2 读取文件内容 + 有了File对象,我们就可以开始从它读取内容。 `read()`:读取文件内容。 @@ -219,7 +228,8 @@ sonnetFile = open('D:\\Datawhale\\python办公自动化\\hello.txt') sonnetFile.readlines() ``` -### 1.1.5.3 写入文件 +#### 1.1.5.3 写入文件 + 需要用“写模式”‘w’和“添加模式”'a'打开一个文件,而不能用读模式打开文件。 “写模式”将覆写原有的文件,从头开始。“添加模式”将在已有文件的末尾添加文本。 @@ -250,7 +260,8 @@ print(content) 注意,`write()`方法不会像print()函数那样,在字符串的末尾自动添加换行字符。必须自己添加该字符。 -### 1.1.5.4 保存变量 +#### 1.1.5.4 保存变量 + 1)、shelve模块 用`shelve`模块,可以将Python中的变量保存到二进制的`shelf`文件中。这样,程序就可以从硬盘中恢复变量的数据。 @@ -335,7 +346,7 @@ myCats.cats[0] myCats.cats[0]['name'] ``` -## 1.1.6 练习 +### 1.1.6 练习 1、如果已有的文件以写模式打开,会发生什么? @@ -367,7 +378,7 @@ myCats.cats[0]['name'] • 利用 random.shuffle()随机调整问题和多重选项的次序。 -## 1.2 组织文件 +### 1.1.7 组织文件 在上一节中,已经学习了如何使用Python创建并写入新文件。本节将介绍如何用程序组织硬盘上已经存在的文件。不知你是否经历过查找一个文件夹,里面有几十个、几百个、甚至上千个文件,需要手工进行复制、改名、移动或压缩。比如下列这样的任务: @@ -379,11 +390,11 @@ myCats.cats[0]['name'] 所有这种无聊的任务,正是在请求用 Python 实现自动化。通过对电脑编程来完成这些任务,你就把它变成了一个快速工作的文件职员,而且从不犯错。 -### 1.2.1 shutil模块 +#### 1.1.1.7.1 shutil模块 `shutil`(或称为shell工具)模块中包含一些函数,可以在Python程序中复制、移动、改名和删除文件。要使用`shutil`的函数,首先需要`import shutil` -#### 1.2.1.1 复制文件和文件夹 +#### 1.1.1.7.2 复制文件和文件夹 `shutil.copy(source, destination)`:将路径source处的文件复制到路径 destination处的文件夹(source 和 destination 都是字符串),并返回新复制文件绝对路径字符串。 @@ -421,7 +432,7 @@ import shutil shutil.copytree('D:\\Datawhale\\python办公自动化','D:\\Datawhale\\practice') ``` -#### 1.2.1.2 文件和文件夹的移动与改名 +#### 1.1.7.3 文件和文件夹的移动与改名 `shutil.move(source, destination)`:将路径 source 处的文件/文件夹移动到路径destination,并返回新位置的绝对路径的字符串。 @@ -446,7 +457,8 @@ shutil.move('D:\\Datawhale\\practice','D:\\Datawhale\\docue') shutil.move('D:\\Datawhale\\docue\\bacon.txt','D:\\Datawhale\\docu\\egg.txt') ``` -#### 1.2.1.3 永久删除文件和文件夹 +#### 1.1.7.4 永久删除文件和文件夹 + `os.unlink(path)`: 删除path处的文件。 `os.rmdir(path)`: 删除path处的文件夹。该文件夹必须为空,其中没有任何文件和文件夹。 @@ -469,7 +481,8 @@ for filename in os.listdir(): print(filename) ``` -#### 1.2.1.4 用send2trash模块安全地删除 +#### 1.1.7.5 用send2trash模块安全地删除 + `shutil.rmtree(path)`会不可恢复的删除文件和文件夹,用起来会有危险。因此使用第三方的`send2trash`模块,可以将文件或文件夹发送到计算机的垃圾箱或回收站,而不是永久删除。因程序缺陷而用send2trash 删除的某些你不想删除的东西,稍后可以从垃圾箱恢复。 注意:使用时,需要非常小心,避免删错文件,一般在第一次运行时,注释掉这些程序,并加上`print()`函数来帮助查看是否是想要删除的文件。 @@ -483,7 +496,8 @@ import send2trash send2trash.send2trash('bacon.txt') ``` -## 1.2.2 遍历目录树 +### 1.1.8 遍历目录树 + `os.walk(path)`:传入一个文件夹的路径,在for循环语句中使用`os.walk()`函数,遍历目录树,和range()函数遍历一个范围的数字类似。不同的是,`os.walk()`在循环的每次迭代中,返回三个值: 1)、当前文件夹称的字符串。 @@ -494,7 +508,7 @@ send2trash.send2trash('bacon.txt') 注:当前文件夹,是指for循环当前迭代的文件夹。程序的当前工作目录,不会因为`os.walk()`而改变。 -![image-20210518154517118](.\图片\image-20210518154517118.png) +![image-20210518154517118](.\图片\1.2.png) 按照下图目录树,创建相应的文件。 @@ -510,11 +524,11 @@ for folderName, subFolders,fileNames in os.walk('D:\\animals'): print('') ``` -## 1.2.3 用zipfile模块压缩文件 +### 1.1.9 用zipfile模块压缩文件 为方便传输,常常将文件打包成.zip格式文件。利用zipfile模块中的函数,Python程序可以创建和打开(或解压)zip文件。 -### 1.2.3.1 创建和添加到zip文件 +#### 1.1.9.1 创建和添加到zip文件 将上述章节中animals文件夹进行压缩。创建一个example.zip的zip文件,并向其中添加文件。 @@ -549,7 +563,7 @@ for folderName, subFolders,fileNames in os.walk('D:\\animals'): newZip.close() ``` -### 1.2.3.2 读取zip文件 +#### 1.1.9.2 读取zip文件 调用`zipfile.ZipFile(filename)`函数创建一个`ZipFile`对象(注意大写字母Z和F),filename是要读取zip文件的文件名。 @@ -587,7 +601,7 @@ print('Compressed file is %s x smaller!' %(round(catInfo.file_size/catInfo.compr exampleZip.close() ``` -### 1.2.3.3 从zip文件中解压缩 +#### 1.1.9.3 从zip文件中解压缩 `ZipFile` 对象的 `extractall()`方法:从zip文件中解压缩所有文件和文件夹,放到当前工作目录中。也可以向`extractall()`传递的一个文件夹名称,它将文件解压缩到那个文件夹, 而不是当前工作目录。如果传递的文件夹名称不存在,就会被创建。 @@ -607,7 +621,7 @@ exampleZip.extract('animals/Miki.txt', 'D:\\animals\\folders') exampleZip.close() ``` -## 1.2.4 练习 +### 1.1.10 练习 1)、编写一个程序,遍历一个目录树,查找特定扩展名的文件(诸如.pdf 或.jpg)。不论这些文件的位置在哪里, 将它们拷贝到一个新的文件夹中。 @@ -615,7 +629,7 @@ exampleZip.close() 3)、编写一个程序, 在一个文件夹中, 找到所有带指定前缀的文件, 诸如 spam001.txt,spam002.txt 等,并定位缺失的编号(例如存在 spam001.txt 和 spam003.txt, 但不存在 spam002.txt)。让该程序对所有后面的文件改名, 消除缺失的编号。作为附加的挑战,编写另一个程序,在一些连续编号的文件中,空出一些编号,以便加入新的文件。 -## 2 自动发送电子邮件 +## 1.2 自动发送电子邮件 使用Python实现自动化邮件发送,可以让你摆脱繁琐的重复性业务,节省非常多的时间。 diff --git a/OfficeAutomation/Task02 Python与Excel.md b/OfficeAutomation/Task02 Python与Excel.md index ba783ec..74453f0 100644 --- a/OfficeAutomation/Task02 Python与Excel.md +++ b/OfficeAutomation/Task02 Python与Excel.md @@ -1,121 +1,181 @@ -# Python自动化之Excel +# Task 02 Python自动化之Excel -- [Python自动化之Excel](#python自动化之excel) - - [0.包的安装](#0包的安装) - - [1.Excel读取](#1excel读取) - - [1.1读取对应表格](#11读取对应表格) - - [1.2读取单元格](#12读取单元格) - - [1.3读取多个格子的值](#13读取多个格子的值) - - [1.4练习题](#14练习题) - - [2.Excel写入](#2excel写入) - - [2.1写入单元格并保存](#21写入单元格并保存) - - [2.2写入行数据并保存](#22写入行数据并保存) - - [2.3将公式写入单元格保存](#23将公式写入单元格保存) - - [2.4插入列数据](#24插入列数据) - - [2.5插入行数据](#25插入行数据) - - [2.6删除](#26删除) - - [2.7移动](#27移动) - - [2.8Sheet表操作](#28sheet表操作) - - [2.9创建新的Excel表](#29创建新的excel表) - - [3.Excel 样式](#3excel-样式) - - [3.1设置字体样式](#31设置字体样式) - - [3.2设置对齐样式](#32设置对齐样式) - - [3.3设置行高与列宽](#33设置行高与列宽) - - [3.4合并、取消合并单元格](#34合并取消合并单元格) - - [3.5练习题](#35练习题) +- [Task 02 Python自动化之Excel](#task-02-python自动化之excel) + - [2.0 包的安装](#20-包的安装) + - [2.1 Excel读取](#21-excel读取) + - [2.1.1 读取对应表格](#211-读取对应表格) + - [2.1.2 读取单元格](#212-读取单元格) + - [2.1.3 读取多个格子的值](#213-读取多个格子的值) + - [2.1.4 练习题](#214-练习题) + - [2.2 Excel写入](#22-excel写入) + - [2.2.1 写入数据并保存](#221-写入数据并保存) + - [2.2.2 将公式写入单元格保存](#222-将公式写入单元格保存) + - [2.2.3 插入数据](#223-插入数据) + - [2.2.4 删除](#224-删除) + - [2.2.5 移动](#225-移动) + - [2.2.6 Sheet表操作](#226-sheet表操作) + - [2.3 Excel 样式](#23-excel-样式) + - [2.3.1设置字体样式](#231设置字体样式) + - [2.3.2 设置对齐样式](#232-设置对齐样式) + - [2.3.3 设置行高与列宽](#233-设置行高与列宽) + - [2.3.4 合并、取消合并单元格](#234-合并取消合并单元格) + - [2.3.5 练习题](#235-练习题) + - [2.4 后记](#24-后记) -## 0.包的安装 +## 2.0 包的安装 + + 操作难度:⭐ 方法一:应用pip执行命令 安装**openpyxl**模块`pip install openpyxl` +​ 注:openpyxl可以读取xlsx的格式,但是不可以去读xls格式;读取xls格式,可以安装**xlrd**模块,`pip install xlrd`,本章节以xlsx格式为主。 + 方法二:在Pycharm中:File->Setting->左侧Project Interpreter -![image-20210521175826192](./图片/pycharm1.png) +![image-20211110131511271](.\图片\2.1.png) -![image-20210521175849021](./图片/pycharm2.png) +![image-20211110131522374](.\图片\2.2.png) -![](./图片/Excel.png) +## 2.1 Excel读取 -## 1.Excel读取 + 项目难度:⭐ -### 1.1读取对应表格 - -1. 打开已经存在的Excel表格 + - Excel全称为Microsoft Office Excel,2003年版本的是xls格式,2007和2007年之后的版本是xlsx格式。 + - xlsx格式通过`openpyxl`模块打开; xls格式通过`xlwt`模块写,`xlrd`模块读取。 + - 本文以xlsx模式为例 ``` +#多行内容显现 +from IPython.core.interactiveshell import InteractiveShell +InteractiveShell.ast_node_interactivity = "all" +``` + +### 2.1.1 读取对应表格 + +``` +#获取当前工作目录 +import os +os.getcwd() + +import warnings +warnings.filterwarnings('ignore') +``` + +**关于路径:** + +文件应在当前工作目录才可引用,可导入`os`,使用函数`os.getcwd()`弄清楚当前工作目录是什么,可使用`os.chdir()`改变当前工作目录,具体可参考第一章节。(此处显现为相对路径) + +1. 查看属性 + +``` +#导入模块,查看属性 +import openpyxl + +wb = openpyxl.load_workbook('用户行为偏好.xlsx') +type(wb) +``` + +**import * 和from...import...** + +`import *`和`from...import...`的区别 + + - `import`导入一个模块,相当于导入的是一个文件夹,相对路径。 + - `from...import...`导入了一个模块中的一个函数,相当于文件夹中的文件,绝对路径。 + +2. 打开已经存在的Excel表格,查询对应sheet的名称 + +``` +#导入模块中的函数,查询对应表的名称 from openpyxl import load_workbook -exl = load_workbook(filename = 'test.xlsx') +exl = load_workbook(filename = '用户行为偏好.xlsx') print(exl.sheetnames) ``` -2. 根据名称获取表格 - ``` -from openpyxl import load_workbook - -exl_1 = load_workbook(filename = 'test.xlsx') -print(exl_1.sheetnames) - -sheet = exl_1['work'] - -'可改为如果表中只有一个sheet可以直接用active:' -sheet = exl_1.active +'''通过传递表名字符串读取表、类型和名称 ''' +sheet = exl.get_sheet_by_name('Sheet3') +sheet +type(sheet) +sheet.title +'''读取工作簿的活动表''' +#活动表是工作簿在Excel中打开时出现的工作表,再取得Worksheet对象后,可通过title属性取得它的名称。 +anotherSheet = exl.active +anotherSheet ``` 3. 获取Excel 内容占据的大小 ``` -print(sheet.dimensions) +sheet.dimensions ``` -### 1.2读取单元格 +### 2.1.2 读取单元格 +![image-20211110131533928](.\图片\2.3.png) +**Cell** -1. 获取某个单元格的具体内容 + - Cell对象有一个value属性,包含这个单元格中保存的值。 + - Cell对象也有row、column和coordinate属性,提供该单元格的位置信息。 + - Excel用字母指定列,在Z列之后,列开始使用两个字母:AA、AB等,所以在调用的cell()方法时,可传入整数作为row和column关键字参数,也可以得到一个单元格。 + - 注:第一行或第一列的整数取1,而不是0. ``` -cell = sheet.cell(row=1,column=2) #指定行列数 -print(cell.value) +# 从表中取得单元格 +## 获取表格名称 +from openpyxl import load_workbook +exl = openpyxl.load_workbook('用户行为偏好.xlsx') +exl.get_sheet_names() -cell_1 = sheet['A1'] #指定坐标 -print(cell_1.value) +# 读取单元格 +sheet = exl.get_sheet_by_name('用户次数偏好') +'''显现单元格格式''' +sheet['A1'] +'''显现单元格文本内容''' +sheet['A1'].value +#另一种表达方式 +a = sheet['A1'] +a.value +'''行、列和数值显现''' +'Row' + str(a.row) + ', Column' + str(a.column) + ' is ' + a.value + +'''显现单元格''' +'Cell ' + a.coordinate + ' is ' + a.value ``` -2. 获取单元格对应的行、列和坐标 - ``` -print(cell_1.row, cell_1.column, cell.coordinate) +# 顺B列打出前8行的奇数行单元格的值 +for i in range(1,8,2): + print(i,sheet.cell(row=i,column=2).value) ``` -### 1.3读取多个格子的值 - -1. 指定坐标范围 - ``` -cells = sheet['A1:C8'] #A1到C8区域的值 +#确定表格的最大行数和最大列数,即表的大小 +sheet.max_row +sheet.max_column ``` -2. 指定行的值 + + +### 2.1.3 读取多个格子的值 ``` -Row = sheet[1] #第1行的值 -Rows = sheet[1:2] #第1到2行的值 +#A1到C8区域的值 +cells = sheet['A1:C8'] +type(cells) +#用enumerate包装一个可迭代对象,同时使用索引和迭代项 +for index, item in enumerate(sheet['A1:C8']): + if index >= 1: + print("\n") + for cell in item: + print(cell.value,end=" ") ``` -3. 指定列的值 - -``` -Column = sheet['A'] #第A列 -Columns = sheet['A:C'] #第A到C列 -``` - -4. 指定范围的值 - ``` +# 指定范围的值 # 行获取 for row in sheet.iter_rows(min_row = 1, max_row = 5, min_col = 2, max_col = 6): @@ -133,15 +193,16 @@ for col in sheet.iter_cols(min_row = 1, max_row = 5, print(cell.value) ``` -### 1.4练习题 +### 2.1.4 练习题 -找出test_1.xlsx中sheet1表中空着的格子,并输出这些格子的坐标 +找出用户行为偏好.xlsx中sheet1表中空着的格子,并输出这些格子的坐标 ``` from openpyxl import load_workbook -exl = load_workbood('test_1.xlsx') +exl = load_workbook('用户行为偏好.xlsx') sheet = exl.active + for row in sheet.iter_rows(min_row = 1, max_row = 29972, min_col = 1, max_col = 10): #具体查看对应表格的行列数 @@ -152,111 +213,98 @@ for row in sheet.iter_rows(min_row = 1, max_row = 29972, -## 2.Excel写入 +## 2.2 Excel写入 -### 2.1写入单元格并保存 + 项目难度:⭐ + +### 2.2.1 写入数据并保存 + +1. 原有工作簿中写入数据并保存 ``` +# 已有的表格赋值保存 from openpyxl import load_workbook -exl = load_workbook(filename = 'test.xlsx') +exl = load_workbook(filename = '用户行为偏好.xlsx') sheet = exl.active sheet['A1'] = 'hello world' #或者cell = sheet['A1'] #cell.value = 'hello world' -exl.save(filename = 'test.xlsx') #存入原Excel表中,若创建新文件则可命名为不同名称 +exl.save(filename = '用户行为偏好.xlsx') #存入原Excel表中,若创建新文件则可命名为不同名称 ``` -### 2.2写入行数据并保存 - -1. 写入一行数据并保存 +2. 创建新的表格写入数据并保存 ``` -import xlwt -workbook = xlwt.Workbook(encoding = 'utf-8') +# openpyxl 写入xsxl +from openpyxl import load_workbook +wb = openpyxl.Workbook() # 创建一个sheet -sheet = workbook.add_sheet('My Worksheet') +sh = wb.active +sh.title = 'My Worksheet' #注:此处在工作簿内的表格名称没有变。 # 写入excel # 参数对应 行, 列, 值 -sheet.write(1,0,label = 'this is test') +sh.cell(1,1).value = 'this is test' # 保存 -workbook.save('new_test.xls') +wb.save('new_test.xlsx') ``` -2. 写入多行数据并保存 +### 2.2.2 将公式写入单元格保存 ``` -import xlwt -exl=xlwt.Workbook(encoding='utf-8') -worksheet=exl.add_sheet('My Worksheet') +# 公式写入单元格保存 +from openpyxl import load_workbook -data = [['hello',22,'hi'], - ['hell',23,'h'], - ['he',25,'him']] -for i in range(len(data)): - for j in range(len(data[i])): - worksheet.write(i,j,data[i][j]) -exl.save(filename = 'test1.xlsx') +exl = load_workbook(filename = '用户行为偏好.xlsx') +sheet = exl.get_sheet_by_name('Sheet3') +sheet.dimensions #先查看原有表格的单元格范围,防止替代原有数据 + +sheet['A30'] = '=SUM(A1:D1)' +exl.save(filename='用户行为偏好.xlsx') ``` -### 2.3将公式写入单元格保存 +### 2.2.3 插入数据 ``` -sheet[‘A2’] = '=SUM(A1:D1)' -exl.save(filename='test.xlsx') -``` - -### 2.4插入列数据 - -1. 插入一列 - -``` -sheet.insert_cols(idx=2) #idx=2第2列,第2列前插入一列 -``` - -2. 插入多列 - -``` -#第2列前插入5列作为举例 +#插入列数据 +'''idx=2第2列,第2列前插入一列''' +sheet.insert_cols(idx=2) +'''第2列前插入5列作为举例''' sheet.insert_cols(idx=2, amount=5) -``` -### 2.5插入行数据 - -第2行前上面插入一行(或多行) - -``` -#插入一行 +#插入行数据 +'''插入一行''' sheet.insert_rows(idx=2) -#插入多行 +'''插入多行''' sheet.insert_rows(idx=2, amount=5) + +exl.save(filename='用户行为偏好.xlsx') ``` -### 2.6删除 - -1. 删除多列 - -``` -sheet.delete_cols(idx=5, amount=2) #第5列前删除2列 -``` - -2. 删除多行 +### 2.2.4 删除 ``` +# 删除多列 +sheet.delete_cols(idx=5, amount=2) +# 删除多行 sheet.delete_rows(idx=2, amount=5) + +exl.save(filename='用户行为偏好.xlsx') ``` -### 2.7移动 +### 2.2.5 移动 当数字为正即向下或向右,为负即为向上或向左 ``` -sheet.move_range('C5:F10', rows=2, cols=-3) +#移动 +'''当数字为正即向下或向右,为负即为向上或向左''' +sheet.move_range('B3:E16',rows=1,cols=-1) ``` -### 2.8Sheet表操作 +### 2.2.6 Sheet表操作 1. 创建新的sheet @@ -270,34 +318,20 @@ workbook.save(filename='new_test.xlsx') exl.create_sheet('new_sheet') ``` -2. 复制已有的sheet - -``` -exl.copy_worksheet(sheet) -``` - -3. 修改sheet表名 +2. 修改sheet表名 ``` sheet = exl.active sheet.title = 'newname' ``` -### 2.9创建新的Excel表 - -``` -from openpyxl import load_workbook - -workbook = Workbook() -sheet = workbook.active -workbook.save(filename = 'new_test.xlsx') -``` +## 2.3 Excel 样式 -## 3.Excel 样式 + 项目难度:⭐⭐ -### 3.1设置字体样式 +### 2.3.1设置字体样式 1. 设置字体样式 @@ -310,7 +344,7 @@ workbook.save(filename = 'new_test.xlsx') workbook = Workbook() sheet = workbook.active cell = sheet['A1'] - font = Font(name='字体', sizee=10, bold=True, italic=True, color='FF0000') + font = Font(name='字体', size=10, bold=True, italic=True, color='FF0000') cell.font = font workbook.save(filename='new_test') ``` @@ -324,13 +358,13 @@ workbook.save(filename = 'new_test.xlsx') workbook = Workbook() sheet = workbook.active cells = sheet[2] - font = Font(name='字体', sizee=10, bold=True, italic=True, color='FF000000') + font = Font(name='字体', size=10, bold=True, italic=True, color='FF000000') for cell in cells: cell.font = font workbook.save(filename='new_test') ``` -### 3.2设置对齐样式 +### 2.3.2 设置对齐样式 水平对齐:`distributed, justify, center, left, fill, centerContinuous, right, general` @@ -338,9 +372,9 @@ workbook.save(filename = 'new_test.xlsx') 1. 设置单元格边框样式 -`Side(style变现样式, color边线颜色)` +`Side`:变现样式,边线颜色等 -`Border(左右上下边线)` +`Border`:左右上下边线 ``` from openpyxl import Workbook @@ -349,7 +383,7 @@ from openpyxl.styles import Font workbook = Workbook() sheet = workbook.active cell = sheet['A1'] -side = Side(style='thin', color='FF000000') +side = Side(border_style='thin', color='FF000000') #先定好side的格式 border = Border(left=side, right=side, top=side, bottom=side) #代入边线中 @@ -363,13 +397,13 @@ workbook.save(filename='new_test') ``` from openpyxl import Workbook -from openpyxl.styles import Font +from openpyxl.styles import PatternFill, Border, Side, Alignment, Font, GradientFill workbook = Workbook() sheet = workbook.active cell = sheet['A1'] -pattern_fill = PatternFill(fill_type='solid', fgColor -cell1.fill = pattern_fill +pattern_fill = PatternFill(fill_type='solid',fgColor="DDDDDD") +cell.fill = pattern_fill #单色填充 cell2 = sheet['A3'] gradient_fill = GradientFill(stop=('FFFFFF', '99ccff','000000')) @@ -378,7 +412,7 @@ cell2.fill = gradient_fill workbook.save(filename='new_test') ``` -### 3.3设置行高与列宽 +### 2.3.3 设置行高与列宽 ``` from openpyxl import Workbook @@ -386,10 +420,11 @@ from openpyxl import Workbook workbook = Workbook() sheet = workbook.active sheet.row_dimensions[1].height = 50 -sheet.column_dimensions['C'].width = 20 workbook.save(filename='new_test') +sheet.column_dimensions['C'].width = 20 +workbook.save(filename='new_test') ``` -### 3.4合并、取消合并单元格 +### 2.3.4 合并、取消合并单元格 ``` sheet.merge_cells('A1:B2') @@ -401,7 +436,7 @@ sheet.unmerge_cells(start_row=1, start_column=3, end_row=2, end_column=4) ``` -### 3.5练习题 +### 2.3.5 练习题 打开test文件,找出文件中购买数量`buy_mount`超过5的行,并对其标红、加粗、附上边框。 @@ -426,9 +461,12 @@ for row in row_lst: for cell in sheet[row]: cell.font = font cell.border = border -workbook.save('new_test'.xlsx') +workbook.save('new_test.xlsx') ``` +## 2.4 后记 +- Python与Excel的自动化内容较多,此篇重在介绍基础,起到抛砖引玉的学习效果。 +- Excel文档参考 [用户行为偏好.xlsx](.\用户行为偏好.xlsx) diff --git a/OfficeAutomation/Task03 python与word.md b/OfficeAutomation/Task03 python与word.md index e0e3d58..5c23c6e 100644 --- a/OfficeAutomation/Task03 python与word.md +++ b/OfficeAutomation/Task03 python与word.md @@ -1,22 +1,23 @@ -# python自动化之word操作 +# Task 03 python与word -- [python自动化之word操作](#python自动化之word操作) - - [1.课前准备](#1课前准备) - - [2.知识要点](#2知识要点) - - [2.1初步认识docx](#21初步认识docx) - - [2.1.1新建空白word并插入文字](#211新建空白word并插入文字) - - [2.2python自动化之word操作](#22python自动化之word操作) - - [2.2.1整体页面结构介绍](#221整体页面结构介绍) - - [2.2.2字体设置](#222字体设置) - - [2.2.3插入图片与表格](#223插入图片与表格) - - [2.2.4设置页眉页脚](#224设置页眉页脚) - - [2.2.5代码延伸](#225代码延伸) - - [3.项目实践](#3项目实践) - - [3.1需求](#31需求) - - [3.2需求分析](#32需求分析) - - [3.3代码](#33代码) - - [4.后记](#4后记) -## 1.课前准备 +- [Task 03 python与word](#task-03-python与word) + - [3.0 课前准备](#30-课前准备) + - [3.1.知识要点](#31知识要点) + - [3.1.1 初步认识docx](#311-初步认识docx) + - [3.1.2 整体页面结构介绍](#312-整体页面结构介绍) + - [3.1.2字体设置](#312字体设置) + - [3.1.3插入图片与表格](#313插入图片与表格) + - [3.1.4设置页眉页脚](#314设置页眉页脚) + - [3.1.5代码延伸](#315代码延伸) + - [3.2 项目实践](#32-项目实践) + - [3.2.1需求](#321需求) + - [3.2.2需求分析](#322需求分析) + - [3.2.3代码](#323代码) + - [3.3 后记](#33-后记) + + + +## 3.0 课前准备 > python 处理 Word 需要用到 python-docx 库,终端执行如下安装命令: @@ -28,11 +29,13 @@ conda install python-docx > 或在pycharm的setting操作安装(示意如下): - ![](.\图片\安装docx.png) + ![](.\图片\3.1.png) -## 2.知识要点 +## 3.1.知识要点 + + 项目难度:⭐ > 说明: > 1. 通过小试牛刀初步认识docx,然后系统学习python对word的操作; @@ -41,12 +44,10 @@ conda install python-docx -### 2.1初步认识docx +### 3.1.1 初步认识docx 相信同学们都进行过word的操作。话不多说,直接上python对word简单操作的代码,先有个直观的感觉,然后再系统学习! -#### 2.1.1新建空白word并插入文字 - ```python # 导入库 from docx import Document @@ -82,9 +83,7 @@ paragraph_3 = doc_1.add_paragraph('这是第二页第一段文字!') doc_1.save('doc_1.docx') ``` - - -### 2.2python自动化之word操作 +--- 上节只是小试牛刀一下,接下来我们系统地学习python自动化之word操作。 @@ -101,7 +100,7 @@ doc_1.save('doc_1.docx') 在**`python-docx`**中,**`run`**是最基本的单位,每个**`run`**对象内的文本样式都是一致的,也就是说,在从**`docx`**文件生成文档对象时,**`python-docx`**会根据样式的变化来将文本切分为一个个的`Run`对象。 -#### 2.2.1整体页面结构介绍 +### 3.1.2 整体页面结构介绍 我们以一个小案例为主线把文档,段落和文字块串一下: @@ -154,7 +153,7 @@ doc_1.save('周杰伦.docx') ``` 通过上例我们可以看到,最小的操作对象为文字块,通过run的指定进行操作。比如字号,颜色等;而再上一个层级--段落是的格式是通过paragraph_format进行设置; -#### 2.2.2字体设置 +### 3.1.2字体设置 通过(1),同学们已经注意到,字体的设置是全局变量。如果我想在不同的部分进行不同字体的设置,那该怎么办呢?这就需要在应用前操作设置一下。 @@ -223,7 +222,7 @@ doc.save('字体设置2.docx') 我们很容易地看出来,字体设置1.py与字体设置2.py的区别在于是否为同一段落,同时字体设置2.py中自定义了一个函数。同学们可以在实际工作中看具体场景进行选择。 -#### 2.2.3插入图片与表格 +### 3.1.3插入图片与表格 ```python #导入库 @@ -253,7 +252,7 @@ doc_1.save('周杰伦为营口加油.docx') ``` -#### 2.2.4设置页眉页脚 +### 3.1.4设置页眉页脚 在python-docx包中则要使用节(section)中的页眉(header)和页脚(footer)对象来具体设置。 @@ -302,9 +301,9 @@ document.save('页眉页脚1.docx') # 保存文档 结果如下: - ![](.\图片\页眉页脚设置.png) + ![](.\图片\3.2.png) -#### 2.2.5代码延伸 +### 3.1.5代码延伸 ```python '''对齐设置''' @@ -346,32 +345,34 @@ from docx.shared import RGBColor,Pt #underline :下划线 ``` -## 3.项目实践 +## 3.2 项目实践 -### 3.1需求 + 项目难度:⭐ ⭐ ⭐ + +### 3.2.1需求 > 你是公司的行政人员,对合作伙伴进行邀请,参加公司的会议; > > 参会人名单如下: - ![](.\图片\参会人名单.png) + ![](.\图片\3.3.png) 拟定的邀请函样式如下: - ![](.\图片\邀请函样式.png) + ![](.\图片\3.4.png) **根据参会人名单,利用python批量生成邀请函。** -### 3.2需求分析 +### 3.2.2需求分析 > 逻辑相对简单: > > - 获取 Excel 文件中每一行的信息,提取 参数;结合获取的参数设计邀请函样式并输出 > - 设计word段落及字体等样式。 -### 3.3代码 +### 3.2.3代码 ```python # 导入库 @@ -454,7 +455,7 @@ for row in sheet.rows: n = n + 1 ``` -### 4.后记 +## 3.3 后记 > 本案例也可适用于批量生产固定格式的word,如工资条,通知单等,面对这种相似且重复的任务,python的自动化运行能大幅提升当前的工作效率。 diff --git a/OfficeAutomation/Task04 Python操作PDF.md b/OfficeAutomation/Task04 Python操作PDF.md deleted file mode 100644 index d01e0e0..0000000 --- a/OfficeAutomation/Task04 Python操作PDF.md +++ /dev/null @@ -1,519 +0,0 @@ -# Python 操作 PDF - -- [Python 操作 PDF](#python-操作-pdf) - - [0.前言](#0前言) - - [1. 相关介绍](#1-相关介绍) - - [2. 批量拆分](#2-批量拆分) - - [3. 批量合并](#3-批量合并) - - [4. 提取文字内容](#4-提取文字内容) - - [5. 提取表格内容](#5-提取表格内容) - - [6. 提取图片内容](#6-提取图片内容) - - [7. 转换为图片](#7-转换为图片) - - [7.1 安装 pdf2image](#71-安装-pdf2image) - - [7.2 安装组件](#72-安装组件) - - [8. 添加水印](#8-添加水印) - - [9. 文档加密与解密](#9-文档加密与解密) - -## 0.前言 - -PDF 操作是本次自动化办公的最后一个知识点,初级的 PDF 自动化包括 PDF 文档的拆分、合并、提取等操作,更高级的还包括 WORD与PDF互转等 - -初级操作一般比较常用,也可以解决较多的办公内容,所以本节将会主要介绍 PDF 的初级操作,具体内容可以索引目录 - -
- -## 1. 相关介绍 - -Python 操作 PDF 会用到两个库,分别是:PyPDF2 和 pdfplumber - -其中 **PyPDF2** 可以更好的读取、写入、分割、合并PDF文件,而 **pdfplumber** 可以更好的读取 PDF 文件中内容和提取 PDF 中的表格 - -对应的官网分别是: - -> PyPDF2:https://pythonhosted.org/PyPDF2/ -> -> pdfplumber:https://github.com/jsvine/pdfplumber - -由于这两个库都不是 Python 的标准库,所以在使用之前都需要单独安装 - -win+r 后输入 cmd 打开 command 窗口,依次输入如下命令进行安装: - -> pip install PyPDF2 -> -> pip install pdfplumber - -安装完成后显示 success 则表示安装成功 - -![](https://raw.githubusercontent.com/double-point/GraphBed/master/python_2_pdf/%E5%AE%89%E8%A3%85%E6%88%90%E5%8A%9F.png) - -
- -## 2. 批量拆分 - -将一个完整的 PDF 拆分成几个小的 PDF,因为主要涉及到 PDF 整体的操作,所以本小节需要用到 PyPDF2 这个库 - -拆分的大概思路如下: - -- 读取 PDF 的整体信息、总页数等 -- 遍历每一页内容,以每个 step 为间隔将 PDF 存成每一个小的文件块 -- 将小的文件块重新保存为新的 PDF 文件 - -需要注意的是,在拆分的过程中,可以手动设置间隔,例如:每5页保存成一个小的 PDF 文件 - -拆分的代码如下: - -```python -def split_pdf(filename, filepath, save_dirpath, step=5): - """ - 拆分PDF为多个小的PDF文件, - @param filename:文件名 - @param filepath:文件路径 - @param save_dirpath:保存小的PDF的文件路径 - @param step: 每step间隔的页面生成一个文件,例如step=5,表示0-4页、5-9页...为一个文件 - @return: - """ - if not os.path.exists(save_dirpath): - os.mkdir(save_dirpath) - pdf_reader = PdfFileReader(filepath) - # 读取每一页的数据 - pages = pdf_reader.getNumPages() - for page in range(0, pages, step): - pdf_writer = PdfFileWriter() - # 拆分pdf,每 step 页的拆分为一个文件 - for index in range(page, page+step): - if index < pages: - pdf_writer.addPage(pdf_reader.getPage(index)) - # 保存拆分后的小文件 - save_path = os.path.join(save_dirpath, filename+str(int(page/step)+1)+'.pdf') - print(save_path) - with open(save_path, "wb") as out: - pdf_writer.write(out) - - print("文件已成功拆分,保存路径为:"+save_dirpath) -``` - -以“易方达中小盘混合型证券投资基金2020年中期报告”为例,整个 PDF 文件一共 46 页,每5页为间隔,最终生成了10个小的 PDF 文件 - -![](https://raw.githubusercontent.com/double-point/GraphBed/master/python_2_pdf/01.png) - -
- -**需要注意的是:** - -如果你是第一次运行代码,在运行过程中,会直接报如下的错误 - -![](https://raw.githubusercontent.com/double-point/GraphBed/master/python_2_pdf/%E6%8B%86%E5%88%86%E6%8A%A5%E9%94%99.png) - -如果是在 Pycharm 下,直接通过报错信息,点击 utils.py 文件,定位到第 238 行原文 - -原文中是这样的: - -```python - r = s.encode('latin-1') - if len(s) < 2: - bc[s] = r - return r -``` - -修改为: - -```python -try: - r = s.encode('latin-1') - if len(s) < 2: - bc[s] = r - return r -except Exception as e: - r = s.encode('utf-8') - if len(s) < 2: - bc[s] = r - return r -``` - -如果你使用的是 **anaconda**,对应的文件路径应该为:anaconda\Lib\site-packages\PyPDF2\utils.py,进行同样的修改操作即可 - -
- -## 3. 批量合并 - -比起拆分来,合并的思路更加简单: - -- 确定要合并的 **文件顺序** -- 循环追加到一个文件块中 -- 保存成一个新的文件 - -对应的代码比较简单,基本不会出现问题: - -```python -def concat_pdf(filename, read_dirpath, save_filepath): - """ - 合并多个PDF文件 - @param filename:文件名 - @param read_dirpath:要合并的PDF目录 - @param save_filepath:合并后的PDF文件路径 - @return: - """ - pdf_writer = PdfFileWriter() - # 对文件名进行排序 - list_filename = os.listdir(read_dirpath) - list_filename.sort(key=lambda x: int(x[:-4].replace(filename, ""))) - for filename in list_filename: - print(filename) - filepath = os.path.join(read_dirpath, filename) - # 读取文件并获取文件的页数 - pdf_reader = PdfFileReader(filepath) - pages = pdf_reader.getNumPages() - # 逐页添加 - for page in range(pages): - pdf_writer.addPage(pdf_reader.getPage(page)) - # 保存合并后的文件 - with open(save_filepath, "wb") as out: - pdf_writer.write(out) - print("文件已成功合并,保存路径为:"+save_filepath) -``` - -
- -## 4. 提取文字内容 - -涉及到具体的 PDF 内容 操作,本小节需要用到 pdfplumber 这个库 - -在进行文字提取的时候,主要用到 extract_text 这个函数 - -具体代码如下: - -```python -def extract_text_info(filepath): - """ - 提取PDF中的文字 - @param filepath:文件路径 - @return: - """ - with pdfplumber.open(filepath) as pdf: - # 获取第2页数据 - page = pdf.pages[1] - print(page.extract_text()) -``` - -可以看到,直接通过下标即可定位到相应的页码,从而通过 extract_text 函数提取该也的所有文字 - -而如果想要提取所有页的文字,只需要改成: - -```python -with pdfplumber.open(filepath) as pdf: - # 获取全部数据 - for page in pdf.pages - print(page.extract_text()) -``` - -例如,提取“易方达中小盘混合型证券投资基金2020年中期报告” 第一页的内容时,源文件是这样的: - -![](https://raw.githubusercontent.com/double-point/GraphBed/master/python_2_pdf/00.png) - -运行代码后提取出来是这样的: - -![](https://raw.githubusercontent.com/double-point/GraphBed/master/python_2_pdf/%E6%8F%90%E5%8F%96%E5%86%85%E5%AE%B9.png) - -> 拓展一下:此处可以结合前面 word 小节,将内容写入 word 文件中 - -
- -## 5. 提取表格内容 - -同样的,本节是对具体内容的操作,所以也需要用到 pdfplumber 这个库 - -和提取文字十分类似的是,提取表格内容只是将 extract_text 函数换成了 extract_table 函数 - -对应的代码如下: - -```python -def extract_table_info(filepath): - """ - 提取PDF中的图表数据 - @param filepath: - @return: - """ - with pdfplumber.open(filepath) as pdf: - # 获取第18页数据 - page = pdf.pages[17] - # 如果一页有一个表格,设置表格的第一行为表头,其余为数据 - table_info = page.extract_table() - df_table = pd.DataFrame(table_info[1:], columns=table_info[0]) - df_table.to_csv('dmeo.csv', index=False, encoding='gbk') -``` - -上面代码可以获取到第 18 页的第一个表格内容,并且将其保存为 csv 文件存在本地 - -> 但是,如果说第 18 页有多个表格内容呢? - -因为读取的表格会被存成二维数组,而多个二维数组就组成一个三维数组 - -遍历这个三位数组,就可以得到该页的每一个表格数据,对应的将 extract_table 函数 改成 extract_tables 即可 - -具体代码如下: - -```python -# 如果一页有多个表格,对应的数据是一个三维数组 -tables_info = page.extract_tables() -for index in range(len(tables_info)): - # 设置表格的第一行为表头,其余为数据 - df_table = pd.DataFrame(tables_info[index][1:], columns=tables_info[index][0]) - print(df_table) - # df_table.to_csv('dmeo.csv', index=False, encoding='gbk') -``` - -以“易方达中小盘混合型证券投资基金2020年中期报告” 第 xx 页的第一个表格为例: - -源文件中的表格是这样的: - -![](https://raw.githubusercontent.com/double-point/GraphBed/master/python_2_pdf/%E7%AC%AC%E4%B8%83%E9%A1%B5%E8%A1%A8%E6%A0%BC.png) - -提取并存入 excel 之后的表格是这样的: - -![](https://raw.githubusercontent.com/double-point/GraphBed/master/python_2_pdf/%E8%A1%A8%E6%A0%BC%E8%BD%AC%E5%AD%98csv%E6%95%88%E6%9E%9C%E5%9B%BE.png) - -
- -## 6. 提取图片内容 - -提取 PDF 中的图片和将 PDF 转存为图片是不一样的(下一小节),需要区分开。 - -提取图片:顾名思义,就是将内容中的图片都提取出来;转存为图片:则是将每一页的 PDF 内容存成一页一页的图片,下一小节会详细说明 - -转存为图片中,需要用到一个模块叫 fitz,fitz 的最新版 1.18.13,非最新版的在部分函数名称上存在差异,代码中会标记出来 - -使用 fitz 需要先安装 PyMuPDF 模块,安装方式如下: - -> pip install PyMuPDF - -提取图片的整体逻辑如下: - -- 使用 fitz 打开文档,获取文档详细数据 -- 遍历每一个元素,通过正则找到图片的索引位置 -- 使用 Pixmap 将索引对应的元素生成图片 -- 通过 size 函数过滤较小的图片 - -实现的具体代码如下: - -```python -if not os.path.exists(pic_dirpath): - os.makedirs(pic_dirpath) -# 使用正则表达式来查找图片 -check_XObject = r"/Type(?= */XObject)" -check_Image = r"/Subtype(?= */Image)" -img_count = 0 - -"""1. 打开pdf,打印相关信息""" -pdf_info = fitz.open(filepath) -# 1.16.8版本用法 xref_len = doc._getXrefLength() -# 最新版本写法 -xref_len = pdf_info.xref_length() -# 打印PDF的信息 -print("文件名:{}, 页数: {}, 对象: {}".format(filepath, len(pdf_info), xref_len-1)) - -"""2. 遍历PDF中的对象,遇到是图像才进行下一步,不然就continue""" -for index in range(1, xref_len): - # 1.16.8版本用法 text = doc._getXrefString(index) - # 最新版本 - text = pdf_info.xref_object(index) - - is_XObject = re.search(check_XObject, text) - is_Image = re.search(check_Image, text) - # 如果不是对象也不是图片,则不操作 - if is_XObject or is_Image: - img_count += 1 - # 根据索引生成图像 - pix = fitz.Pixmap(pdf_info, index) - pic_filepath = os.path.join(pic_dirpath, 'img_' + str(img_count) + '.png') - """pix.size 可以反映像素多少,简单的色素块该值较低,可以通过设置一个阈值过滤。以阈值 10000 为例过滤""" - # if pix.size < 10000: - # continue - - """三、 将图像存为png格式""" - if pix.n >= 5: - # 先转换CMYK - pix = fitz.Pixmap(fitz.csRGB, pix) - # 存为PNG - pix.writePNG(pic_filepath) -``` - -以本节示例的“易方达中小盘混合型证券投资基金2020年中期报告” 中的图片为例,代码运行后提取的图片如下: - -![](https://raw.githubusercontent.com/double-point/GraphBed/master/python_2_pdf/%E6%8F%90%E5%8F%96%E5%9B%BE%E7%89%87.png) - -这个结果和文档中的共 1 张图片的 **结果符合** - -
- -## 7. 转换为图片 - -转换为照片比较简单,就是将一页页的 PDF 转换为一张张的图片。大致过程如下: - -### 7.1 安装 pdf2image - -首先需要安装对应的库,最新的 pdf2image 库版本应该是 1.14.0 - -它的 github地址 为:https://github.com/Belval/pdf2image ,感兴趣的可以自行了解 - -安装方式如下: - -> pip install pdf2image - -### 7.2 安装组件 - -对于不同的平台,需要安装相应的组件,这里以 windows 平台和 mac 平台为例: - -**Windows 平台** - -对于 windows 用户需要安装 poppler for Windows,安装链接是:http://blog.alivate.com.au/poppler-windows/ - -另外,还需要添加环境变量, 将 bin 文件夹的路径添加到环境变量 PATH 中 - -> 注意这里配置之后需要重启一下电脑才会生效,不然会报如下错误: - -**Mac** - -对于 mac 用户,需要安装 poppler for Mac,具体可以参考这个链接:http://macappstore.org/poppler/ - -
- -详细代码如下: - -```python -if not os.path.exists(pic_dirpath): - os.makedirs(pic_dirpath) - -images = convert_from_bytes(open(filepath, 'rb').read()) -# images = convert_from_path(filepath, dpi=200) -for image in images: - # 保存图片 - pic_filepath = os.path.join(pic_dirpath, 'img_'+str(images.index(image))+'.png') - image.save(pic_filepath, 'PNG') -``` - -以本节示例的“易方达中小盘混合型证券投资基金2020年中期报告” 中的图片为例,该文档共 46 页,保存后的 PDF 照片如下: - -![](https://raw.githubusercontent.com/double-point/GraphBed/master/python_2_pdf/%E8%BD%AC%E6%8D%A2%E4%B8%BAPDF%E5%9B%BE%E7%89%87.png) - -一共 46 张图片 - -
- -## 8. 添加水印 - -PDF 中添加水印,首先需要一个水印PDF文件,然后依次通过 mergePage 操作将每一页的 PDF 文件合并到水印文件上,据此,每一页的 PDF 文件将是一个带有水印的 PDF 文件 - -最后,将每一页的水印 PDF 合并成一个 PDF 文件即可 - -**生成水印** - -生成水印的方式比较多,例如在图片添加水印,然后将图片插入到 word 中,最后将 word 保存成 PDF 文件即可 - -生成一张 A4 纸大小的空白图片,参考这篇文章:[Python 批量加水印!轻松搞定!](https://mp.weixin.qq.com/s/_oJA6lbsdMlRRsBf6DPxsg) 给图片添加水印,最终的水印背景图片是这样的: - -![](https://raw.githubusercontent.com/double-point/GraphBed/master/python_2_pdf/%E7%A9%BA%E7%99%BD%E7%85%A7%E7%89%87.png) - -然后将图片插入到 word 中并最终生成一个水印 PDF 文档 - -PDF 文档添加水印的主要代码如下: - -```python -watermark = PdfFileReader(watermark_filepath) -watermark_page = watermark.getPage(0) - -pdf_reader = PdfFileReader(filepath) -pdf_writer = PdfFileWriter() - -for page_index in range(pdf_reader.getNumPages()): - current_page = pdf_reader.getPage(page_index) - # 封面页不添加水印 - if page_index == 0: - new_page = current_page - else: - new_page = copy(watermark_page) - new_page.mergePage(current_page) - pdf_writer.addPage(new_page) -# 保存水印后的文件 -with open(save_filepath, "wb") as out: - pdf_writer.write(out) -``` - -以本节示例的“易方达中小盘混合型证券投资基金2020年中期报告” 为例,添加水印后的文档如下: - -![](https://raw.githubusercontent.com/double-point/GraphBed/master/python_2_pdf/%E5%B8%A6%E6%B0%B4%E5%8D%B0%E7%9A%84PDF.png) - -
- -## 9. 文档加密与解密 - -你可能在打开部分 PDF 文件的时候,会弹出下面这个界面: - -![](https://raw.githubusercontent.com/double-point/GraphBed/master/python_2_pdf/PDF%E5%B7%B2%E5%8A%A0%E5%AF%86.png) - -这种就是 PDF 文件被加密了,在打开的时候需要相应的密码才行 - -本节所提到的也只是基于 PDF 文档的加密解密,而不是所谓的 PDF 密码破解。 - -在对 PDF 文件加密需要使用 encrypt 函数,对应的加密代码也比较简单: - -```python -pdf_reader = PdfFileReader(filepath) -pdf_writer = PdfFileWriter() - -for page_index in range(pdf_reader.getNumPages()): - pdf_writer.addPage(pdf_reader.getPage(page_index)) - -# 添加密码 -pdf_writer.encrypt(passwd) -with open(save_filepath, "wb") as out: - pdf_writer.write(out) -``` - -代码执行成功后再次打开 PDF 文件则需要输入密码才行 - -根据这个思路,破解 PDF 也可以通过暴力求解实现,例如:通过本地密码本一个个去尝试,或者根据数字+字母的密码形式循环尝试,最终成功打开的密码就是破解密码 - -> 上述破解方法耗时耗力,不建议尝试 - -
- -另外,针对已经加密的 PDF 文件,也可以使用 decrypt 函数进行解密操作 - -解密代码如下: - -```python -pdf_reader = PdfFileReader(filepath) -# PDF文档解密 -pdf_reader.decrypt('xiaoyi') - -pdf_writer = PdfFileWriter() -for page_index in range(pdf_reader.getNumPages()): - pdf_writer.addPage(pdf_reader.getPage(page_index)) - -with open(save_filepath, "wb") as out: - pdf_writer.write(out) -``` - -解密完成后的 PDF 文档打开后不再需要输入密码,如需加密可再次执行加密代码。 - - - ----- - - - -**END.** - ---- By: xiaoyi - ->**Datawhale成员,数据分析从业者,金融风控爱好者** -> ->**公众号:小一的学习笔记** - -关于Datawhale: - -> Datawhale是一个专注于数据科学与AI领域的开源组织,汇集了众多领域院校和知名企业的优秀学习者,聚合了一群有开源精神和探索精神的团队成员。 -> -> Datawhale 以“for the learner,和学习者一起成长”为愿景,鼓励真实地展现自我、开放包容、互信互助、敢于试错和勇于担当。 -> -> 同时 Datawhale 用开源的理念去探索开源内容、开源学习和开源方案,赋能人才培养,助力人才成长,建立起人与人,人与知识,人与企业和人与未来的联结。 diff --git a/OfficeAutomation/Task04 python与pdf.md b/OfficeAutomation/Task04 python与pdf.md new file mode 100644 index 0000000..e6809ee --- /dev/null +++ b/OfficeAutomation/Task04 python与pdf.md @@ -0,0 +1,663 @@ + +# Task 04 Python办公自动化--PDF篇 + +PDF 操作是的知识点,初级的 PDF 自动化包括 PDF 文档的拆分、合并、提取等操作,更高级的还包括 WORD与PDF互转等 + +初级操作一般比较常用,也可以解决较多的办公内容,所以本节将会主要介绍 PDF 的初级操作,具体内容将会从以下几个小节展开: + +- [Task 04 Python办公自动化--PDF篇](#task-04-python办公自动化--pdf篇) + - [4.1. 相关介绍](#41-相关介绍) + - [4.2. 批量拆分](#42-批量拆分) + - [4.3. 批量合并](#43-批量合并) + - [4.4. 提取文字内容](#44-提取文字内容) + - [4.5. 提取表格内容](#45-提取表格内容) + - [4.6 提取图片内容](#46-提取图片内容) + - [4.7 转换为图片](#47-转换为图片) + - [4.7.1 安装 pdf2image](#471-安装-pdf2image) + - [4.7.2 安装组件](#472-安装组件) + - [4.8. 添加水印](#48-添加水印) + - [4.9. 文档加密与解密](#49-文档加密与解密) + +下面直接开始本节内容。 + + + +## 4.1. 相关介绍 + +Python 操作 PDF 会用到两个库,分别是:PyPDF2 和 pdfplumber + +其中 **PyPDF2** 可以更好的读取、写入、分割、合并PDF文件,而 **pdfplumber** 可以更好的读取 PDF 文件中内容和提取 PDF 中的表格 + +对应的官网分别是: + +> PyPDF2:https://pythonhosted.org/PyPDF2/ +> +> pdfplumber:https://github.com/jsvine/pdfplumber + +由于这两个库都不是 Python 的标准库,所以在使用之前都需要单独安装 + +win+r 后输入 cmd 打开 command 窗口,依次输入如下命令进行安装: + +> pip install PyPDF2 +> +> pip install pdfplumber + +安装完成后显示 success 则表示安装成功 + +![](./图片/4.11.png) + +另外,在下文中需要用到两个文件:一个是本次教程的处理目标PDF、一个是在添加水印章节需要用到的水印PDF文件 + +其中第一个PDF大家可以换成自己想要处理的目标PDF,在下文的代码中修改相应的名称即可;第二个PDF大家可以根据文中给出的教程自行生成水印PDF文件(嫌麻烦的同学可以直接用我提供的水印PDF即可) + +上述两个文件的下载链接如下:https://pan.baidu.com/s/10OpbHzyQBTWhJ9Rz7t1_BQ 提取码:1025 + +> 另外,需要注意的是,第一个PDF和第二个PDF直接放在运行代码的同级目录下即可,运行的时候无需创建文件夹,大家注意下! + + + +## 4.2. 批量拆分 + +将一个完整的 PDF 拆分成几个小的 PDF,因为主要涉及到 PDF 整体的操作,所以本小节需要用到 PyPDF2 这个库 + +拆分的大概思路如下: + +- 读取 PDF 的整体信息、总页数等 +- 遍历每一页内容,以每个 step 为间隔将 PDF 存成每一个小的文件块 +- 将小的文件块重新保存为新的 PDF 文件 + +需要注意的是,在拆分的过程中,可以手动设置间隔,例如:每5页保存成一个小的 PDF 文件 + +拆分的代码如下: + +```python +import os +from PyPDF2 import PdfFileWriter, PdfFileReader + +def split_pdf(filename, filepath, save_dirpath, step=5): + """ + 拆分PDF为多个小的PDF文件, + @param filename:文件名 + @param filepath:文件路径 + @param save_dirpath:保存小的PDF的文件路径 + @param step: 每step间隔的页面生成一个文件,例如step=5,表示0-4页、5-9页...为一个文件 + @return: + """ + if not os.path.exists(save_dirpath): + os.mkdir(save_dirpath) + pdf_reader = PdfFileReader(filepath) + # 读取每一页的数据 + pages = pdf_reader.getNumPages() + for page in range(0, pages, step): + pdf_writer = PdfFileWriter() + # 拆分pdf,每 step 页的拆分为一个文件 + for index in range(page, page+step): + if index < pages: + pdf_writer.addPage(pdf_reader.getPage(index)) + # 保存拆分后的小文件 + save_path = os.path.join(save_dirpath, filename+str(int(page/step)+1)+'.pdf') + print(save_path) + with open(save_path, "wb") as out: + pdf_writer.write(out) + + print("文件已成功拆分,保存路径为:"+save_dirpath) + +filename = '易方达中小盘混合型证券投资基金2020年中期报告.pdf' +filepath = os.path.join(os.getcwd(), filename) +save_dirpath = os.path.join(os.getcwd(), '易方达中小盘混合型证券投资基金2020年中期报告【拆分】') +split_pdf(filename, filepath, save_dirpath, step=5) + +``` + +以“易方达中小盘混合型证券投资基金2020年中期报告”为例,整个 PDF 文件一共 46 页,每5页为间隔,最终生成了10个小的 PDF 文件 + +![](./图片/4.1.png) + + + +**需要注意的是:** + +如果你是第一次运行代码,在运行过程中报如下的错误,可以通过以下方法解决: + +![](./图片/4.2.png) + +如果是在 Pycharm 下,直接通过报错信息,点击 utils.py 文件,定位到第 238 行原文 + +原文中是这样的: + +```python + r = s.encode('latin-1') + if len(s) < 2: + bc[s] = r + return r +``` + +修改为: + +```python +try: + r = s.encode('latin-1') + if len(s) < 2: + bc[s] = r + return r +except Exception as e: + r = s.encode('utf-8') + if len(s) < 2: + bc[s] = r + return r +``` + +如果你使用的是 **anaconda**,对应的文件路径应该为:anaconda\Lib\site-packages\PyPDF2\utils.py,进行同样的修改操作即可 + + + +## 4.3. 批量合并 + +比起拆分来,合并的思路更加简单: + +- 确定要合并的 **文件顺序** +- 循环追加到一个文件块中 +- 保存成一个新的文件 + +对应的代码比较简单,基本不会出现问题: + +```python +import os +from PyPDF2 import PdfFileReader, PdfFileWriter + +def concat_pdf(filename, read_dirpath, save_filepath): + """ + 合并多个PDF文件 + @param filename:文件名 + @param read_dirpath:要合并的PDF目录 + @param save_filepath:合并后的PDF文件路径 + @return: + """ + pdf_writer = PdfFileWriter() + # 对文件名进行排序 + list_filename = os.listdir(read_dirpath) + list_filename.sort(key=lambda x: int(x[:-4].replace(filename, ""))) + for filename in list_filename: + print(filename) + filepath = os.path.join(read_dirpath, filename) + # 读取文件并获取文件的页数 + pdf_reader = PdfFileReader(filepath) + pages = pdf_reader.getNumPages() + # 逐页添加 + for page in range(pages): + pdf_writer.addPage(pdf_reader.getPage(page)) + # 保存合并后的文件 + with open(save_filepath, "wb") as out: + pdf_writer.write(out) + print("文件已成功合并,保存路径为:"+save_filepath) + +filename = '易方达中小盘混合型证券投资基金2020年中期报告.pdf' +read_dirpath = os.path.join(os.getcwd(), '易方达中小盘混合型证券投资基金2020年中期报告【拆分】') +save_filepath = os.path.join(os.getcwd(), '易方达中小盘混合型证券投资基金2020年中期报告-合并后.pdf') +concat_pdf(filename, read_dirpath, save_filepath) + +``` + + + +## 4.4. 提取文字内容 + +涉及到具体的 PDF 内容 操作,本小节需要用到 pdfplumber 这个库 + +在进行文字提取的时候,主要用到 extract_text 这个函数 + +具体代码如下: + +```python +import os +import pdfplumber + +def extract_text_info(filepath): + """ + 提取PDF中的文字 + @param filepath:文件路径 + @return: + """ + with pdfplumber.open(filepath) as pdf: + # 获取第2页数据 + page = pdf.pages[1] + print(page.extract_text()) + +filename = '易方达中小盘混合型证券投资基金2020年中期报告.pdf' +filepath = os.path.join(os.getcwd(), filename) +# 提取文字内容 +extract_text_info(filepath) + +``` + +可以看到,直接通过下标即可定位到相应的页码,从而通过 extract_text 函数提取该也的所有文字 + +而如果想要提取所有页的文字,只需要改成: + +```python +with pdfplumber.open(filepath) as pdf: + # 获取全部数据 + for page in pdf.pages + print(page.extract_text()) +``` + +例如,提取“易方达中小盘混合型证券投资基金2020年中期报告” 第一页的内容时,源文件是这样的: + +![](./图片/4.3.png) + +运行代码后提取出来是这样的: + +![](./图片/4.4.png) + +> 拓展一下:此处可以结合前面 word 小节,将内容写入 word 文件中 + + + +## 4.5. 提取表格内容 + +同样的,本节是对具体内容的操作,所以也需要用到 pdfplumber 这个库 + +和提取文字十分类似的是,提取表格内容只是将 extract_text 函数换成了 extract_table 函数 + +对应的代码如下: + +```python +import os +import pandas as pd +import pdfplumber + +def extract_table_info(filepath): + """ + 提取PDF中的图表数据 + @param filepath: + @return: + """ + with pdfplumber.open(filepath) as pdf: + # 获取第18页数据 + page = pdf.pages[17] + # 如果一页有一个表格,设置表格的第一行为表头,其余为数据 + table_info = page.extract_table() + df_table = pd.DataFrame(table_info[1:], columns=table_info[0]) + df_table.to_csv('dmeo.csv', index=False, encoding='gbk') + +filename = '易方达中小盘混合型证券投资基金2020年中期报告.pdf' +filepath = os.path.join(os.getcwd(), filename) +# 提取表格内容 +extract_table_info(filepath) + +``` + +上面代码可以获取到第 18 页的第一个表格内容,并且将其保存为 csv 文件存在本地 + +> 但是,如果说第 18 页有多个表格内容呢? + +因为读取的表格会被存成二维数组,而多个二维数组就组成一个三维数组 + +遍历这个三位数组,就可以得到该页的每一个表格数据,对应的将 extract_table 函数 改成 extract_tables 即可 + +具体代码如下: + +```python +import os +import pandas as pd +import pdfplumber + +def extract_table_info(filepath): + """ + 提取PDF中的图表数据 + @param filepath: + @return: + """ + with pdfplumber.open(filepath) as pdf: + # 获取第7页数据 + page = pdf.pages[6] + # 如果一页有多个表格,对应的数据是一个三维数组 + tables_info = page.extract_tables() + for index in range(len(tables_info)): + # 设置表格的第一行为表头,其余为数据 + df_table = pd.DataFrame(tables_info[index][1:], columns=tables_info[index][0]) + df_table.to_csv('dmeo.csv', index=False, encoding='gbk') + +filename = '易方达中小盘混合型证券投资基金2020年中期报告.pdf' +filepath = os.path.join(os.getcwd(), filename) +# 提取表格内容 +extract_table_info(filepath) + +``` + +以“易方达中小盘混合型证券投资基金2020年中期报告” 第 xx 页的第一个表格为例: + +源文件中的表格是这样的: + +![](./图片/4.5.png) + +提取并存入 excel 之后的表格是这样的: + +![](./图片/4.6.png) + + + +## 4.6 提取图片内容 + +提取 PDF 中的图片和将 PDF 转存为图片是不一样的(下一小节),需要区分开。 + +提取图片:顾名思义,就是将内容中的图片都提取出来;转存为图片:则是将每一页的 PDF 内容存成一页一页的图片,下一小节会详细说明 + +转存为图片中,需要用到一个模块叫 fitz,fitz 的最新版 1.18.13,非最新版的在部分函数名称上存在差异,代码中会标记出来 + +使用 fitz 需要先安装 PyMuPDF 模块,安装方式如下: + +> pip install PyMuPDF + +提取图片的整体逻辑如下: + +- 使用 fitz 打开文档,获取文档详细数据 +- 遍历每一个元素,通过正则找到图片的索引位置 +- 使用 Pixmap 将索引对应的元素生成图片 +- 通过 size 函数过滤较小的图片 + +实现的具体代码如下: + +```python +import os +import re +import fitz + +def extract_pic_info(filepath, pic_dirpath): + """ + 提取PDF中的图片 + @param filepath:pdf文件路径 + @param pic_dirpath:要保存的图片目录路径 + @return: + """ + if not os.path.exists(pic_dirpath): + os.makedirs(pic_dirpath) + # 使用正则表达式来查找图片 + check_XObject = r"/Type(?= */XObject)" + check_Image = r"/Subtype(?= */Image)" + img_count = 0 + + """1. 打开pdf,打印相关信息""" + pdf_info = fitz.open(filepath) + # 1.16.8版本用法 xref_len = doc._getXrefLength() + # 最新版本 + xref_len = pdf_info.xref_length() + # 打印PDF的信息 + print("文件名:{}, 页数: {}, 对象: {}".format(filepath, len(pdf_info), xref_len-1)) + + """2. 遍历PDF中的对象,遇到是图像才进行下一步,不然就continue""" + for index in range(1, xref_len): + # 1.16.8版本用法 text = doc._getXrefString(index) + # 最新版本 + text = pdf_info.xref_object(index) + + is_XObject = re.search(check_XObject, text) + is_Image = re.search(check_Image, text) + # 如果不是对象也不是图片,则不操作 + if is_XObject or is_Image: + img_count += 1 + # 根据索引生成图像 + pix = fitz.Pixmap(pdf_info, index) + pic_filepath = os.path.join(pic_dirpath, 'img_' + str(img_count) + '.png') + """pix.size 可以反映像素多少,简单的色素块该值较低,可以通过设置一个阈值过滤。以阈值 10000 为例过滤""" + # if pix.size < 10000: + # continue + + """三、 将图像存为png格式""" + if pix.n >= 5: + # 先转换CMYK + pix = fitz.Pixmap(fitz.csRGB, pix) + # 存为PNG + pix.writePNG(pic_filepath) + +filename = '易方达中小盘混合型证券投资基金2020年中期报告.pdf' +filepath = os.path.join(os.getcwd(), filename) +pic_dirpath = os.path.join(os.getcwd(), '易方达中小盘混合型证券投资基金2020年中期报告【文中图片】') +# 提取图片内容 +extract_pic_info(filepath, pic_dirpath) + +``` + +以本节示例的“易方达中小盘混合型证券投资基金2020年中期报告” 中的图片为例,代码运行后提取的图片如下: + +![](./图片/4.7.png) + +这个结果和文档中的共 1 张图片的 **结果符合** + + + +## 4.7 转换为图片 + +转换为照片比较简单,就是将一页页的 PDF 转换为一张张的图片。大致过程如下: + +### 4.7.1 安装 pdf2image + +首先需要安装对应的库,最新的 pdf2image 库版本应该是 1.14.0 + +它的 github地址 为:https://github.com/Belval/pdf2image ,感兴趣的可以自行了解 + +安装方式如下: + +> pip install pdf2image + +### 4.7.2 安装组件 + +对于不同的平台,需要安装相应的组件,这里以 windows 平台和 mac 平台为例: + +**Windows 平台** + +对于 windows 用户需要安装 poppler for Windows,安装链接是:http://blog.alivate.com.au/poppler-windows/ + +另外,还需要添加环境变量, 将 bin 文件夹的路径添加到环境变量 PATH 中 + +> 注意这里配置之后需要重启一下电脑才会生效,不然会报如下错误: + +**Mac** + +对于 mac 用户,需要安装 poppler for Mac,具体可以参考这个链接:http://macappstore.org/poppler/ + + + +详细代码如下: + +```python +import os +from pdf2image import convert_from_path, convert_from_bytes + +def convert_to_pic(filepath, pic_dirpath): + """ + 每一页的PDF转换成图片 + @param filepath:pdf文件路径 + @param pic_dirpath:图片目录路径 + @return: + """ + print(filepath) + if not os.path.exists(pic_dirpath): + os.makedirs(pic_dirpath) + + images = convert_from_bytes(open(filepath, 'rb').read()) + # images = convert_from_path(filepath, dpi=200) + for image in images: + # 保存图片 + pic_filepath = os.path.join(pic_dirpath, 'img_'+str(images.index(image))+'.png') + image.save(pic_filepath, 'PNG') + +# PDF转换为图片 +convert_to_pic(filepath, pic_dirpath) + + +filename = '易方达中小盘混合型证券投资基金2020年中期报告.pdf' +filepath = os.path.join(os.getcwd(), filename) +pic_dirpath = os.path.join(os.getcwd(), '易方达中小盘混合型证券投资基金2020年中期报告【转换为图片】') +# PDF转换为图片 +convert_to_pic(filepath, pic_dirpath) + +``` + +以本节示例的“易方达中小盘混合型证券投资基金2020年中期报告” 中的图片为例,该文档共 46 页,保存后的 PDF 照片如下: + +![](./图片/4.8.png) + +一共 46 张图片 + + + +## 4.8. 添加水印 + +PDF 中添加水印,首先需要一个水印PDF文件,然后依次通过 mergePage 操作将每一页的 PDF 文件合并到水印文件上,据此,每一页的 PDF 文件将是一个带有水印的 PDF 文件 + +最后,将每一页的水印 PDF 合并成一个 PDF 文件即可 + +**生成水印** + +生成水印的方式比较多,例如在图片添加水印,然后将图片插入到 word 中,最后将 word 保存成 PDF 文件即可 + +生成一张 A4 纸大小的空白图片,参考这篇文章:[Python 批量加水印!轻松搞定!](https://mp.weixin.qq.com/s/_oJA6lbsdMlRRsBf6DPxsg) 给图片添加水印,最终的水印背景图片是这样的: + +![](./图片/4.8.png) + +然后将图片插入到 word 中并最终生成一个水印 PDF 文档 + +PDF 文档添加水印的主要代码如下: + +```python +import os +from copy import copy +from PyPDF2 import PdfFileReader, PdfFileWriter + +def add_watermark(filepath, save_filepath, watermark_filepath): + """ + 添加水印 + @param filepath:PDF文件路径 + @param save_filepath:最终的文件保存路径 + @param watermark_filepath:水印PDF文件路径 + @return: + """ + """读取PDF水印文件""" + # 可以先生成一个空白A4大小的png图片,通过 https://mp.weixin.qq.com/s/_oJA6lbsdMlRRsBf6DPxsg 教程的方式给图片加水印,将图片插入到word中并最终生成一个水印PDF文档 + watermark = PdfFileReader(watermark_filepath) + watermark_page = watermark.getPage(0) + + pdf_reader = PdfFileReader(filepath) + pdf_writer = PdfFileWriter() + + for page_index in range(pdf_reader.getNumPages()): + current_page = pdf_reader.getPage(page_index) + # 封面页不添加水印 + if page_index == 0: + new_page = current_page + else: + new_page = copy(watermark_page) + new_page.mergePage(current_page) + pdf_writer.addPage(new_page) + # 保存水印后的文件 + with open(save_filepath, "wb") as out: + pdf_writer.write(out) + +filename = '易方达中小盘混合型证券投资基金2020年中期报告.pdf' +filepath = os.path.join(os.getcwd(), filename) +save_filepath = os.path.join(os.getcwd(), '易方达中小盘混合型证券投资基金2020年中期报告-水印.pdf') +watermark_filepath = os.path.join(os.getcwd(), 'watermark.pdf') +# 添加水印 +add_watermark(filepath, save_filepath, watermark_filepath) + +``` + +以本节示例的“易方达中小盘混合型证券投资基金2020年中期报告” 为例,添加水印后的文档如下: + +![](https://raw.githubusercontent.com/double-point/GraphBed/master/python_2_pdf/%E5%B8%A6%E6%B0%B4%E5%8D%B0%E7%9A%84PDF.png) + + + +## 4.9. 文档加密与解密 + +你可能在打开部分 PDF 文件的时候,会弹出下面这个界面: + +![](./图片/4.10.png) + +这种就是 PDF 文件被加密了,在打开的时候需要相应的密码才行 + +本节所提到的也只是基于 PDF 文档的加密解密,而不是所谓的 PDF 密码破解。 + +在对 PDF 文件加密需要使用 encrypt 函数,对应的加密代码也比较简单: + +```python +import os +from PyPDF2 import PdfFileReader, PdfFileWriter + +def encrypt_pdf(filepath, save_filepath, passwd='xiaoyi'): + """ + PDF文档加密 + @param filepath:PDF文件路径 + @param save_filepath:加密后的文件保存路径 + @param passwd:密码 + @return: + """ + pdf_reader = PdfFileReader(filepath) + pdf_writer = PdfFileWriter() + + for page_index in range(pdf_reader.getNumPages()): + pdf_writer.addPage(pdf_reader.getPage(page_index)) + + # 添加密码 + pdf_writer.encrypt(passwd) + with open(save_filepath, "wb") as out: + pdf_writer.write(out) + +filename = '易方达中小盘混合型证券投资基金2020年中期报告.pdf' +filepath = os.path.join(os.getcwd(), filename) +save_filepath = os.path.join(os.getcwd(), '易方达中小盘混合型证券投资基金2020年中期报告-加密后.pdf') +# 文档加密 +encrypt_pdf(filepath, save_filepath, passwd='xiaoyi') + +``` + +代码执行成功后再次打开 PDF 文件则需要输入密码才行 + +根据这个思路,破解 PDF 也可以通过暴力求解实现,例如:通过本地密码本一个个去尝试,或者根据数字+字母的密码形式循环尝试,最终成功打开的密码就是破解密码 + +> 上述破解方法耗时耗力,不建议尝试 + + + +另外,针对已经加密的 PDF 文件,也可以使用 decrypt 函数进行解密操作 + +解密代码如下: + +```python +def decrypt_pdf(filepath, save_filepath, passwd='xiaoyi'): + """ + 解密 PDF 文档并且保存为未加密的 PDF + @param filepath:PDF文件路径 + @param save_filepath:解密后的文件保存路径 + @param passwd:密码 + @return: + """ + pdf_reader = PdfFileReader(filepath) + # PDF文档解密 + pdf_reader.decrypt('xiaoyi') + + pdf_writer = PdfFileWriter() + for page_index in range(pdf_reader.getNumPages()): + pdf_writer.addPage(pdf_reader.getPage(page_index)) + + with open(save_filepath, "wb") as out: + pdf_writer.write(out) + +filename = '易方达中小盘混合型证券投资基金2020年中期报告-加密后.pdf' +filepath = os.path.join(os.getcwd(), filename) +save_filepath = os.path.join(os.getcwd(), '易方达中小盘混合型证券投资基金2020年中期报告-解密后.pdf') +# 文档解密 +decrypt_pdf(filepath, save_filepath, passwd='xiaoyi') + +``` + +解密完成后的 PDF 文档打开后不再需要输入密码,如需加密可再次执行加密代码。 + + **Task04 END.** + +--- By: xiaoyi + +>**Datawhale成员,数据分析从业者,金融风控爱好者** +> +>**公众号:小一的学习笔记** diff --git a/OfficeAutomation/Task05 爬虫入门与综合应用.md b/OfficeAutomation/Task05 爬虫入门与综合应用.md index b3c7ee1..2a07cac 100644 --- a/OfficeAutomation/Task05 爬虫入门与综合应用.md +++ b/OfficeAutomation/Task05 爬虫入门与综合应用.md @@ -1,23 +1,23 @@ -# 爬虫入门与综合应用 +# Task 05爬虫入门与综合应用 -- [爬虫入门与综合应用](#爬虫入门与综合应用) - - [0.前言](#0前言) - - [1.Requests简介](#1requests简介) - - [1.1访问百度](#11访问百度) - - [1.2下载txt文件](#12下载txt文件) - - [1.3下载图片](#13下载图片) - - [2.HTML解析和提取](#2html解析和提取) - - [3.BeautifulSoup简介](#3beautifulsoup简介) - - [4.实践项目1:自如公寓数据抓取](#4实践项目1自如公寓数据抓取) - - [5.实践项目2:36kr信息抓取与邮件发送](#5实践项目236kr信息抓取与邮件发送) +- [Task 05爬虫入门与综合应用](#task-05爬虫入门与综合应用) + - [5.0 前言](#50-前言) + - [5.1 Requests简介](#51-requests简介) + - [5.1.1 访问百度](#511-访问百度) + - [5.1.2 下载txt文件](#512-下载txt文件) + - [5.1.3 下载图片](#513-下载图片) + - [5.2 HTML解析和提取](#52-html解析和提取) + - [5.3 BeautifulSoup简介](#53-beautifulsoup简介) + - [5.4 实践项目1:自如公寓数据抓取](#54-实践项目1自如公寓数据抓取) + - [5.5 实践项目2:36kr信息抓取与邮件发送](#55-实践项目236kr信息抓取与邮件发送) -## 0.前言 +## 5.0 前言 对于自动化办公而言,网络数据的批量获取完数据可以节约相当的时间,因此爬虫在自动化办公中占据了一个比较重要的位置。 因而本节针对爬虫项目进行一个介绍,力求最大程度还原实际的办公场景。 -## 1.Requests简介 +## 5.1 Requests简介 Requests是一款目前非常流行的http请求库,使用python编写,能非常方便的对网页Requests进行爬取,也是爬虫最常用的发起请求第三方库。 @@ -36,7 +36,7 @@ rs.content 响应内容的二进制形式 rs.encoding 响应内容的编码 ``` -### 1.1访问百度 +### 5.1.1 访问百度 试一试对百度首页进行数据请求: @@ -55,7 +55,7 @@ print(re.status_code) **res.text** 返回的是服务器响应内容的字符串形式,也就是文本内容 -### 1.2下载txt文件 +### 5.1.2 下载txt文件 例:用爬虫下载孔乙己的文章,网址是https://apiv3.shanbay.com/codetime/articles/mnvdu @@ -79,7 +79,7 @@ with open('鲁迅文章.txt', 'w') as file: re.txt就是网页中的内容,将内容保存到txt文件中 -### 1.3下载图片 +### 5.1.3 下载图片 **re.text用于文本内容的获取、下载 re.content用于图片、视频、音频等内容的获取、下载** @@ -94,12 +94,13 @@ res=requests.get('https://img-blog.csdnimg.cn/20210424184053989.PNG') # 以二进制写入的方式打开一个名为 info.jpg 的文件 with open('datawhale.png','wb') as ff: # 将数据的二进制形式写入文件中 + print('爬取图片') ff.write(res.content) ``` **re.encoding** 爬取内容的编码形似,常见的编码方式有 ASCII、GBK、UTF-8 等。如果用和文件编码不同的方式去解码,我们就会得到一些乱码。 -## 2.HTML解析和提取 +## 5.2 HTML解析和提取 **浏览器工作原理:** @@ -138,7 +139,7 @@ print(res.text) 那么我们如何解析html页面呢? -## 3.BeautifulSoup简介 +## 5.3 BeautifulSoup简介 我们一般会使用BeautifulSoup这个第三方库 @@ -189,7 +190,7 @@ BeautifulSoup 为我们提供了一些方法 **find()方法**和**find_all()方法**: - **find()** 返回符合条件的**首个**数据 -- **find_all()** 返回符合条件的所有**数据 +- **find_all()** 返回符合条件的**所有**数据 ```python import io @@ -224,7 +225,7 @@ HTML定位方法:https://www.cnblogs.com/bosslv/p/8992410.html 理论看百遍,不如上手一练 -## 4.实践项目1:自如公寓数据抓取 +## 5.4 实践项目1:自如公寓数据抓取 > 首先是先说一声抱歉,在课程设计时,没有想到自如公寓在价格上增加一定程度的反爬措施,因此自如公寓的价格在本节不讨论,在以后的课程中,我们会详细讲解相关的方法。 > @@ -494,7 +495,7 @@ if __name__ == '__main__': 运行完成后,会在文件夹中看到刚才爬取好的信息保存在wuhan_ziru.csv中 -## 5.实践项目2:36kr信息抓取与邮件发送 +## 5.5 实践项目2:36kr信息抓取与邮件发送 > 本节内容为作者原创的项目,课程难度为5星,建议读者跟着课程一步一步的来,如果有不明白的地方,可以在群里面与其他伙伴进行交流。 > diff --git a/OfficeAutomation/pdf版本/Task01 文件自动化与邮件处理.pdf b/OfficeAutomation/pdf版本/Task01 文件自动化与邮件处理.pdf new file mode 100644 index 0000000..99e1691 Binary files /dev/null and b/OfficeAutomation/pdf版本/Task01 文件自动化与邮件处理.pdf differ diff --git a/OfficeAutomation/pdf版本/Task02 Python与Excel.pdf b/OfficeAutomation/pdf版本/Task02 Python与Excel.pdf new file mode 100644 index 0000000..c1eba99 Binary files /dev/null and b/OfficeAutomation/pdf版本/Task02 Python与Excel.pdf differ diff --git a/OfficeAutomation/pdf版本/Task03 python与word.pdf b/OfficeAutomation/pdf版本/Task03 python与word.pdf new file mode 100644 index 0000000..6803639 Binary files /dev/null and b/OfficeAutomation/pdf版本/Task03 python与word.pdf differ diff --git a/OfficeAutomation/pdf版本/Task04 python与pdf.pdf b/OfficeAutomation/pdf版本/Task04 python与pdf.pdf new file mode 100644 index 0000000..6e26550 Binary files /dev/null and b/OfficeAutomation/pdf版本/Task04 python与pdf.pdf differ diff --git a/OfficeAutomation/pdf版本/Task05 爬虫入门与综合应用.pdf b/OfficeAutomation/pdf版本/Task05 爬虫入门与综合应用.pdf new file mode 100644 index 0000000..02e16bf Binary files /dev/null and b/OfficeAutomation/pdf版本/Task05 爬虫入门与综合应用.pdf differ diff --git a/OfficeAutomation/readme.md b/OfficeAutomation/readme.md index fac7a0e..8d6a538 100644 --- a/OfficeAutomation/readme.md +++ b/OfficeAutomation/readme.md @@ -1,94 +1,94 @@ -## 简介 - -课程基本信息 - -- 学习周期:14天,每天平均花费时间1小时-3小时不等,根据个人学习接受能力强弱有所浮动。 -- 学习形式:理论学习 + 练习 -- 人群定位:有Python语言编程基础,对自动化办公有需求的学员。 -- 先修内容:Python编程语言 -- 相关课程:数据采集 -- 难度系数:⭐⭐ - -课程大纲 - -**Task00:熟悉规则(1天)** -- 组队、修改群昵称。 -- 熟悉打卡规则。 - - -**Task01 文件自动化处理&邮件批量处理 (3天)** -- 文件路径识别、处理、文件夹的操作理论学习 -- 文件自动化处理实践 -- 邮件自动发送理论学习,使用python发送邮件附带excel附件 - -**Task02 python与excel (2天)** -- Excel读取与写入 -- Excel样式调整 -- 综合练习 - -**Task03 python与word (2天)** -- python与word相关的理论知识学习 -- 使用python进行邀请函批量生成实践 - -**Task04 python与pdf(3天)** -- python与pdf相关的理论知识学习 -- 使用python进行pdf操作实践 - -**Task05 爬虫入门与综合应用(3天)** -- requests库的理论与实践 -- HTML页面解析与提取方法 -- 自如公寓数据抓取 -- 36kr信息抓取与邮件发送 - -**致谢** - -感谢以下成员对项目推进作出的贡献(排名不分先后): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
内容贡献者名单
成员个人简介及贡献个人主页
聂雄伟(牧小熊)华中农业大学研究生,Datawhale成员,项目负责人,Task05 知乎
刘雯静互联网用户数据分析研究员,Datawhale成员,Task02
张晓东blank
吴争光数据分析师,Datawhale成员,Task04 github
隆军上海交通大学博士生,Datawhale成员,Task01
- -特别致谢课程测试成员:晓鹏 - -关于Datawhale: Datawhale是一个专注于数据科学与AI领域的开源组织,汇集了众多领域院校和知名企业的优秀学习者,聚合了一群有开源精神和探索精神的团队成员。Datawhale 以“for the learner,和学习者一起成长”为愿景,鼓励真实地展现自我、开放包容、互信互助、敢于试错和勇于担当。同时 Datawhale 用开源的理念去探索开源内容、开源学习和开源方案,赋能人才培养,助力人才成长,建立起人与人,人与知识,人与企业和人与未来的联结。 本次数据挖掘路径学习,专题知识将在天池分享,详情可关注Datawhale: - -![logo.png](https://camo.githubusercontent.com/8578ee173c78b587d5058439bbd0b98fa39c173def229a8c3d957e62aac0b649/68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f323032303039313330313032323639382e706e67237069635f63656e746572) +## 简介 + +课程基本信息 + +- 学习周期:14天,每天平均花费时间1小时-3小时不等,根据个人学习接受能力强弱有所浮动。 +- 学习形式:理论学习 + 练习 +- 人群定位:有Python语言编程基础,对自动化办公有需求的学员。 +- 先修内容:Python编程语言 +- 相关课程:数据采集 +- 难度系数:⭐⭐ + +课程大纲 + +**Task00:熟悉规则(1天)** +- 组队、修改群昵称。 +- 熟悉打卡规则。 + + +**Task01 文件自动化处理&邮件批量处理 (3天)** +- 文件路径识别、处理、文件夹的操作理论学习 +- 文件自动化处理实践 +- 邮件自动发送理论学习,使用python发送邮件附带excel附件 + +**Task02 python与excel (2天)** +- Excel读取与写入 +- Excel样式调整 +- 综合练习 + +**Task03 python与word (2天)** +- python与word相关的理论知识学习 +- 使用python进行邀请函批量生成实践 + +**Task04 python与pdf(3天)** +- python与pdf相关的理论知识学习 +- 使用python进行pdf操作实践 + +**Task05 爬虫入门与综合应用(3天)** +- requests库的理论与实践 +- HTML页面解析与提取方法 +- 自如公寓数据抓取 +- 36kr信息抓取与邮件发送 + +**致谢** + +感谢以下成员对项目推进作出的贡献(排名不分先后): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
内容贡献者名单
成员个人简介及贡献个人主页
聂雄伟(牧小熊)华中农业大学研究生,Datawhale成员,项目负责人,Task05 知乎
刘雯静互联网用户数据分析研究员,Datawhale成员,Task02
张晓东数分析师,《数据分析与挖掘算法pyton实战》作者,Task03
吴争光数据分析师,Datawhale成员,Task04 github
隆军上海交通大学博士生,Datawhale成员,Task01
+ +特别致谢课程测试成员:晓鹏 + +关于Datawhale: Datawhale是一个专注于数据科学与AI领域的开源组织,汇集了众多领域院校和知名企业的优秀学习者,聚合了一群有开源精神和探索精神的团队成员。Datawhale 以“for the learner,和学习者一起成长”为愿景,鼓励真实地展现自我、开放包容、互信互助、敢于试错和勇于担当。同时 Datawhale 用开源的理念去探索开源内容、开源学习和开源方案,赋能人才培养,助力人才成长,建立起人与人,人与知识,人与企业和人与未来的联结。 本次数据挖掘路径学习,专题知识将在天池分享,详情可关注Datawhale: + +![logo.png](https://camo.githubusercontent.com/8578ee173c78b587d5058439bbd0b98fa39c173def229a8c3d957e62aac0b649/68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f323032303039313330313032323639382e706e67237069635f63656e746572) diff --git a/OfficeAutomation/图片/image-20210518151332873.png b/OfficeAutomation/图片/1.1.png similarity index 100% rename from OfficeAutomation/图片/image-20210518151332873.png rename to OfficeAutomation/图片/1.1.png diff --git a/OfficeAutomation/图片/image-20210518154517118.png b/OfficeAutomation/图片/1.2.png similarity index 100% rename from OfficeAutomation/图片/image-20210518154517118.png rename to OfficeAutomation/图片/1.2.png diff --git a/OfficeAutomation/图片/pycharm2.png b/OfficeAutomation/图片/2.1.png similarity index 100% rename from OfficeAutomation/图片/pycharm2.png rename to OfficeAutomation/图片/2.1.png diff --git a/OfficeAutomation/图片/pycharm1.png b/OfficeAutomation/图片/2.2.png similarity index 100% rename from OfficeAutomation/图片/pycharm1.png rename to OfficeAutomation/图片/2.2.png diff --git a/OfficeAutomation/图片/Excel.png b/OfficeAutomation/图片/2.3.png similarity index 100% rename from OfficeAutomation/图片/Excel.png rename to OfficeAutomation/图片/2.3.png diff --git a/OfficeAutomation/图片/安装docx.png b/OfficeAutomation/图片/3.1.png similarity index 100% rename from OfficeAutomation/图片/安装docx.png rename to OfficeAutomation/图片/3.1.png diff --git a/OfficeAutomation/图片/页眉页脚设置.png b/OfficeAutomation/图片/3.2.png similarity index 100% rename from OfficeAutomation/图片/页眉页脚设置.png rename to OfficeAutomation/图片/3.2.png diff --git a/OfficeAutomation/图片/参会人名单.png b/OfficeAutomation/图片/3.3.png similarity index 100% rename from OfficeAutomation/图片/参会人名单.png rename to OfficeAutomation/图片/3.3.png diff --git a/OfficeAutomation/图片/邀请函样式.png b/OfficeAutomation/图片/3.4.png similarity index 100% rename from OfficeAutomation/图片/邀请函样式.png rename to OfficeAutomation/图片/3.4.png diff --git a/OfficeAutomation/图片/4.1.png b/OfficeAutomation/图片/4.1.png new file mode 100644 index 0000000..ecb3299 Binary files /dev/null and b/OfficeAutomation/图片/4.1.png differ diff --git a/OfficeAutomation/图片/4.10.png b/OfficeAutomation/图片/4.10.png new file mode 100644 index 0000000..f9d2c6d Binary files /dev/null and b/OfficeAutomation/图片/4.10.png differ diff --git a/OfficeAutomation/图片/4.11.png b/OfficeAutomation/图片/4.11.png new file mode 100644 index 0000000..68f952d Binary files /dev/null and b/OfficeAutomation/图片/4.11.png differ diff --git a/OfficeAutomation/图片/4.2.png b/OfficeAutomation/图片/4.2.png new file mode 100644 index 0000000..8c252f8 Binary files /dev/null and b/OfficeAutomation/图片/4.2.png differ diff --git a/OfficeAutomation/图片/4.3.png b/OfficeAutomation/图片/4.3.png new file mode 100644 index 0000000..f04edcd Binary files /dev/null and b/OfficeAutomation/图片/4.3.png differ diff --git a/OfficeAutomation/图片/4.4.png b/OfficeAutomation/图片/4.4.png new file mode 100644 index 0000000..554f738 Binary files /dev/null and b/OfficeAutomation/图片/4.4.png differ diff --git a/OfficeAutomation/图片/4.5.png b/OfficeAutomation/图片/4.5.png new file mode 100644 index 0000000..1f4d527 Binary files /dev/null and b/OfficeAutomation/图片/4.5.png differ diff --git a/OfficeAutomation/图片/4.6.png b/OfficeAutomation/图片/4.6.png new file mode 100644 index 0000000..868da48 Binary files /dev/null and b/OfficeAutomation/图片/4.6.png differ diff --git a/OfficeAutomation/图片/4.7.png b/OfficeAutomation/图片/4.7.png new file mode 100644 index 0000000..85682be Binary files /dev/null and b/OfficeAutomation/图片/4.7.png differ diff --git a/OfficeAutomation/图片/4.8.png b/OfficeAutomation/图片/4.8.png new file mode 100644 index 0000000..b7c6414 Binary files /dev/null and b/OfficeAutomation/图片/4.8.png differ diff --git a/OfficeAutomation/图片/4.9.png b/OfficeAutomation/图片/4.9.png new file mode 100644 index 0000000..920da85 Binary files /dev/null and b/OfficeAutomation/图片/4.9.png differ diff --git a/OfficeAutomation/用户行为偏好.xlsx b/OfficeAutomation/用户行为偏好.xlsx new file mode 100644 index 0000000..2616111 Binary files /dev/null and b/OfficeAutomation/用户行为偏好.xlsx differ diff --git a/Scratch/等级考试-04/01. 奇偶之和.md b/Scratch/等级考试-04/01. 奇偶之和.md new file mode 100644 index 0000000..ba7a77d --- /dev/null +++ b/Scratch/等级考试-04/01. 奇偶之和.md @@ -0,0 +1,13 @@ +# 奇偶之和 + + +## 1. 准备工作 + +(1)保留舞台中的小猫角色; + +## 2. 功能实现 + +(1)分别计算1~100中,奇数之和,偶数之和; + +(2)说出奇数之和,偶数之和。 + diff --git a/Scratch/等级考试-04/02. 创意画图.md b/Scratch/等级考试-04/02. 创意画图.md new file mode 100644 index 0000000..3b5d884 --- /dev/null +++ b/Scratch/等级考试-04/02. 创意画图.md @@ -0,0 +1,17 @@ + +# 创意画图 + +观察下边图形。尝试编写程序绘画下图效果。 + +![](https://img-blog.csdnimg.cn/1edff6dbb2a84ed595870bf469f59d57.png) + +## 1. 准备工作 + +(1)隐藏小猫角色; + +(2)白色背景; + + +## 2. 功能实现 + +用画笔工具,绘制三角形,三角形的边长从40开始,每画一个三角形,边长增加2,并旋转10°,直到边长大于120停止程序。 \ No newline at end of file diff --git a/Scratch/等级考试-04/03. 数字之和.md b/Scratch/等级考试-04/03. 数字之和.md new file mode 100644 index 0000000..8951413 --- /dev/null +++ b/Scratch/等级考试-04/03. 数字之和.md @@ -0,0 +1,24 @@ +# 数字之和 + +编写程序,要求用户输入一个正整数,程序将其每位数字相加后显示。 + +![01](https://img-blog.csdnimg.cn/070eaf4eb323489c8e79f97d31ae8e3e.png) + +![02](https://img-blog.csdnimg.cn/a4608ff3e68942968c851f67c9204d0d.png) + + + + +## 1. 准备工作 + +(1)保留舞台上的小猫角色。 + +## 2. 功能实现 + +(1)小猫询问并等待“请输入一个正整数”; + +(2)计算各位之和; + +(3)小猫说“xxxx的各位之和为xxxx”; + +(4)例如输入“3456”,说“3456各位之和为18”。 \ No newline at end of file diff --git a/Scratch/等级考试-04/04. 用逗号分隔列表.md b/Scratch/等级考试-04/04. 用逗号分隔列表.md new file mode 100644 index 0000000..8a2ef01 --- /dev/null +++ b/Scratch/等级考试-04/04. 用逗号分隔列表.md @@ -0,0 +1,23 @@ + +# 用逗号分隔列表 + +在列表中自动添加10个数字,分别是:1 3 5 7 9 11 13 15 17 19,用逗号分隔列表,让小猫说出结果。 + +![01](https://img-blog.csdnimg.cn/6d639517538446b5b1cccd9281250193.png) + +![02](https://img-blog.csdnimg.cn/07681905a3314520b5f76c541c84f11c.png) + + + + +## 1. 准备工作 + +(1)保留小猫角色和白色背景 + +## 2. 功能实现 + +(1)编写程序在列表data中自动添加1 3 5 7 9 11 13 15 17 19这10个数字,不能存入“,”; + +(2)小猫说:“现在开始用逗号分隔”2秒; + +(3)小猫说出分隔后的结果,如图所示。 \ No newline at end of file diff --git a/Scratch/等级考试-04/05. 数字反转.md b/Scratch/等级考试-04/05. 数字反转.md new file mode 100644 index 0000000..cfdd749 --- /dev/null +++ b/Scratch/等级考试-04/05. 数字反转.md @@ -0,0 +1,31 @@ + +# 数字反转 + +Jaime想去城堡探险,在城堡门口遇到了小猫,Jaime必须答对小猫提出的问题才能进入城堡。 + +小猫出题啦:找到一个四位数,该四位数的各位数字翻转(个位变千位,十位变百位,百位变十位,千位变个位)后组成一个新的四位数,原来的四位数是这个新四位数的4倍。 + +Jaime想请你帮助他编写程序并找到这个四位数。 + +![01](https://img-blog.csdnimg.cn/dcd5d50d273c469f99539fee5990609f.png) + +![02](https://img-blog.csdnimg.cn/5235e1ddac29421fb8f91b2dffcb0cf7.png) + + + + +## 1. 准备工作 + +(1)保留小猫角色,添加背景“Castle 1”和角色“Jaime”。 + +## 2. 功能实现 + +(1)点击绿旗后,小猫说:“请说出符合要求的四位数”; + +(2)遍历所有的四位数; + +(3)获得每个四位数的反转数; + +(4)判断该反转数的4倍是否等于未反转前的四位数; + +(5)如果满足(4),Jaime说出该四位数2秒钟,如:8712=4*2178。 \ No newline at end of file diff --git a/Scratch/等级考试-04/06. 解密.md b/Scratch/等级考试-04/06. 解密.md new file mode 100644 index 0000000..07f2301 --- /dev/null +++ b/Scratch/等级考试-04/06. 解密.md @@ -0,0 +1,20 @@ + +# 解密 + +罗马共和国的恺撒曾经用一种特殊的加密方式和他的将军们联系,他是这样做的:把一个单词里的每一个字母都向后移,比如A向后移3位,变成D,B就变成E,以此类推。等到X的时候,会变成A,Y变成B,Z变成C。请你编写一段脚本,在输入加密内容后,能够还原出加密前的内容。 + +![01](https://img-blog.csdnimg.cn/ec720e06e59d4d9f9de6e373a2d05fff.png) + +![02](https://img-blog.csdnimg.cn/7827a35f9b824d44896a1a3763248cfc.png) + +![03](https://img-blog.csdnimg.cn/96b2ba83fbb44257ad183909dd157575.png) + + + +## 1. 准备工作 + +保留舞台的小猫,白色背景。 + +## 2. 功能实现 + +点击绿旗以后,输入加密内容,小猫会说出正确的加密前的内容。比如输入:“khoor”,小猫会说:“hello”。 \ No newline at end of file diff --git a/Scratch/等级考试-04/07. 加减法混合运算器.md b/Scratch/等级考试-04/07. 加减法混合运算器.md new file mode 100644 index 0000000..544b358 --- /dev/null +++ b/Scratch/等级考试-04/07. 加减法混合运算器.md @@ -0,0 +1,19 @@ +# 加减法混合运算器 + +请编程实现:输入加减混合运算算式,自动计算出算式结果的功能。 + +![01](https://img-blog.csdnimg.cn/1fc6232182cb4fca9d6b167bc767a8f1.png) + +![02](https://img-blog.csdnimg.cn/111f19b2b6824f8e8154cc46dfe5cd26.png) + + + +## 1. 准备工作 + +保留舞台的小猫,白色背景。 + +## 2. 功能实现 + +点击绿旗以后,输入任意加减混合运算算式,小猫会说出正确的运算结果。 + +提示:在使用运算模块里的【加法】积木时,1+(+3)=4,加上带“+”的数字也能正常计算;1+(-1)=0,加上带“-”的数字相当于减去这个数字。 \ No newline at end of file diff --git a/Scratch/等级考试-04/08. 绘制雪花.md b/Scratch/等级考试-04/08. 绘制雪花.md new file mode 100644 index 0000000..cf246d5 --- /dev/null +++ b/Scratch/等级考试-04/08. 绘制雪花.md @@ -0,0 +1,16 @@ +# 绘制雪花 + +观察下边的图形,编写程序把它画出来(45°的标注是提示,不用绘制)。 + +![](https://img-blog.csdnimg.cn/8c480deec6474dbc9165d50144362074.png) + + +## 1. 准备工作 + +(1)隐藏小猫角色; + +(2)白色背景; + +## 2. 功能实现 + +用画笔相关积木绘制雪花,注意不得使用图章。雪花枝干和枝桠的边长、画笔的粗细没有限制,但要画出如图所示结构一模一样的蓝色雪花图案。 \ No newline at end of file diff --git a/Scratch/等级考试-04/09. 绘图程序优化.md b/Scratch/等级考试-04/09. 绘图程序优化.md new file mode 100644 index 0000000..cfd7c9f --- /dev/null +++ b/Scratch/等级考试-04/09. 绘图程序优化.md @@ -0,0 +1,21 @@ +# 绘图程序优化 + +小明编了一个画多边形的程序,程序如下: + +![](https://img-blog.csdnimg.cn/036ec033d51143099b96c51aedaee5d4.png) + + +## 1. 准备工作 + +(1)保留小猫; + +(2)白色背景。 + + +## 2. 功能实现 + +(1)可以画任意正多边形,边数由键盘输入; + +(2)多边形完整呈现在舞台范围内。 + +![](https://img-blog.csdnimg.cn/2468ef3a58a44a7591ec9b4dc67614f0.gif) \ No newline at end of file diff --git a/Scratch/等级考试-04/10. 程序优化 - 副本.md b/Scratch/等级考试-04/10. 程序优化 - 副本.md new file mode 100644 index 0000000..10b5cc7 --- /dev/null +++ b/Scratch/等级考试-04/10. 程序优化 - 副本.md @@ -0,0 +1,31 @@ +# 程序优化 + +下图第一行有1个正三角形,第二行有2个正三角形,第三行有3个正三角形,每一行的第1个正三角形是上下对齐的。 + +小刚想绘制这个图形,便编写了如下图所示的程序,请根据要求优化程序。 + +![](https://img-blog.csdnimg.cn/bf308b79289f4cf9b17ee14c59b3e646.png) + +![](https://img-blog.csdnimg.cn/f0b8e92ed56741d492471f4cc0fddd03.png) + + + + +## 1. 准备工作 + +(1)保留舞台为默认的白背景; + +(2)保留默认小猫角色,小猫在舞台中间位置。 + +## 2. 功能实现 + +(1)用键盘任意输入一个数字表示行数(考虑到舞台的大小,可以提醒输入的数字在1-9之间),之后小猫会根据输入的行数自动绘制一个多行的图形(如果输入5,那么就绘制5行); + +![](https://img-blog.csdnimg.cn/e4854395275549c0a4fd2c14e3871b7f.png) + +(2)绘制的图形规则为:假设绘制n行,第一行绘制一个三角形,第二行绘制2个三角形......第n行绘制n个正三角形; + +(3)每行中任意两个正三角形之间都间隔20个坐标值,任意相邻两行的间隔也是20个坐标值,每行的第1个三角形的x坐标值是一样的,即在同一列; + +(4)要用函数绘制正三角形,用循环嵌套以及函数来优化程序。 + diff --git a/Scratch/等级考试-04/10. 程序优化.md b/Scratch/等级考试-04/10. 程序优化.md new file mode 100644 index 0000000..10b5cc7 --- /dev/null +++ b/Scratch/等级考试-04/10. 程序优化.md @@ -0,0 +1,31 @@ +# 程序优化 + +下图第一行有1个正三角形,第二行有2个正三角形,第三行有3个正三角形,每一行的第1个正三角形是上下对齐的。 + +小刚想绘制这个图形,便编写了如下图所示的程序,请根据要求优化程序。 + +![](https://img-blog.csdnimg.cn/bf308b79289f4cf9b17ee14c59b3e646.png) + +![](https://img-blog.csdnimg.cn/f0b8e92ed56741d492471f4cc0fddd03.png) + + + + +## 1. 准备工作 + +(1)保留舞台为默认的白背景; + +(2)保留默认小猫角色,小猫在舞台中间位置。 + +## 2. 功能实现 + +(1)用键盘任意输入一个数字表示行数(考虑到舞台的大小,可以提醒输入的数字在1-9之间),之后小猫会根据输入的行数自动绘制一个多行的图形(如果输入5,那么就绘制5行); + +![](https://img-blog.csdnimg.cn/e4854395275549c0a4fd2c14e3871b7f.png) + +(2)绘制的图形规则为:假设绘制n行,第一行绘制一个三角形,第二行绘制2个三角形......第n行绘制n个正三角形; + +(3)每行中任意两个正三角形之间都间隔20个坐标值,任意相邻两行的间隔也是20个坐标值,每行的第1个三角形的x坐标值是一样的,即在同一列; + +(4)要用函数绘制正三角形,用循环嵌套以及函数来优化程序。 + diff --git a/Scratch/等级考试-04/11. 十字回文诗.md b/Scratch/等级考试-04/11. 十字回文诗.md new file mode 100644 index 0000000..f4dc5d2 --- /dev/null +++ b/Scratch/等级考试-04/11. 十字回文诗.md @@ -0,0 +1,52 @@ +# 十字回文诗 + +十字回文诗,又称为转尾(鳞迭)连环回文诗,是古人创造的一种七言绝句诗体,由10个字连环往复,读成一首28个字的七绝。以清朝女诗人吴绛雪 +《咏四季》中的春为例:莺啼绿柳弄春晴晓月明,十个字回环往复,可读成 + +以下28个字的七绝: + +莺啼绿柳弄春晴(前七个字,正序读) + +柳弄春晴晓月明(后七个字,正序读) + +明月晓晴春弄柳(后七个字,倒序读) + +晴春弄柳绿啼莺(前七个字,倒序读) + +《咏四季》的另外三首分别为: + +夏:香莲碧水动风凉夏日长 + +秋:秋江楚雁宿沙洲浅水流 + +冬:红炉透炭炙寒冬遇雪风 + + + + +请根据十字回文诗的成诗规律,编写程序,将《咏四季》读成的四首七绝通过列表展示出来。 + +![](https://img-blog.csdnimg.cn/9475d8272e3042348e42cd7d2432dc67.png) + + + +## 1. 准备工作 + +(1)背景:保留初始背景“背景1”; + +(2)角色:隐藏初始角色“角色1”(小猫); + +(3)创建列表“咏四季·春”、“咏四季·夏”、“咏四季·秋”、“咏四季·冬”。 + + +## 2. 功能实现 + +(1)将其中一首十字回文诗生成28字七绝; + +(2)将另外三首十字回文诗生成七绝; + +(3)使用自制积木对程序进行抽象和化简; + +(4)命名规范易读,并为代码添加合理注释。 + + 提示:加粗红色字可供复制。 \ No newline at end of file diff --git a/Scratch/等级考试-04/12. 绘制花瓣.md b/Scratch/等级考试-04/12. 绘制花瓣.md new file mode 100644 index 0000000..ea6a51e --- /dev/null +++ b/Scratch/等级考试-04/12. 绘制花瓣.md @@ -0,0 +1,23 @@ +# 绘制花瓣 + +下图为六个平行四边形组成的花瓣,请仔细观察图形,编写程序利用循环语句绘制该花瓣图形(花瓣中心的圆点不用绘制) 。 + +![](https://img-blog.csdnimg.cn/e7c0d1076cc64e64836d96735fe9f38c.png) + +## 1. 准备工作 + +(1)舞台背景为白色; + +(2)导入下图角色“Pencil”角色,设置造型中心为笔尖(即通过铅笔笔尖部位绘制图形)。 + +![](https://img-blog.csdnimg.cn/e57b69136c734531b998ae5d441186ad.png) + +## 2. 功能实现 + +(1)利用角色Pencil绘制图形,设置画笔颜色为黑色,画笔粗细为4; + +(2)设置平行四边形的长(100步)、宽(60步)以及其中一个角的度数(30°),利用函数完成一个平行四边形的绘制; + +(3)通过循环语句和函数完成上图花瓣的绘制; + +(4)当按下空格键,擦除绘制的图形。 diff --git a/Scratch/等级考试-04/13. 绳子算法.md b/Scratch/等级考试-04/13. 绳子算法.md new file mode 100644 index 0000000..d595e58 --- /dev/null +++ b/Scratch/等级考试-04/13. 绳子算法.md @@ -0,0 +1,28 @@ +# 绳子算法 + +故事情境:最近在学绳子算术的小星星非常苦恼,他常常在想,如果有一款程序能实现根据输入的两根绳子长度,可以把两根长绳截成长度相等的小段后,直接求出一共可以截成多少段,每段最长多少米就好了。小猫知道后,决定设计一个程序帮助小星星走出绳子算术的困境。 + + +## 1. 准备工作 + + + +(1)保留舞台默认白色背景及小猫角色,将小猫角色调整到舞台上合适的位置; + +(2)建立名为“绳子”的列表用于存储数据。 + +舞台效果如下图所示。 + +![](https://img-blog.csdnimg.cn/e8f6526a50d540a191aad7e262658f67.png) + + +## 2. 功能实现 + +(1)点击绿旗,询问“输入绳子长度”并等待; + +(2)将输入的绳子长度保存到列表“绳子”后,小猫分别说两根绳子的长度3秒; + +(3)根据输入的两根绳子长度,设计算法实现:把两根长绳截成长度相等的小段。求出一共可以截成多少段,每段最长多少米; + +(4)计算完成后,小猫分别说“一共可以截成多少段,每段最长多少米。”3秒。 + diff --git a/Scratch/等级考试-04/14. 计算三角形面积.md b/Scratch/等级考试-04/14. 计算三角形面积.md new file mode 100644 index 0000000..767e109 --- /dev/null +++ b/Scratch/等级考试-04/14. 计算三角形面积.md @@ -0,0 +1,15 @@ +# 计算三角形面积 + +编写程序自动计算三角形的面积。手动输入三角形的底和高,程序自动计算出三角形的面积,三角形的面积等于底*高/2。 + +## 1. 准备工作 + +小猫角色,白色背景。 + +## 2. 功能实现 + +(1)询问“请输入三角形的底”,输入一个数值; + +(2)询问“请输入三角形的高”,输入一个数值; + +(3)小猫说“三角形的面积是xxx”2秒。 diff --git a/Scratch/等级考试-04/15. 词语接龙.md b/Scratch/等级考试-04/15. 词语接龙.md new file mode 100644 index 0000000..32315cd --- /dev/null +++ b/Scratch/等级考试-04/15. 词语接龙.md @@ -0,0 +1,35 @@ +# 词语接龙 + +小猫从“中国"开始岀题,以“国”字开头接下一个词语,如果输入的不是两字词语或者输入的词语不是以“国”开头,游戏结束。 + + +![01](https://img-blog.csdnimg.cn/bf7b15d49f854a91b8bcf531cbab859d.png) + +![02](https://img-blog.csdnimg.cn/dc1c5d5a69934be18cd08c0aca4b03fb.png) + +![03](https://img-blog.csdnimg.cn/1494291d75ca47ac930cb1391a3e112b.png) + +![04](https://img-blog.csdnimg.cn/6434cc9a5fae4003b72a43af3c7d6186.png) + +![05](https://img-blog.csdnimg.cn/b78fe48a29d34d7b84ec14ee029c4627.png) + + + + +## 1. 准备工作 + +(1)保留舞台默认白色背景及小猫角色; + +(2)建立名为“词语接龙”的列表。 + + + + +## 2. 功能实现 + +(1)点击绿旗,将中国加入到列表后,小猫询问“中国,请输出以“国”开头的词语”; + +(2)输入词语,如果用户输入的不是两字词语或者词语不以“国”开头,就说“游戏结束”,否则将正确的词语加入列表; + +(3)继续询问,例如如果第2步输入的是“国家”,小猫继续询问“国家,请输入以“家”开头的两字词语”,依次类推,直到游戏结束。 + diff --git a/Scratch/等级考试-04/16. 食堂取餐.md b/Scratch/等级考试-04/16. 食堂取餐.md new file mode 100644 index 0000000..96bda8e --- /dev/null +++ b/Scratch/等级考试-04/16. 食堂取餐.md @@ -0,0 +1,28 @@ +# 食堂取餐 + +食堂有6个取餐口,男生按B键、女生按G键就可以获取一个取餐号,显示在列表中。例如第一个男生的取餐号为Boy1,第一个女生取餐号为Girl1,获得取餐号后角色Dot会播报:“Boy1排队中”。隔一段时间后,小猫播报:“请Boy1到2号窗口”,1号男生就可以去2号窗口取餐了。 + +![01](https://img-blog.csdnimg.cn/e16314f9108641eaaca68ef686501aec.png) + +![02](https://img-blog.csdnimg.cn/cd501288e7524863a7a7b7d02b45fcc2.png) + + + +## 1. 准备工作 + +(1)保留舞台默认白色背景; + +(2)小猫角色,添加角色Dot; + +(3)建立列表:等待列表。 + + + +## 2. 功能实现 + +(1)第一次按下G键,“Girl1”添加到“等待列表中”,角色Dot说:“Girl1排队中”;第二次按下G键,“Girl2”添加到“等待列表中”,角色Dot说:“Girl2排队中”,以此类推; + +(2)第一次按下B键,“Boy1”添加到“等待列表中”,角色Dot说:“Boy1排队中”;第二次按下B键,“Boy2”添加到“等待列表中”,角色Dot说:“Boy2排队中”;以此类推; + +(3)点击绿旗后,每隔2至8秒,小猫播报“等待队列”列表中第一个编号到几号窗口取餐,窗口号1至6随机分配,例如小猫说“请编号几到几号窗口取餐”2秒。播报完成后删除第一个列表项,继续等待2-8秒后播报。 + diff --git a/Scratch/等级考试-04/17. 从小到大排序.md b/Scratch/等级考试-04/17. 从小到大排序.md new file mode 100644 index 0000000..32f4d46 --- /dev/null +++ b/Scratch/等级考试-04/17. 从小到大排序.md @@ -0,0 +1,21 @@ + +# 从小到大排序 + +小猴子询问输入5个数,存入列表后,将这些数字从小到大排列后重新存入列表。 + +![](https://img-blog.csdnimg.cn/78dab7b69e514bccbacec97d5a736229.png) + +## 1. 准备工作 + +(1)保留舞台默认白色背景; + +(2)删除小猫角色,添加Monkey角色。 + + +## 2. 功能实现 + +(1)点击绿旗,询问“请输入一个数:”,等待输入,重复5次输入5个数字; + +(2)将输入的5个数保存到列表“数组”中; + +(3)小猴子说“输入的5个数字从小到大排序”,列表中的数字会小到大排列。 \ No newline at end of file diff --git a/Scratch/等级考试-04/18. 计算并联电阻的值.md b/Scratch/等级考试-04/18. 计算并联电阻的值.md new file mode 100644 index 0000000..26382d0 --- /dev/null +++ b/Scratch/等级考试-04/18. 计算并联电阻的值.md @@ -0,0 +1,24 @@ +# 计算并联电阻的值 + +两个电阻为r1和r2,两个电阻并联后的电阻R,计算公式如下: + +R = 1/( 1/r1 + 1/r2) + +分别输入r1和r2,计算并联电阻R。 + + + +## 1. 准备工作 + +(1)小猫角色,白色背景。 + + +## 2. 功能实现 + + +(1)询问“请输入第一个电阻”; + +(2)询问“请输入第二个电阻”; + +(3)小猫说出“并联后电阻值为xxx”。 + diff --git a/Scratch/等级考试-04/19. 小猫钓鱼.md b/Scratch/等级考试-04/19. 小猫钓鱼.md new file mode 100644 index 0000000..4750b9e --- /dev/null +++ b/Scratch/等级考试-04/19. 小猫钓鱼.md @@ -0,0 +1,20 @@ +# 小猫钓鱼 + + 小猫一共钓到了8条鱼,小猫挑选出长度小于6cm的鱼放生了,剩下的鱼保留下来。 + + ![](https://img-blog.csdnimg.cn/45c72bf291864774beda695dca20481d.png) + +## 1. 准备工作 + +(1)导入名为“Beach Malibu”的舞台背景; + +(2)保留默认的小猫角色; + +(3)建立名为“鱼”的列表,用于保存鱼的长度。 + + +## 2. 功能实现 + +(1)依次询问“第几条鱼的长度”,通过键盘依次输入8条鱼的长度(只需要输入数字),保存到列表“鱼”中; + +(2)当按下s键时,从列表中找出长度小于6(不包括6)的项,并删除。 \ No newline at end of file diff --git a/Scratch/等级考试-04/20. 成语接龙.md b/Scratch/等级考试-04/20. 成语接龙.md new file mode 100644 index 0000000..e7757fc --- /dev/null +++ b/Scratch/等级考试-04/20. 成语接龙.md @@ -0,0 +1,27 @@ +# 成语接龙 + +小猫从“一鸣惊人"开始岀题,以“人”字开头接下一个成语,如果输入的不是四字成语或者输入成语的第一个字不是上一个成语的最后一个字,游戏结束。 + +![01](https://img-blog.csdnimg.cn/09ee7505025a4e84b2c17b27ea5e32c2.png) + +![02](https://img-blog.csdnimg.cn/9e81b432feff4b78a86c123c817e48ac.png) + +![03](https://img-blog.csdnimg.cn/01648e2e6d67460f8d7cf1a2949f5855.png) + +![04](https://img-blog.csdnimg.cn/a8911ba9009441c0a20cd8535c178f5a.png) + + +## 1. 准备工作 + +(1)保留舞台默认白色背景及小猫角色; + +(2)建立名为“词语接龙”的列表。 + + +## 2. 功能实现 + +(1)点击绿旗,将“一鸣惊人”加入到列表后,小猫询问“一鸣惊人,请输出以“人”开头的四字成语”; + +(2)输入成语,如果用户输入的不是四个字或者输入不匹配的词语时,就说“游戏结束”,否则将正确的成语加入列表; + +(3)继续询问,例如如果第2步输入的是“人山人海”,小猫继续询问“人山人海,请输入以“海”开头的四字成语”,依次类推,直到游戏结束。 diff --git a/Scratch/等级考试-04/21. 找出出现次数最多的数字.md b/Scratch/等级考试-04/21. 找出出现次数最多的数字.md new file mode 100644 index 0000000..b40f8e0 --- /dev/null +++ b/Scratch/等级考试-04/21. 找出出现次数最多的数字.md @@ -0,0 +1,24 @@ +# 找出现次数最多的数字 + +找出出现次数最多的数字,如果有多个数字出现次数相同,则答案为后输入的数字。 + +![01](https://img-blog.csdnimg.cn/e913a0afa6734bdea6dcf22634dc044e.png) + +![02](https://img-blog.csdnimg.cn/99f8c79982284f00a90637584566a0c0.png) + +![03](https://img-blog.csdnimg.cn/e2b29d6ebe844793bdc0c11a6f08c119.png) + + +## 1. 准备工作 + +(1)保留默认的白色背景及小猫角色; + +(2)建立名为“list”的列表。 + +## 2. 功能实现 + +(1)运行程序,小猫询问“请问要输入几个数字?” + +(2)小猫依次询问“请输入第x个数字”,将输入的各个数字存放在列表“list”中。(x是指第几个,例如:请输入第1个数字;请输入第2个数字) + +(3)找出其中出现次数最多的数字,小猫说“出现次数最多的数字是xxx”。如果有多个数字出现的次数相等,都是最多的,小猫说出最后一个。(例如:小猫说“出现次数最多的数字是26“) \ No newline at end of file diff --git a/Scratch/等级考试-04/参考答案.md b/Scratch/等级考试-04/参考答案.md new file mode 100644 index 0000000..133c9bc --- /dev/null +++ b/Scratch/等级考试-04/参考答案.md @@ -0,0 +1,25 @@ +四级编程题: + +- [01 加减法混合运算器](https://mp.weixin.qq.com/s/P62AfeZWt7_YXFynZYhR4g) +- [02 程序优化](https://mp.weixin.qq.com/s/CtH1EtzJ0aLptGjQIa_T-A) +- [03 绘制雪花](https://mp.weixin.qq.com/s/NFTImh58-A2DI0FwWUcoNw) +- [04 解密](https://mp.weixin.qq.com/s/t5sI4mj1ZOpW75v01LgB4w) +- [05 食堂取餐](https://mp.weixin.qq.com/s/8w-KbePgKOMlbeNpNj5T2Q) +- [06 计算并联电阻的值](https://mp.weixin.qq.com/s/o5H4DDGKm2AqumANDHkYFQ) +- [07 创意画图](https://mp.weixin.qq.com/s/kx7G84Y3kglsMGf77CEY3Q) +- [08 小猫钓鱼](https://mp.weixin.qq.com/s/96hfxiZOz46IlaUwU83QVw) +- [09 奇偶之和](https://mp.weixin.qq.com/s/Lj15HlG9sVPp0LlmlEx7pA) +- [10 词语接龙](https://mp.weixin.qq.com/s/h1OpNGY14zfP2fd6wqu4jw) +- [11 绳子算法_1](https://mp.weixin.qq.com/s/Fw0OIqc7huxVmurEsTtXFg) +- [12 绳子算法_2](https://mp.weixin.qq.com/s/ZywsKwIa4T_ywQKJ3CBq0g) +- [13 十字回文诗](https://mp.weixin.qq.com/s/MVUl_AlD4q-4MexfFFQ-vQ) +- [14 数字反转](https://mp.weixin.qq.com/s/u6bcexP28c17w3xC8D9H2w) +- [15 找出现次数最多的数字](https://mp.weixin.qq.com/s/9rx4Xxh4xq-oAmPHLINTag) +- [16 成语接龙](https://mp.weixin.qq.com/s/4RYJPc-svg17m3umDHN-hg) +- [17 绘图程序优化](https://mp.weixin.qq.com/s/BEyTuu8Km8HFuoM-I766iw) +- [18 从小到大排序](https://mp.weixin.qq.com/s/JzdEY41GNJeRBU3DyT_pxw) +- [19 绘制花瓣](https://mp.weixin.qq.com/s/WExI_O6QdZ8MvILGGWvHpA) +- [20 数字之和](https://mp.weixin.qq.com/s/nxvJXxKwNcvm3Fd_6yXwxQ) +- [21 用逗号分隔列表](https://mp.weixin.qq.com/s/clqmRrPO7apRqMNSzZ16dg) +- [22 计算三角形面积](https://mp.weixin.qq.com/s/GW6ClpgZVGlUI0bleYLr8g) +