阅读视图

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

用 SwiftUI 打造一个 iOS「设置」界面

用 SwiftUI 打造一个 iOS「设置」界面

在开发 iOS App 时,我们常常需要构建「设置页面」。原生的 Settings.bundle 虽然能提供系统级设置,但多数情况下我们更希望在应用内部实现一套「拟系统」的设置界面。

本文将带你从 0 到 1,用 SwiftUI 写出一个高度还原 iOS 设置风格的组件库,包括:

  • 可复用的设置行组件(SettingRow)
  • 分组容器(SettingsSection)
  • 多类型行支持(开关、导航、输入框、详情、按钮等)
  • 完整 Demo 界面(SettingsView)

最终效果如下 👇(几乎就是 iOS 设置的翻版):

示例效果
(你可以运行代码自己体验,UI 风格完全遵循 iOS 官方人机界面规范)


1. 定义图标类型

我们首先需要一个「图标枚举」,为不同的设置项绑定系统 SF Symbols 图标和颜色。这样能确保代码整洁,也方便后续扩展:

enum SettingIconType {
    case general, notifications, privacy, screen, battery, wifi, bluetooth, cellular
    case language, keyboard, accessibility, storage, camera, photos
    case appstore, safari, mail, messages, facetime, music
    case resetSettings, exportData, deviceInfo
    
    var icon: String {
        switch self {
        case .general: return "gear"
        case .wifi: return "wifi"
        case .battery: return "battery.100"
        // 其他略...
        default: return "gear"
        }
    }
    
    var color: Color {
        switch self {
        case .wifi: return .blue
        case .battery: return .green
        case .resetSettings: return .red
        default: return .gray
        }
    }
}

这里用到了 SF Symbols,几乎可以覆盖绝大多数系统图标需求。


2. 设置行类型

不同的行需要不同的交互:有的跳转,有的开关,有的输入文字。我们用枚举 SettingRowType 来抽象这些类型:

enum SettingRowType {
    case navigation(action: () -> Void)
    case toggle(binding: Binding<Bool>)
    case detail(text: String, action: (() -> Void)? = nil)
    case stepper(value: Binding<Double>, range: ClosedRange<Double>, step: Double = 1.0)
    case picker(selection: Binding<String>, options: [String], action: (() -> Void)? = nil)
    case textField(text: Binding<String>, placeholder: String = "")
    case display(text: String)
    case button(style: ButtonStyle = .normal, action: () -> Void)
    
    enum ButtonStyle {
        case normal, destructive, prominent
    }
}

这种写法的好处是:所有 UI 类型都集中在一个枚举中,扩展时只需要新增 case


3. 复用的行组件 SettingRow

接下来就是核心组件 —— SettingRow。它接收一个 SettingIconType(图标)、一个标题、一个 SettingRowType(类型),自动渲染出对应的 UI。

struct SettingRow: View {
    let iconType: SettingIconType
    let title: String
    let type: SettingRowType
    
    var body: some View {
        HStack(spacing: 12) {
            Image(systemName: iconType.icon)
                .foregroundColor(iconType.color)
                .frame(width: 25, height: 25)
                .overlay(
                    RoundedRectangle(cornerRadius: 6)
                        .stroke(Color(.systemGray5), lineWidth: 1)
                )
            
            Text(title)
                .font(.system(size: 17))
            
            Spacer()
            
            rightContent
        }
        .padding(.horizontal, 16)
        .frame(minHeight: 44)
        .background(Color(.secondarySystemGroupedBackground))
        .contentShape(Rectangle())
        .onTapGesture { handleTap() }
    }
}

核心逻辑在 rightContent ——它会根据不同的 SettingRowType 渲染出右侧的 Toggle、Chevron、Stepper、TextField 等控件。


4. 分组容器 SettingsSection

iOS 设置页面通常会分组显示,每组有 Header/Footer。我们写一个 SettingsSection 来包裹行组件:

struct SettingsSection<Content: View>: View {
    let header: String?
    let footer: String?
    @ViewBuilder var content: Content
    
    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            if let header = header {
                Text(header.uppercased())
                    .font(.footnote)
                    .foregroundColor(Color(.secondaryLabel))
                    .padding(.horizontal)
            }
            
            VStack(spacing: 0) {
                content
            }
            .background(Color(.secondarySystemGroupedBackground))
            .clipShape(RoundedRectangle(cornerRadius: 10))
            .padding(.horizontal)
            
            if let footer = footer {
                Text(footer)
                    .font(.footnote)
                    .foregroundColor(Color(.secondaryLabel))
                    .padding(.horizontal)
            }
        }
    }
}

这样我们就能用类似 UIKit 的 TableView Section 的方式组织 UI。


5. 完整示例 SettingsView

最后,写一个完整页面示例,把 Wi-Fi、蓝牙、亮度、语言等都组合起来:

struct SettingsView: View {
    @State private var wifiEnabled = true
    @State private var bluetoothEnabled = false
    @State private var brightness: Double = 50
    @State private var deviceName = "我的 iPhone"
    @State private var language = "简体中文"
    
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                // 连接
                SettingsSection(header: "连接") {
                    SettingRow.toggle(iconType: .wifi, title: "Wi-Fi", isOn: $wifiEnabled)
                    SettingRow.toggle(iconType: .bluetooth, title: "蓝牙", isOn: $bluetoothEnabled)
                    SettingRow.navigation(iconType: .cellular, title: "蜂窝网络") {
                        print("进入蜂窝网络")
                    }
                }
                
                // 显示与亮度
                SettingsSection(header: "显示与亮度") {
                    SettingRow(iconType: .screen, title: "亮度", type: .stepper(value: $brightness, range: 0...100, step: 5))
                }
                
                // 通用
                SettingsSection(header: "通用") {
                    SettingRow.navigation(iconType: .general, title: "关于本机") {
                        print("关于本机")
                    }
                    SettingRow.detail(iconType: .language, title: "语言与地区", detail: language) {
                        print("选择语言")
                    }
                    SettingRow(iconType: .keyboard, title: "设备名称", type: .textField(text: $deviceName, placeholder: "输入名称"))
                }
                
                // 设备信息
                SettingsSection(header: "设备信息", footer: "这些信息用于识别您的设备") {
                    SettingRow.display(iconType: .deviceInfo, title: "型号", value: "iPhone 15 Pro")
                    SettingRow.display(iconType: .deviceInfo, title: "iOS 版本", value: "17.5.1")
                }
                
                // 重置
                SettingsSection(footer: "重置设置不会删除数据") {
                    SettingRow.button(iconType: .resetSettings, title: "重置所有设置", style: .destructive) {
                        print("重置")
                    }
                    SettingRow.button(iconType: .exportData, title: "导出数据", style: .prominent) {
                        print("导出")
                    }
                }
            }
            .padding(.vertical)
        }
        .background(Color(.systemGroupedBackground))
    }
}

6. 总结

通过这种方式,我们实现了一个:

✅ 高度还原 iOS 系统设置的界面
✅ 可复用的 SettingRow 组件,支持导航/开关/输入等多种类型
✅ 分组化 SettingsSection,完全符合系统风格
✅ 简单扩展,方便日后增加新类型

如果你正在开发一个需要「偏系统风格」的 App,这套代码可以作为你的基础设置模块,直接拿来用。


👉 源码已贴全,可以直接复制到项目中运行。
如果你觉得有帮助,点个赞 👍 或收藏 🔖,下次写 UI 时就能快速用上啦!

SwiftUI TabView 源码解析

SwiftUI TabView 解析

TabView 是 SwiftUI 中用于创建标签页视图的组件,类似于 UIKit 中的 UITabBarController。下面我将解释源代码并提供使用示例。

源代码解析

基本结构

public struct TabView<SelectionValue, Content> : View 
    where SelectionValue : Hashable, Content : View
  • TabView 是一个泛型结构体,遵循 View 协议
  • SelectionValue 是用于跟踪当前选中标签的值的类型,必须遵循 Hashable 协议
  • Content 是视图内容的类型,必须遵循 View 协议

初始化方法

  1. 旧版初始化方法 (已弃用)
@available(iOS, deprecated: 100000.0, message: "Use TabContentBuilder-based TabView initializers instead")
public init(selection: Binding<SelectionValue>?, @ViewBuilder content: () -> Content)
  • 这个初始化方法已被标记为弃用
  • 使用 @ViewBuilder 构建内容
  • 接受一个可选的 Binding<SelectionValue> 用于跟踪当前选中的标签

使用示例

import SwiftUI

struct OldTabViewExample: View {
    @State private var selectedTab: Int = 0

    var body: some View {
        TabView(selection: $selectedTab) { // selection 是可选 Binding
            HomeView()
                .tabItem { Label("首页", systemImage: "house.fill") }
                .tag(0)

            CalendarView()
                .tabItem { Label("日历", systemImage: "calendar") }
                .tag(1)

            SettingsView()
                .tabItem { Label("设置", systemImage: "gearshape") }
                .tag(2)
        }
    }
}
  1. 新版初始化方法 (iOS 18+)
@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
public init<C>(selection: Binding<SelectionValue>, @TabContentBuilder<SelectionValue> content: () -> C) 
    where Content == TabContentBuilder<SelectionValue>.Content<C>, C : TabContent
  • 使用新的 @TabContentBuilder 构建内容
  • 需要 iOS 18+ 等新系统版本
  • 提供更好的类型安全性和更简洁的语法

使用示例

import SwiftUI

struct NewTabViewExample: View {
    @State private var selectedTab: Int = 0

    var body: some View {
        if #available(iOS 18.0, *) {
            TabView(selection: $selectedTab) { // 必须有 Binding
                Tab(value: 0) {
                    HomeView()
                }
                .tabItem { Label("首页", systemImage: "house.fill") }

                Tab(value: 1) {
                    CalendarView()
                }
                .tabItem { Label("日历", systemImage: "calendar") }

                Tab(value: 2) {
                    SettingsView()
                }
                .tabItem { Label("设置", systemImage: "gearshape") }
            }
        } else {
            Text("请升级到 iOS 18 使用该写法")
        }
    }
}

扩展方法

  1. 当 SelectionValue 为 Int 时的简化初始化
extension TabView where SelectionValue == Int {
    nonisolated public init(@ViewBuilder content: () -> Content)
}
  • 当标签选择值是 Int 类型时,可以省略 selection 参数

这也就是最常见的使用方法了

示例

import SwiftUI

struct IntTabViewExample: View {
    var body: some View {
        TabView { // 没有 selection
            HomeView()
                .tabItem { Label("首页", systemImage: "house.fill") }
                .tag(0)

            CalendarView()
                .tabItem { Label("日历", systemImage: "calendar") }
                .tag(1)

            SettingsView()
                .tabItem { Label("设置", systemImage: "gearshape") }
                .tag(2)
        }
    }
}
  1. 当 SelectionValue 为 Never 时的初始化 (iOS 18+)
extension TabView {
    public init<C>(@TabContentBuilder<Never> content: () -> C) 
        where SelectionValue == Never, Content == TabContentBuilder<Never>.Content<C>, C : TabContent
}
  • 用于不需要选择功能的标签视图
  • 需要 iOS 18+ 等新系统版本

示例

struct StaticTabView: View {
    var body: some View {
        TabView {
            Tab("Home") {
                Text("Home Content")
            }
            
            Tab("About") {
                Text("About Content")
            }
        }
    }
}

更多的使用示例

基础示例 (iOS 13+ 兼容)

struct BasicTabView: View {
    @State private var selectedTab = 0
    
    var body: some View {
        TabView(selection: $selectedTab) {
            Text("First Tab")
                .tabItem {
                    Image(systemName: "1.square.fill")
                    Text("First")
                }
                .tag(0)
            
            Text("Second Tab")
                .tabItem {
                    Image(systemName: "2.square.fill")
                    Text("Second")
                }
                .tag(1)
        }
    }
}

简化版 (当 SelectionValue 是 Int)

struct SimpleTabView: View {
    var body: some View {
        TabView {
            Text("Home")
                .tabItem {
                    Label("Home", systemImage: "house")
                }
            
            Text("Settings")
                .tabItem {
                    Label("Settings", systemImage: "gear")
                }
        }
    }
}

iOS 18+ 新 API 示例

@available(iOS 18.0, macOS 15.0, *)
struct NewTabViewExample: View {
    @State private var selectedTab: String = "home"
    
    var body: some View {
        TabView(selection: $selectedTab) {
            Tab("Home", value: "home") {
                Text("Home Content")
            }
            
            Tab("Profile", value: "profile") {
                Text("Profile Content")
            }
            
            Tab("Settings", value: "settings") {
                Text("Settings Content")
            }
        }
    }
}

不需要选择功能的标签视图 (iOS 18+)

@available(iOS 18.0, macOS 15.0, *)
struct StaticTabView: View {
    var body: some View {
        TabView {
            Tab("Home") {
                Text("Home Content")
            }
            
            Tab("About") {
                Text("About Content")
            }
        }
    }
}

tabItem

另外tabItem的作用是布局一个视图作为标签页元素,所有可以向其中加入视图,包括允许你指定每个标签页在标签栏中显示的图标和标题。

tabItem 闭包中,你通常会提供一个 Label、Image 或 Text 视图,或者它们的组合。

主要特性

  • 必须与 TabView 一起使用:tabItem 只能应用于 TabView 的子视图
  • 需要配合 tag 修饰符使用:如果你需要跟踪当前选中的标签
  • 自动适应外观:系统会根据当前环境自动调整标签项的外观

高级用法,动态改变标签项

.tabItem {
    Label(showDetails ? "Detailed" : "Simple", 
          systemImage: showDetails ? "info.circle.fill" : "info.circle")
}

总结

  • TabView 是 SwiftUI 中创建标签页界面的主要组件
  • 旧版 API 使用 tabItemtag 修饰符定义标签
  • iOS 18+ 引入了新的 Tab 视图和 @TabContentBuilder,提供了更简洁的语法
  • 根据是否需要跟踪当前选中的标签,可以选择不同的初始化方法
  • 当选择值是 Int 类型时,可以使用简化版的初始化方法

新的 iOS 18 API 提供了更直观的方式来定义标签视图,但如果你需要支持旧版系统,仍然需要使用传统的 tabItem 方法。

本文使用 「Markdown 在线编辑器 | 公众号内容排版工具」 排版

❌