SwiftUI 入门

文档

1 组件

1.1 基础组件

Text

Text("Hamlet")
    .font(.largeTitle)
    .fontWeight(.bold)
    .italic()

Text("Hello")
    .font(.system(size: 20, weight: .bold, design: .serif))

Button

三种风格

Button("bordered button") {}
    .buttonStyle(.bordered)
    .tint(.blue)

Button("elevated button") {}
    .buttonStyle(.borderedProminent)
    .tint(.blue)

Button {} label: {
    Text("custom button")
        .padding()
}
.buttonStyle(.borderless)
.foregroundColor(.white)
//.accentColor(.blue) accentColor 通常用在父组件,作为主题色使用,且这里会被 foregroundColor 覆盖
//.backgroundStyle(.blue) backgroundStyle 不起作用
.background(.blue)
.cornerRadius(10)

效果:

自定义按钮风格

struct NeumorphicButtonStyle: ButtonStyle {
    var bgColor: Color

    func makeBody(configuration: Self.Configuration) -> some View {
        configuration.label
            .padding(20)
            .background(
                ZStack {
                    RoundedRectangle(cornerRadius: 10, style: .continuous)
                        .shadow(color: .white, radius: configuration.isPressed ? 7 : 10, x: configuration.isPressed ? -5 : -15, y: configuration.isPressed ? -5 : -15)
                        .shadow(color: .black, radius: configuration.isPressed ? 7 : 10, x: configuration.isPressed ? 5 : 15, y: configuration.isPressed ? 5 : 15)
                        .blendMode(.overlay)
                    RoundedRectangle(cornerRadius: 10, style: .continuous)
                        .fill(bgColor)
                }
            )
            .scaleEffect(configuration.isPressed ? 0.95 : 1)
            .foregroundColor(.primary)
            .animation(.spring())
    }
}

// 使用
Button("Neumorphic", action: {}).buttonStyle(NeumorphicButtonStyle(bgColor: .neuBackground))

全屏宽度

VStack {
    //无法 fullwidth
    Button("Bordered Prominent") {}
            .buttonStyle(.borderedProminent)
            .frame(maxWidth: .infinity)
            .border(.pink)
    //正确
    Button {
        
    } label: {
        Text("Bordered Prominent")
            .frame(maxWidth: .infinity)
    }
    .buttonStyle(.borderedProminent)
}

效果:

Image

字体图标

HStack {
    Image(systemName: "folder.badge.plus")
    Image(systemName: "heart.circle.fill")
    Image(systemName: "alarm")
}
//monochrome 默认值, palette, multicolor, hierarchical
.symbolRenderingMode(.monochrome)
.font(.largeTitle)

图片全屏

Image("sea")
    .resizable()
    // 等价于 .aspectRatio(contentMode: .fill)
    .scaledToFill()
    // 忽略安全区
    .ignoresSafeArea(.container, edges: .all)

图片裁剪

// 保持比例不变,进行缩放并裁剪 frame + scaledToFill + clipped
VStack {
    Image("sea")
        .resizable()
        // 作用:以最大宽或高为基础,择其较大者,按原比例调整大小
        // scaledToFill 等价于 .aspectRatio(contentMode: .fill)
        // 举例:这里就是以最大高200为基础进行缩放
        .scaledToFill()
        .frame(maxWidth: 200, maxHeight: 200)
        .border(.yellow)

    Image("sea")
        .resizable()
        .scaledToFill()
        .frame(width: 200, height: 200)
        .border(.yellow)
        // scaledToFill 放大后,可将超出的部分进行裁剪
        .clipped()
}

效果:

Label

Label("你们好", systemImage: "heart")
    .labelStyle(.titleAndIcon)
    .font(.largeTitle)

自定义 labelStyle

struct AdaptiveLabelStyle: LabelStyle {
    @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?

    func makeBody(configuration: Configuration) -> some View {
        if verticalSizeClass == .compact {
            HStack {
                configuration.icon
                configuration.title
            }
        } else {
            VStack {
                configuration.icon
                configuration.title
            }
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {
            Label("Favorites", systemImage: "heart")
                .labelStyle(AdaptiveLabelStyle())
        }
    }
}

使用 @ViewBuilder

// @ViewBuilder 方式
Label {
    Text("Favorites")
        .bold()
        .foregroundColor(.pink)
} icon: {
    Image(systemName: "heart")
        .symbolVariant(.fill)
        .foregroundColor(.pink)
}

Shapes

HStack {
    Rectangle()
        .foregroundColor(.blue)
    Circle()
        .foregroundColor(.orange)
    RoundedRectangle(cornerRadius: 15, style: .continuous)
        .foregroundColor(.green)
    //胶囊
    Capsule()
}
.aspectRatio(4.0, contentMode: .fit)

1.2 布局组件

线性布局

HStack {
    TrainCar(.rear)
    Spacer()
    TrainCar(.middle)
    Spacer()
    TrainCar(.front)
}

布局平滑切换

struct ContentView: View {
    @State private var isHorizontal = true

    var body: some View {
        let layout: AnyLayout = isHorizontal ? AnyLayout(HStackLayout()) : AnyLayout(VStackLayout())

        VStack(spacing: 20) {
            // ① 移除旧的组件,创建新的组件
            // 动画:fading transition
            if isHorizontal {
                HStack {
                    Text("1")
                        .frame(width: 50)
                        .border(.yellow)
                    Text("2")
                        .frame(width: 100)
                        .border(.blue)
                    Text("3")
                        .frame(width: 200)
                        .border(.green)
                }
                .border(.pink)
                .font(.title)
            } else {
                VStack {
                    Text("1")
                        .frame(width: 50)
                        .border(.yellow)
                    Text("2")
                        .frame(width: 100)
                        .border(.blue)
                    Text("3")
                        .frame(width: 200)
                        .border(.green)
                }
                .border(.pink)
                .font(.title)
            }

            // ② 布局平滑切换,组件的 identity 保持不变(还是同一个组件)
            // 动画:smooth transition
            layout {
                Text("1")
                    .frame(width: 50)
                    .border(.yellow)
                Text("2")
                    .frame(width: 100)
                    .border(.blue)
                Text("3")
                    .frame(width: 200)
                    .border(.green)
            }
            .border(.pink)
            .font(.title)

            Spacer().frame(height: 50)

            Button("Toggle Layout") {
                withAnimation {
                    isHorizontal.toggle()
                }
            }
        }
    }
}

ZStack 布局

ZStack(alignment: .topTrailing) {
    Image(systemName: symbel.name)
        .resizable()
        .scaledToFit()
        .symbolRenderingMode(.hierarchical)
        .foregroundColor(.accentColor)
        .ignoresSafeArea(.container, edges: .bottom)
        .cornerRadius(8)
    if isEditing {
        Button {} label: {
            Image(systemName: "xmark.square.fill")
                .font(.title)
                .symbolRenderingMode(.palette)
                .foregroundStyle(.white, .red)
        }
        .offset(x: 7, y: -7)
    }
}

ZStack 的优先级容器

VStack {
    // 在 ZStack 中,使用 layoutPriority 提高某个组件的优先级
    ZStack {
        Image("girl")
            .resizable()
            .scaledToFit()
            .frame(width: 200)
            .border(Color.blue)

        Text("Hello, world!")
            .foregroundColor(.white)
            .font(.system(size: 28, weight: .black))
            .padding(100)
            .border(Color.blue)
            // 提高优先级,将 ZStack容器大小限制为该组件大小
            .layoutPriority(1)
    }
    .background(.pink)

    // 使用 .backgroud 自带优先级
    Text("Hello, world!")
        .foregroundColor(.white)
        .font(.system(size: 28, weight: .black))
        .padding(100)
        .border(Color.blue)
        .background {
            Image("girl")
                .resizable()
                .scaledToFit()
                .frame(width: 200)
                .border(Color.blue)
        }
        .background(.pink)
}

效果:

Grid 布局

1、LazyVGrid

struct ContentView: View {
    // horizontal spacing = 0
    let columns = Array(repeating: GridItem(spacing: 0), count: 3)

    var body: some View {
        ScrollView {
            // vertical spacing = 0
            LazyVGrid(columns: columns, spacing: 0) {
                ForEach(0 ..< 100) { i in
                    Text("Item \(i)")
                        .frame(maxWidth: .infinity, minHeight: 50)
                        .background(.yellow)
                        .padding(0)
                }.padding(0)
            }
        }
    }
}

GridItem 的各种用法

let columns = [
    GridItem(.fixed(50)),
    GridItem(.fixed(100)),
    GridItem(.fixed(200))
]

let columns = [
    GridItem(.flexible(minimum: 10, maximum: 100)),
    GridItem(.flexible(minimum: 200, maximum: 200)),
    GridItem(.flexible(minimum: 10, maximum: 300))
]

let columns = [
    // adaptive 相比于 flexible,针对的是剩余的每个子项,而不是单个项目
    GridItem(.adaptive(minimum: 100, maximum: 150)),
    GridItem(.fixed(300))
]

let columns = [
    GridItem(.fixed(50), spacing: 0),
    GridItem(.fixed(50), spacing: 20)
]

let columns = [
    GridItem(.fixed(40)),
    GridItem(.fixed(100), alignment: .top),
    GridItem(.fixed(100), alignment: .center)
]

2、Grid

类似于 Table 组件

Grid(horizontalSpacing: 100, verticalSpacing: 12) {
    GridRow {
        Text("蛋白质")
        Text("脂肪")
        Text("碳水")
    }
    
    Divider()
        .gridCellUnsizedAxes(.horizontal)
    
    GridRow {
        Text("10 g")
        Text("3 g")
        Text("25 g")
    }
}

效果:

合并单元格

struct ColorSquare: View {
    let color: Color
    
    var body: some View {
        color
        .frame(width: 50, height: 50)
    }
}

struct HalfColorSquare: View {
    let color: Color
    
    var body: some View {
        color
        .frame(width: 25, height: 50)
    }
}

struct ContentView: View {

    var body: some View {
        Grid(alignment: .center) {
            GridRow {
                ForEach(0..<5) { _ in
                    ColorSquare(color: .orange)
                }
            }
            GridRow {
                ForEach(0..<3) { _ in
                    ColorSquare(color: .pink)
                }
                // 默认居中,现在改成居左
                HalfColorSquare(color: .blue)
                    .gridColumnAlignment(.leading)
            }
            GridRow {
                // 合并4个项目,且居中
                ColorSquare(color: .green)
                    .gridCellColumns(4)
                ColorSquare(color: .yellow)
            }
        }
    }
}

效果:

1.3 表单组件

Toggle

@State private var longerTrain = true

Toggle("Long Train", isOn: $longerTrain)

Step

Stepper(columnsText, value: $numColumns, in: 1...6, step: 1) { _ in
    withAnimation { gridColumns = Array(repeating: GridItem(.flexible()), count: numColumns) }
}

Picker

struct ContentView: View {
    @State private var selectedColor = FavoriteColor.red

    var body: some View {
        VStack {
            Picker("Favorite Color", selection: $selectedColor, content: {
                ForEach(FavoriteColor.allCases, content: { color in
                    // 由于使用 ForEach,可以省略 .tag()
//                    Text(color.rawValue.capitalized).tag(color.id)
                    Text(color.rawValue.capitalized)
                })
            })
//风格:inline, wheel, menu, segmented
            .pickerStyle(.segmented)
            Text("Selected color: \(selectedColor.rawValue.capitalized)")
        }
    }
}

1.4 列表类组件

List

List + Section 组件的使用

List {
    Section("博霉") {
        Text("小白三随")
        Text("Happy纠随")
    }
    Section("哈十其") {
        Text("老品八随")
        Text("娃娃两随")
    }
}
.listStyle(.insetGrouped)

ListStyle 的4种风格:

除了风格还可以设置 item 的背景色与线条

let pomeranian: [String] = ["老品八随", "娃娃两随"]
let husky: [String] = ["老品八随", "娃娃两随"]
var body: some View {
    List {
        Section("博霉") {
            ForEach(pomeranian, id: \.self) { dog in
                Text(dog)
            }
            // 分割线
            .listRowSeparatorTint(.red)
            .listSectionSeparator(.visible, edges: .bottom)
            .listSectionSeparatorTint(.purple, edges: .all)
        }
        Section("哈十其") {
            ForEach(husky.indices, id: \.self) { index in
                Text(husky[index])
                    // 背景色
                    .listRowSeparator(.hidden)
                    .listRowBackground(Color.teal.opacity(index % 2 == 0 ? 0.2 : 0.1))
            }
        }
    }
    .listStyle(.insetGrouped)
}

效果:

ScrollView

// [.horizontal, .vertical] 同时支持水平、垂直滚动
// showsIndicators: false 不显示滚动条
ScrollView([.horizontal, .vertical], showsIndicators: false) {
    VStack(spacing: 20) {
        ForEach(0..<20) { row in
            HStack {
                ForEach(0..<20) { column in
                    Text("\(row),\(column)")
                        .foregroundColor(.white)
                        .frame(width: 100, height: 100)
                        .background(Color.pink)
                        .cornerRadius(50)
                }
            }
        }
    }.frame(maxWidth: .infinity)
}.font(.largeTitle)

1.5 弹窗组件

Alert

struct ContentView: View {
    @State private var presentAlert = false

    var body: some View {
        HStack {
            Button {
                presentAlert = true
            } label: {
                Text("Press this!")
            }
            .buttonStyle(.bordered)
        }
        .alert(isPresented: $presentAlert) {
            Alert(title: Text("good"),
                  primaryButton: .destructive(Text("Delete"), action: { print("deleted")
                  }), secondaryButton: .cancel())
        }
    }
}

注:iOS 15 支持简化写法:

struct ContentView: View {
    @State private var presentAlert = false

    var body: some View {
        HStack {
            Button {
                presentAlert = true
            } label: {
                Text("Press this!")
            }
            .buttonStyle(.bordered)
        }
        .alert("Title", isPresented: $presentAlert, actions: {
            Button("Destructive", role: .destructive, action: {})
        }, message: {
            Text("iOS 15")
        })
    }
}

第二种用法

struct AlertInfo: Identifiable {
    let id = UUID()
    let title: String
}


struct ContentView: View {
    @State private var alertInfo: AlertInfo?

    var body: some View {
        HStack {
            Button {
                alertInfo = AlertInfo(title: "hello")
            } label: {
                Text("Press this!")
            }
            .buttonStyle(.bordered)
        }
        .alert(item: $alertInfo) { info in
            Alert(title: Text(info.title))
        }
    }
}

第三种用法


struct AlertInfo: Identifiable {
    let id = UUID()
    let title: String
    let message: String
}

class AlertController: ObservableObject {
    @Published var info: AlertInfo?
}

@main
struct SwiftUIDemoApp: App {
    @StateObject private var alertController = AlertController()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(alertController)
                .alert(item: $alertController.info, content: { info in
                    Alert(title: Text(info.title),
                          message: Text(info.message))
                })
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var alertController: AlertController

    var body: some View {
        HStack {
            Button {
                alertController.info = AlertInfo(title: "hello", message: "good job")
            } label: {
                Text("Press this!")
            }
            .buttonStyle(.bordered)
        }
    }
}

2 调整器

2.1 间距 padding

// 只在水平有间距
Color.red
    .padding([.horizontal], 10)

// 可简写为
Color.red
    .padding(.horizontal, 10)

// 仅在上方和底部有间距
Color.red.padding([.top, .bottom], 50)

2.2 大小 frame

Image(systemName: hobby)
    .resizable()
    .frame(maxWidth: 80, maxHeight: 60)

固有尺寸

struct CustomView: View {
    var body: some View {
        Rectangle()
            .frame(idealWidth: 200, idealHeight: 200)
            .foregroundColor(Color.pink)
    }
}

struct ContentView: View {
    var body: some View {
        // 默认是铺满全屏
        CustomView()
//            .fixedSize()
            .fixedSize(horizontal: true, vertical: false)
    }
}

固有尺寸(如果 .fixedSize修饰的组件有多个子组件,则固有尺寸最高的子组件高度即为容器的高度)

HStack {
    VStack {
        Text("Hello,")
        Text("world")
        Text("!")
    }
    .frame(maxHeight: .infinity)
    .background(.pink)
    Text("Hi, hello!")
        .frame(maxHeight: .infinity)
        .background(.yellow)
}
.fixedSize(horizontal: false, vertical: true)
.background(.orange)
.font(.system(size: 36).bold())

效果:

2.3 ShapeStyle

// ShapStyle 可以是颜色
Circle().fill(.teal.gradient)

// ShapStyle 可以是图片
Circle().fill(.image(Image("dinner"), scale: 0.2))

// ShapStyle 可以是渐变层
Circle()
    .fill(
        .linearGradient(colors: [.pink, .indigo],
                        startPoint: .topLeading,
                        endPoint: .bottomTrailing)
    )

//在非 shape 组件中可以用 foregroundStyle 调整器
Text("Hello")
    .foregroundStyle(
        .linearGradient(colors: [.pink, .indigo],
                        startPoint: .topLeading,
                        endPoint: .bottomTrailing)
    )

2.4 overlay

Circle().fill(.teal)
    // 上层
    .overlay {
        Text("Hello")
            .font(.system(size: 100))
            .foregroundStyle(
                .linearGradient(colors: [.pink, .indigo],
                                startPoint: .topLeading,
                                endPoint: .bottomTrailing)
            )
    }
    // 底层
    .background {
        Color.brown
            .scaleEffect(x: 1.5, y: 1.3)
    }

2.5 背景

Text(text)
  .padding()
  // 背景色与形状
  .background(Color("TextContrast").opacity(0.75),
              in: RoundedRectangle(cornerRadius: 10.0, style: .continuous))
  .padding()
// 背景可以是一个 stack
.background {
    ZStack(alignment: .top) {
        Rectangle()
            .frame(maxHeight: stripeHeight)
    }
    .foregroundColor(.teal)
}

设置全屏背景

① 使用 background 修饰符

VStack {
    Text("Hello, SwiftUI!")
        .font(.largeTitle)
        .bold()
    Button("Getting Started") {}
}
.frame(maxWidth: .infinity, maxHeight: .infinity) // 1
.accentColor(Color.black)
.background(Color.pink)

② 使用 ZStack

ZStack {
    Color.pink.ignoresSafeArea() // 1
    VStack {
        Text("Hello, SwiftUI!")
            .font(.largeTitle)
            .bold()
        Button("Getting Started") {}
    }
}
.accentColor(Color.black)
struct ContentView: View {

    // 渐变色
    let backgroundGradient = LinearGradient(
        gradient: Gradient(colors: [Color.pink, Color.yellow]),
        startPoint: .top, endPoint: .bottom)

    var body: some View {
        ZStack {
            backgroundGradient
                .ignoresSafeArea()
            VStack {
                Text("Hello, SwiftUI!")
                    .font(.largeTitle)
                    .bold()
                Button("Getting Started") {}
            }
        }
        .accentColor(Color.black)
    }
}

注:尽量不要直接在 ZStack 上使用 ignoresSafeArea,这样屏幕文字都会被安全区覆盖。

ZStack {
    Color.pink
    Text("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.")
        .font(.largeTitle)
}
.ignoresSafeArea()

效果:

③ 使用 overlay

Color.pink
    .ignoresSafeArea()
    .overlay(
        VStack {
            Text("Hello, SwiftUI!")
                .font(.largeTitle)
                .bold()
            Button("Getting Started") {}
        }
    )
    .accentColor(Color.black)

2.6 裁剪

1、背景图片剪切 clipped

VStack {
    Text("good picture")
        .font(.system(size: 60))
        .foregroundColor(.white)
        .padding()
        .border(Color.green, width: 2)
        .background(
            // 图片被拉伸至文字大小
            Image("sea").resizable()
        )

    // background + aspectFill + clipped
    Text("good picture")
        .font(.system(size: 60))
        .foregroundColor(.white)
        .padding()
        .border(Color.green, width: 2)
        .background(
            // .fit 图片尽可能小,且不改变原图比例
            // .fill 图片尽可能大,但不改变原图比例
            Image("sea").resizable().aspectRatio(contentMode: .fill)
        )
        // 搭配 .clipped,将背景图超出部分进行裁剪
        .clipped()
}

效果:

2、头像裁剪

// frame + scaledToFill + clipShape

VStack {
    Image("airplane")
        .resizable()
        .scaledToFill()
        .frame(width: 200, height: 200)
        .border(.orange)
    
    Image("airplane")
        .resizable()
        .scaledToFill()
        .frame(width: 200, height: 200)
        .border(.orange)
        .clipShape(Circle())
    
    Image("airplane")
        .resizable()
        .scaledToFill()
        .frame(width: 200, height: 200)
        .border(.orange)
        .clipShape(RoundedRectangle(cornerRadius: 30))
}

效果:

3、自定义形状

// 左上是原点

struct NFTShape: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            // 1
            // let width: CGFloat = 200
            // let height: CGFloat = 200
            let width: CGFloat = rect.width
            let height: CGFloat = rect.height

            path.move(
                to: CGPoint(
                    x: 0.2 * width,
                    y: 0 * height
                )
            )

            path.addLine(
                to: CGPoint(
                    x: 0.8 * width,
                    y: 0 * height
                )
            )

            path.addLine(
                to: CGPoint(
                    x: 1 * width,
                    y: 0.5 * height
                )
            )

            path.addLine(
                to: CGPoint(
                    x: 0.8 * width,
                    y: 1 * height
                )
            )

            path.addLine(
                to: CGPoint(
                    x: 0.2 * width,
                    y: 1 * height
                )
            )

            path.addLine(
                to: CGPoint(
                    x: 0 * width,
                    y: 0.5 * height
                )
            )

            path.closeSubpath()
        }
    }
}

// 构建 AvatarView 组件
struct AvatarView: View {
    let name: String
    var body: some View {
        Image(name)
            .resizable()
            .scaledToFill()
            .frame(width: 200, height: 200)
//            .clipShape(Circle())
            .clipShape(NFTShape())
    }
}

AvatarView(name: "avatar")

2.7 标题栏与工具栏

NavigationView {
    RecipeListView(selection: $selection, selectedSidebarItem: selectedSidebarItem)
    //导航标题
        .navigationTitle(selectedSidebarItem.title)
.navigationTitle("Navigation Title")
//导航工具栏主题,iOS 16 可用
//.toolbarColorScheme(.dark, for: .navigationBar)
//设置工具栏背景透明
//.toolbarBackground(.hidden, for: .navigationBar)
//设置工具栏背景色
.toolbarBackground(.pink, for: .navigationBar)
//主题色
.preferredColorScheme(.light)
//标题行内联
.navigationBarTitleDisplayMode(.inline)
//隐藏导航栏
//.navigationBarHidden(true)
//隐藏返回按钮
//.navigationBarBackButtonHidden(true)
    //工具栏
        .toolbar {
            ToolbarItem(placement: .primaryAction) {
                Button(isEditing ? "Done" : "Edit") {
                    withAnimation { isEditing.toggle() }
                }
            }
            ToolbarItem(placement: .navigationBarTrailing) {
                Button {
                    isAddingSymbol = true
                } label: {
                    Image(systemName: "plus")
                }
                .disabled(isEditing)
            }
        }
}

2.71 菜单路由跳转

NavigationStack {
    VStack {
        Text("立即启动")
            .foregroundColor(.accentColor)
            .font(.largeTitle)
    }
    .toolbar {
        ToolbarItemGroup(placement: .navigationBarTrailing) {
            Menu {
                NavigationLink {
                    NodesView()
                } label: {
                    Label("节点", systemImage: "server.rack")
                }
                NavigationLink {
                    SettingsView()
                } label: {
                    Label("设置", systemImage: "gearshape.fill")
                }
                Divider()
                Button {
                    print("check update")
                } label: {
                    Label("检查更新", systemImage: "arrow.up.circle")
                }

            } label: {
                Label("主菜单", systemImage: "ellipsis")
                    .labelStyle(.iconOnly)
                    .padding(.horizontal, 12)
                    .padding(.vertical, 20)
                    .background(
                        RoundedRectangle(cornerRadius: 12)
                            .fill(Color(.quaternarySystemFill))
                    )
            }
        }
    }
}

2.8 底部 TabView

展示 badge

struct ContentView: View {
    
    @State var unreadNotifications: Int = 111

    var badgeValue: String? {
        if unreadNotifications > 99 {
            return "99+"
        } else if unreadNotifications == 0 {
            return nil

        } else {
            return unreadNotifications.description
        }
    }
    
    var body: some View {
        TabView {
            Group {
                Text("Home")
                    .tabItem {
                        Label("Home", systemImage: "house")
                    }
                Text("Search")
                    .tabItem {
                        Label("Search", systemImage: "magnifyingglass")
                    }
                Text("Notification")
                    .tabItem {
                        Label("Notification", systemImage: "bell")
                    }
                    .badge(badgeValue)
                Text("Settings")
                    .tabItem {
                        Label("Settings", systemImage: "gearshape")
                    }
            }
        }
    }
}

2.9 事件

点击 onTapGesture

Image(systemName: "star")
    .symbolVariant(value <= rating ? .fill : .none)
    .foregroundColor(.accentColor)
    .onTapGesture {
        if value != rating {
            rating = value
        } else {
            rating = 0
        }
    }

滑动 swipeActions

List {
    Text("Item 1")
        .swipeActions {
            Button("Default", action: {})
            Button("Delete", role: .destructive, action: {})
        }
}

2.10 弹出层

确认框 .confirmationDialog

VStack {
    Button("Bottom Sheet") {
        isPresented = true
    }
    .buttonStyle(.borderedProminent)
}
.confirmationDialog("Confirmation Dialog", isPresented: $isPresented, actions: {
    Button("Default", action: {})
    Button("Cancel", role: .cancel, action: {})
    Button("Delete", role: .destructive, action: {})
})

效果:

底部弹出层 .sheet

struct ContentView: View {
    @State var presentSheet = false

    var body: some View {
        NavigationView {
            Button("Modal") {
                presentSheet = true
            }
            .navigationTitle("Main")
        }.sheet(isPresented: $presentSheet) {
            Text("Detail")
//                三种方式设置止动距离
//                .presentationDetents([.height(50)])
//                .presentationDetents([.fraction(0.1)])
                .presentationDetents([.medium, .large])
//             隐藏拖拉指示器
//                .presentationDragIndicator(.hidden)
        }
    }
}

3 动画

3.1 飞行动画

Group {
    if isExpanded {
        VStack {
            RoundedRectangle(cornerRadius: 10)
                .foregroundColor(Color.pink)
                .frame(width: 60, height: 60)
                .matchedGeometryEffect(id: "rect", in: namespace)
            Text("Hello SwiftUI!").fontWeight(.semibold)
                .matchedGeometryEffect(id: "text", in: namespace)
        }
    } else {
        HStack {
            Text("Hello SwiftUI!").fontWeight(.semibold)
                .matchedGeometryEffect(id: "text", in: namespace)
            RoundedRectangle(cornerRadius: 10)
                .foregroundColor(Color.pink)
                .frame(width: 60, height: 60)
                .matchedGeometryEffect(id: "rect", in: namespace)
        }
    }
}.onTapGesture {
    withAnimation {
        isExpanded.toggle()
    }
}

效果:

5 机制

5.1 遍历

ForEach(information.foods, id: \.self) {food in
    Text(food)
}

通过索引遍历

struct ContentView: View {
    let positions = ["First", "Second", "Third"]

    var body: some View {
        ForEach(0..<positions.count) { index in
            Text(positions[index])
        }
    }
}

5.2 条件渲染

//不渲染
HStack {
    Image(systemName: "train.side.rear.car")
    if longerTrain {
        Image(systemName: "train.side.middle.car")
    }
    Image(systemName: "train.side.front.car")
}
//隐藏
Image(systemName: "train.side.middle.car")
    .opacity(longerTrain ? 1 : 0)

5.3 动态尺寸

struct ContentView: View {
    // 辅助模式字体放大的情况, margin, padding, or anything around text 默认不会放大,
    // @ScaledMetric,则会将这些原本不变的尺寸随着字体同比例进行放大
    @ScaledMetric(relativeTo: .body) var scaledPadding: CGFloat = 20

    var body: some View {
        VStack {
            Color.red
                .padding(scaledPadding)

            Color.red
                .padding(20)
        }
    }
}

5.4 调整器的复用

struct FontStyle: ViewModifier {
    var size: CGFloat
    func body(content: Content) -> some View {
        content
            .font(.system(size: size, weight: .medium, design: .rounded))
            .foregroundColor(.brown)
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello")
                .modifier(FontStyle(size: 50))
        }
    }
}

5.5 @Environment 注解

参考 https://developer.apple.com/documentation/swiftui/environmentvalues

5.6 格式化

日期

Date().formatted()
// 6/28/2021, 1:38 PM

Date().formatted(date: .long, time: .omitted)
// June 28, 2021

Date().formatted(.dateTime.year())
// Jun 2021

数字、货币

0.2.formatted()
// 0.2

0.2.formatted(.number.precision(.significantDigits(2)))
// 0.20

1.5.formatted(.currency(code: "thb"))
// THB 1.50

List

["Alice", "Bob", "Trudy"].formatted()
// Alice, Bob, and Trudy

["Alice", "Bob", "Trudy"].formatted(.list(type: .or))
// Alice, Bob, or Trudy

5.7 布局组件 Layout

// 自定义布局容器组件
struct BackslashStack: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        // 来自父组件的约束
        print("proposal.width \(proposal.width)")

        let subviewSizes: [CGSize] = subviews.map { proxy in
            proxy.sizeThatFits(.unspecified)
        }

        let combinedSize: CGSize = subviewSizes.reduce(.zero) { currentSize, subviewSize in
            // 长为子组件的长之和,宽为子组件的宽之和
            CGSize(
                width: currentSize.width + subviewSize.width,
                height: currentSize.height + subviewSize.height)
        }
        // 当前组件的固有size
        print("combinedSize.width \(combinedSize.width)")
        return combinedSize
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let subviewSizes = subviews.map { proxy in
            proxy.sizeThatFits(.unspecified)
        }

        // 当前组件的固有区域尺寸
        print("bounds.width \(bounds.width)")

        // 当前组件固有区域的左上坐标
        var x = bounds.minX
        var y = bounds.minY

        for index in subviews.indices {
            let subviewSize = subviewSizes[index]
            let sizeProposal = ProposedViewSize(
                width: subviewSize.width,
                height: subviewSize.height)

            // 设定子组件偏移量与约束
            subviews[index]
                .place(
                    // 子组件的偏移量
                    at: CGPoint(x: x, y: y),
                    // 原点坐标:左上
                    anchor: .topLeading,
                    // 约束
                    proposal: sizeProposal)

            x += subviewSize.width
            y += subviewSize.height
        }
    }
}

struct ContentView: View {
    var body: some View {
        BackslashStack {
            Text("1")
                .frame(width: 50)
                .border(.yellow)
            Text("2")
                .frame(width: 100)
                .border(.blue)
            Text("3")
                .frame(width: 200)
                .border(.green)
        }
//        .frame(width: 80, height: 80)
        .border(.pink)
        .font(.title)
    }
}

效果:

posted on 2022-12-03 15:54  Lemo_wd  阅读(417)  评论(0编辑  收藏  举报

导航