阅读视图

发现新文章,点击刷新页面。

IOS开发SwiftUI相关学习记录

UI布局

  • Swift开发不使用StoryBoard和xib来进行UI布置,属性和事件也不需要连线。没有单独的UIViewController的概念。是使用SwiftUI,声明式布局。

一个最简单的布局

struct ContentView:View {
    var body: some View {
        Text("hello word")
    }
}
  • ContentView 是一个结构体,它遵循了 View 协议。
  • 协议唯一的要求是提供一个 body 计算属性,其类型是 some View(某种视图)。
  • body 描述了 ContentView 这个视图具体由什么构成(这里是一个hello word文本)。

iOS中的路由 NavigationView

// 导航容器
NavigationView {
    // 根视图
    VStack {
        NavigationLink("跳转到详情页", destination: DetailView())
    }
    .navigationTitle("首页") // 导航栏标题
    .navigationBarTitleDisplayMode(.inline) // 标题显示模式(large/inline/automatic)
    .navigationBarItems(trailing: Button("设置") {
        print("点击设置")
    }) // 右侧按钮
}
  • 这里的路由很简单,利用NavigationView包含根视图。利用NavigationLink,进行点击跳转到目标视图。

视图的三大支柱

属性

  • 属性:存储视图的状态与数据
struct GreetingView: View {
   let name: String // 传入的常量属性
   @State private var isOn = false // 私有的可变状态
   
   var body: some View { ... }
}
  1. 用常规属性(如 let name)存储传入的、不变的数据
  2. 用 @State@Binding@ObservedObject 等属性包装器来管理可变状态,这是 SwiftUI 数据驱动的核心。

修饰符

  • 修饰符:修改视图的外观与行为
Text("示例")
    .font(.headline) // 修改字体
    .padding()       // 添加内边距
    .background(.yellow) // 设置背景
    .onTapGesture {  // 添加交互手势
        print("被点击")
    }
  1. 链式调用,顺序有时会影响效果。
  2. 每个修饰符(如 .font.padding)通常会返回一个新的视图,而非修改原视图。

视图构建

  • 视图构建器:组合多个视图

body 中使用特定的语法(由 @ViewBuilder 驱动)来组合视图:

var body: some View {
    VStack { // 垂直堆叠多个子视图
        Image(systemName: "star")
        Text("标题")
        HStack { // 内嵌一个水平堆叠
            Text("左")
            Text("右")
        }
    }
}
  1. 常用的容器视图:VStack(垂直)、HStack(水平)、ZStack(重叠)、List(列表)、Group(逻辑分组)。

SwiftUI交互事件

SwiftUI中处理点击事件主要有Button控件手势修饰符两种核心方式。为帮助你快速选择,下表汇总了它们的特点和典型用途:

方法 核心组件/修饰符 主要特点 适用场景
控件触发 Button 语义化控件,内置交互样式(如按压效果) 按钮、明确的用户操作
手势识别 onTapGesture 通用点击检测,可加在任何视图上 自定义视图、图片、文本等非按钮元素的点击
手势识别 TapGesture 更灵活的手势配置,可组合使用 需要与其它手势(如长按)配合的场景

方法一:使用Button控件

Button 是用于触发操作的标准控件,使用 action 闭包来处理点击事件。你可以方便地自定义其外观。

Button(action: {
    // 点击后执行的操作
    print("按钮被点击")
}) {
    // 定义按钮外观
    Text("点击我")
        .padding()
        .background(Color.blue)
        .foregroundColor(.white)
        .cornerRadius(10)
}

如果你想以编程方式触发按钮的点击事件(例如在一定时间后自动点击),可以直接调用该按钮action闭包中的逻辑。

方法二:使用手势修饰符

1. 使用 onTapGesture 修饰符 这是为任何视图(如TextImage)添加点击监听最快捷的方式。

Text("点击这段文字")
    .onTapGesture {
        print("文字被点击")
    }

2. 使用 TapGesture 手势类型 它比onTapGesture更灵活,允许你使用 count 参数来监听双击或多击事件。

Text("双击我")
    .gesture(
        TapGesture(count: 2)
            .onEnded { _ in
                print("检测到双击")
            }
    )

方法三:

  • 用NavigationLink包装的组件,可以直接跳转至目标页。
  • 用Link包装的组件,可直接跳转至目标网址。

进阶技巧与常见问题

掌握了基本用法后,了解以下技巧能帮你解决更复杂的需求:

  • 控制按钮点击频率:通过disabled(_:)修饰符和状态变量,可以防止按钮在短时间内被重复提交。
  • 在动态列表(ForEach)中处理点击:关键在于确保数据模型(如@State数组)是可变的,这样点击后更新数据,视图才会随之刷新。
  • 处理手势冲突:当多个手势重叠时,可以使用 highPriorityGesture()simultaneousGesture() 来管理优先级或允许同时识别。
  • 高级手势:除了点击,SwiftUI还内置了LongPressGesture(长按)、DragGesture(拖拽)等,可通过.gesture()修饰符使用。

实际开发注意事项

在应用中处理点击事件时,还需要留意两点:

  • 视图层次影响:如果父视图有手势,可能会被子视图拦截。确保手势添加在了正确的视图层级上。
  • 状态管理:点击操作常伴随界面变化(如颜色、显示内容)。务必使用@State@ObservedObject等将相关数据声明为响应式,这样视图才会自动更新。

Link 控件解析

Link 控件解析

Link("lil.software", destination: URL(string: "https://lil.software")!)
  • 第一个参数 "lil.software":是用户在界面上看到的可点击文本。
  • 第二个参数 destination:指定点击后要跳转的目标网址(URL)。这里是 https://lil.software

用户点击蓝色、带下划线的 “lil.software” 文字后,系统会自动打开 Safari 浏览器并跳转到这个网站。

Link 与 Button 的核心区别

虽然看起来像按钮,但 LinkButton 有明确分工:

控件 核心用途 系统行为 默认样式
Link 专用于打开本地/网络URL 跳转 Safari 或相应 App 蓝色文字,带下划线
Button 触发应用内任意操作 执行你定义的代码(如弹窗、导航) 无默认样式,需完全自定义

简单来说Link = 专用于“跳转出去”的快捷工具;Button = 处理“内部事务”的通用触发器。

如何自定义 Link 样式

Button 一样,你也可以用修饰符来改变 Link 的外观,让它更符合你的应用设计:

Link("访问官网", destination: URL(string: "https://www.example.com")!)
    .font(.headline)
    .foregroundColor(.white)
    .padding()
    .background(Color.orange)
    .cornerRadius(8)
// 这样它就看起来像一个圆角橙色按钮,但点击功能仍是打开网页

使用提示

  1. 确保 URL 有效:如果提供的 URL(string:) 初始化失败(比如链接格式错误),Link 在点击时可能不会有任何反应。
  2. 平台差异:在 iOS/iPadOS 上点击会跳转至 Safari;在 macOS 上会使用默认浏览器打开。

状态管理

一个最小的状态管理实例:

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 修饰符,该视图所有可动画的变化都会生效。

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 函数包裹状态变化的代码,可以更精确地控制。

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 指定动画效果。

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 提供多种内置动画曲线:

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
        )
}

实际应用:加载动画

一个实用的加载指示器动画:

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
    }
}

动画最佳实践

  1. 明确动画依赖值:使用 .animation(.easeInOut, value: someState) 指定动画监听的状态,避免不必要的动画。
  2. 性能优先:优先动画简单属性(位置、大小、透明度、旋转),复杂的形状路径动画可能影响性能。
  3. 组合使用:将 .transition.animation 结合,创建更丰富的视图层级变化效果。
  4. 测试中断:确保用户能随时中断动画(如快速点击),避免界面“卡死”。

Swift网络请求相关

Swift 最常用的网络请求框架是 Alamofire(第三方)和 URLSession(苹果官方)。以下是它们的特点对比和简单用法:

框架对比

框架 类型 特点 适合场景
Alamofire 第三方框架 语法优雅、功能丰富、社区活跃 快速开发、复杂网络需求
URLSession 苹果官方 无需依赖、轻量可控、安全可靠 简单需求、不想引入第三方库

Alamofire(最流行)

安装依赖(Swift Package Manager)

在 Xcode 项目中:

  1. File → Add Packages...
  2. 输入 URL:https://github.com/Alamofire/Alamofire.git
  3. 选择版本规则(推荐 "Up to Next Major")
  4. 点击 Add Package

简单使用示例

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(苹果官方,无需依赖)

简单使用示例

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()
}

网络层封装示例(实用版)

对于真实项目,建议进行简单封装:

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
}

选择建议

  1. 新手或简单项目:从 URLSession 开始,理解基础原理
  2. 生产环境或复杂需求:使用 Alamofire,提升开发效率
  3. 需要高级功能:Alamofire 支持:
    • 请求/响应拦截器
    • 网络状态监听
    • 自动重试
    • 文件上传/下载进度
    • 证书验证

实际项目使用技巧

Alamofire 进阶用法:

// 添加请求头
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
    // 处理上传结果
}

错误处理增强:

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 分支的代码。

二、这段代码中该指令的具体作用

#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. 完整语法结构
#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. 多条件组合

可通过 ||(或)、&&(与)组合多个条件,比如:

#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- 用兼容代码

示例对比:

// 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 跨平台开发的典型实践:

  1. SwiftUI 天然跨平台:同一份代码可运行在 iOS/macOS/watchOS 等平台,但不同平台的 UI 规范、屏幕尺寸、交互逻辑差异大;
  2. 按需定制样式:iOS 平台需要额外的 padding/font 优化视觉,其他平台保持原生样式即可,避免“一刀切”导致的跨平台布局问题;
  3. 减少冗余代码:通过条件编译,非目标平台的样式代码不会被编译,降低最终产物体积,且代码结构更清晰。

六、拓展:其他常用条件编译指令

除了 os(...),Swift 还支持其他实用的条件编译:

  1. 判断是否为模拟器:
    #if targetEnvironment(simulator)
        // 仅模拟器编译的代码(比如测试日志)
        print("运行在模拟器中")
    #else
        // 真机编译的代码
        print("运行在真机中")
    #endif
    
  2. 判断编译器版本:
    #if compiler(>=5.9)
        // Swift 5.9+ 编译器支持的语法(比如新的宏)
    #endif
    
  3. 判断调试/发布模式:
    #if DEBUG
        // 调试模式编译(比如打印调试日志)
        Label(title, systemImage: icon).border(.red) // 调试时显示边框
    #else
        // 发布模式编译
        Label(title, systemImage: icon)
    #endif
    

总结

这段代码中的 #if os(iOS) 是为了在编译阶段区分 iOS 和其他平台,给 iOS 端的 Label 增加专属的字体和内边距样式,其他平台保持基础样式,既兼顾 SwiftUI 跨平台的代码复用,又适配不同平台的 UI 规范。核心要记住:它是编译时指令,而非运行时判断,这是和 #available 最关键的区别。

❌