普通视图

发现新文章,点击刷新页面。
昨天以前首页

SwiftUI快速入门指南-Modifier篇

作者 xiAo_Ju
2025年11月25日 20:33

背景

本文帮助有Swift基础的同学,快速入门SwiftUI,基于cursour整理

主要分为四个部分:

1. 什么是 Modifier?

Modifier 是用于修改视图外观和行为的方法。每个 modifier 都会返回一个新的视图。

Text("Hello")
    .font(.title)           // 修改字体
    .foregroundColor(.blue) // 修改颜色
    .padding()              // 添加内边距
    .background(.yellow)    // 添加背景

核心概念:

  • ✅ Modifier 不修改原视图,而是创建新视图
  • ✅ 支持链式调用
  • ✅ 顺序很重要!

2. Modifier 分类

A. 文本 Modifier
Text("SwiftUI Modifier")
    // 字体
    .font(.title)
    .font(.system(size: 24, weight: .bold, design: .rounded))
    .fontWeight(.semibold)
    
    // 颜色
    .foregroundColor(.blue)
    .foregroundStyle(.red)
    
    // 样式
    .italic()
    .bold()
    .underline()
    .strikethrough()
    .kerning(2)              // 字间距
    .tracking(3)             // 字符间距
    .baselineOffset(5)       // 基线偏移
    
    // 多行
    .lineLimit(3)
    .lineSpacing(8)
    .multilineTextAlignment(.center)
    .truncationMode(.tail)
B. 布局 Modifier
VStack {
    Text("布局示例")
}
// 内边距
.padding()
.padding(.horizontal, 20)
.padding(.top, 10)
.padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20))

// 尺寸
.frame(width: 200, height: 100)
.frame(minWidth: 100, maxWidth: .infinity)
.frame(maxHeight: 300)

// 对齐
.frame(width: 300, height: 200, alignment: .topLeading)

// 偏移
.offset(x: 10, y: 20)

// 位置
.position(x: 100, y: 100)
C. 背景和边框 Modifier
Text("样式示例")
    // 背景
    .background(.blue)
    .background(Color.blue.opacity(0.3))
    .background(
        LinearGradient(
            colors: [.blue, .purple],
            startPoint: .leading,
            endPoint: .trailing
        )
    )
    
    // 边框
    .border(.red, width: 2)
    
    // 圆角边框
    .cornerRadius(10)
    .clipShape(RoundedRectangle(cornerRadius: 15))
    .clipShape(Circle())
    
    // 描边
    .overlay(
        RoundedRectangle(cornerRadius: 10)
            .stroke(.red, lineWidth: 2)
    )
D. 阴影和效果 Modifier
Text("效果示例")
    // 阴影
    .shadow(radius: 5)
    .shadow(color: .gray, radius: 10, x: 5, y: 5)
    
    // 模糊
    .blur(radius: 3)
    
    // 透明度
    .opacity(0.8)
    
    // 旋转
    .rotationEffect(.degrees(45))
    .rotation3DEffect(.degrees(45), axis: (x: 1, y: 0, z: 0))
    
    // 缩放
    .scaleEffect(1.5)
    .scaleEffect(x: 1.2, y: 0.8)
E. 交互 Modifier
Text("点击我")
    // 点击
    .onTapGesture {
        print("被点击了")
    }
    .onTapGesture(count: 2) {
        print("双击")
    }
    
    // 长按
    .onLongPressGesture {
        print("长按")
    }
    
    // 拖拽
    .gesture(
        DragGesture()
            .onChanged { value in
                print("拖拽中")
            }
    )
    
    // 禁用
    .disabled(true)
F. 生命周期 Modifier
Text("生命周期")
    // 出现
    .onAppear {
        print("视图出现")
    }
    
    // 消失
    .onDisappear {
        print("视图消失")
    }
    
    // 值变化
    .onChange(of: someValue) { oldValue, newValue in
        print("值改变了")
    }
    
    // 任务
    .task {
        await loadData()
    }

3. Modifier 顺序的重要性 ⚠️

这是最容易出错的地方!Modifier 的顺序会产生完全不同的结果。

// 示例 1: 先 padding 后 background
Text("Hello")
    .padding(20)        // 先添加内边距
    .background(.blue)  // 背景覆盖整个区域(包括 padding)

// 结果:蓝色背景包含文字和内边距

// 示例 2: 先 background 后 padding
Text("Hello")
    .background(.blue)  // 背景只覆盖文字
    .padding(20)        // 在背景外添加内边距

// 结果:蓝色背景只包含文字,外面有空白

// 边框和圆角
Text("示例")
    .padding()
    .background(.blue)
    .cornerRadius(10)    // ✅ 正确:圆角应用到背景
    .border(.red, width: 2)  // 边框在圆角外

Text("示例")
    .padding()
    .cornerRadius(10)    // ❌ 错误:圆角应用到文字(没效果)
    .background(.blue)
    .border(.red, width: 2)
    
// Frame 和 Background
Text("示例")
    .frame(width: 200, height: 100)
    .background(.blue)   // ✅ 蓝色填满整个 frame

Text("示例")
    .background(.blue)   
    .frame(width: 200, height: 100)  // ❌ 蓝色只在文字周围

4. 自定义 Modifier

方法一:使用 ViewModifier 协议
// 定义自定义 modifier
struct CardModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(.white)
            .cornerRadius(10)
            .shadow(color: .gray.opacity(0.4), radius: 5, x: 0, y: 2)
    }
}

// 扩展 View 以便于使用
extension View {
    func cardStyle() -> some View {
        self.modifier(CardModifier())
    }
}

// 使用
Text("卡片样式")
    .cardStyle()
方法二:直接扩展 View
extension View {
    func primaryButton() -> some View {
        self
            .font(.headline)
            .foregroundColor(.white)
            .padding()
            .frame(maxWidth: .infinity)
            .background(.blue)
            .cornerRadius(10)
    }
}

// 使用
Text("登录")
    .primaryButton()
带参数的自定义 Modifier
struct BorderModifier: ViewModifier {
    var color: Color
    var width: CGFloat
    var cornerRadius: CGFloat
    
    func body(content: Content) -> some View {
        content
            .padding()
            .overlay(
                RoundedRectangle(cornerRadius: cornerRadius)
                    .stroke(color, lineWidth: width)
            )
    }
}

extension View {
    func customBorder(
        color: Color = .blue,
        width: CGFloat = 2,
        cornerRadius: CGFloat = 8
    ) -> some View {
        self.modifier(BorderModifier(
            color: color,
            width: width,
            cornerRadius: cornerRadius
        ))
    }
}

// 使用
Text("自定义边框")
    .customBorder(color: .red, width: 3, cornerRadius: 15)

5. 条件 Modifier

// 方法一:使用 @ViewBuilder
extension View {
    @ViewBuilder
    func `if`<Transform: View>(
        _ condition: Bool,
        transform: (Self) -> Transform
    ) -> some View {
        if condition {
            transform(self)
        } else {
            self
        }
    }
}

// 使用
Text("条件样式")
    .if(isHighlighted) { view in
        view
            .font(.largeTitle)
            .foregroundColor(.red)
    }
    
// 方法二:三元运算符
Text("示例")
    .foregroundColor(isActive ? .blue : .gray)
    .font(isLarge ? .title : .body)
    
// 方法三:使用 modifier
struct ConditionalModifier: ViewModifier {
    var condition: Bool
    
    func body(content: Content) -> some View {
        if condition {
            content
                .background(.yellow)
                .cornerRadius(10)
        } else {
            content
                .background(.gray)
        }
    }
}

// 使用
Text("条件")
    .modifier(ConditionalModifier(condition: isSpecial))

6. 组合 Modifier 实战示例

struct ProfileCard: View {
    @State private var isLiked = false
    
    var body: some View {
        VStack(spacing: 12) {
            // 头像
            Image(systemName: "person.circle.fill")
                .resizable()
                .scaledToFit()
                .frame(width: 80, height: 80)
                .foregroundColor(.blue)
                .clipShape(Circle())
                .overlay(
                    Circle()
                        .stroke(.gray, lineWidth: 2)
                )
                .shadow(radius: 5)
            
            // 名字
            Text("张三")
                .font(.title2)
                .fontWeight(.bold)
            
            // 描述
            Text("iOS 开发工程师")
                .font(.subheadline)
                .foregroundColor(.secondary)
            
            // 按钮
            Button(action: { isLiked.toggle() }) {
                HStack {
                    Image(systemName: isLiked ? "heart.fill" : "heart")
                    Text(isLiked ? "已关注" : "关注")
                }
                .font(.headline)
                .foregroundColor(isLiked ? .red : .white)
                .padding(.horizontal, 20)
                .padding(.vertical, 10)
                .background(isLiked ? .white : .blue)
                .cornerRadius(20)
                .overlay(
                    RoundedRectangle(cornerRadius: 20)
                        .stroke(isLiked ? .red : .blue, lineWidth: 2)
                )
            }
        }
        .padding(20)
        .background(.white)
        .cornerRadius(15)
        .shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 5)
        .padding()
    }
}

7. 常用 Modifier 组合模板

extension View {
    // 卡片样式
    func card() -> some View {
        self
            .padding()
            .background(.white)
            .cornerRadius(12)
            .shadow(color: .gray.opacity(0.3), radius: 8, x: 0, y: 4)
    }
    
    // 主按钮样式
    func primaryButtonStyle() -> some View {
        self
            .font(.headline)
            .foregroundColor(.white)
            .padding()
            .frame(maxWidth: .infinity)
            .background(
                LinearGradient(
                    colors: [.blue, .purple],
                    startPoint: .leading,
                    endPoint: .trailing
                )
            )
            .cornerRadius(10)
            .shadow(radius: 5)
    }
    
    // 输入框样式
    func textFieldStyle() -> some View {
        self
            .padding()
            .background(.gray.opacity(0.1))
            .cornerRadius(8)
            .overlay(
                RoundedRectangle(cornerRadius: 8)
                    .stroke(.gray.opacity(0.5), lineWidth: 1)
            )
    }
    
    // 标签样式
    func tag(color: Color = .blue) -> some View {
        self
            .font(.caption)
            .padding(.horizontal, 12)
            .padding(.vertical, 6)
            .background(color.opacity(0.2))
            .foregroundColor(color)
            .cornerRadius(12)
    }
}

// 使用示例
struct ContentView: View {
    @State private var email = ""
    
    var body: some View {
        VStack(spacing: 20) {
            // 卡片
            VStack {
                Text("用户信息")
                Text("详细内容")
            }
            .card()
            
            // 输入框
            TextField("邮箱", text: $email)
                .textFieldStyle()
            
            // 按钮
            Text("登录")
                .primaryButtonStyle()
            
            // 标签
            HStack {
                Text("热门").tag(color: .red)
                Text("新品").tag(color: .green)
                Text("推荐").tag(color: .blue)
            }
        }
        .padding()
    }
}

8. 高级 Modifier 技巧

A. 环境 Modifier

影响所有子视图:

VStack {
    Text("标题")
    Text("副标题")
    Text("内容")
}
.font(.title)          // 所有子视图都使用 title 字体
.foregroundColor(.blue) // 所有子视图都是蓝色
B. 几何读取器配合 Modifier
GeometryReader { geometry in
    Text("响应式")
        .frame(width: geometry.size.width * 0.8)
        .position(x: geometry.size.width / 2, y: geometry.size.height / 2)
}
C. 动画 Modifier
struct AnimatedView: View {
    @State private var isExpanded = false
    
    var body: some View {
        RoundedRectangle(cornerRadius: isExpanded ? 50 : 10)
            .fill(isExpanded ? .blue : .red)
            .frame(width: isExpanded ? 200 : 100, height: 100)
            .animation(.spring(response: 0.5, dampingFraction: 0.6), value: isExpanded)
            .onTapGesture {
                isExpanded.toggle()
            }
    }
}

9. Modifier 最佳实践

 推荐做法
// 1. 提取重复的 modifier 为自定义 modifier
extension View {
    func standardCard() -> some View {
        self
            .padding()
            .background(.white)
            .cornerRadius(10)
            .shadow(radius: 5)
    }
}

// 2. 注意顺序
Text("示例")
    .padding()
    .background(.blue)
    .cornerRadius(10)  // 正确顺序

// 3. 使用语义化命名
extension View {
    func errorStyle() -> some View {
        self.foregroundColor(.red).bold()
    }
    
    func successStyle() -> some View {
        self.foregroundColor(.green).bold()
    }
}

 避免做法
// 1. 避免过长的 modifier 链
Text("Bad")
    .font(.title).foregroundColor(.blue).padding().background(.yellow).cornerRadius(10).shadow(radius: 5).opacity(0.9)
    // 太长了!应该换行

// 2. 避免重复代码
Text("Button 1")
    .padding()
    .background(.blue)
    .cornerRadius(10)

Text("Button 2")
    .padding()
    .background(.blue)
    .cornerRadius(10)
// 应该提取为自定义 modifier

// 3. 避免错误的顺序
Text("Wrong")
    .cornerRadius(10)   // 错误:在 background 之前
    .background(.blue)

总结

Modifier 核心要点:

  • ✅ Modifier 创建新视图,不修改原视图
  • ✅ 顺序非常重要
  • ✅ 支持链式调用
  • ✅ 可以自定义和复用
  • ✅ 使用语义化命名
  • ✅ 注意性能(避免过度嵌套)

SwiftUI快速入门指南-关键字篇

作者 xiAo_Ju
2025年11月25日 20:32

背景

本文帮助有Swift基础的同学,快速入门SwiftUI,基于cursour整理

主要分为四个部分:

Some

some 表示"某个特定的类型,该类型遵循某个协议"。它的特点是:

  • 隐藏具体类型:调用者不知道具体是什么类型,只知道它遵循某个协议
  • 类型固定:返回的始终是同一个具体类型(编译器知道)
  • 类型推断:编译器会自动推断出具体类型

some vs any 核心区别

特性 some any
类型确定 编译时确定,固定不变 运行时可变
性能 快(静态派发) 慢(动态派发,有装箱开销)
类型一致性 必须始终返回同一类型 可以返回不同类型
引入版本 Swift 5.1 Swift 5.6
使用场景 返回类型、属性 需要类型灵活性时
// some - 固定的具体类型
func makeSomeView() -> some View {
    Text("Hello")  // 每次调用都返回 Text 类型
}

// any - 可以是任何符合协议的类型
func makeAnyView(condition: Bool) -> any View {
    if condition {
        return Text("Hello")   // 这次返回 Text
    } else {
        return Image("icon")   // 下次可能返回 Image
    }
}

关键字

属性包装器 用途 拥有数据 数据类型 典型场景
@State 当前View状态处理 ✅ 是 值类型 简单的 UI 状态
@Binding 父子View间状态传递 ❌ 否 任意 子视图修改父状态
@StateObject 当前View引用对象,对象的生命周期在当前View ✅ 是 引用类型 视图的 ViewModel
@ObservedObject 父子View间对象状态传递,对象在父View ❌ 否 引用类型 传入的对象
@EnvironmentObject 跨View间状态传递 ❌ 否 引用类型 全局共享数据
@Environment 系统环境 ❌ 否 系统提供 系统设置和服务

1. @State - 私有状态 用于管理视图内部的简单值类型状态。

struct CounterView: View {
    @State private var count = 0
    @State private var isOn = false
    @State private var name = ""
    
    var body: some View {
        VStack {
            Text("计数: \(count)")
            Button("增加") {
                count += 1  // 修改会触发视图刷新
            }
            
            Toggle("开关", isOn: $isOn)
            TextField("姓名", text: $name)
        }
    }
}

特点:

  • ✅ 用于值类型(Int, String, Bool, struct 等)
  • ✅ 视图拥有这个状态
  • ✅ 声明为 private
  • ✅ SwiftUI 管理其生命周期
  • ✅ 修改会自动刷新视图

2. @Binding - 双向绑定

创建对父视图状态的双向绑定。

struct ParentView: View {
    @State private var isPresented = false
    
    var body: some View {
        VStack {
            Button("显示") {
                isPresented = true
            }
            
            // 传递绑定
            ChildView(isPresented: $isPresented)
        }
    }
}

struct ChildView: View {
    @Binding var isPresented: Bool  // 绑定到父视图的状态
    
    var body: some View {
        Toggle("显示状态", isOn: $isPresented)
        // 修改会同步到父视图
    }
}

特点:

  • ✅ 创建双向连接
  • ✅ 子视图可以读写父视图的状态
  • ✅ 使用 $ 传递绑定
  • ✅ 不拥有数据

3. @StateObject - 引用类型的拥有者

用于创建和拥有 ObservableObject 实例

// 1. 创建可观察对象
class ViewModel: ObservableObject {
    @Published var items: [String] = []
    @Published var isLoading = false
    
    func loadData() {
        isLoading = true
        // 加载数据...
        items = ["Item 1", "Item 2"]
        isLoading = false
    }
}

// 2. 在视图中使用
struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    
    var body: some View {
        List(viewModel.items, id: \.self) { item in
            Text(item)
        }
        .onAppear {
            viewModel.loadData()
        }
    }
}

3. @ObservedObject - 引用类型的观察者

用于观察已存在的 ObservableObject(不拥有)。

class ViewModel: ObservableObject {
    @Published var count = 0
}

struct ParentView: View {
    @StateObject private var viewModel = ViewModel()  // 拥有
    
    var body: some View {
        ChildView(viewModel: viewModel)  // 传递
    }
}

struct ChildView: View {
    @ObservedObject var viewModel: ViewModel  // 观察(不拥有)
    
    var body: some View {
        VStack {
            Text("计数: \(viewModel.count)")
            Button("增加") {
                viewModel.count += 1
            }
        }
    }
}

特点:

  • ✅ 观察从外部传入的对象
  • ❌ 不拥有对象
  • ⚠️ 视图重建时可能导致对象重新初始化(如果使用不当)

5. @EnvironmentObject - 环境对象 在视图层级中共享对象,无需逐层传递。

class UserSettings: ObservableObject {
    @Published var username = "Guest"
    @Published var isDarkMode = false
}

@main
struct MyApp: App {
    @StateObject private var settings = UserSettings()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(settings)  // 注入
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var settings: UserSettings  // 自动获取
    
    var body: some View {
        VStack {
            Text("用户: \(settings.username)")
            SettingsView()  // 子视图也能访问
        }
    }
}

struct SettingsView: View {
    @EnvironmentObject var settings: UserSettings  // 直接访问
    
    var body: some View {
        Toggle("深色模式", isOn: $settings.isDarkMode)
    }
}

特点:

  • ✅ 跨层级共享数据
  • ✅ 无需逐层传递
  • ⚠️ 如果未注入会崩溃
  • ✅ 适合全局状态(用户设置、主题等)

6. @Environment - 系统环境值

访问 SwiftUI 提供的系统环境值。

struct MyView: View {
    @Environment(\.colorScheme) var colorScheme  // 深色/浅色模式
    @Environment(\.dismiss) var dismiss  // 关闭动作
    @Environment(\.horizontalSizeClass) var sizeClass  // 尺寸类别
    
    var body: some View {
        VStack {
            Text("当前模式: \(colorScheme == .dark ? "深色" : "浅色")")
            
            Button("关闭") {
                dismiss()
            }
        }
    }
}

常用环境值:

  • .colorScheme - 颜色方案
  • .dismiss - 关闭当前视图
  • .horizontalSizeClass / .verticalSizeClass - 尺寸类别
  • .locale - 本地化
  • .accessibilityEnabled - 辅助功能

最佳实践

// 1. 简单值用 @State
@State private var count = 0

// 2. 创建对象用 @StateObject
@StateObject private var viewModel = ViewModel()

// 3. 传递对象用 @ObservedObject
@ObservedObject var viewModel: ViewModel

// 4. 传递绑定用 @Binding
@Binding var isPresented: Bool

// 5. 全局共享用 @EnvironmentObject
@EnvironmentObject var settings: AppSettings

iOS一个Fancy UI的Tricky实现

作者 xiAo_Ju
2025年11月25日 20:26

背景

最近接到了一个Fancy的动效UI,主要是为了在首屏放出更多有用信息,提升用户购买转化率

这也是我近几年遇到的一个相对复杂的UI效果了。一开始看到这个效果,其实心里是没有底能不能实现的。因为在我github star的1.4k+库中,就没有见过类似的效果,而且单从视频看下来,有物理上的滑动冲突。但是别无选择,最终还是通过各种demo实验,把效果实现了。下面就给大家介绍一下实现的方式tricky在哪里

设计效果

那么这个效果Fancy在哪里呢?我们来拆解一下:

  • 可以看到头部图片区域在上滑的时候有一个放大的效果,头部区域有高斯模糊和渐变效果
  • 主要信息区域有一个Title的展开Alpha渐变动画
  • 在列表上滑,在头部放大,Title展开的同时,列表还可能往下顶

头部图片放大效果实现

其实同步的放大效果,相对来说是比较简单的,就是一个上滑的偏移量变化,计算出上滑放大的效果

Screenshot 2023-09-24 at 15.41.04.png

上滑的进度 = 当前上滑距离 / 可以上滑距离

可以上滑距离 = P2 - P1

当前上滑距离 = contentOffsetY (系统UI控件可以获取)

头图高度 = min(最小高度 + (最大高度 - 最小高度) * 上滑进度, 最大高度)

最小高度 = 半屏时头图的高度,默认是200pt

最大高度 = 全屏时屏幕的宽度,因为头图的最大尺寸宽高比是1:1

聪明的同学会发现,上面的公式中,在满足 最小高度 + (最大高度 - 最小高度) * 上滑进度 < 最大高度 时

有可能 (最大高度 - 最小高度) * 上滑进度 > 可以上滑距离

这个点,其实也是我在看到这个效果时比较担心的一个点,因为这个时候手指在屏幕上往上推,但视图却在往下顶,是不跟手的状态。

好在真机体验没有明显的体感问题,所以也没有什么特殊处理

为什么这里需要用一个上滑的进度,而不用上滑的绝对值呢?其实我一开始用的是绝对值,但是在(最大高度 - 最小高度) * 上滑进度 > 可以上滑距离时,直接把剩余的高度暴力加上,就会出现一个严重的跳动效果。

文字展开动画效果实现

这部分也是整个效果最难的,那么他到底难在哪里?下面我给大家拆解一下

首先iOS的文字UI控件,是没法做到视频中逐行展开并且带有Alpha动画的。

那么系统的控件实现不了,有什么其他办法呢?脑海里疯狂回忆我star的1.4k+库里面搜寻类似效果,结果当然是无果 又是一顿Google搜索,iOS expandable UILabel animationiOS expandable UILabel...,换了各种关键字,结果都没有找到好的解决方案。

只能硬着头皮自己想。

首先我不考虑展开效果和Alpha动画的事情,先做到,从一行上滑时变成多行。

初始效果.gif

达到这个效果还是比较简单的,我们只需要把Title label的展示行数设置成无数行,然后高度强制设置成一行的高度,滑动的时候用类似头部放大效果的公式,即可达到该效果

到这里,我内心稍微放松了一下,想的是终于有一个可以保底交付的效果了,展开动效的要是做不了,就用这个交付吧。。。

我想啊想啊想,逐行展开,逐行展开。关键是先要逐行,逐行之后再做y坐标偏移动画就简单了。

那么我能不能把文字UI控件截图,然后逐行裁剪做动画呢?

管他的,先搞个demo试试

我擦,牛逼呀,这个方法可以诶。再来看看这个方法的原理

Screenshot 2023-09-24 at 17.08.59.png

  • 第一步把文字部分生成一张图片
  • 计算出有多少行文字
  • 将每一行文字裁切成一张图片

最终效果

done.gif

完美啊!

❌
❌