Build a Basic UI

选中多个 views,Embed In Stack 将它们转化为一个 stack

可以通过 Update Frames 和 Resolve Auto Layout Issues 来修正约束问题。选用后者时,如果所有选项都是灰色,试试先选中界面的 view controller 或者其中一个 view。在这里也可以清除所有约束。

Select the scene’s view controller to update all the views in the scene. You can also Option-click the Update Frames button to update only the selected view.

UI to Code

在 storyboard 中,一个 scene 代表一个 view controller

iOS 中所有的 view controller objects 都是 UI view controller 类型或者是它的子类

当新建一个 scene 时,需要在 Identity inspector 中自建连接(在 ViewController.swift 中新的类)

@IBOutlet weak var nameTextField: UITextField! 中:IB 代表 interface builder;weak 代表防止系统释放 object 的引用

对于需要保持活跃且常驻内存中的 object,则将需要强引用

从界面控件中获取值或者在代码中更改界面控件都只需要一个指向界面的 outlet。对于 Button 控件则可以不需要创建 outlet

outlet 负责指向,做出响应则是 action,它在用户与控件交互时触发,创建方式同样是 control-drag,在弹出对话框中更改 Connection 为 action,如下:

@IBAction func setDefaultLabelText(_ sender: UIButton) {}

sender 控件指可响应的控件(例中为 button)

target-action pattern (目标-动作模式)是 iOS app 设计中的一种 design pattern (设计模式),即当特定事件发生时,一个控件将信息传递给另一个控件

In this case: The event is the user tapping the Set Default Text button. The action is setDefaultLabelText(_). The target is ViewController (where the action method is defined). The sender is the Set Default Label Text button.

sender 通常是 button, slider, 或 switch 这些可以触发事件的控件

使 view controller 采用 Delegate 协议,如

class ViewController: UIViewController, UITextFieldDelegate {}

To set the ViewController object as the delegate of its nameTextField property nameTextField.delegate = self self 指 ViewController 类

UITextViewDelegate 定义了 8 个 optional methods

使用 //MARK: SOMETHING 注释以被 Xcode 识别为一个 section

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    // Hide the keyboard.
    textField.resignFirstResponder()
    return true
}

return true 的作用:告诉系统需要在用户按下 Return (Done) 的时候进行处理;因为 resign 了 first-responder 状态,系统接下来就会调用 textFieldDidEndEditing(_:)

Work with View Controllers

intrinsic content size 是视内容而定的优先选用的尺寸,一个空的 image view 没有 intrinsic content size;一旦你添加一张图片到视图中,视图就会变成图片的实际大小;设定一个 placeholder 尺寸是暂时性的,仅在 interface builder 中设计时用到,实际运行时则会使用图片的实际尺寸替代

aspect ratio constraints – 固定长宽比例

asset catalog – 存放和管理 app 所用到的图片

viewscontrols 的区别:view 显示内容,而 controls 用于更改内容,一个 control (UIControl) 是 UIView 的子类

In fact, you’ve already worked with both views (labels, image views) and controls (text fields, buttons) in your interface.

由上,view 可以使用 Gesture Recognizer 来完成某些响应

UIImagePickerController 的 delegate protocol – UIImagePickerControllerDelegate

给予访问相册权限:Info.plist,添加 key:Privacy – Photo Library Usage Description

Implement a Custom Control

创建视图一般有两种方式,一种是程序初始化,第二种是从 storyboard 载入。对于两种方法分别有一种初始器,init(frame:)init?(coder:)

Recall that an initializer is a method that prepares an instance of a class for use, which involves setting an initial value for each property and performing any other setup.

当用程序方式实例化一个视图的时候,translatesAutoresizingMaskIntoConstraints 默认是 true,即自动调整大小位置和创建约束,所以最好先将它更改为 false

Define Your Data Model

UIKit 框架包含默认的 Foundation 框架

一般写的测试主要是两种:功能测试和性能测试

Create a Table View

对 table view 的原型单元(prototype cell)定义的设计和行为,可以作为 table 中创建新的单元的实例

将场景中的视图控件连接到自定义的子类:

  1. 修改 Attributes inspector – Identifier
  2. 修改 Identity inspector – Class

在 storyboard 中,你能配置一个 table view 以显示静态数据(在 storyboard 中提供)或者动态数据(程序式地通过 table view controller 提供)。table view controller 默认使用的是动态数据,这意味着在 storyboard 中创建的单元(cell)只是一个 cell 的原型(prototype),我们仍需在代码中创建这个 cell 的实例,并用你的 app 的数据来填充

向项目中添加图片:

  1. Assets.xcassets
  2. 左下角 – New folder(如必要)
  3. 左下角 – New Image Set
  4. 将 Finder 中的图片拖入槽中

如果 app 的数据的类中的初始器是可失败(failable)的(e. g. init!(name:, photo:, rating:)),那么需要检查初始器返回的结果 -> 使用 guard 语句,以使得出错时可发现

要将动态数据展示出来,需要:data sourcedelegate默认情况下,UITableViewController 以及它的子类采用必要的协议来使 table view controller 对于关联的 table view 来说,既是 data source (UITableViewDataSource protocol),也是 delegate (UITableViewDelegate protocol)。所以,在 table view controller 中实现合适的协议方法后,就能使得 table view 正确表现

一个有效的 table view 需要三个 data source 方法

func numberOfSections(in tableView: UITableView) -> Int
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

第一项,numberOfSections(In:) 告诉 table view 有多少个 section 需要展示。section 是一组 cells

第二项,tableView(_:numberOfRowsInSection:) 告诉 table view 在给出的 section 中展示多少行

第三项,tableView(_:cellForRowAt:) 配置并提供在给定的行(row)中要展示的 单元(cell),一个 row 对应一个 cell dequeueReusableCell(withIdentifier:for:) 方法从 table view 中请求一个 cell,identifier 告诉该函数某种类型的 cell 应该被创建或者重用

Implement Navigation

scene 之间的转换称为 segue

navigation controller 管理的一套视图控制器叫做 navigation stack,添加到 item 中的第一个项目被称作 root view controller,且它永远不会被移出 navigation stack

每一个在 navigation stack 中的 controller 都有一个 bar

每一个视图控制器有一个 navigationItem 属性,它定义 navigation bar 对该视图控制器的样式

在 segue 的 Attributes inspector 中更改 Kind (Show -> Present Modally)

为 segue 选用 Modal 类型的视图控制器,不会被加入到 navigation stack,所以在 Interface Builder 中不会有 navigation bar,所以需要嵌入一个它自己的 navigation controller

接下来,要实现用户在点按 Save 后,将 Meal 对象传给之前的视图控制器,需要用到 Unwind egue。通常的 segue 是为新的视图控制器创建一个新的实例,而 unwind segue 是返回本已存在的视图控制器

使用 Unwind segue: 在目标 view controller 中添加 method:

@IBAction func unwindToVC (sender: UIStoryboardSegue) {
}

然后,控件 control drag 到 Exit,选择函数名

一个 segue 被触发时,提供了一个地方可添加自己的代码并执行,这个方法就是 prepare(for:sender:) ,可以通过它来存取数据或者在源视图控制器中做必要的清理

实现 prepare(for:sender:)import os.log — 导入统一登录系统 ( unified logging system),它让你能将信息像 print() 一样传给控制台,但是它能让你控制信息何时出现以及如何存储

It doesn’t do anything, but it’s a good habit to always call super.prepare(for:sender:) whenever you override prepare(for:sender:). That way you won’t forget it when you subclass a different class.

let name = nameTextField.text ?? "" 这里使用了空值合并操作符 [nil coalescing operator (??) ],如果 optional 含有值,则返回值,否则返回指定的值(这里为空字符串)

as? 将一个对象实例 downcast 到子类类型

Implement Edit and Delete Behavior

Cancel button 设定的 dismiss 方法应取决与 scene 是如何展现的,对于以模式(modally)展示的,应该用 dismissViewControllerAnimated(_:completion:)来 dismiss;对于以 push navigation 展示的,应通过 navigation controller 来 dismiss

popViewController(animated:) 方法,将当前的视图控制器撤离(pop off)navigation stack 并且动态切换

navigationItem.leftBarButtonItem = editButtonItem ,navigation controller 内建的 edit mode(删除需自行添加)

在 edit mode 下,需要使用 delegate 方法 tableView(_:commit:forRowAt:) 来做编辑

Persist Data

data persistence 是 iOS 开发中关键的一部分,iOS 有许多数据持久化存储方案,课程中将使用 NSCoding 这一机制

NSCoding 是存档对象和其它结构的一种轻量的解决方案。存档的对象可被存储到磁盘并可在以后检索到

实现一个 coding key 结构体来存取常量。为了能够编码及解码它本身,结构体所处的类需要遵循 NSCoding protocol。为了遵循 NSCoding,它还需要作为 NSObject 的子类,NSObject 定义运行状态下的系统的基本界面

作为 NSObject 的子类,类的初始器必须调用 NSObject 类指定的唯一的初始器 — init(),Swift 编译器将会自动调用,所以只需在需要的时候添加调用 super.init()

采用 NSCoding 协议应声明两个 method:

encode(with aCoder: NSCoder)
init?(coder aDecoder: NSCoder)

这样,该类的实例才可以编码和解码

encode(with:) 方法为类的信息归档做准备,编码各个属性的值并以对应的 key 存取;而初始器在类被创建时 unarchive 数据。所以,为了实现正确地存取和加载数据,encode(with:) 方法和初始器两者都需要应用

required 修饰语,表明在所有自定义初始器的子类中都应实现这个初始器

convenience 修饰语,表明是二级初始器,必须在同一个类中调用指定的初始器 (??)

decodeObject(forKey:) — 编码 method,返回 Any? 可选值

decodeInteger(forKey:) method 返回的是一个整数

argument -> 实参, parameter -> 形参

接下来,需要在文件系统中有一个 persistent path,这样才能找到存取和载入的数据

//MARK: Archiving Paths
 
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("meals")

static 关键词,表示它们属于类而不是类的一个实例。在类外访问路径,使用句法 Meal.ArchiveURL.path。不管你创建了多少个该类的实例,这样的属性都只有一份

DocumentsDirectory 常量使用文件管理器的 urls(for:in:) 方法来查找 app 的文件目录的 URL,这个目录是你的 app 为用户存取数据的目录。方法返回 URL 数组,first 参数返回一个包含数组中第一个 URL 的可选值。然而,只要这里的枚举是正确的,返回的数组应该总是刚好返回一个匹配,所以,强制解包这个可选值是安全的。

Comments
Write a Comment