普通视图

发现新文章,点击刷新页面。
昨天以前首页

SwiftUI TabView 源码解析

作者 Lucklylin
2025年8月13日 23:16

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 在线编辑器 | 公众号内容排版工具」 排版

如何使用和自定义 SwiftUI 标签

作者 Lucklylin
2025年8月13日 22:19

1 如何在 SwiftUI 中添加标签?

SwiftUI 中的 Label 组件结合了标题和可选的系统图像,使其成为适用于各种 UI 元素的多功能工具。要创建标签,可以使用以下语法:

Label("Home", systemImage: "house")

第一个参数是标签的标题,可以是简单字符串,也可以是用于国际化的本地化字符串。第二个参数 systemImage 允许您从 SF Symbols 集合中添加图标到标签。

2 如何更改标签的大小

您还可以使用字体修饰符自定义标签的大小。例如,要使标签更大,可以使用标题字体:

这将增加标签标题和图标的尺寸。如果要独立于文本放大或缩小图像,可以使用图像比例修改器:

    Label("Swift", systemImage: "swift")
        .imageScale(.small)
    
    Label("Swift", systemImage: "swift")
        .imageScale(.medium)
    
    Label("Swift", systemImage: "swift")
        .imageScale(.large)

SwiftUI 标签就像文本一样,会响应动态字体大小。图标和文本可以完美地一起缩放:

SwiftUI 标签适应动态类型大小

SwiftUI 标签适应动态类型大小

3 如何更改 SwiftUI Label 的颜色

    Label("Add New Folder", 
          systemImage: "folder.badge.plus")
    
    Label("Add New Folder", 
          systemImage: "folder.badge.plus")
        .foregroundStyle(.accent)
    
    Label("Add New Folder", 
          systemImage: "folder.badge.plus")
        .foregroundStyle(.blue, .pink)

Swiftui 标签与前景样式改变颜色

Swiftui 标签与前景样式改变颜色

如果要更改列表内标签的图标颜色,则应使用色调修饰符而不是前景样式:

    struct LabelListExampleView: View {
        var body: some View {
            NavigationStack {
                List {
                    Label("Folder 1", systemImage: "folder.fill")
                    Label("Folder 2", systemImage: "folder.fill")
                    Label("Folder 3", systemImage: "folder.fill")
                    Label("New Folder", systemImage: "folder.badge.plus")
                }
                .navigationTitle("Labels")
               //  .foregroundStyle(.orange)
               //  .foregroundStyle(.pink, .blue)
                .tint(.cyan)
            }
        }
    }

.tint(.cyan)会将视图的主色调修改为青色。

4 将样式应用于标签

除了基本的自定义选项外,SwiftUI 还提供了各种可应用于标签的标签样式。这些样式定义了标签的外观和行为,例如,它是仅显示标题、仅显示图标,还是两者兼而有之。

要应用标签样式,可以使用 labelStyle修饰符。例如,要创建仅包含图标的标签,可以使用:

    Label("Home", systemImage: "house")
        .labelStyle(IconOnlyLabelStyle())
        
    Label("Home", systemImage: "house")
         .labelStyle(.iconOnly)    // for iOS 14+, macOS 11+

这将显示仅带有房屋图标的标签,并删除标题。

您还可以选择始终使用标题和图标或仅使用标题:

    Label("Home", systemImage: "house")
         .labelStyle(.titleAndIcon)
         
    Label("Home", systemImage: "house")
         .labelStyle(.titleOnly)

5 通过采用 LabelStyle 协议创建自定义标签样式

您还可以通过采用 LabelStyle 协议来创建自定义标签样式。这允许您创建与您的应用设计和品牌相匹配的独特标签样式。

    extension LabelStyle where Self == SocialFeedTagLabelStyle {
        static var socialFeedTag: SocialFeedTagLabelStyle {
            SocialFeedTagLabelStyle()
        }
    }
    
    struct SocialFeedTagLabelStyle: LabelStyle {
        @ScaledMetric(relativeTo: .footnote) private var iconWidth = 14.0
        
        func makeBody(configuration: Configuration) -> some View {
            HStack {
                configuration.icon
                    .foregroundColor(.secondary)
                    .frame(width: iconWidth)
                configuration.title
            }
            .padding(6)
            .background(in: RoundedRectangle(cornerRadius: 5, style: .continuous))
            .compositingGroup()
            .shadow(radius: 1)
            .font(.caption)
        }
    }

makeBody(configuration:) 是 SwiftUI 样式协议(如 LabelStyle、ButtonStyle、ToggleStyle 等) 的核心方法,它的作用是:

接收组件的配置(configuration)(比如 Label 的图标和文本) 返回一个自定义的 View,决定如何渲染该组件

结构体实现了 LabelStyle 协议,定义了标签的自定义外观:

@ScaledMetric: 这是一个属性包装器,它会根据用户的字体大小设置自动调整 iconWidth 的值。relativeTo: .footnote 表示缩放是基于脚注文本大小的。 makeBody 方法: 定义了标签的布局和样式:

使用 HStack 水平排列图标和标题

configuration.icon: 标签的图标部分,设置为次要颜色,并限制宽度为 iconWidth configuration.title: 标签的文本部分

扩展LabelStyle协议,添加一个计算属性是官方推荐的方法,静态属性直接实例化这个结构体,在使用时就不用实例化,而是使用.socialFeedTag的书写方式。如果不扩展,那么就像这样使用。

Label("标签文字", systemImage: "tag")
    .labelStyle(SocialFeedTagLabelStyle())  // 显式创建样式实例

然后,您可以将此标签样式应用于任何标签。例如,我将其用于流式布局中的标签列表:

    FlowLayout(alignment: .leading) {
        ForEach(tags) { tag in
            Label(tag.title, systemImage: tag.icon)
                .labelStyle(.socialFeedTag)
        }
    }

结论

SwiftUI 中的 Label 组件是一款功能强大的工具,可用于向用户界面添加描述性信息。它能够组合标题和图标,并提供各种自定义选项,让您可以创建视觉上引人入胜且信息丰富的标签,从而提升用户体验。

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

戴上眼镜,起飞!影翎 A1 全景无人机首发评测

作者 Lin
2025年8月14日 21:00

你大概率见过这样一幕——一台航拍无人机,身上绑着一部全景相机。早在 2022 年,影石就嗅到了这股创意的风,推出了适配大疆 Air2 系列的全景相机套件「瞳 Sphere」,让无人机玩家第一次尝到了 「全景飞行」 的滋味。

▲ 为了给无人机挂上全景相机,用户和品牌都绞尽了脑汁

而今天,故事迎来了升级版:全球首款全景无人机—— 影翎 A1 登场。毫不意外,背后那双最熟悉的推手,依然来自影石。

品牌中文名叫「影翎」,英文名是「AntiGravity」,直译为「反重力」,这个词一听就是来搞无人机的,它是由影石和第三方团队共同孵化的品牌,全景无人机 A1 是他们的第一个产品。

▲套装包含:无人机本体、体感遥控手柄、飞行眼镜及电池

无人机本体的外观非常科幻。正面竖向排列的视觉避障模组就像电影中的外星生物,而上下两边的半球形凸起就是全景相机模组,一上一下的镜头放置方式和「瞳 Sphere」相同。

▲ 开机自动展开起落架,避免底部的全景镜头接触地面

让我成为一只鸟,在空中自由探索

既然是影石孵化的品牌,身上自然也流淌着影石的血液,尤其是全景合成技术。

影翎 A1 能够在飞行过程中实时合成全景影像,结合经特别设计的镜头布局,它能够实现出色的「隐形」效果,在飞行眼镜中环顾四周也不会看到机身和螺旋桨的踪迹。

▲ 戴着飞行眼镜转头可以轻松变换视线方向

想象你正在北京环球影城的哈利波特禁忌之旅上,飞车极速前进时,四周的景象尽收眼底。即便飞车在前进,你的视角却可以任意转动,就像是身临其境,飞车的方向并不限制你的视野。

影翎 A1 带给我的就是这种自由的体验,仿佛化身为一只在天空中翱翔的鸟,一边飞行,一边自由探索眼前的美景。这样的飞行体验,非常适合在各大景区应用——工作人员摇身一变成为无人机驾驶员,游客则可以「坐」在无人机上,随心所欲地欣赏四周的风光。

全景飞行的模式使其操作方式有别于传统 FPV 无人机——飞行方向只由手柄控制,不再与头显的视角同步。当你的视角与飞行方向不一致时,头显屏幕上会自动弹出一个悬浮窗,实时显示无人机前方的影像。而你只需轻轻转动手柄上的滚轮,就能迅速回到飞行器前方的视角并调整飞行方向,避免盲飞的风险。

其实对于新手来说是很好上手的,但如果你以前就飞过 FPV,那我建议你放下所有的经验,像个新手一样享受这份空中飞行的自由感。

很好玩,也很好拍

在这次体验的前半部分,我完全沉迷在自由的观光感受里了,直到我想起它还是一部能够拍摄全景视频的生产力工具,然后马上就发现了全景无人机的妙处——先专心飞,再随意构图。

影翎 A1 能够录制最高 8K@30fps 规格的全景视频(可惜由于是早期工程样机,我们只体验到了 5.7K@50fps 的视频规格),然后在后期软件里自由调整视角,设置关键帧打点实现一些高难度的镜头运动。

这意味着,在前期拍摄时飞手无需纠结构图与运镜。只要专注于操控飞行,确保无人机与拍摄对象的距离与相对位置即可。

以前,拍摄这些镜头时需要操控 FPV 无人机进行翻转、甩尾等高难度动作,还要冒着很大的炸机风险。但现在,只需在后期软件中拖动画面、设置关键帧,便能轻松重现这些效果。

也难怪早就有玩家们在无人机上绑全景相机了,确实好用。

让航拍过程成为一种享受

爱范儿觉得影翎 A1 会是一款彻底改变过往飞行体验的产品。

作为全球首款一体式全景无人机,影翎 A1 融合了手势体感控制摇杆、实时合成全景视频和全方位观测的飞行眼镜,为飞行者带来一种前所未有的体验。它让每个人都能在空中自由探索,尽情欣赏大自然的鬼斧神工。

对于影像创作者来说,这样的一体化设计也比曾经专机专用的外挂式配件来得更加可靠,无论是飞行操控还是拍摄体验都更上一层。

最重要的是,影翎 A1 突破了传统飞行的束缚,它解放了创作者们 「机不可失,失不再来」 的焦虑,将他们的飞行体验提升到全新的高度,让航拍的飞行过程本身也成为一种享受

给创作者们提供了大胆飞行、自由创作的底气,这才是影翎 A1 的独特魅力所在。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


❌
❌