继续设计卡片小游戏
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)
可以顺便看看闭包:swift 闭包(闭包表达式、尾随闭包、逃逸闭包、自动闭包)
我们如果仔细看看 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")
}
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()
}