进一步 SwiftUI 入门

设置 Text View 的字体大小和加粗

每次调用方法它都返回自身的this,妥妥的函数式编程

import SwiftUI

struct ContentView: View {
    var body: some View {
        // z轴堆叠 还有 HStack
        VStack(alignment:.center,  content: {
            // 还能作用的 Image 上
            Image(systemName: "globe").font(.largeTitle).bold(true)
            Circle()
            Text("Welcom Stanford CS193p").font(.largeTitle).bold(true)
        }
        ).padding()
    }
}

#Preview {
    ContentView()
}

Text 设置前景色

Text("Welcom Stanford CS193p")
    .font(.largeTitle)
    .bold(true)
    .foregroundStyle(.green)

在 VStack 设置字大小

试一下,VStack其实也是个 View,效果结果是 VStack 里的内容都设置了 .font(.largeTitle)

VStack(alignment:.center,  content: {
    Image(systemName: "globe")
    Circle()
    Text("Welcom Stanford CS193p")
}).padding().font(.largeTitle)

但是这个 .padding() 就不是,它是只作用于 VStack 的,如 Text 想要 padding 就要自己设置。

方法调用顺序对效果有影响

两者是不同的,一个先padding再设置背景色,一个先设置背景色在padding。

后padding的,边框和content padding出的间隔 没有颜色。而先padding后设置背景色就有。

Text("Welcom Stanford CS193p").padding().background(.green)
Text("Welcom Stanford CS193p").background(.green).padding()

准备做 CodeBreaker 游戏

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack{
            peges
            peges
            peges
            peges
        }
    }
    
    var peges: some View {
        HStack{
            Circle().foregroundStyle(.green)
            Circle().foregroundStyle(.yellow)
            Circle().foregroundStyle(.blue)
            Circle().foregroundStyle(.yellow)
        }
    }
}

#Preview {
    ContentView()
}

改造成函数

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack{
            peges(color:[.green, .blue, .blue, .red])
            peges(color:[.green, .blue, .red, .red])
            peges(color:[.yellow, .blue, .yellow, .green])
            peges(color:[.yellow, .green, .yellow, .red])
        }
    }

    func peges(color:Array<Color>)-> some View {
        HStack{
            Circle().foregroundStyle(color[0])
            Circle().foregroundStyle(color[1])
            Circle().foregroundStyle(color[2])
            Circle().foregroundStyle(color[3])
        }
    }
}

#Preview {
    ContentView()
}

使用 Foreach

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack{
            peges(colors: [Color.green, Color.blue, Color.blue, Color.red])
            peges(colors: [Color.green, Color.blue, Color.red, Color.red])
            peges(colors: [Color.yellow, Color.blue, Color.yellow, Color.green])
            peges(colors: [Color.yellow, Color.green, Color.yellow])
        }
    }
    
    func peges(colors: Array<Color>)-> some View {
        HStack{
            ForEach(colors.indices, id: \.self) { index in
                Circle().foregroundStyle(colors[index])
            }
        }
    }
}

#Preview {
    ContentView()
}

stroke 与 strokeBorder 设置边框

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack{
            peges(colors: [Color.green, Color.blue, Color.blue, Color.red])
            peges(colors: [Color.green, Color.blue, Color.red, Color.red])
            peges(colors: [Color.yellow, Color.blue, Color.yellow, Color.green])
            peges(colors: [Color.yellow, Color.green, Color.yellow, Color.green])
        }
    }
    
    func peges(colors: Array<Color>)-> some View {
        HStack{
            ForEach(colors.indices, id: \.self) { index in
                RoundedRectangle(cornerRadius: 10).aspectRatio(1, contentMode: .fit)
                    .foregroundStyle(colors[index])
            }
            HStack {
                VStack {
                    Circle().fill(.green)
                    // Color.primary 在深色模式下为白色 在正常模式下为黑色
                    // stroke 绘制边框会将边框宽度 1/2 放到内部 1/2 放外部 影响整体大小
                    Circle().stroke(Color.primary, lineWidth: 3).fill(.white)
                }
                VStack {
                    // strokeBorder 绘制边框 整体宽度在内部 不影响整体大小
                    Circle().strokeBorder(Color.primary, lineWidth: 3).aspectRatio(1 ,contentMode: .fit)
                    Circle()
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

opacity 设置透明度

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack{
            peges(colors: [Color.green, Color.blue, Color.blue, Color.red])
            peges(colors: [Color.green, Color.blue, Color.red, Color.red])
            peges(colors: [Color.yellow, Color.blue, Color.yellow, Color.green])
            peges(colors: [Color.yellow, Color.green, Color.yellow, Color.green])
        }
    }
    
    func peges(colors: Array<Color>)-> some View {
        HStack{
            ForEach(colors.indices, id: \.self) { index in
                RoundedRectangle(cornerRadius: 10).aspectRatio(1, contentMode: .fit)
                    .foregroundStyle(colors[index])
            }
            HStack {
                VStack {
                    Circle().fill(.green)
                    // Color.primary 在深色模式下为白色 在正常模式下为黑色
                    // stroke 绘制边框会将边框宽度 1/2 放到内部 1/2 放外部 影响整体大小
                    Circle().stroke(Color.primary, lineWidth: 3).fill(.white)
                }
                VStack {
                    // strokeBorder 绘制边框 整体宽度在内部 不影响整体大小
                    Circle().strokeBorder(Color.primary, lineWidth: 3).aspectRatio(1 ,contentMode: .fit)
                    // 设置透明度 0 - 1 0为完全透明 1为完全不透明
                    Circle().opacity(0)
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

分离内容到自定义 View 中

剪切代码段,然后 New “xx.swift” from Clipborder

MatchMarkders.swift

import SwiftUI

struct MatchMarkers: View {
    var body: some View {
        HStack {
            VStack {
                Circle().fill(.green)
                // Color.primary 在深色模式下为白色 在正常模式下为黑色
                // stroke 绘制边框会将边框宽度 1/2 放到内部 1/2 放外部 影响整体大小
                Circle().stroke(Color.primary, lineWidth: 3).fill(.white)
            }
            VStack {
                // strokeBorder 绘制边框 整体宽度在内部 不影响整体大小
                Circle().strokeBorder(Color.primary, lineWidth: 3).aspectRatio(1 ,contentMode: .fit)
                // 设置透明度 0 - 1 0为完全透明 1为完全不透明
                Circle().opacity(0)
            }
        }
    }
}

ContentView.swift

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack{
            peges(colors: [Color.green, Color.blue, Color.blue, Color.red])
            peges(colors: [Color.green, Color.blue, Color.red, Color.red])
            peges(colors: [Color.yellow, Color.blue, Color.yellow, Color.green])
            peges(colors: [Color.yellow, Color.green, Color.yellow, Color.green])
        }
    }
    
    func peges(colors: Array<Color>)-> some View {
        HStack{
            ForEach(colors.indices, id: \.self) { index in
                RoundedRectangle(cornerRadius: 10).aspectRatio(1, contentMode: .fit)
                    .foregroundStyle(colors[index])
            }
            MatchMarkers()
        }
    }
}



#Preview {
    ContentView()
}

@ViewBuilder

在 SwiftUI 中,@ViewBuilder 是一个闭包属性包装器(closure property wrapper),它的作用是:

将多个视图构造语句(如 VStack { ... }, HStack { ... }, 条件语句 if/else,switch 等)自动“拼接”成一个单一的 View 类型。

SwiftUI 的 View 协议要求 body 属性返回的是 一个 类型(some View),但现实中我们经常需要写多个视图组合在一起,例如:

写了 @ViewBuilder 连 return 都不用写了,会自动处理返回

@ViewBuilder
func peges(colors: Array<Color>)-> some View {
    HStack{
        ForEach(colors.indices, id: \.self) { index in
            RoundedRectangle(cornerRadius: 10).aspectRatio(1, contentMode: .fit)
                .foregroundStyle(colors[index])
        }
        MatchMarkers2(matches: [Match.exact, Match.inexact, Match.nomatch, Match.exact])
    }
}

如果swift函数内只有一行,类型和函数返回值类型相同,也不用显式写 return,这只是语法糖。

最终临时效果

ContentView.swift

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack{
            peges(colors: [Color.green, Color.blue, Color.blue, Color.red])
            peges(colors: [Color.green, Color.blue, Color.red, Color.red])
            peges(colors: [Color.yellow, Color.blue, Color.yellow, Color.green])
            peges(colors: [Color.yellow, Color.green, Color.yellow, Color.green])
        }
    }
    
    func peges(colors: Array<Color>)-> some View {
        HStack{
            ForEach(colors.indices, id: \.self) { index in
                RoundedRectangle(cornerRadius: 10).aspectRatio(1, contentMode: .fit)
                    .foregroundStyle(colors[index])
            }
            MatchMarkers(matches: [Match.exact, Match.inexact, Match.nomatch, Match.exact])
        }
    }
}

#Preview {
    ContentView()
}

MatchMarkers.swift

import SwiftUI

enum Match {
    case nomatch
    case exact
    case inexact
}

struct MatchMarkers2: View {
    // var matches: [Match]
    var matches: Array<Match>
    
    var body: some View {
        HStack {
            VStack {
                matchMarker(peg: 0)
                matchMarker(peg: 1)
            }
            VStack {
                matchMarker(peg: 2)
                matchMarker(peg: 3)
            }
        }
    }
    
    func isNotNomatch(matchItem: Match)-> Bool {
        return matchItem != Match.nomatch
    }
    
    func matchMarker(peg: Int) -> some View {
        let exactCount = self.matches.count(where: { matchItem in matchItem == Match.exact})
        // 传递函数写法:
        // let foundCount = self.matches.count(where: isNotNomatch)
        // foundCount 改为内联写法:
        // let foundCount = self.matches.count(where: {(matchItem: Match) -> Bool in
        //      matchItem != Match.nomatch
        // })
        // 极限简化后写法用 $number 代表函数参数
        // let foundCount = matches.count(where: {$0 != Match.nomatch})
        // 终极简化
        let foundCount = matches.count{ $0 != Match.nomatch }

        // 普通模式下 如果 exactCount > peg 则填充为黑色
        //          如果 foundCount > peg 边框为黑色当白钉子
        //          填充为白 边框为白则表示没有钉子
        return Circle().fill(exactCount > peg ? Color.primary : Color.clear)
            .strokeBorder(foundCount > peg ? Color.primary : Color.clear, lineWidth: 2)
            .aspectRatio(1, contentMode: ContentMode.fit)
    }
}

#Preview {
    // exactCount为1 foundCount为3 一个黑钉 两个白钉
    MatchMarkers2(matches: [Match.exact, Match.inexact, Match.nomatch, Match.inexact])
}

传递函数演化

func isNotNomatch(matchItem: Match)-> Bool {
    return matchItem != Match.nomatch
}

let exactCount = self.matches.count(where: { matchItem in matchItem == Match.exact})

// 传递函数写法:
// let foundCount = self.matches.count(where: isNotNomatch)

// foundCount 改为内联写法:
// let foundCount = self.matches.count(where: {(matchItem: Match) -> Bool in
//      matchItem != Match.nomatch
// })

// 极限简化后写法用 $number 代表函数参数
// let foundCount = matches.count(where: {$0 != Match.nomatch})

// 终极简化
let foundCount = matches.count{ $0 != Match.nomatch }