Stanford cs193p More SwiftUI
本文是斯坦福大学 cs193p 公开课程第02集的相关笔记。
cs193p 课程介绍:
The lectures for the Spring 2023 version of Stanford University's course CS193p (Developing Applications for iOS using SwiftUI) were given in person but, unfortunately, were not video recorded. However, we did capture the laptop screen of the presentations and demos as well as the associated audio. You can watch these screen captures using the links below. You'll also find links to supporting material that was distributed to students during the quarter (homework, demo code, etc.).
cs193p 课程网址: https://cs193p.sites.stanford.edu/2023
继续设计卡片小游戏
some View
请看一段演示代码,如下:
struct ContentView: View {
var body: some View {
Text("Hello")
}
}
这段演示代码同样可以这样写:
struct ContentView: View {
var body: Text {
Text("Hello")
}
}
但是如果我们放一些返回值不为Text
的结构,编译器会报错:(Cannot convert return expression of type 'VStack<TupleView<(Text, Text, Text)>>'
to return type 'Text'
)
struct ContentView: View {
var body: Text {
VStack {
Text("Hello")
Text("Hello")
Text("Hello")
}
}
}
some View
可以让编译器自动识别不同的返回类型
尾随闭包 (Trailing closure syntax)
我们如果仔细看看 VStack
, 我们传入了一个名为content
的参数。
struct CardView: View {
var isFaceUp: Bool = false
var body: some View {
ZStack(alignment: .top, content: {
// ZStack Code
})
}
}
如果一个函数的最后一个参数本身是一个函数,此时我们可以使用尾随闭包。
struct CardView: View {
var isFaceUp: Bool = false
var body: some View {
ZStack(alignment: .top) {
// ZStack Code
}
}
}
RoundedRectangle
当我们使用 RoundedRectangle 时,如果我们不指定具体的修改器,Swift会默认填充。
RoundedRectangle(cornerRadius: 12)
// These two codes are identical in terms of functionality.
RoundedRectangle(cornerRadius: 12).fill()
局部变量 (Local Variable)
我们可以创建一个局部变量:
struct CardView: View {
var isFaceUp: Bool = false
var body: some View {
ZStack {
if isFaceUp {
RoundedRectangle(cornerRadius: 12).fill(.white)
RoundedRectangle(cornerRadius: 12).strokeBorder(lineWidth: 2)
Text("👻").font(.largeTitle)
} else {
RoundedRectangle(cornerRadius: 12).fill()
}
}
}
}
创建了一个局部变量名为 base
:
struct CardView: View {
var isFaceUp: Bool = false
var body: some View {
ZStack {
let base: RoundedRectangle = RoundedRectangle(cornerRadius: 12)
if isFaceUp {
base.fill(.white)
base.strokeBorder(lineWidth: 2)
Text("👻").font(.largeTitle)
} else {
base.fill()
}
}
}
}
IMPORTANT: 我们使用了关键字 let
而不是 var
,因为这个变量一旦创建就不再能被改变。(let
通常用来创建常量)
类型推论 (Type Inference)
我们可以省略变量类型让 Swift 自动判定。
// Without omit the type
let base: RoundedRectangle = RoundedRectangle(cornerRadius: 12)
// Omited the type (using type inference)
let base = RoundedRectangle(cornerRadius: 12)
我们可以按住 option
键然后点击 base
变量,Swift 会显示自动判定的变量类型。
Note: 我们在生产环境几乎都使用类型推论,不手动指定变量类型。
.onTapGesture
单击:
struct CardView: View {
@State var isFaceUp = true
var body: some View {
ZStack {
// ZStack Code
}
.onTapGesture {
isFaceUp.toggle()
}
}
}
双击:
struct CardView: View {
@State var isFaceUp = true
var body: some View {
ZStack {
// ZStack Code
}
.onTapGesture(count: 2) {
isFaceUp.toggle()
}
}
}
@State
通常来说,一个变量在函数被调用后就不可改变。@State
关键字允许变量有临时的状态,因为@State
会创建一个指针指向堆 (Heap) 中。因此,指针本身没有被改变,改变的是堆里存的数据。
@State var isFaceUp = true
数组
Swift接受以下两种方式新建数组,
// A valid array notation
let emojis: Array<String> = ["👻", "🎃", "🕷️", "😈"]
// Alternate array notation
let emojis: [String] = ["👻", "🎃", "🕷️", "😈"]
我们也可以使用类型推论省略类型:
let emojis = ["👻", "🎃", "🕷️", "😈"]
ForEach 循环
ForEach 不包含最后一个数字:
// iterate from 0 to 3 (NOT including 4)
ForEach(0..<4, id: \.self) { index in
CardView(content: emojis[index])
}
ForEach 包含最后一个数字:
// iterate from 0 to 4 (including 4)
ForEach(0...4, id: \.self) { index in
CardView(content: emojis[index])
}
ForEach (基于数组的长度)循环整个数组:
struct ContentView: View {
let emojis = ["👻", "🎃", "🕷️", "😈"]
var body: some View {
HStack {
ForEach(emojis.indices, id: \.self) { index in
CardView(content: emojis[index])
}
}
.foregroundColor(.orange)
.padding()
}
}
按钮
文本按钮
语法结构:
Button("Remove card") {
// action
}
示例:
struct ContentView: View {
let emojis = ["👻", "🎃", "🕷️", "😈", "💩", "🎉", "😎"]
@State var cardCount = 4
var body: some View {
VStack {
HStack {
ForEach(0..<cardCount, id: \.self) { index in
CardView(content: emojis[index])
}
}
.foregroundColor(.orange)
HStack {
Button("Remove card") {
cardCount -= 1
}
Spacer()
Button("Add card") {
cardCount += 1
}
}
}
.padding()
}
}
图标按钮
语法结构:
Button(action: {
// action
}, label: {
// button icon, images, etc...
})
示例:
struct ContentView: View {
let emojis = ["👻", "🎃", "🕷️", "😈", "💩", "🎉", "😎"]
@State var cardCount = 4
var body: some View {
VStack {
HStack {
ForEach(0..<cardCount, id: \.self) { index in
CardView(content: emojis[index])
}
}
.foregroundColor(.orange)
HStack {
Button(action: {
cardCount -= 1
}, label: {
Image(systemName: "rectangle.stack.badge.minus.fill")
})
Spacer()
Button(action: {
cardCount += 1
}, label: {
Image(systemName: "rectangle.stack.badge.plus.fill")
})
}
.imageScale(.large)
}
.padding()
}
}
超出索引的问题
如果我们添加了太多的卡片,由于索引超出范围会导致程序崩溃。其中一种避免程序的方法是添加一个 if
逻辑。
Button(action: {
if cardCount < emojis.count {
cardCount += 1
}
}, label: {
Image(systemName: "rectangle.stack.badge.plus.fill")
})
另一种方法是使用 .disabled
视图修改器
func cardCountAdjuster(by offset: Int, symbol: String) -> some View {
Button(action: {
cardCount += offset
}, label: {
Image(systemName: symbol)
})
.disabled(cardCount + offset < 1 || cardCount + offset > emojis.count)
}
Note: 这节课的后半部分讲解了 Swift 中的函数。
整理代码
我们先看看 body
中包含的代码,
struct ContentView: View {
let emojis = ["👻", "🎃", "🕷️", "😈", "💩", "🎉", "😎"]
@State var cardCount = 4
var body: some View {
VStack {
HStack {
ForEach(0..<cardCount, id: \.self) { index in
CardView(content: emojis[index])
}
}
.foregroundColor(.orange)
HStack {
Button(action: {
if cardCount > 1 {
cardCount -= 1
}
}, label: {
Image(systemName: "rectangle.stack.badge.minus.fill")
})
Spacer()
Button(action: {
if cardCount < emojis.count {
cardCount += 1
}
}, label: {
Image(systemName: "rectangle.stack.badge.plus.fill")
})
}
.imageScale(.large)
}
.padding()
}
}
现在看起来十分不整洁。我们可以创建其它视图提高代码的可读性。
struct ContentView: View {
let emojis = ["👻", "🎃", "🕷️", "😈", "💩", "🎉", "😎"]
@State var cardCount = 4
var body: some View {
VStack {
cards
cardCountAdjusters
}
.padding()
}
var cards: some View {
HStack {
ForEach(0..<cardCount, id: \.self) { index in
CardView(content: emojis[index])
}
}
.foregroundColor(.orange)
}
var cardCountAdjusters: some View {
HStack {
cardRemover
Spacer()
cardAdder
}
.imageScale(.large)
}
var cardRemover: some View {
Button(action: {
if cardCount > 1 {
cardCount -= 1
}
}, label: {
Image(systemName: "rectangle.stack.badge.minus.fill")
})
}
var cardAdder: some View {
Button(action: {
if cardCount < emojis.count {
cardCount += 1
}
}, label: {
Image(systemName: "rectangle.stack.badge.plus.fill")
})
}
}
在整理后,我们 body
中的代码现在看起来非常容易理解。
隐式返回值 (Implicit return)
如果一个函数只有 1 行代码,我们就可以使用隐式返回。
var cards: some View {
HStack {
ForEach(0..<cardCount, id: \.self) { index in
CardView(content: emojis[index])
}
}
.foregroundColor(.orange)
}
当然我们也可以使用 return
关键字显式返回。
var cards: some View {
return HStack {
ForEach(0..<cardCount, id: \.self) { index in
CardView(content: emojis[index])
}
}
.foregroundColor(.orange)
}
函数 (Function)
语法结构:
func <function name>(<para name>: <data type>) -> <return type> {
// function code
}
示例:
func cardCountAdjuster(by offset: Int, symbol: String) -> some View {
Button(action: {
cardCount += offset
}, label: {
Image(systemName: symbol)
})
}
IMPORTANT: by offset: Int
我们有时候会使用 2 个标签代表一个参数,第一个参数 by
在调用函数时使用,而第二个标签在函数内使用。第一个标签被称为 external parameter name,第二个标签被称为internal parameter name。
现在我们的代码看起来更漂亮了,
func cardCountAdjuster(by offset: Int, symbol: String) -> some View {
Button(action: {
cardCount += offset
}, label: {
Image(systemName: symbol)
})
}
var cardRemover: some View {
return cardCountAdjuster(by: -1, symbol: "rectangle.stack.badge.minus.fill")
}
var cardAdder: some View {
return cardCountAdjuster(by: 1, symbol: "rectangle.stack.badge.plus.fill")
}
Note: 由于我们删除了 if
逻辑,我们程序可能由于数组超出索引范围而崩溃。但是我们在超出索引的问题章节讲了如何解决.
LazyVGrid
为了让这些卡片看起来比较正常,我们需要用LazyVGrid
替代HStack
。
var cards: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))]) {
ForEach(0..<cardCount, id: \.self) { index in
CardView(content: emojis[index])
}
}
.foregroundColor(.orange)
}
我们需要在cards
和 cardCountAdjusters
之间添加一个Spacer()
,这样它们不会挤到一起去。
var body: some View {
VStack {
cards
Spacer()
cardCountAdjusters
}
.padding()
}
由于LazyVGrid
会使用尽可能少的空间,因此,当两张卡片都为背面时会被挤压到一起去。
.opacity
我们需要修改CardView
的逻辑,
struct CardView: View {
let content: String
@State var isFaceUp = true
var body: some View {
ZStack {
let base = RoundedRectangle(cornerRadius: 12)
Group {
base.foregroundColor(.white)
base.strokeBorder(lineWidth: 2)
Text(content).font(.largeTitle)
}
.opacity(isFaceUp ? 1 : 0)
base.fill().opacity(isFaceUp ? 0 : 1)
}
.onTapGesture {
isFaceUp.toggle()
}
}
}
问题解决!
.aspectRatio
var cards: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))]) {
ForEach(0..<cardCount, id: \.self) { index in
CardView(content: emojis[index])
.aspectRatio(2/3, contentMode: .fit)
}
}
.foregroundColor(.orange)
}
ScrollView
var body: some View {
VStack {
ScrollView {
cards
}
Spacer()
cardCountAdjusters
}
.padding()
}
评论已关闭