SwiftUI 详细教程
SwiftUI 是苹果公司推出的声明式 UI 框架,用于构建 Apple 平台(iOS、macOS、watchOS 和 tvOS)的用户界面。以下是 SwiftUI 的全面教程:
1. SwiftUI 简介
主要特点
- 声明式语法:描述 UI 应该做什么,而不是如何做
- 实时预览:Xcode 提供实时交互式预览
- 跨平台:一套代码适配所有 Apple 平台
- 数据驱动:自动响应数据变化更新 UI
- 原生性能:直接映射到原生视图控件
系统要求
- macOS 10.15 或更高版本
- Xcode 11 或更高版本
- iOS 13/watchOS 6/tvOS 13 或更高版本(部署目标)
2. 创建第一个 SwiftUI 项目
- 打开 Xcode,选择 "Create a new Xcode project"
- 选择 "App" 模板(iOS/macOS/watchOS/tvOS)
- 在界面技术中选择 "SwiftUI"
- 完成项目创建
项目结构
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI!")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
3. 基本视图和布局
文本视图
Text("Hello, SwiftUI!")
.font(.title) // 字体
.foregroundColor(.blue) // 颜色
.bold() // 加粗
.italic() // 斜体
.underline() // 下划线
图片视图
Image(systemName: "star.fill") // SF Symbols
.resizable() // 可调整大小
.frame(width: 100, height: 100) // 尺寸
.foregroundColor(.yellow) // 颜色
Image("custom-image") // 项目资源中的图片
.resizable()
.aspectRatio(contentMode: .fit)
按钮
Button(action: {
print("Button tapped")
}) {
Text("Tap Me")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
// 使用系统样式
Button("Delete", action: delete)
.buttonStyle(.bordered)
.tint(.red)
布局容器
VStack (垂直栈)
VStack {
Text("First")
Text("Second")
Text("Third")
}
.spacing(20) // 子视图间距
HStack (水平栈)
HStack {
Image(systemName: "star")
Text("Favorite")
Spacer() // 填充剩余空间
Text("4.8")
}
ZStack (层叠栈)
ZStack {
Circle()
.fill(Color.blue)
.frame(width: 100, height: 100)
Text("Center")
.foregroundColor(.white)
}
LazyVStack/LazyHStack (惰性加载栈)
ScrollView {
LazyVStack {
ForEach(1...1000, id: \.self) { item in
Text("Row \(item)")
}
}
}
间距和填充
VStack(spacing: 20) { // 子视图间距
Text("Hello")
Text("World")
}
.padding() // 四周内边距
.padding(.horizontal, 10) // 水平内边距
.padding(EdgeInsets(top: 10, leading: 15, bottom: 20, trailing: 25))
滚动视图
ScrollView {
VStack {
ForEach(0..<100) { index in
Text("Item \(index)")
.frame(maxWidth: .infinity)
.padding()
.background(index % 2 == 0 ? Color.gray.opacity(0.2) : Color.white)
}
}
}
// 水平滚动
ScrollView(.horizontal) {
HStack {
ForEach(0..<50) { index in
Circle()
.fill(Color.blue)
.frame(width: 50, height: 50)
.overlay(Text("\(index)"))
}
}
}
4. 状态管理
@State
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
.font(.largeTitle)
Button("Increment") {
count += 1
}
}
}
}
@Binding
struct ChildView: View {
@Binding var value: Int
var body: some View {
Button("Increment in Child") {
value += 1
}
}
}
struct ParentView: View {
@State private var counter = 0
var body: some View {
VStack {
Text("Parent value: \(counter)")
ChildView(value: $counter)
}
}
}
@ObservedObject
class UserSettings: ObservableObject {
@Published var score = 0
}
struct SettingsView: View {
@ObservedObject var settings: UserSettings
var body: some View {
VStack {
Text("Score: \(settings.score)")
Button("Increase Score") {
settings.score += 1
}
}
}
}
struct ContentView: View {
@StateObject var settings = UserSettings()
var body: some View {
SettingsView(settings: settings)
}
}
@StateObject
class DataModel: ObservableObject {
@Published var items = [String]()
init() {
// 初始化数据
loadData()
}
func loadData() {
// 加载数据
items = ["Apple", "Banana", "Orange"]
}
}
struct ItemsView: View {
@StateObject private var model = DataModel()
var body: some View {
List(model.items, id: \.self) { item in
Text(item)
}
}
}
@EnvironmentObject
class AppSettings: ObservableObject {
@Published var isDarkMode = false
}
struct ContentView: View {
var body: some View {
MainView()
.environmentObject(AppSettings())
}
}
struct MainView: View {
@EnvironmentObject var settings: AppSettings
var body: some View {
Toggle("Dark Mode", isOn: $settings.isDarkMode)
}
}
@AppStorage (UserDefaults)
struct SettingsView: View {
@AppStorage("username") var username = "Anonymous"
@AppStorage("isDarkMode") var isDarkMode = false
var body: some View {
Form {
TextField("Username", text: $username)
Toggle("Dark Mode", isOn: $isDarkMode)
}
}
}
5. 列表和导航
基本列表
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
// 动态列表
let items = ["Apple", "Banana", "Orange"]
List(items, id: \.self) { item in
Text(item)
}
// 带分区的列表
List {
Section(header: Text("Fruits")) {
Text("Apple")
Text("Banana")
}
Section(header: Text("Vegetables")) {
Text("Carrot")
Text("Broccoli")
}
}
.listStyle(.grouped) // 分组样式
导航视图
struct ContentView: View {
let fruits = ["Apple", "Banana", "Orange"]
var body: some View {
NavigationView {
List(fruits, id: \.self) { fruit in
NavigationLink(destination: DetailView(fruit: fruit)) {
Text(fruit)
}
}
.navigationTitle("Fruits")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add") {
print("Add tapped")
}
}
}
}
}
}
struct DetailView: View {
let fruit: String
var body: some View {
Text("Selected: \(fruit)")
.navigationTitle(fruit)
}
}
编辑列表
struct EditableListView: View {
@State private var items = ["Item 1", "Item 2", "Item 3"]
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteItems)
.onMove(perform: moveItems)
}
.navigationTitle("Edit Items")
.toolbar {
EditButton()
Button("Add") {
addItem()
}
}
}
}
func deleteItems(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
}
func moveItems(from source: IndexSet, to destination: Int) {
items.move(fromOffsets: source, toOffset: destination)
}
func addItem() {
items.append("Item \(items.count + 1)")
}
}
6. 表单和用户输入
表单基础
Form {
Section(header: Text("Personal Information")) {
TextField("Name", text: $name)
TextField("Email", text: $email)
.keyboardType(.emailAddress)
.textContentType(.emailAddress)
DatePicker("Birthday", selection: $birthday, displayedComponents: .date)
}
Section(header: Text("Preferences")) {
Toggle("Notifications", isOn: $notificationsEnabled)
Picker("Theme", selection: $theme) {
Text("Light").tag(0)
Text("Dark").tag(1)
Text("System").tag(2)
}
Stepper("Age: \(age)", value: $age, in: 0...120)
}
Section {
Button("Save") {
saveSettings()
}
}
}
文本输入
struct TextInputView: View {
@State private var username = ""
@State private var password = ""
@State private var bio = ""
var body: some View {
Form {
TextField("Username", text: $username)
.textContentType(.username)
.autocapitalization(.none)
.disableAutocorrection(true)
SecureField("Password", text: $password)
.textContentType(.password)
TextEditor(text: $bio)
.frame(height: 100)
.border(Color.gray, width: 1)
}
}
}
选择器
struct PickerView: View {
let colors = ["Red", "Green", "Blue"]
@State private var selectedColor = "Red"
@State private var selectedNumber = 1
var body: some View {
Form {
Picker("Color", selection: $selectedColor) {
ForEach(colors, id: \.self) {
Text($0)
}
}
.pickerStyle(.menu) // 默认样式
Picker("Number", selection: $selectedNumber) {
ForEach(1..<100) { number in
Text("\(number)")
}
}
.pickerStyle(.wheel) // 滚轮样式
}
}
}
滑块和步进器
struct ControlsView: View {
@State private var volume = 50.0
@State private var brightness = 0.5
@State private var quantity = 1
var body: some View {
Form {
Slider(value: $volume, in: 0...100, step: 1) {
Text("Volume")
} minimumValueLabel: {
Text("0")
} maximumValueLabel: {
Text("100")
}
Text("\(Int(volume))")
Slider(value: $brightness)
Text(String(format: "%.2f", brightness))
Stepper("Quantity: \(quantity)", value: $quantity, in: 1...10)
}
}
}
7. 动画和过渡
隐式动画
struct ImplicitAnimationView: View {
@State private var scale: CGFloat = 1.0
var body: some View {
Button("Tap Me") {
scale += 0.5
}
.scaleEffect(scale)
.animation(.default, value: scale) // 隐式动画
}
}
显式动画
struct ExplicitAnimationView: View {
@State private var isRotated = false
var body: some View {
Button(action: {
withAnimation(.easeInOut(duration: 1.0)) {
isRotated.toggle()
}
}) {
Text("Rotate")
.rotationEffect(.degrees(isRotated ? 180 : 0))
.animation(.spring(), value: isRotated)
}
}
}
过渡动画
struct TransitionView: View {
@State private var showDetails = false
var body: some View {
VStack {
Button("Toggle") {
withAnimation {
showDetails.toggle()
}
}
if showDetails {
Text("Details go here")
.transition(.asymmetric(
insertion: .move(edge: .leading),
removal: .opacity
))
}
Spacer()
}
}
}
复杂动画
struct AdvancedAnimationView: View {
@State private var progress: CGFloat = 0.0
var body: some View {
VStack {
Circle()
.trim(from: 0, to: progress)
.stroke(Color.blue, lineWidth: 5)
.frame(width: 100, height: 100)
.rotationEffect(.degrees(-90))
.animation(
.easeInOut(duration: 1.0)
.repeatForever(autoreverses: true),
value: progress
)
Button("Animate") {
progress = progress == 0.0 ? 1.0 : 0.0
}
}
}
}
8. 绘图和自定义视图
基本形状
struct ShapesView: View {
var body: some View {
VStack(spacing: 20) {
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 50)
RoundedRectangle(cornerRadius: 10)
.fill(Color.blue)
.frame(width: 100, height: 50)
Circle()
.fill(Color.green)
.frame(width: 50, height: 50)
Capsule()
.fill(Color.orange)
.frame(width: 100, height: 50)
Ellipse()
.fill(Color.purple)
.frame(width: 100, height: 50)
}
}
}
路径绘制
struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
return path
}
}
struct Star: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let points = 5
let outerRadius = min(rect.width, rect.height) / 2
let innerRadius = outerRadius * 0.4
let center = CGPoint(x: rect.midX, y: rect.midY)
for i in 0..<points * 2 {
let angle = CGFloat(i) * .pi / CGFloat(points)
let radius = i % 2 == 0 ? outerRadius : innerRadius
let point = CGPoint(
x: center.x + radius * sin(angle),
y: center.y + radius * cos(angle)
)
if i == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
path.closeSubpath()
return path
}
}
struct CustomShapesView: View {
var body: some View {
VStack(spacing: 20) {
Triangle()
.fill(Color.red)
.frame(width: 100, height: 100)
Star()
.fill(Color.blue)
.frame(width: 100, height: 100)
}
}
}
视图组合
struct Badge: View {
var count: Int
var body: some View {
ZStack {
Circle()
.fill(Color.red)
.frame(width: 20, height: 20)
Text("\(count)")
.foregroundColor(.white)
.font(.caption)
}
}
}
struct NotificationIcon: View {
var hasUnread: Bool
var unreadCount: Int
var body: some View {
ZStack(alignment: .topTrailing) {
Image(systemName: "bell.fill")
.font(.title)
if hasUnread {
Badge(count: unreadCount)
.offset(x: 5, y: -5)
}
}
}
}
struct CombinedView: View {
var body: some View {
HStack {
NotificationIcon(hasUnread: true, unreadCount: 3)
NotificationIcon(hasUnread: false, unreadCount: 0)
}
}
}
9. 网络请求和数据持久化
网络请求 (URLSession)
struct Post: Codable, Identifiable {
let id: Int
let title: String
let body: String
}
class PostsViewModel: ObservableObject {
@Published var posts = [Post]()
@Published var isLoading = false
@Published var error: Error?
func fetchPosts() {
isLoading = true
error = nil
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
return
}
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
DispatchQueue.main.async {
self?.isLoading = false
if let error = error {
self?.error = error
return
}
guard let data = data else { return }
do {
self?.posts = try JSONDecoder().decode([Post].self, from: data)
} catch {
self?.error = error
}
}
}.resume()
}
}
struct PostsView: View {
@StateObject var viewModel = PostsViewModel()
var body: some View {
Group {
if viewModel.isLoading {
ProgressView("Loading...")
} else if let error = viewModel.error {
Text("Error: \(error.localizedDescription)")
} else {
List(viewModel.posts) { post in
VStack(alignment: .leading) {
Text(post.title)
.font(.headline)
Text(post.body)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
}
.navigationTitle("Posts")
.onAppear {
viewModel.fetchPosts()
}
}
}
Core Data 集成
// 1. 创建 Core Data 模型文件 (.xcdatamodeld)
// 2. 定义实体和属性
import CoreData
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "Model")
init() {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
}
}
@main
struct CoreDataApp: App {
@StateObject private var dataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}
struct Task: Identifiable {
let id: UUID
let title: String
let isCompleted: Bool
}
struct TaskListView: View {
@Environment(\.managedObjectContext) var moc
@FetchRequest(
entity: TaskEntity.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \TaskEntity.createdAt, ascending: true)]
) var tasks: FetchedResults<TaskEntity>
@State private var newTaskTitle = ""
var body: some View {
NavigationView {
List {
ForEach(tasks) { task in
HStack {
Text(task.title ?? "Unknown")
Spacer()
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundColor(task.isCompleted ? .green : .gray)
}
}
.onDelete(perform: deleteTasks)
HStack {
TextField("New task", text: $newTaskTitle)
Button("Add") {
addTask()
}
.disabled(newTaskTitle.isEmpty)
}
}
.navigationTitle("Tasks")
.toolbar {
EditButton()
}
}
}
func addTask() {
let newTask = TaskEntity(context: moc)
newTask.id = UUID()
newTask.title = newTaskTitle
newTask.isCompleted = false
newTask.createdAt = Date()
try? moc.save()
newTaskTitle = ""
}
func deleteTasks(at offsets: IndexSet) {
for index in offsets {
let task = tasks[index]
moc.delete(task)
}
try? moc.save()
}
}
10. 高级主题
自定义视图修饰符
struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.white)
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 2)
}
}
extension View {
func cardStyle() -> some View {
self.modifier(CardModifier())
}
}
struct ContentView: View {
var body: some View {
VStack {
Text("Hello")
.cardStyle()
Text("World")
.cardStyle()
}
.padding()
}
}
自定义按钮样式
struct GradientButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding()
.foregroundColor(.white)
.background(
LinearGradient(
gradient: Gradient(colors: [Color.blue, Color.purple]),
startPoint: .leading,
endPoint: .trailing
)
)
.cornerRadius(10)
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.opacity(configuration.isPressed ? 0.9 : 1.0)
.animation(.easeOut(duration: 0.2), value: configuration.isPressed)
}
}
struct CustomButtonView: View {
var body: some View {
Button("Press Me") {
print("Button pressed")
}
.buttonStyle(GradientButtonStyle())
}
}
视图偏好键
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct SizeReportingView: View {
var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometry.size)
}
}
}
struct SizeReaderView: View {
@State private var size: CGSize = .zero
var body: some View {
VStack {
Text("Width: \(size.width, specifier: "%.1f")")
Text("Height: \(size.height, specifier: "%.1f")")
Rectangle()
.fill(Color.blue)
.frame(width: 200, height: 100)
.background(SizeReportingView())
.onPreferenceChange(SizePreferenceKey.self) { newSize in
size = newSize
}
}
}
}
拖放支持
struct DragDropView: View {
@State private var items = ["Apple", "Banana", "Orange", "Grapes"]
@State private var draggedItem: String?
var body: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: 20) {
ForEach(items, id: \.self) { item in
Text(item)
.frame(width: 100, height: 100)
.background(Color.blue)
.cornerRadius(10)
.foregroundColor(.white)
.onDrag {
draggedItem = item
return NSItemProvider(object: item as NSString)
}
.onDrop(
of: [.text],
delegate: DropViewDelegate(
item: item,
items: $items,
draggedItem: $draggedItem
)
)
}
}
.padding()
}
}
struct DropViewDelegate: DropDelegate {
let item: String
@Binding var items: [String]
@Binding var draggedItem: String?
func performDrop(info: DropInfo) -> Bool {
return true
}
func dropEntered(info: DropInfo) {
guard let draggedItem = draggedItem else { return }
let fromIndex = items.firstIndex(of: draggedItem)!
let toIndex = items.firstIndex(of: item)!
if fromIndex != toIndex {
items.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex)
}
}
}
11. 多平台适配
条件编译
struct ContentView: View {
var body: some View {
VStack {
#if os(iOS)
Text("Running on iOS")
#elseif os(macOS)
Text("Running on macOS")
#elseif os(watchOS)
Text("Running on watchOS")
#elseif os(tvOS)
Text("Running on tvOS")
#endif
}
}
}
平台特定视图
struct PlatformAdaptiveView: View {
var body: some View {
Group {
#if os(iOS)
iOSView()
#elseif os(macOS)
macOSView()
#endif
}
}
}
struct iOSView: View {
var body: some View {
NavigationView {
List {
Text("iOS Specific UI")
}
.navigationTitle("iOS")
}
}
}
struct macOSView: View {
var body: some View {
NavigationView {
List {
Text("macOS Specific UI")
}
.frame(minWidth: 300)
.navigationTitle("macOS")
}
}
}
设备方向检测
struct OrientationView: View {
@State private var orientation = UIDeviceOrientation.unknown
var body: some View {
Group {
if orientation.isPortrait {
Text("Portrait")
} else if orientation.isLandscape {
Text("Landscape")
} else {
Text("Unknown")
}
}
.onAppear {
orientation = UIDevice.current.orientation
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
orientation = UIDevice.current.orientation
}
}
}
12. 测试和调试
预览调试
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
.previewDevice("iPhone 13")
.previewDisplayName("iPhone 13")
ContentView()
.previewDevice("iPhone SE (2nd generation)")
.previewDisplayName("iPhone SE")
ContentView()
.previewDevice("iPad Pro (11-inch) (3rd generation)")
.previewDisplayName("iPad Pro")
}
}
}
调试修饰符
struct DebugView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") {
count += 1
}
}
.debugAction {
print("Current count: \(count)")
}
.border(Color.red) // 可视化视图边界
}
}
extension View {
func debugAction(_ action: () -> Void) -> some View {
#if DEBUG
action()
#endif
return self
}
}
单元测试
import XCTest
@testable import YourApp
class YourAppTests: XCTestCase {
func testExample() throws {
let viewModel = PostsViewModel()
XCTAssertTrue(viewModel.posts.isEmpty)
viewModel.fetchPosts()
// 使用 XCTestExpectation 测试异步代码
}
}
13. 性能优化
惰性加载
struct LazyLoadingView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(1...1000, id: \.self) { item in
RowView(item: item)
.onAppear {
print("Loading item \(item)")
}
}
}
}
}
}
struct RowView: View {
let item: Int
var body: some View {
Text("Row \(item)")
.padding()
}
}
绘图性能
struct HighPerformanceDrawing: View {
let colors: [Color] = [.red, .green, .blue, .orange, .purple]
var body: some View {
Canvas { context, size in
for _ in 0..<1000 {
let x = CGFloat.random(in: 0..<size.width)
let y = CGFloat.random(in: 0..<size.height)
let radius = CGFloat.random(in: 5..<20)
let color = colors.randomElement()!
let rect = CGRect(
x: x - radius,
y: y - radius,
width: radius * 2,
height: radius * 2
)
context.fill(
Path(ellipseIn: rect),
with: .color(color)
)
}
}
}
}
避免不必要的视图更新
struct OptimizedView: View {
@StateObject private var model = DataModel()
var body: some View {
VStack {
// 只有变化的子视图会重新计算
Text("Static Title")
// 使用 EquatableView 防止不必要的重绘
EquatableView(content: ExpensiveView(data: model.expensiveData))
Button("Update") {
model.update()
}
}
}
}
struct ExpensiveView: View, Equatable {
let data: [String]
var body: some View {
List(data, id: \.self) { item in
Text(item)
}
}
static func == (lhs: ExpensiveView, rhs: ExpensiveView) -> Bool {
lhs.data == rhs.data
}
}
14. 资源与进阶学习
官方资源
学习资源
开源项目
社区
SwiftUI 是一个强大而现代的 UI 框架,本教程涵盖了 SwiftUI 的主要特性和概念。要精通 SwiftUI 需要不断实践,建议通过实际项目来加深理解。随着 SwiftUI 的不断发展,保持学习最新特性和最佳实践非常重要。