Stanford-cs193p-02|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

继续上一节记忆卡片的游戏制作,笔记按照课程项目制作步骤进行整理,在每个步骤依次对涉及的语法糖进行说明。建议在看第二节视频前预览一遍,了解教程框架,便于针对性的学习。


创建一个 CardView struct

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

同样,如果我们放入多个 Text 的结构,编译器仍然会报错:

struct ContentView: View {
    var body: Text {
		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 会显示自动判定的变量类型。

type-inference

Note: 我们在生产环境几乎都使用类型推断,不手动指定变量类型。如果需要对返回的类型进行检查,则可指定变量类型。

.onTapGesture

单击:

struct CardView: View {
    @State var isFaceUp = true
    var body: some View {
        ZStack {
            // ZStack Code
        }
        .onTapGesture {
            isFaceUp.toggle()
        }
    }
}
  • 尾随闭包:单击的方法为 .onTapGesture(perform:{}) ,此处由于最后一个参数本身是一个函数,所以使用尾随闭包 .onTapGesture { isFaceUp.toggle() }

双击:

struct CardView: View {
    @State var isFaceUp = true
    var body: some View {
        ZStack {
            // ZStack Code
        }
        .onTapGesture(count: 2) {
            isFaceUp.toggle()
        }
    }
}

@State

这一段代码会报错 Error: Cannot assign to property: 'self' is immutable。因为结构体的属性默认是不可变的。也就是说,在结构体的实例方法或闭包中不能直接修改它的属性,除非明确标记为 mutating。因此,当你在 struct 中使用一个属性(比如 isFaceUp),它默认是不可变的,在 .onTapGesture 中试图修改 isFaceUp 则会导致这个错误。

struct CardView: View {
    var isFaceUp = true
    var body: some View {
        ZStack {
            // ZStack Code
        }
        .onTapGesture {
        	isFaceUp = !isFaceUp
        }
    }
}

通常来说,一个变量在函数被调用后就不可改变。@State 关键字允许变量有临时的状态,因为 @State 会创建一个指针指向堆 (Heap) 中。因此,指针本身没有被改变,改变的是堆里存的数据。

SwiftUI 中, @State 属性包装器用来管理视图中的可变状态的,将 isFaceUp 声明为 @State,SwiftUI 会知道这个属性的变化会导致视图重新渲染。

struct CardView: View {
    @State private var isFaceUp = true

    var body: some View {
        ZStack {
            // ZStack Code
        }
        .onTapGesture {
            isFaceUp.toggle()
        }
    }
}

这样,isFaceUp 可以在视图中动态可变,且不会触发不可变性错误。

生成一组 Card

数组

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

Note:id:\.self 会在 cs193p 后续课程中讲解

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

通常在数组长度未知时使用该 .indices 方法。

控制 Card 个数

按钮

文本按钮

语法结构:

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

text-button

图标按钮

语法结构:

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

icon-button

超出索引的问题

如果我们添加了太多的卡片,由于索引超出范围会导致程序崩溃。其中一种避免程序的方法是添加一个 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 中的代码现在看起来非常容易理解。

organized-code

隐式返回值 (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 逻辑,我们程序可能由于数组超出索引范围而崩溃。但是我们在超出索引的问题章节讲了如何解决.

cardCountAdjuster-function

优化布局

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

LazyVGridLazyHGrid 分别是垂直的网格布局、水平网格布局。LazyVGrid 通过制定列的数量然后来填充布局。GridItem 通过设置 Size 来控制格子的宽度:

  1. fixed 通过指定固定大小来确定格子宽度
  2. flexible 弹性大小,会和其他flexible的格子分割剩余的空间,可以设置期望的最大和最小值。如果指定的最小值过大可能会超出屏幕
  3. adaptive 自适应分布, 这个尺寸会提供一个或者多个格子。需要指定一个最小值, 也可以设置最大值,然后根据最小值在把自身占用的区域平分成若干个满足最小值最大值的的格子。可以理解adaptive为一个flexible大格子,获得空间后再把这个大格子平分成若干个满足设置定值的小格子。

我们需要在cardscardCountAdjusters之间添加一个Spacer(),这样它们不会挤到一起去。

var body: some View {
    VStack {
        cards
        Spacer()
        cardCountAdjusters
    }
    .padding()
}

由于LazyVGrid会使用尽可能少的空间,因此,当两张卡片都为背面时会被挤压到一起去。

lazyvgrid-issue

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

问题解决!

solve-lazyvgrid-issue-use-opacity

.aspectRatio

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

aspectRatio

ScrollView

var body: some View {
    VStack {
        ScrollView {
            cards
        }
        Spacer()
        cardCountAdjusters
    }
    .padding()
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值