SwiftUI 入门:Swift语法、项目结构、声明式视图、响应式数据、组件拆分

学习原生 iOS 开发,快速入门 Swift 语言,使用 SwiftUI 控件,类似 React 的声明式语法、响应式数据、组件拆分。

使用 Swift + SwiftUI + SwiftData ,可以低成本开发苹果生态的跨端应用。该方案虽然对设备系统兼容性有要求,但用于入门和体验原生 APP 开发流程足够了。

Swift 语法

通过以下链接快速过一遍 Swift 基础语法,并结合 Unwrap 这个免费 APP,适合有其他编程语言经验的开发者:

https://www.hackingwithswift.com/articles/242/learn-essential-swift-in-one-hour

SwiftUI 学习思路

掌握基本语法后,接着就可以学习 SwiftUI 的教程了:

也可以借助 AI 工具生成 SwiftUI 项目,在项目中学习,有了语言基础和基本的概念后,对 AI 生成的项目,调整起来也会比较顺手。

Xcode 基本操作

文件操作

新建文件: File > New > File From Template (快捷键 Cmd+N) ,如创建一个新的 SwiftUI 视图。

显示文件扩展名:XCode > Settings > General > File Extensions ,选择 Show All

查看代码 print 信息: View > Debug Area > Show Debug Area (快捷键:Shift+Cmd+Y)

真机调试

  • 在 Xcode 中配置真机设备部署,通过数据线连接真机,设置信任电脑
  • iPhone 需要开启开发者模式,并且在通用-设备管理中信任自己的开发者证书
  • 在 Windows - Device & Simulators 中管理真机设备
  • 真机联通后,后续可以使用 wifi 无线调试,无需额外配置

创建项目

在模板选择器中,选择 iOS 作为平台,选择 App 模板。

输入项目名称,如 WeSplit ,选择 SwiftUI 作为界面,选择 Swift 作为语言,存储选项设置为无。

需要创建 .gitignore 文件,忽略用户特定的 Xcode 设置 xcuserdata 、编译产物 build 和 DerivedData 等文件。

SwiftUI App 的基本结构

从文件目录可以看到以下文件被创建了出来:

源代码文件

  • WeSplitApp.swift :应用程序的入口点,定义了App结构体,通常包含@main属性标识应用入口。
  • ContentView.swift :主要视图文件,使用SwiftUI构建应用的用户界面。

资源文件

  • Assets.xcassets :资产目录,用于管理应用中的图像、颜色和图标等资源,包含AppIcon和AccentColor等预设资源集。

预览文件

  • Preview Content/Preview Assets.xcassets:预览专用的资源目录,例如存放预览用的图片、颜色配置等,与应用正式发布时使用的主资源目录相区分。

App 结构体

  • @main 属性标识这个是应用程序的入口点,专为SwiftUI设计
  • 结构体遵循 App 协议,必须实现 var body: some Scene 计算属性来提供应用的场景内容
  • WindowGroup :创建一个窗口场景,适用于多窗口应用
  • 包含 ContentView() 作为根视图,这是应用启动后显示的第一个界面
import SwiftUI

@main
struct iTourApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

ContentView 主要视图组件

ContentView.swift 是SwiftUI应用的主要视图组件,定义了应用的用户界面内容和布局结构。

View 协议

  • struct ContentView: View 声明遵循 View 协议的结构体,这是 SwiftUI 中构建界面的基本单元,所有文本、按钮、图像等都是 View ,包括自己的组合其他视图的布局。
  • View 协议只有一个要求,即有一个名为 body 的计算属性,该属性返回 some View。可以向视图结构体添加更多属性和方法。

视图组件

Swift UI 中,通过声明式语法定义视图,通过传入属性调用组件

  • Swift UI 通过声明式语法来构建界面
    • 本质上 VStack Form 等可嵌套内容的组件,都是闭包函数
    • 原理是 Swift 的“尾随闭包”语法允许将函数的最后一个参数移到函数调用外部,且“单表达式闭包”中“隐式返回语法”允许省略 return 关键字
  • 使用 VStack (垂直堆栈)组织界面元素,内部包裹了多个元素,并在外层应用了 .padding 修饰符调整样式
  • Text 定义文本,Image 定义图片,传递了 systemName 属性,应用了 .imageScale 、 .foregroundStyle 等修饰符调整样式
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

修饰符

SwiftUI 修饰符(Modifiers)是用于定制视图外观和行为的核心工具,它们以链式调用的方式作用于视图,每个修饰符都会返回一个新视图(原始视图的变体)。

某些修饰符是通用的,某些修饰符仅适用于特定容器(如 .listRowSeparator(_:) 只对 List 有效)。

修饰符链式调用,顺序敏感。定义的顺序影响最终效果(例如先加背景还是先加内边距)。

Text("A").padding().background(.red) // 背景包裹整个文本+内边距
Text("B").background(.red).padding() // 背景仅包裹文本

如先定义圆角再定义背景,圆角不会生效于背景,呈现的背景图里没有圆角。

.cornerRadius(29) // 圆角不会
.background(.blue) 

实时预览

开启实时预览:编辑区右上角按钮,打开更多菜单,启用 Canvas ,并设置 Layout - Canvas On Right 。

实时预览中的刷新快捷键:“Preview paused” 时需手动点击刷新按钮,可使用快捷键:Option+Cmd+P

Preview 声明为视图创建预览,支持在 Xcode 中实时查看界面效果。

在选择真机设备后,会在 Xcode Previews 应用中呈现预览效果。

Preview 比起使用模拟器或者真机安装应用的方式,效率和体验都提升了很多。

#Preview {
    ContentView()
}

Selectable

Preview 预览时,左下角默认选中 Live 模式,可切换 Selectable 模式,点击可选择 SwiftUI 控件。

有些组件嵌套后不太好选中某个组件,如使用 NavigationStack 后里面的组件无法再被选中。

SwiftUI Insecptor

可以在 View -> Insecptors -> Show Insecptor 打开 SwiftUI Insecptor ,可操作选中的组件对应的属性和修饰符,如 Button 修改背景色、字体色等。

Live 模式,可通过把光标放到代码中的某个组件名词中来选中该组件,即可直接操作 Insecptor 。

Insecptor 中的属性值不是全量的,适合初学时做参考。

响应式数据

创建响应式数据

通过 @State 创建响应式数据,定义为 private 属性。值被修改后,会刷新界面中回显的内容。

@State private var tapCount = 0

var body: some View {
    Button("\(tapCount)") {
        tapCount += 1
    }
}

双向绑定

通过 $ 符号创建双向绑定,可用于 TextField 等表单控件。仅用于显示时,不需要 $ 符号。

@State private var name = ""
var body: some View {
    Form {
        TextField("Enter your name", text: $name)
        Text("Your name is \(name)")
    }
}

计算属性

计算属性就通过 Swift 结构体的,如果参与计算的是 State ,改变后对应计算属性也会更新。

var totalPerPerson: Double {
    let peopleCount = Double(numberOfPeople + 2)
    let tipSelection = Double(tipPercentage)

    let tipValue = checkAmount / 100 * tipSelection
    let grandTotal = checkAmount + tipValue
    let amountPerPerson = grandTotal / peopleCount

    return amountPerPerson
}

数据监听

通过修饰符 .onChange(of: isReminderEnabled) { _ in ... } 监听值变化并执行操作,如:

Toggle("开启每日提醒", isOn: $isReminderEnabled)
    .onChange(of: isReminderEnabled) { _, newValue in
        if newValue {
            notificationManager.scheduleDailyReminder(at: reminderTime)
        } else {
            notificationManager.cancelAllNotifications()
        }
    }

@FocusState 绑定输入框焦点状态

用于关闭键盘,避免弹出键盘后不会消失。

定义变量值,不需要设置默认值:

@FocusState private var amountIsFocused: Bool

通过 TextField 的 .focused() 修饰符绑定该状态:

.focused($amountIsFocused)

NavigationStack 增加 toolbar 修饰符,也是作用在直接子元素,用于判断当键盘激活时显示菜单按钮

.toolbar {
    if amountIsFocused {
        Button("Done") {
            amountIsFocused = false
        }
    }
}

设置 amountIsFocused 为 false 后,会生效到 TextField 的 focused 修饰符里,使得键盘自动关闭。

拆分组件

定义一个新组件(struct + View),如定义了一个可复用的按钮组件,接受 title 作为参数:

struct MyButton: View {
    var title: String

    var body: some View {
        Button(action: {
            print("按钮被点击")
        }) {
            Text(title)
                .padding()
                .background(Color.blue)
                .foregroundColor(.white)
                .cornerRadius(8)
        }
    }
}

在主视图中使用:

struct ContentView: View {
    var body: some View {
        VStack {
            MyButton(title: "点击我")
            MyButton(title: "提交")
        }
    }
}

拆分的组件可以单独在 Preview 中自由调用,方便临时调试单个组件,或使用 Selectable 模式查看。

#Preview {
    VStack {
        MyButton(title: "点击我")
        MyButton(title: "提交")
    }
}

自定义修饰符

通过创建可复用的封装简化代码:

// 定义修饰符
struct PrimaryButtonStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(10)
    }
}

// 使用扩展简化调用
extension View {
    func primaryButtonStyle() -> some View {
        modifier(PrimaryButtonStyle())
    }
}

// 应用
Button("Submit") { ... }
    .primaryButtonStyle()

项目设置与 icon

在左侧项目导航栏中,找到 Assets.xcassets ,选中 AppIcon,有各种 icon slot ,把对应尺寸的 PNG 拖拽到对应 slot 上,即可设置成功。

进入项目设置:在左侧文件导航栏中,点击最顶层的蓝色项目图标(项目名称),在 General - App Icons and Launch Images 设置图标,默认已设置了该图标的名字。