阅读视图
SwiftUI快速入门指南-关键字篇
背景
本文帮助有Swift基础的同学,快速入门SwiftUI,基于cursour整理
主要分为四个部分:
- 关键字
- Modifier
- 布局
- Viewbuilder
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实现
背景
最近接到了一个Fancy的动效UI,主要是为了在首屏放出更多有用信息,提升用户购买转化率
这也是我近几年遇到的一个相对复杂的UI效果了。一开始看到这个效果,其实心里是没有底能不能实现的。因为在我github star的1.4k+库中,就没有见过类似的效果,而且单从视频看下来,有物理上的滑动冲突。但是别无选择,最终还是通过各种demo实验,把效果实现了。下面就给大家介绍一下实现的方式tricky在哪里
设计效果
那么这个效果Fancy在哪里呢?我们来拆解一下:
- 可以看到头部图片区域在上滑的时候有一个放大的效果,头部区域有高斯模糊和渐变效果
- 主要信息区域有一个Title的展开Alpha渐变动画
- 在列表上滑,在头部放大,Title展开的同时,列表还可能往下顶
头部图片放大效果实现
其实同步的放大效果,相对来说是比较简单的,就是一个上滑的偏移量变化,计算出上滑放大的效果
上滑的进度 = 当前上滑距离 / 可以上滑距离
可以上滑距离 = P2 - P1
当前上滑距离 = contentOffsetY (系统UI控件可以获取)
头图高度 = min(最小高度 + (最大高度 - 最小高度) * 上滑进度, 最大高度)
最小高度 = 半屏时头图的高度,默认是200pt
最大高度 = 全屏时屏幕的宽度,因为头图的最大尺寸宽高比是1:1
聪明的同学会发现,上面的公式中,在满足 最小高度 + (最大高度 - 最小高度) * 上滑进度 < 最大高度 时
有可能 (最大高度 - 最小高度) * 上滑进度 > 可以上滑距离
这个点,其实也是我在看到这个效果时比较担心的一个点,因为这个时候手指在屏幕上往上推,但视图却在往下顶,是不跟手的状态。
好在真机体验没有明显的体感问题,所以也没有什么特殊处理
为什么这里需要用一个上滑的进度,而不用上滑的绝对值呢?其实我一开始用的是绝对值,但是在(最大高度 - 最小高度) * 上滑进度 > 可以上滑距离时,直接把剩余的高度暴力加上,就会出现一个严重的跳动效果。
文字展开动画效果实现
这部分也是整个效果最难的,那么他到底难在哪里?下面我给大家拆解一下
首先iOS的文字UI控件,是没法做到视频中逐行展开并且带有Alpha动画的。
那么系统的控件实现不了,有什么其他办法呢?脑海里疯狂回忆我star的1.4k+库里面搜寻类似效果,结果当然是无果
又是一顿Google搜索,iOS expandable UILabel animation,iOS expandable UILabel...,换了各种关键字,结果都没有找到好的解决方案。
只能硬着头皮自己想。
首先我不考虑展开效果和Alpha动画的事情,先做到,从一行上滑时变成多行。
达到这个效果还是比较简单的,我们只需要把Title label的展示行数设置成无数行,然后高度强制设置成一行的高度,滑动的时候用类似头部放大效果的公式,即可达到该效果
到这里,我内心稍微放松了一下,想的是终于有一个可以保底交付的效果了,展开动效的要是做不了,就用这个交付吧。。。
我想啊想啊想,逐行展开,逐行展开。关键是先要逐行,逐行之后再做y坐标偏移动画就简单了。
那么我能不能把文字UI控件截图,然后逐行裁剪做动画呢?
管他的,先搞个demo试试
我擦,牛逼呀,这个方法可以诶。再来看看这个方法的原理
- 第一步把文字部分生成一张图片
- 计算出有多少行文字
- 将每一行文字裁切成一张图片
最终效果
完美啊!