ColorPicker 是 SwiftUI 提供的颜色选择器,用来让用户选择颜色,并绑定到一个 Color 类型变量。
它相当于:HTML:<input type="color">
最简单的例子
import SwiftUI
struct ContentView: View {
@State private var color: Color = .blue
var body: some View {
VStack {
Circle()
.fill(color)
.frame(width: 150, height: 150)
ColorPicker("选择颜色", selection: $color, supportsOpacity: true)
.labelsHidden()
}
.padding()
}
}
#Preview {
ContentView()
}综合 Demo
import SwiftUI
struct ContentView: View {
@State private var textColor: Color = .white
@State private var backgroundColor: Color = .blue
@State private var circleColor: Color = .orange
var body: some View {
ZStack {
backgroundColor
.ignoresSafeArea()
VStack(spacing: 30) {
Text("ColorPicker Demo")
.font(.largeTitle)
.bold()
.foregroundStyle(textColor)
Circle()
.fill(circleColor)
.frame(width: 120, height: 120)
Form {
ColorPicker("文字颜色", selection: $textColor)
ColorPicker("背景颜色", selection: $backgroundColor)
ColorPicker(
"圆形颜色",
selection: $circleColor,
supportsOpacity: false
)
}
}
.padding()
}
}
}
#Preview {
ContentView()
}项目中放入 Assets.xcassets
Image("cat")
// 对应 Assets.xcassets cat
// 例如
struct ContentView: View {
var body: some View {
Image("cat")
}
}Apple 内置了几千个图标,下面这些都是矢量图标
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "heart.fill")
Image(systemName: "trash")
Image(systemName: "folder")
Image(systemName: "person.fill")
Image(systemName: "wifi")
}
}
}
#Preview {
ContentView()
}如果图片很大,SwiftUI 会按原尺寸显示,通常需要 只有调用
.resizable() 图片才能改变大小。
Image("cat")
.resizable()这样调整会拉伸
Image("cat")
.resizable()
.frame(width: 200, height: 150)保持比例完整显示,其实最好不要定死的 大小,而是使用 max-width,min-height 之类的。
Image("cat")
.resizable()
.scaledToFit()
.frame(width: 250)铺满
Image("cat")
.resizable()
.scaledToFill()
.frame(width: 250, height: 200)图片会超出,需要 .clipped()
Image("cat")
.resizable()
.scaledToFill()
.frame(width: 250, height: 200)
.clipped()SF Symbols 支持 tint。
Image(systemName: "heart.fill")
.foregroundStyle(.red)
// 如 .foregroundStyle(.pink)
// 多颜色
Image(systemName: "person.crop.circle.badge.plus")
.foregroundStyle(.blue, .green)SF Symbols 实际上会跟随字体大小
Image(systemName: "star.fill")
.font(.largeTitle)
// 或
Image(systemName: "bolt.fill")
.font(.system(size: 80))默认,采用系统默认颜色
Image(systemName: "star.fill")template 模版模式,所有非透明像素都会变成蓝色
Image("logo")
.renderingMode(.template)
.foregroundStyle(.blue)original 保持原图颜色
Image("logo")
.renderingMode(.original)opacity 透明图设置
Image("cat")
.opacity(0.5)import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
AsyncImage(url: URL(string: "https://picsum.photos/400/300")) { image in
image
.resizable()
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 20))
} placeholder: {
ProgressView() // 加载中的骨架屏
}
.frame(width: 200, height: 150)
.clipped()
}
}
}
#Preview {
ContentView()
}.clipShape
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
AsyncImage(url: URL(string: "https://picsum.photos/400/300")) { image in
image
.resizable()
.scaledToFill()
.frame(width: 120, height: 120)
.clipShape(Circle())
} placeholder: {
ProgressView() // 加载中的骨架屏
}
.frame(width: 200, height: 150)
.clipped()
}
}
}
#Preview {
ContentView()
}配合 overlay 实现
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
AsyncImage(url: URL(string: "https://picsum.photos/400/300")) { image in
image
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(Circle())
.overlay(
Circle()
.stroke(.red, lineWidth: 4)
)
} placeholder: {
ProgressView() // 加载中的骨架屏
}
.frame(width: 200, height: 150)
.clipped()
}
}
}
#Preview {
ContentView()
}.shadow
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
AsyncImage(url: URL(string: "https://picsum.photos/400/300")) { image in
image.shadow(color: .black.opacity(0.3),
radius: 8,
x: 0,
y: 5)
} placeholder: {
ProgressView() // 加载中的骨架屏
}
.frame(width: 200, height: 150)
.clipped()
}
}
}
#Preview {
ContentView()
}.rotationEffect
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
AsyncImage(url: URL(string: "https://picsum.photos/400/300")) { image in
image.rotationEffect(.degrees(75))
} placeholder: {
ProgressView() // 加载中的骨架屏
}
.frame(width: 200, height: 150)
.clipped()
}
}
}
#Preview {
ContentView()
}.scaleEffect
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
AsyncImage(url: URL(string: "https://picsum.photos/400/300")) { image in
image.scaleEffect(0.7)
} placeholder: {
ProgressView() // 加载中的骨架屏
}
}
}
}
#Preview {
ContentView()
}.scaleEffect(x: -1)import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: 30) {
Image("cat")
.resizable()
.scaledToFill()
.frame(width: 220, height: 180)
.clipShape(RoundedRectangle(cornerRadius: 20))
.shadow(radius: 8)
Image(systemName: "heart.circle.fill")
.font(.system(size: 80))
.foregroundStyle(.red)
Image(systemName: "person.crop.circle.fill")
.font(.system(size: 100))
.foregroundStyle(.blue)
}
.padding()
}
}
#Preview {
ContentView()
}.sheet 是 SwiftUI 的模态弹窗 Modal
Presentation。iPhone那种从底部向上弹出的一层卡片页面基本就是这个东西。
点击按钮,Sheet 从底部滑出来。
import SwiftUI
struct ContentView: View {
@State private var showSheet = false
var body: some View {
Button("打开 Sheet") {
showSheet = true
}
.sheet(
isPresented: $showSheet,
onDismiss: {
print("✅ Sheet 已关闭")
}
) {
SheetView()
}
}
}
struct SheetView: View {
@Environment(\.dismiss)
private var dismiss
var body: some View {
VStack(spacing: 20) {
Text("Hello Sheet")
.font(.largeTitle)
Button("关闭") {
dismiss()
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
#Preview {
ContentView()
}关闭打开的状态。
.sheet(isPresented: $showSheet) {
}
@State private var showSheet = false
// 打开
showSheet = true
// 关闭
showSheet = falseSheet 内部可以,自行关闭
import SwiftUI
struct DetailView: View {
@Environment(\.dismiss)
private var dismiss
var body: some View {
Button("关闭") {
dismiss()
}
}
}
struct ContentView: View {
@State private var showSheet = false
var body: some View {
Button("打开 Sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
DetailView()
}
}
}
#Preview {
ContentView()
}很多时候不是一个 Bool。而是:当前打开哪个对象。
struct User: Identifiable {
let id = UUID()
let name: String
}
@State private var selectedUser: User?
.sheet(item: $selectedUser) { user in
Text(user.name)
}
// 点击
selectedUser = User(name: "Tom")
// 关闭
selectedUser = nil用 item 方式不用多写一个 Bool
import SwiftUI
struct User: Identifiable {
let id = UUID()
let name: String
}
struct ContentView: View {
let users = [
User(name: "Tom"),
User(name: "Lucy"),
User(name: "Jack")
]
@State private var selectedUser: User?
var body: some View {
List(users) { user in
Button(user.name) {
selectedUser = user
}
}
.sheet(item: $selectedUser) { user in
VStack {
Text(user.name)
.font(.largeTitle)
Button("关闭") {
selectedUser = nil
}
}
.padding()
}
}
}
#Preview {
ContentView()
}| 修饰符 | 作用 |
|---|---|
.presentationDetents() |
设置可停留高度 |
.presentationBackground() |
设置背景 |
.presentationCornerRadius() |
设置圆角 |
.presentationDragIndicator() |
控制顶部拖拽条 |
.interactiveDismissDisabled() |
禁止手势关闭 |
.presentationContentInteraction() |
控制内容滚动与拖拽的交互方式 |
.presentationCompactAdaptation() |
控制紧凑尺寸环境下的展示方式 |
.onSubmit 是 SwiftUI
中用于处理用户提交输入的修饰符,最常见于
TextField、SecureField、TextEditor(部分场景)以及 searchable。
下面样例,输入内容输入回车就会触发 .onSubmit。
import SwiftUI
struct ContentView: View {
@State private var username = ""
@State private var password = ""
var body: some View {
TextField("用户名", text: $username)
.textFieldStyle(.roundedBorder)
.padding()
.onSubmit {
print("提交:\(username)")
}
TextField("密码", text: $password)
.textFieldStyle(.roundedBorder)
.padding()
.onSubmit {
print("提交:\(password)")
}
}
}
#Preview {
ContentView()
}.submitLabel(_:) 是 SwiftUI
用来设置键盘右下角提交按钮文字/样式的 Modifier。
它不会处理提交事件,只负责修改键盘上的按钮显示。真正响应点击的是
.onSubmit {}。
TextField("请输入用户名", text: $username)
.submitLabel(.done)
.onSubmit {
print("点击完成")
}常见的 SubmitLabel SwiftUI 提供了多个选项。
| Label | 键盘按钮 | 适用场景 |
|---|---|---|
.done |
Done(完成) | 输入结束 |
.go |
Go | 打开页面、搜索 |
.send |
Send | 发送消息 |
.search |
Search | 搜索框 |
.join |
Join | 加入房间 |
.next |
Next | 跳到下一个输入框 |
.continue |
Continue | 下一步 |
.return |
Return | 回车 |
.route |
Route | 导航 |
.emergencyCall |
Emergency Call | 特殊用途 |
多个 TextField
@FocusState 和 .focused 是 SwiftUI 中
管理输入焦点(Focus) 的一套 API。
可以把它理解成:
@State 保存普通状态。@FocusState 保存哪个控件拥有输入焦点。.focused() 将 View 与 @FocusState
绑定。import SwiftUI
struct ContentView: View {
enum Field {
case username
case password
}
@State private var username = ""
@State private var password = ""
@FocusState private var focus: Field?
var body: some View {
VStack {
TextField("用户名", text: $username)
.focused($focus, equals: .username)
.submitLabel(.next)
.onSubmit {
focus = .password
}
SecureField("密码", text: $password)
.focused($focus, equals: .password)
.submitLabel(.done)
.onSubmit {
login()
}
}
.padding()
.onAppear {
focus = .username
}
}
func login() {
print("登录")
}
}.autocapitalization 是 SwiftUI
中用于控制文本输入时是否自动大写的修饰符,主要用于 TextField。
不过,它已经废弃(Deprecated),现在应该使用
.textInputAutocapitalization(_:)。
TextField("用户名", text: $username)
.autocapitalization(.none)import SwiftUI
struct ContentView: View {
@State private var username = ""
var body: some View {
TextField("用户名", text: $username)
.textInputAutocapitalization(.words)
}
}
#Preview {
ContentView()
}完整示例
import SwiftUI
struct ContentView: View {
@State private var name = ""
@State private var email = ""
@State private var code = ""
var body: some View {
Form {
TextField("姓名", text: $name)
.textInputAutocapitalization(.words)
TextField("邮箱", text: $email)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
TextField("激活码", text: $code)
.textInputAutocapitalization(.characters)
}
}
}
#Preview {
ContentView()
}.autocorrectionDisabled(_:) 是 SwiftUI 中控制
自动纠错(Auto Correction) 的
Modifier,用于决定输入框是否启用系统的拼写纠错功能。
.autocorrectionDisabled(_ disabled: Bool = true)
// true 关闭自动纠错
// false 开启自动纠错输入一些离谱的内容,系统可能会在输入的内容下面画红线提示。
import SwiftUI
struct ContentView: View {
@State private var username = ""
var body: some View {
TextField("Username", text: $username)
.textFieldStyle(.roundedBorder)
.autocorrectionDisabled(false)
.padding()
}
}
#Preview {
ContentView()
}import SwiftUI
struct ContentView: View {
@State private var email = ""
@State private var password = ""
var body: some View {
Form {
TextField("Email", text: $email)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.autocorrectionDisabled(false)
SecureField("Password", text: $password)
.autocorrectionDisabled()
}
}
}
#Preview {
ContentView()
}.disabled(_:) 是 SwiftUI 用来禁用用户交互的
Modifier。
当设置为 true 时,视图不能响应点击、输入等事件,同时系统会自动调整外观(通常会变灰)。
import SwiftUI
struct ContentView: View {
@State private var username = ""
@State private var password = ""
@State private var isLoading = false
var body: some View {
VStack(spacing: 20) {
TextField("用户名", text: $username)
.textFieldStyle(.roundedBorder)
SecureField("密码", text: $password)
.textFieldStyle(.roundedBorder)
Button {
isLoading = true
Task {
try? await Task.sleep(for: .seconds(2))
isLoading = false
}
} label: {
if isLoading {
ProgressView()
} else {
Text("登录")
}
}
.disabled(
username.isEmpty ||
password.isEmpty ||
isLoading
)
}
.padding()
}
}
#Preview {
ContentView()
}.alert 是 SwiftUI 中用于
弹出警告框(Alert)
的修饰符,用来提示用户信息、确认操作或者展示错误。
最简单的 Alert 通过 Bool 控制显示
import SwiftUI
struct ContentView: View {
@State private var showAlert = false
var body: some View {
Button("删除文件") {
showAlert = true
}
.alert("确认删除?", isPresented: $showAlert) {
Button("取消", role: .cancel) {}
Button("删除", role: .destructive) {
print("删除")
}
} message: {
Text("删除后无法恢复。")
}
}
}
#Preview {
ContentView()
}完整示例
import SwiftUI
struct ContentView: View {
@State private var showAlert = false
@State private var result = ""
var body: some View {
VStack(spacing: 20) {
Button("删除文件") {
showAlert = true
}
Text(result)
.font(.title3)
}
.padding()
.alert("确认删除?", isPresented: $showAlert) {
Button("取消", role: .cancel) {
result = "取消删除"
}
Button("删除", role: .destructive) {
result = "已删除"
}
} message: {
Text("删除后无法恢复。")
}
}
}
#Preview {
ContentView()
}role 的作用,按钮可以指定角色
Button("取消", role: .cancel) { }
Button("删除", role: .destructive) { }
Button("确定") { }| role | 效果 |
|---|---|
.cancel |
取消按钮 |
.destructive |
红色危险按钮 |
| nil | 普通按钮 |
可以理解成一棵 View 树共享的数据,例如
RootView
│
├── A
│ └── C
│
└── B
└── DRoot 可以提供一个值
.environment(\.locale, Locale(identifier: "zh_CN"))所有子 View 都可以读取,而不用一级一级穿参数,这就是 Environment。
@Environment(\.locale) var localeSwiftUI 自带很多,例如
// 暗黑模式
@Environment(\.colorScheme) var colorScheme
// 语言
@Environment(\.locale) var locale
// 关闭 Sheet
@Environment(\.dismiss) var dismiss
// App 生命周期
@Environment(\.scenePhase) var scenePhase
// 字体大小
@Environment(\.dynamicTypeSize) var size
// 打开链接
@Environment(\.openURL) var openURL还有等等等等。
@Environment(\.scenePhase) 是 SwiftUI
提供的环境值(Environment Value),用于获取当前
Scene(窗口)的生命周期状态。不过它监听的是 Scene,而不是整个
App,因此支持多窗口(如 iPad、macOS)。
enum ScenePhase {
case active // 应用正在使用
case inactive // 应用暂时不能响应用户
case background // 应用进入后台
}import SwiftUI
struct ContentView: View {
@Environment(\.scenePhase) private var scenePhase
var body: some View {
Text("Hello")
.onChange(of: scenePhase) { oldValue, newValue in
print(oldValue)
print(newValue)
}
}
}
#Preview {
ContentView()
}第一步 定义 EnvironmentKey
struct WordsKey: EnvironmentKey {
static let defaultValue: [String] = []
}第二部 扩展 EnvironmentValues
extension EnvironmentValues {
var words: [String] {
get { self[WordsKey.self] }
set { self[WordsKey.self] = newValue }
}
}现在就有了
\.words第三步 注入
ContentView()
.environment(\.words, [
"Swift",
"SwiftUI",
"Apple"
])第四步 读取
struct ChildView: View {
@Environment(\.words)
private var words
var body: some View {
List(words, id: \.self) {
Text($0)
}
}
}为什么不用 @State,那么每一级都要传,参数层层透传(Prop
Drilling)会让代码越来越臃肿。
使用 Environment 后,所有子 View 都可以直接读取,中间 View 完全不用关心这个数据。
ViewModifier 是 SwiftUI 中封装一组 View 修饰器的协议。
如果你发现一堆 .padding().background().cornerRadius()...
到处都在写,就应该考虑抽成一个
ViewModifier。程序员最大的乐趣之一,就是把重复代码包装起来,然后假装自己发明了新的语法。
Modifier 内部不能使用 @State
虽然可以编译,但不要这么做,Modifier 本身不是一个独立
View,生命周期依赖宿主 View,状态容易令人困惑。
import SwiftUI
struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.blue.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(radius: 5)
}
}
// 使用
Text("Hello")
.modifier(CardModifier())
// 效果等价于
Text("Hello")
.padding()
.background(.blue.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(radius: 5)完整 Demo
import SwiftUI
struct RedBorderModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.border(.red, width: 2)
.font(.title)
}
}
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
Text("Hello")
Button("Button") {}
}
.modifier(RedBorderModifier())
}
}
#Preview {
ContentView()
}例如颜色可以传入:
struct MyModifier: ViewModifier {
let color: Color
func body(content: Content) -> some View {
content
.padding()
.background(color)
.foregroundStyle(.white)
}
}
// 使用
Text("SwiftUI")
.modifier(MyModifier(color: .blue))多个参数
struct BadgeModifier: ViewModifier {
var background: Color
var cornerRadius: CGFloat
func body(content: Content) -> some View {
content
.padding()
.background(background)
.clipShape(
RoundedRectangle(cornerRadius: cornerRadius)
)
}
}
// 调用
Text("VIP")
.modifier(
BadgeModifier(
background: .orange,
cornerRadius: 20
)
)虽然可以自定义 Modifier 但 SwiftUI 更推荐扩展 View。
// 虽然可以直接
Text("Hello")
.modifier(CardModifier())
// 但可以
extension View {
func cardStyle() -> some View {
modifier(CardModifier())
}
}
// 以后
Text("Hello")
.cardStyle()
Button("OK") {}
.cardStyle()这也是 SwiftUI 大量内置修饰器(如
.padding()、.foregroundStyle()
等)的设计思路。
struct HighlightModifier: ViewModifier {
let color: Color
func body(content: Content) -> some View {
content
.padding()
.background(color)
.clipShape(Capsule())
}
}
extension View {
func highlight(_ color: Color) -> some View {
modifier(HighlightModifier(color: color))
}
}
// 使用
Text("Swift")
.highlight(.green)
Text("Apple")
.highlight(.blue)