UI布局
- Swift开发不使用StoryBoard和xib来进行UI布置,属性和事件也不需要连线。没有单独的UIViewController的概念。是使用SwiftUI,声明式布局。
一个最简单的布局
css
struct ContentView:View {
var body: some View {
Text("hello word")
}
}
ContentView是一个结构体 ,它遵循了View协议。- 协议唯一的要求是提供一个
body计算属性,其类型是some View(某种视图)。 body描述了ContentView这个视图具体由什么构成(这里是一个hello word文本)。
iOS中的路由 NavigationView
scss
// 导航容器
NavigationView {
// 根视图
VStack {
NavigationLink("跳转到详情页", destination: DetailView())
}
.navigationTitle("首页") // 导航栏标题
.navigationBarTitleDisplayMode(.inline) // 标题显示模式(large/inline/automatic)
.navigationBarItems(trailing: Button("设置") {
print("点击设置")
}) // 右侧按钮
}
- 这里的路由很简单,利用NavigationView包含根视图。利用NavigationLink,进行点击跳转到目标视图。
视图的三大支柱
属性
- 属性:存储视图的状态与数据
swift
struct GreetingView: View {
let name: String // 传入的常量属性
@State private var isOn = false // 私有的可变状态
var body: some View { ... }
}
- 用常规属性(如
let name)存储传入的、不变的数据。 - 用
@State,@Binding,@ObservedObject等属性包装器 来管理可变状态,这是 SwiftUI 数据驱动的核心。
修饰符
- 修饰符:修改视图的外观与行为
scss
Text("示例")
.font(.headline) // 修改字体
.padding() // 添加内边距
.background(.yellow) // 设置背景
.onTapGesture { // 添加交互手势
print("被点击")
}
- 链式调用,顺序有时会影响效果。
- 每个修饰符(如
.font,.padding)通常会返回一个新的视图,而非修改原视图。
视图构建
- 视图构建器:组合多个视图
body 中使用特定的语法(由 @ViewBuilder 驱动)来组合视图:
scss
var body: some View {
VStack { // 垂直堆叠多个子视图
Image(systemName: "star")
Text("标题")
HStack { // 内嵌一个水平堆叠
Text("左")
Text("右")
}
}
}
- 常用的容器视图:
VStack(垂直)、HStack(水平)、ZStack(重叠)、List(列表)、Group(逻辑分组)。
SwiftUI交互事件
SwiftUI中处理点击事件主要有Button控件 和手势修饰符两种核心方式。为帮助你快速选择,下表汇总了它们的特点和典型用途:
| 方法 | 核心组件/修饰符 | 主要特点 | 适用场景 |
|---|---|---|---|
| 控件触发 | Button |
语义化控件,内置交互样式(如按压效果) | 按钮、明确的用户操作 |
| 手势识别 | onTapGesture |
通用点击检测,可加在任何视图上 | 自定义视图、图片、文本等非按钮元素的点击 |
| 手势识别 | TapGesture |
更灵活的手势配置,可组合使用 | 需要与其它手势(如长按)配合的场景 |
方法一:使用Button控件
Button 是用于触发操作的标准控件,使用 action 闭包来处理点击事件。你可以方便地自定义其外观。
swift
Button(action: {
// 点击后执行的操作
print("按钮被点击")
}) {
// 定义按钮外观
Text("点击我")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
如果你想以编程方式触发按钮的点击事件 (例如在一定时间后自动点击),可以直接调用该按钮action闭包中的逻辑。
方法二:使用手势修饰符
1. 使用 onTapGesture 修饰符 这是为任何视图(如Text、Image)添加点击监听最快捷的方式。
swift
Text("点击这段文字")
.onTapGesture {
print("文字被点击")
}
2. 使用 TapGesture 手势类型 它比onTapGesture更灵活,允许你使用 count 参数来监听双击或多击事件。
swift
Text("双击我")
.gesture(
TapGesture(count: 2)
.onEnded { _ in
print("检测到双击")
}
)
方法三:
- 用NavigationLink包装的组件,可以直接跳转至目标页。
- 用Link包装的组件,可直接跳转至目标网址。
进阶技巧与常见问题
掌握了基本用法后,了解以下技巧能帮你解决更复杂的需求:
- 控制按钮点击频率 :通过
disabled(_:)修饰符和状态变量,可以防止按钮在短时间内被重复提交。 - 在动态列表(ForEach)中处理点击 :关键在于确保数据模型(如
@State数组)是可变的,这样点击后更新数据,视图才会随之刷新。 - 处理手势冲突 :当多个手势重叠时,可以使用
highPriorityGesture()或simultaneousGesture()来管理优先级或允许同时识别。 - 高级手势 :除了点击,SwiftUI还内置了
LongPressGesture(长按)、DragGesture(拖拽)等,可通过.gesture()修饰符使用。
实际开发注意事项
在应用中处理点击事件时,还需要留意两点:
- 视图层次影响:如果父视图有手势,可能会被子视图拦截。确保手势添加在了正确的视图层级上。
- 状态管理 :点击操作常伴随界面变化(如颜色、显示内容)。务必使用
@State、@ObservedObject等将相关数据声明为响应式,这样视图才会自动更新。
Link 控件解析
Link 控件解析
swift
Link("lil.software", destination: URL(string: "https://lil.software")!)
- 第一个参数
"lil.software":是用户在界面上看到的可点击文本。 - 第二个参数
destination:指定点击后要跳转的目标网址(URL)。这里是https://lil.software。
用户点击蓝色、带下划线的 "lil.software" 文字后,系统会自动打开 Safari 浏览器并跳转到这个网站。
Link 与 Button 的核心区别
虽然看起来像按钮,但 Link 和 Button 有明确分工:
| 控件 | 核心用途 | 系统行为 | 默认样式 |
|---|---|---|---|
Link |
专用于打开本地/网络URL | 跳转 Safari 或相应 App | 蓝色文字,带下划线 |
Button |
触发应用内任意操作 | 执行你定义的代码(如弹窗、导航) | 无默认样式,需完全自定义 |
简单来说 :Link = 专用于"跳转出去"的快捷工具;Button = 处理"内部事务"的通用触发器。
如何自定义 Link 样式
和 Button 一样,你也可以用修饰符来改变 Link 的外观,让它更符合你的应用设计:
swift
Link("访问官网", destination: URL(string: "https://www.example.com")!)
.font(.headline)
.foregroundColor(.white)
.padding()
.background(Color.orange)
.cornerRadius(8)
// 这样它就看起来像一个圆角橙色按钮,但点击功能仍是打开网页
使用提示
- 确保 URL 有效 :如果提供的
URL(string:)初始化失败(比如链接格式错误),Link在点击时可能不会有任何反应。 - 平台差异:在 iOS/iPadOS 上点击会跳转至 Safari;在 macOS 上会使用默认浏览器打开。
状态管理
一个最小的状态管理实例:
scss
import SwiftUI
struct CounterView: View {
// 1. 使用 @State 创建可观察的状态
@State private var count: Int = 0
var body: some View {
VStack(spacing: 20) {
// 2. 显示状态值
Text("点击次数: \(count)")
.font(.largeTitle)
// 3. 按钮修改状态值
Button("点我增加") {
// 修改状态,视图会自动更新
count += 1
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
// 4. 另一个按钮重置状态
Button("重置") {
count = 0
}
.foregroundColor(.red)
}
.padding()
}
}
// 预览
#Preview {
CounterView()
}
Swift动画相关
SwiftUI 的动画是声明式 且状态驱动的。与直接描述动画过程不同,你只需声明视图的最终状态,SwiftUI 会自动计算并渲染出平滑的过渡效果。
核心概念:隐式动画 vs. 显式动画
| 类型 | 使用方法 | 特点 | 适用场景 |
|---|---|---|---|
| 隐式动画 | .animation(_:) 修饰符 |
自动为指定视图的所有合格变化添加动画。 | 视图的简单属性变化(如缩放、颜色)。 |
| 显式动画 | withAnimation { } 闭包 |
明确地包裹触发状态变化的代码,作用范围更精确。 | 响应事件(如按钮点击),需要同步动画多个视图。 |
隐式动画示例
在视图上添加 .animation 修饰符,该视图所有可动画的变化都会生效。
swift
struct ImplicitAnimationView: View {
@State private var isScaled = false
@State private var angle: Double = 0
var body: some View {
VStack(spacing: 30) {
// 1. 缩放动画
Circle()
.fill(isScaled ? .orange : .blue)
.frame(width: isScaled ? 150 : 100,
height: isScaled ? 150 : 100)
.scaleEffect(isScaled ? 1.5 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.6),
value: isScaled) // 指定监听 isScaled 变化
// 2. 旋转动画
Rectangle()
.fill(.green)
.frame(width: 100, height: 100)
.rotationEffect(.degrees(angle))
.animation(.linear(duration: 2), value: angle) // 线性旋转
// 3. 控制按钮
Button("触发动画") {
// 改变状态,视图会自动产生动画
isScaled.toggle()
angle += 180
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.padding()
}
}
显式动画示例
使用 withAnimation 函数包裹状态变化的代码,可以更精确地控制。
swift
struct ExplicitAnimationView: View {
@State private var isExpanded = false
@State private var offsetX: CGFloat = 0
var body: some View {
VStack(spacing: 40) {
// 1. 多个视图同步动画
RoundedRectangle(cornerRadius: isExpanded ? 50 : 10)
.fill(isExpanded ? .purple : .pink)
.frame(width: isExpanded ? 300 : 100,
height: isExpanded ? 300 : 100)
.offset(x: offsetX)
.animation(.easeInOut(duration: 0.6), value: isExpanded)
HStack(spacing: 20) {
Button("展开并右移") {
// 用一个动画闭包控制两个状态变化
withAnimation(.spring(dampingFraction: 0.6)) {
isExpanded = true
offsetX = 100
}
}
Button("重置") {
// 这个重置操作也有动画
withAnimation(.easeOut(duration: 0.8)) {
isExpanded = false
offsetX = 0
}
}
.tint(.red)
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
转场动画
当视图插入或移除视图层次时,使用 .transition 指定动画效果。
swift
struct TransitionView: View {
@State private var showMessage = false
var body: some View {
VStack {
Button(showMessage ? "隐藏消息" : "显示消息") {
withAnimation(.easeInOut(duration: 0.8)) {
showMessage.toggle()
}
}
.padding()
if showMessage {
Text("你好,SwiftUI!")
.font(.title)
.padding()
.background(Color.yellow)
.cornerRadius(10)
.transition(
.asymmetric( // 进入和退出使用不同动画
insertion: .scale.combined(with: .opacity), // 进入:缩放+淡入
removal: .move(edge: .leading).combined(with: .opacity) // 退出:向左滑出+淡出
)
)
}
Spacer()
}
.padding()
}
}
动画曲线与时长预设
SwiftUI 提供多种内置动画曲线:
swift
VStack(spacing: 20) {
// 1. 基础缓动曲线
Circle()
.animation(.easeIn(duration: 1), value: isAnimated) // 先慢后快
Circle()
.animation(.easeOut(duration: 1), value: isAnimated) // 先快后慢
Circle()
.animation(.easeInOut(duration: 1), value: isAnimated) // 慢-快-慢
// 2. 弹性动画
Circle()
.animation(.spring(
response: 0.5, // 动画持续时间 (seconds)
dampingFraction: 0.6, // 阻尼:越小弹力越强 (0-1)
blendDuration: 0.25 // 混合时间
), value: isAnimated)
// 3. 重复动画
Circle()
.animation(
.linear(duration: 1)
.repeatForever(autoreverses: true), // 永久重复且自动反向
value: isAnimated
)
// 4. 延迟动画
Circle()
.animation(
.easeInOut(duration: 1)
.delay(0.5), // 延迟 0.5 秒执行
value: isAnimated
)
}
实际应用:加载动画
一个实用的加载指示器动画:
swift
struct LoadingAnimationView: View {
@State private var isLoading = false
@State private var progress: CGFloat = 0.0
var body: some View {
VStack(spacing: 40) {
// 1. 旋转加载指示器
Circle()
.trim(from: 0, to: 0.7) // 剪裁出缺口
.stroke(Color.blue, lineWidth: 5)
.frame(width: 50, height: 50)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.animation(
.linear(duration: 1)
.repeatForever(autoreverses: false),
value: isLoading
)
// 2. 进度条动画
VStack {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.frame(width: geometry.size.width, height: 8)
.opacity(0.3)
.foregroundColor(.gray)
Rectangle()
.frame(
width: min(progress * geometry.size.width,
geometry.size.width),
height: 8
)
.foregroundColor(.blue)
.animation(.linear(duration: 0.5), value: progress)
}
.cornerRadius(4)
}
.frame(height: 20)
Text("\(Int(progress * 100))%")
.font(.caption)
}
.frame(width: 200)
// 3. 控制按钮
Button(isLoading ? "停止加载" : "开始加载") {
if isLoading {
stopLoading()
} else {
startLoading()
}
}
.buttonStyle(.borderedProminent)
}
.onAppear {
startLoading()
}
}
func startLoading() {
isLoading = true
progress = 0
// 模拟进度更新
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
if progress >= 1.0 {
timer.invalidate()
isLoading = false
} else {
withAnimation(.linear(duration: 0.1)) {
progress += 0.05
}
}
}
}
func stopLoading() {
isLoading = false
}
}
动画最佳实践
- 明确动画依赖值 :使用
.animation(.easeInOut, value: someState)指定动画监听的状态,避免不必要的动画。 - 性能优先:优先动画简单属性(位置、大小、透明度、旋转),复杂的形状路径动画可能影响性能。
- 组合使用 :将
.transition与.animation结合,创建更丰富的视图层级变化效果。 - 测试中断:确保用户能随时中断动画(如快速点击),避免界面"卡死"。
Swift网络请求相关
Swift 最常用的网络请求框架是 Alamofire (第三方)和 URLSession(苹果官方)。以下是它们的特点对比和简单用法:
框架对比
| 框架 | 类型 | 特点 | 适合场景 |
|---|---|---|---|
| Alamofire | 第三方框架 | 语法优雅、功能丰富、社区活跃 | 快速开发、复杂网络需求 |
| URLSession | 苹果官方 | 无需依赖、轻量可控、安全可靠 | 简单需求、不想引入第三方库 |
Alamofire(最流行)
安装依赖(Swift Package Manager)
在 Xcode 项目中:
- File → Add Packages...
- 输入 URL:
https://github.com/Alamofire/Alamofire.git - 选择版本规则(推荐 "Up to Next Major")
- 点击 Add Package
简单使用示例
swift
import Alamofire
// 1. 基础 GET 请求
func fetchDataWithAlamofire() {
AF.request("https://jsonplaceholder.typicode.com/posts/1")
.responseJSON { response in
switch response.result {
case .success(let value):
print("请求成功: \(value)")
case .failure(let error):
print("请求失败: \(error)")
}
}
}
// 2. 带参数的 GET 请求
func fetchDataWithParameters() {
let parameters = ["userId": "1"]
AF.request("https://jsonplaceholder.typicode.com/posts",
parameters: parameters)
.responseDecodable(of: [Post].self) { response in
switch response.result {
case .success(let posts):
print("获取到 \(posts.count) 条帖子")
case .failure(let error):
print("错误: \(error)")
}
}
}
// 3. POST 请求
func postData() {
let parameters = [
"title": "测试标题",
"body": "测试内容",
"userId": 1
] as [String : Any]
AF.request("https://jsonplaceholder.typicode.com/posts",
method: .post,
parameters: parameters,
encoding: JSONEncoding.default)
.responseJSON { response in
print("POST 响应: \(response)")
}
}
// 4. 配合 Codable 模型
struct Post: Codable {
let id: Int?
let title: String
let body: String
let userId: Int
}
func fetchDecodableData() {
AF.request("https://jsonplaceholder.typicode.com/posts/1")
.responseDecodable(of: Post.self) { response in
if let post = response.value {
print("帖子标题: \(post.title)")
}
}
}
URLSession(苹果官方,无需依赖)
简单使用示例
swift
import Foundation
// 1. 基础 GET 请求
func fetchDataWithURLSession() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// 确保在主线程更新 UI
DispatchQueue.main.async {
if let error = error {
print("请求失败: \(error)")
return
}
guard let data = data else {
print("没有数据")
return
}
do {
// 解析 JSON
let json = try JSONSerialization.jsonObject(with: data, options: [])
print("请求成功: \(json)")
} catch {
print("JSON 解析失败: \(error)")
}
}
}
task.resume() // 开始请求
}
// 2. 配合 Codable 的改进版本
func fetchDataWithCodable() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
DispatchQueue.main.async {
if let error = error {
print("错误: \(error)")
return
}
guard let data = data else {
print("没有数据")
return
}
do {
// 使用 JSONDecoder 解码到模型
let decoder = JSONDecoder()
let post = try decoder.decode(Post.self, from: data)
print("获取到帖子: \(post.title)")
} catch {
print("解码失败: \(error)")
}
}
}
task.resume()
}
// 3. POST 请求
func postWithURLSession() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body = [
"title": "测试标题",
"body": "测试内容",
"userId": 1
] as [String: Any]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [])
} catch {
print("创建请求体失败: \(error)")
return
}
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
// 处理响应...
}
}
task.resume()
}
网络层封装示例(实用版)
对于真实项目,建议进行简单封装:
swift
import Alamofire
class NetworkManager {
static let shared = NetworkManager()
private init() {}
// 通用请求方法
func request(
_ url: String,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
completion: @escaping (Result) -> Void
) {
AF.request(url,
method: method,
parameters: parameters,
encoding: JSONEncoding.default)
.validate() // 验证响应状态码
.responseDecodable(of: T.self) { response in
switch response.result {
case .success(let value):
completion(.success(value))
case .failure(let error):
completion(.failure(error))
}
}
}
}
// 使用封装后的方法
func fetchUserData() {
NetworkManager.shared.request(
"https://jsonplaceholder.typicode.com/users/1"
) { (result: Result) in
switch result {
case .success(let user):
print("用户: \(user.name)")
case .failure(let error):
print("错误: \(error)")
}
}
}
struct User: Codable {
let id: Int
let name: String
let email: String
}
选择建议
- 新手或简单项目 :从 URLSession 开始,理解基础原理
- 生产环境或复杂需求 :使用 Alamofire,提升开发效率
- 需要高级功能 :Alamofire 支持:
- 请求/响应拦截器
- 网络状态监听
- 自动重试
- 文件上传/下载进度
- 证书验证
实际项目使用技巧
Alamofire 进阶用法:
swift
// 添加请求头
let headers: HTTPHeaders = [
"Authorization": "Bearer token123",
"Accept": "application/json"
]
AF.request(url, headers: headers).responseJSON { response in
// ...
}
// 上传图片
AF.upload(multipartFormData: { multipartFormData in
if let imageData = image.jpegData(compressionQuality: 0.8) {
multipartFormData.append(imageData,
withName: "image",
fileName: "photo.jpg",
mimeType: "image/jpeg")
}
}, to: "https://api.example.com/upload").responseJSON { response in
// 处理上传结果
}
错误处理增强:
swift
enum NetworkError: Error {
case invalidURL
case noData
case decodingError
case serverError(String)
}
func handleNetworkError(_ error: AFError) -> NetworkError {
if error.isResponseSerializationError {
return .decodingError
} else if let statusCode = error.responseCode {
return .serverError("服务器错误: \(statusCode)")
} else {
return .serverError(error.localizedDescription)
}
}
建议
- 学习阶段:先掌握 URLSession,理解网络请求基本原理
- 开发阶段:根据项目需求选择框架,大部分情况下 Alamofire 更高效
- 保持更新:关注 Swift 官方网络库的更新,未来可能会有更好用的原生方案
Swift 条件编译指令
一、#if os(iOS) 核心本质:Swift 条件编译指令
#if os(iOS) 是 Swift 提供的编译时条件判断指令 ,核心作用是:根据编译目标的操作系统(平台),决定是否编译某一段代码 ------只有当工程的编译目标是 iOS 时,#if os(iOS) 和 #else 之间的代码才会被编译进最终产物;非 iOS 平台(如 macOS、watchOS、tvOS、visionOS 等)则编译 #else 分支的代码。
二、这段代码中该指令的具体作用
swift
#if os(iOS)
// 仅 iOS 平台执行:Label 加字体+垂直内边距
Label(title, systemImage: icon).font(.headline).padding(.vertical, 8)
#else
// 非 iOS 平台(macOS/watchOS/tvOS 等)执行:仅基础 Label
Label(title, systemImage: icon)
#endif
- iOS 平台 :给
Label增加font(.headline)(标题字体)和padding(.vertical, 8)(垂直方向8pt内边距),适配 iOS 系统的 UI 设计规范(比如 iOS 列表项通常需要内边距和醒目字体,贴合 Settings/备忘录等原生 App 风格); - 非 iOS 平台 :仅保留基础
Label,不添加额外样式------因为不同平台的 UI 逻辑不同(比如 macOS 的Label嵌入NavigationLink时,默认样式已适配侧边栏/列表布局,额外 padding 会导致布局拥挤;watchOS 屏幕尺寸极小,多余内边距会浪费空间)。
三、关键语法细节
1. 完整语法结构
swift
#if 条件
// 条件满足时编译的代码
#else
// 条件不满足时编译的代码
#endif // 必须配对结束,否则编译报错
#if/#else/#endif是 Swift 保留的编译指令 ,不是普通的运行时if-else;- 编译阶段就决定代码是否被包含,而非运行时判断(这是和
if #available(...)的核心区别)。
2. 支持的平台参数
os(平台) 中可填写的常用平台值:
| 参数 | 对应平台 | 补充说明 |
|---|---|---|
| iOS | iOS/iPadOS | iPadOS 编译时仍识别为 iOS |
| macOS | macOS | 包括 Intel/Apple Silicon 机型 |
| watchOS | watchOS | 苹果手表系统 |
| tvOS | tvOS | 苹果电视系统 |
| visionOS | visionOS | Vision Pro 系统 |
| Linux/Windows | Linux/Windows | Swift 跨平台支持 |
3. 多条件组合
可通过 ||(或)、&&(与)组合多个条件,比如:
swift
#if os(iOS) || os(visionOS) // iOS 或 Vision Pro 平台
Label(title, systemImage: icon).font(.headline).padding(.vertical, 8)
#elseif os(macOS) // macOS 平台单独处理
Label(title, systemImage: icon).font(.subheadline).padding(.horizontal, 4)
#else // 其他平台(watchOS/tvOS)
Label(title, systemImage: icon)
#endif
四、和 #available 的核心区别(易错点)
很多开发者会混淆 #if os(...) 和 #available,两者完全不同:
| 特性 | #if os(iOS)(条件编译) |
#available(iOS 17.0, *)(运行时判断) |
|---|---|---|
| 执行阶段 | 编译时(决定代码是否被打包) | 运行时(代码已打包,仅判断是否执行) |
| 作用 | 区分不同平台编译不同代码 | 区分同一平台的不同系统版本执行代码 |
| 产物体积 | 非目标平台代码不会被编译,体积小 | 所有分支代码都编译,体积稍大 |
| 示例场景 | iOS 加 padding,macOS 不加 | iOS 17+ 用新 API,iOS 16- 用兼容代码 |
示例对比:
swift
// 1. 条件编译(不同平台编译不同代码)
#if os(iOS)
// 仅 iOS 编译这段代码,macOS 产物中无此代码
Label(/* iOS 样式 */)
#endif
// 2. 运行时判断(同一平台不同版本执行不同代码)
if #available(iOS 17.0, *) {
// iOS 17+ 设备运行时执行
Label(/* iOS 17 新样式 */)
} else {
// iOS 16- 设备运行时执行
Label(/* 兼容样式 */)
}
五、该写法的设计初衷(为什么要这么做)
这段代码是 SwiftUI 跨平台开发的典型实践:
- SwiftUI 天然跨平台:同一份代码可运行在 iOS/macOS/watchOS 等平台,但不同平台的 UI 规范、屏幕尺寸、交互逻辑差异大;
- 按需定制样式:iOS 平台需要额外的 padding/font 优化视觉,其他平台保持原生样式即可,避免"一刀切"导致的跨平台布局问题;
- 减少冗余代码:通过条件编译,非目标平台的样式代码不会被编译,降低最终产物体积,且代码结构更清晰。
六、拓展:其他常用条件编译指令
除了 os(...),Swift 还支持其他实用的条件编译:
-
判断是否为模拟器:
swift#if targetEnvironment(simulator) // 仅模拟器编译的代码(比如测试日志) print("运行在模拟器中") #else // 真机编译的代码 print("运行在真机中") #endif -
判断编译器版本:
swift#if compiler(>=5.9) // Swift 5.9+ 编译器支持的语法(比如新的宏) #endif -
判断调试/发布模式:
swift#if DEBUG // 调试模式编译(比如打印调试日志) Label(title, systemImage: icon).border(.red) // 调试时显示边框 #else // 发布模式编译 Label(title, systemImage: icon) #endif
总结
这段代码中的 #if os(iOS) 是为了在编译阶段区分 iOS 和其他平台 ,给 iOS 端的 Label 增加专属的字体和内边距样式,其他平台保持基础样式,既兼顾 SwiftUI 跨平台的代码复用,又适配不同平台的 UI 规范。核心要记住:它是编译时指令 ,而非运行时判断,这是和 #available 最关键的区别。