相关资料:


Struct

通过编写 struct 可以创建自己的自定义复杂数据类型。只需为其命名,然后将结构体的代码放在大括号内即可。

基本声明

//例子:创建名为 Album 的 struct,其中包含 title 和 artist 两个字符串常量,以及整数常量 year。还添加了一个函数来总结三个常量的值。
struct Album {
    let title: String
    let artist: String
    let year: Int
    func printSummary() {
        print("\\(title) (\\(year)) by \\(artist)")
    }
}

//实例化:一个新的 Album 就像调用函数一样 - 只需要按照定义的顺序为每个属性提供值
let red = Album(title: "Red", artist: "Taylor Swift", year: 2012)
let wings = Album(title: "Wings", artist: "BTS", year: 2016)

//red 和 wings 都来自同一个 Album 结构,但是一旦创建它们,它们就像创建两个字符串一样是分开的独立的
print(red.title)
print(wings.artist)

嵌套声明结构

嵌套结构指的是将一个结构放置在另一个结构中。这不会影响项目中的代码,但它有助于保持代码的组织性。如果想象一个具有数百个自定义类型的项目,那么添加这个额外的上下文确实会有帮助

// 当采用嵌套结构,读取子结构时,需要改写成 Mission.CrewRole
struct Mission: Codable, Identifiable {

    struct CrewRole: Codable {
        let name: String
        let role: String
    }

    let id: Int
    let launchDate: Date?
    let crew: [CrewRole]
    let description: String
}

和 Tuple 元组的区别

// 以下元组和结构体具有相同的效果
(name: String, age: Int, city: String)

struct User {
    var name: String
    var age: Int
    var city: String
}

// 使用时对比
func authenticate(_ user: User) { ... }
func showProfile(for user: User) { ... }
func signOut(_ user: User) { ... }

func authenticate(_ user: (name: String, age: Int, city: String)) { ... }
func showProfile(for user: (name: String, age: Int, city: String)) { ... }
func signOut(_ user: (name: String, age: Int, city: String)) { ... }

属性

原本 Swift 中的变量和常量,放到了 struct 里就称为【属性】。struct 有两种属性:存储属性 和 计算属性

//如果 struct 里的属性有默认赋值了...
let name: String
var vacationRemaining = 14

//那实例化时,以下两种方式都是有效的(有默认值的属性可以不填,您可以只填充您需要的部分)
let kane = Employee(name: "Lana Kane")
let poovey = Employee(name: "Pam Poovey", vacationRemaining: 35)

//如果将默认值分配给了【常量属性 let】,则该默认值将从初始值设定项中完全删除。
//要分配默认值,但保留在需要时覆盖它的可能性,请使用【变量属性 var】

存储属性

存储属性:是在 struct 实例内保存一段数据的变量或常量

计算属性

计算属性会在每次访问时,动态计算属性的值。计算属性是“存储属性”和“函数”的混合,它们像存储属性一样访问,但像函数一样工作

// 例子1:我们为该员工分配 14 天的假期,然后根据天数减去它们。虽然可以争取显示剩余假期的天数
// 但这样的方式会丢失有价值的信息:员工最初被授予的天数
struct Employee {
    let name: String
    var vacationRemaining: Int
}

var archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.vacationRemaining -= 5
print(archer.vacationRemaining)    //9
archer.vacationRemaining -= 3
print(archer.vacationRemaining)    //6

// 这时可以改写成计算属性
// 不再将 vacationRemaining 直接分配给它,而是通过从分配的假期数中减去他们已享受的假期数来计算
struct Employee {
    let name: String
    var vacationAllocated = 14
    var vacationTaken = 0
    var vacationRemaining: Int {
        vacationAllocated - vacationTaken
    }
}

// 我们不再将 vacationRemaining 直接分配给它,而是通过从分配的假期数中减去他们已享受的假期数来计算
var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4         //10
print(archer.vacationRemaining)
archer.vacationTaken += 4         //6
print(archer.vacationRemaining)
// 例子2:通过return返回
struct Wine {
	var age: Int
	var isVintage: Bool
	var price: Int {
		if isVintage {
			return age + 20
		} else {
			return age + 5
		}
	}
}

让计算属性可写入 set

计算属性是由执行函数具体算出来的,那能否直接由我们写入呢?为了解决这个问题,swift 提供了 getter 和 setter 方法区分“可读取”和“可写入”,它们分别代表 “读取时的代码” 和 “写入时的代码” 。

// 当 set 一个计算属性时,其它属性应该怎么变化,这是由我们自行写代码决定的!
// 假设当设置 vacationRemaining 时,我们想改变的是 vacationAllocated,那可以这样写
struct Employee {
    let name: String
    var vacationAllocated = 14
    var vacationTaken = 0
    var vacationRemaining: Int {
        get { vacationAllocated - vacationTaken }
        set { vacationAllocated = vacationTaken + newValue }
    }
}

var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4
archer.vacationRemaining = 8     //这里 8 对应的就是上面的 newValue
print(archer.vacationAllocated)  //打印 12 (4+8)
        

// 但当设置 vacationRemaining 时,如果我们想改变的是 vacationRemaining,也可以这样写
set { vacationTaken = vacationAllocated - newValue }
        

// 如果逻辑不规划好,甚至会发生“给计算属性赋值后,它重新计算后的值会跟你赋值的值不一致”的情况
// 例如 setter 代码这样写,就算赋值 vacationRemaining = 8 ,访问时也会发现 vacationRemaining = 16,因为它get时又重新计算了
set { vacationAllocated = 20 }

<aside> 💡 请注意 newValue 是由 swift 自动提供的,swift 会存储用户尝试分配给属性的任何值。

</aside>

让计算属性只读 get

属性可以有 getter 和 setter。但当属性没有 setter 时,它被称为“只读”属性。在右边示例中,personType 是计算属性中的【只读属性】。

只读属性可以进行简写:

Screenshot - 2023-09-14 20.12.03.png

惰性属性 lazy

详细说明见: lazy 惰性


属性观察

<aside> 💡 枚举 enum 中不能用属性观察器,在 Swift 中,属性观察通常用于类(class)和结构体(struct)中的属性,而不是枚举(enum)的属性。枚举的成员(case)通常是不具有属性观察的。

</aside>

Swift 支持的属性观察器(property observer),是在属性更改时运行的特殊代码片段。

属性观察器主要有 didSet & willSet 两种形式:

struct App {
    var contacts = [String]() {
        willSet { print("New value will be: \\(newValue)") }
        didSet { print("Old value was \\(oldValue)") }
    }
}

var app = App()
app.contacts.append("Adrian E")
app.contacts.append("Allen W")

大多数情况你会使用 didSet ,因为通常希望在更改发生后采取行动,以便可以更新用户界面、保存更改或执行其他操作。但这不代表 willSet 没有用,只是在实践中它的受欢迎程度明显低于其对应项。

willSet 最常见的使用时间是当您需要在进行更改之前了解程序的状态时。例如,SwiftUI 在某些地方使用 来处理动画,以便它可以在更改之前拍摄用户界面的快照。当它同时具有“之前”和“之后”快照时,它就可以比较两者以查看用户界面中需要更新的所有部分。


方法

原本的函数,放到了 struct 里就称为【方法】。方法和函数的区别是:


结构体的初始化 initializer

成员式初始化方法(默认)

结构体的初始化相对于类的初始化是简单的。当创建一个结构体实例时,实际上是调用了它的初始化方法。但我们看不到结构体里有定义这个方法,事实上是如果我们没有给结构体创建初始化方法,Swift 会在结构体中默默地创建一个名为 init() 的特殊函数,它使用结构体的【所有属性/成员】作为其参数。

// 实际上,下面两种代码都是可以的,我们常用的第1种其实是第2种的简写
var archer1 = Employee(
		name: "Sterling Archer", 
		vacationRemaining: 14
)
var archer2 = Employee.init(
		name: "Sterling Archer", 
		vacationRemaining: 14
)

自定义初始化方法

除了使用默认的初始化方法,还可以创建自定义初始化方法,这样可以在创建实例时添加一些额外功能:

// 例子
struct Player {
    let name: String
    let number: Int
    init(name: String, number: Int) {
		    // 如果没有 self ,就会有 name = name ,这是没有意义的
        self.name = name
        self.number = number
    }
}

// 例子:当属性设置了默认值时,初始化函数可以不设置该属性的值
struct Cottage {
	var rooms: Int
	var rating = 5
	init(rooms: Int) {
		self.rooms = rooms
	}
}
let bailbrookHouse = Cottage(rooms: 4)

// 例子: 有些属性未必要在初始化时填,只要实例化后确定有值就行
struct Player {
    let name: String
    let number: Int
    init(name: String) {
        self.name = name
        number = Int.random(in: 1...99)
    }
}

let player = Player(name: "Megan R")
print(player.number)

// 例子: 为 @ObservedObject、@Binding、@State 等特殊属性初始化:
@State var count: Int
@StateObject var note: Note
@ObservedObject var note: Note
@Binding var homePagePath: NavigationPath

// 1. @ObservedObject 属性是从上级视图传入的,初始化和通常一样,将参数赋值给属性就好
// 2. @Binding 属性,代表传入参数的类型要包裹上 Binding<>
// 3. 初始化 @Binding 属性时,要使用下划线作为前缀 _homePagePath,这是因为初始化绑定属性需要直接访问其底层存储
// @State 和 @StateObject 也和 @Binding 属性一样
init(note: Note, homePagePath: Binding<NavigationPath>) {
    self.note = note
    _homePagePath = homePagePath
}