cs193p-02 Memorize

97 阅读5分钟

继续设计卡片小游戏

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)
}

我们需要在cardscardCountAdjusters之间添加一个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()
}