普通视图

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

侠客行・iOS 26 Liquid Glass TabBar 破阵记

2025年11月9日 14:15

在这里插入图片描述

引子

话说侠客岛旁的 “码农山庄” 里,有位青年开发者石破天,一手 SwiftUI 功夫练得炉火纯青,身旁常伴着心思缜密的产品女侠阿绣。

在这里插入图片描述

这日,山庄接到一桩棘手活计 —— 玄铁老怪掌管的 “APP 审核阁” 放出话来,凡要上 iOS 26 的 APP,必过Liquid Glass设计关,尤其Tab Bar这块,稍有差池便打回重练。

在本篇侠客行中,您将学到如下内容:

  • 引子
    1. 📱 初探 iOS 26 的 Tab Bar:旧功新用,基础先扎牢
    1. 🔍 拆解 Tab Bar 的模糊特效:藏在 “滚动容器” 里的玄机
    1. 📜 给 TabView 加 “缩骨功”:tabBarMinimizeBehavior 显神通
    1. 🧩 给 Tab Bar 加 “配件”:tabViewBottomAccessory 的坑与悟
    1. 🔍 误入 “搜索 - tab” 歧途:role: .search 的真实用途
    1. ✨ 柳暗花明:ZStack+glassEffect 造 “玻璃态浮动按钮”
  • 尾声:技术如侠,心法为上

石破天与阿绣不敢怠慢,即刻开干,誓要破解这 Liquid Glass 的 Tab Bar 玄机。


1. 📱 初探 iOS 26 的 Tab Bar:旧功新用,基础先扎牢

阿绣翻出 iOS 18 的适配文档,对石破天道:“天哥,若咱们之前吃透了 iOS 18 的 Tab Bar 更新,这次应对 Liquid Glass 便是胸有成竹;若是没做过,也得先搭个最简框架试试水。”

在这里插入图片描述

说罢,石破天便敲出一段TabView基础代码 —— 用Tab包裹页面,配上系统图标,正是健身 APP 常用的 “运动记录” 与 “动作库” 标签:

TabView {
  // Workouts标签:对应“运动记录”页面,用哑铃填充图标
  Tab("Workouts", systemImage: "dumbbell.fill") {
    WorkoutsView()
  }

  // Exercises标签:对应“动作库”页面,用传统力量训练图标
  Tab("Exercises", systemImage: "figure.strengthtraining.traditional") {
    ExercisesView()
  }
}

他用 Xcode 26 编译后,先在 iOS 18 设备上运行 ——Tab Bar 还是老样子,扎实却少了灵动感:

在这里插入图片描述

可一换 iOS 26 设备,屏幕上的 Tab Bar 竟变了模样:

在这里插入图片描述

通体透着Liquid Glass的通透感,像蒙了一层薄雾,与页面浑然一体。

在这里插入图片描述

阿绣指着屏幕皱眉:“你看,之前咱们在 Tab Bar 上方加的紫色‘添加按钮’,现在挡住了下方内容,这可不符 Liquid Glass‘分层不遮挡’的规矩,玄铁老怪见了必定挑刺。”

在这里插入图片描述

2. 🔍 拆解 Tab Bar 的模糊特效:藏在 “滚动容器” 里的玄机

石破天盯着屏幕犯愁,阿绣却忽然想起苹果健康 APP 的设计:“健康 APP 的列表是能滚到 Tab Bar 底下的,还带着模糊效果,不如咱们也试试把列表‘拉’到 Tab Bar 下面?” 。

在这里插入图片描述

二人先分析当前页面结构 —— 原来他们用VStack叠了筛选栏、列表和按钮,按钮挡住了列表,导致 Tab Bar 无法 “穿透” 显示模糊:

VStack {
  ScrollView(.horizontal) { /* 横向滚动的筛选栏 */ }
  List { /* 运动动作列表 */ }
  Button { /* 紫色“添加动作”按钮——问题根源 */
}

石破天试着删掉按钮,再运行时,奇迹出现了:

在这里插入图片描述

列表果然延伸到了 Tab Bar 下方,Tab Bar 自动透出一层模糊,与健康 APP 如出一辙!

在这里插入图片描述

他拍腿大笑:“原来如此!Liquid Glass 的模糊特效是默认给‘覆盖在滚动容器上的 Tab Bar’的,之前有按钮挡着,滚动容器没贴到 Tab Bar,自然出不来效果。”

3. 📜 给 TabView 加 “缩骨功”:tabBarMinimizeBehavior 显神通

刚解决模糊问题,玄铁老怪便飘然而至,扫了眼屏幕道:“模糊是有了,可用户滚动时 Tab Bar 还这么显眼,不够灵动,算不得精通 Liquid Glass。”

在这里插入图片描述

石破天心中一紧,阿绣却递过一份 iOS 26 新 API 文档:“天哥,试试这个tabBarMinimizeBehavior修饰符,就像给 Tab Bar 练了‘缩骨功’,滚动时能自动变小!”

石破天立刻给TabView加上修饰符:

TabView {
  /* 里面还是原来的两个Tab页面 */
}
.tabBarMinimizeBehavior(.onScrollDown) // 关键:用户向下滚动时,Tab Bar自动最小化

运行后,手指向下滑动列表,Tab Bar 果然悄悄 “缩” 了一圈,既不遮挡内容,又没完全消失 —— 玄铁老怪眯眼瞧了瞧,没说话,但眉头舒展了些。

在这里插入图片描述

石破天暗自庆幸:“还好这修饰符只在 Liquid Glass 模式下生效,要是在 iOS 18 老设计(Old Style)里用了没反应,今日可就栽了。”

4. 🧩 给 Tab Bar 加 “配件”:tabViewBottomAccessory 的坑与悟

解决了最小化,石破天又惦记起之前删掉的 “添加动作” 按钮:“能不能把按钮加回 Tab Bar 上方?” 。

在这里插入图片描述

阿绣指着文档里的tabViewBottomAccessory说:“这是 iOS 26 新出的‘配件视图’,能放在 Tab Bar 上面,试试?”

在这里插入图片描述

石破天依言添加了如下代码:

TabView {
  /* 原有Tab页面 */
}
.tabBarMinimizeBehavior(.onScrollDown)
.tabViewBottomAccessory { // 给Tab Bar加“配件”——这里放“添加动作”按钮
  Button("Add exercise") {
    // 点击后打开“添加新运动动作”的逻辑
  }.purpleButton() // 自定义的紫色按钮样式
}

可运行后却发现问题:无论切到 “运动记录” 还是 “动作库”,这按钮都在 —— 阿绣摇头道:“苹果的用法是‘全局配件’,比如音乐 APP 的播放器控制,每个页面都需要;咱们这按钮只在‘动作库’有用,放这就画蛇添足了。” 。

在这里插入图片描述

石破天只好删掉配件,叹道:“看来此路不通,得另想办法。”

5. 🔍 误入 “搜索 - tab” 歧途:role: .search 的真实用途

二人正琢磨,阿绣忽然想起健康 APP 右下角有个搜索按钮:“要不试试给 Tab 加个‘搜索角色’?文档里说role: .search能把 Tab 放右边。” 。

石破天马上修改了代码:

// 新增一个Tab,角色设为.search,想当“添加按钮”用
Tab("Add", systemImage: "plus", value: Tabs.exercises, role: .search) {
  /* 原本想放添加页面,结果打开是全屏 */
}

可一点这个 “加号 Tab”,竟弹出个全屏页面 —— 哪里是浮动按钮!?

在这里插入图片描述

阿绣哭笑不得:“原来role: .search 是给‘搜索页面’用的,不是随便放按钮的,咱们这是‘张冠李戴’了。” 石破天挠挠头:“看来得放弃 TabView 的思路,直接在页面上做文章。”

6. ✨ 柳暗花明:ZStack+glassEffect 造 “玻璃态浮动按钮”

眼看天色渐暗,阿绣忽然灵光一闪:“Liquid Glass 讲究‘分层’,咱们用ZStack把按钮‘浮’在页面上,再加个glassEffect,不就贴合设计了?”

在这里插入图片描述

石破天眼睛一亮,立刻敲出代码:

ZStack(alignment: .bottomTrailing) { // 对齐方式设为右下,按钮贴右下角
  // 这里放“动作库”的主要内容:筛选栏+列表
  VStack {
    ScrollView(.horizontal) { /* 筛选栏 */ }
    List { /* 动作列表 */ }
  }

  // 浮动按钮:核心是glassEffect修饰符
  Button(action: {
    // 点击打开“添加新动作”弹窗
  }) {
    Label("Add Exercise", systemImage: "plus")
      .bold() // 字体加粗,突出按钮
      .labelStyle(.iconOnly) // 只显图标,不显文字,更简洁
      .padding() // 内部加边距,增大点击区域
  }
  .glassEffect(.regular.interactive()) // 关键!添加Liquid Glass玻璃态,与系统融合
  .padding([.bottom, .trailing], 12) // 外部右下加12pt边距,避免贴边
}

运行后,一个带着薄雾质感的 “加号按钮” 浮在列表右下角,滚动时既不遮挡内容,又和 Tab Bar 的 Liquid Glass 风格浑然一体—— 玄铁老怪凑过来细看,手指点了点按钮,又滑动列表,半晌才道:“这按钮虽没用到 TabView 的 API,却吃透了 Liquid Glass 的‘分层融合’心法,算你们过关。”

在这里插入图片描述

尾声:技术如侠,心法为上

此事过后,石破天与阿绣悟得一理 —— iOS 26 的 Liquid Glass 从不是刁难人的 “武功秘籍”,而是倒逼开发者贴合用户体验的 “心法”。

在这里插入图片描述

TabView的种种新特性,无论是tabBarMinimizeBehavior的灵动、tabViewBottomAccessory的全局适配,还是glassEffect的通透,核心都在 “让界面服务内容,而非喧宾夺主”。

玄铁老怪虽严苛,见二人不墨守成规、能灵活拆解问题,也不禁点头:“后生可畏,这关,你们过了!” 而石破天与阿绣也明白,往后应对新系统,只需紧抓设计哲学,再难的技术关,也能如侠客破阵般,迎刃而解。

在这里插入图片描述

那么,看到这里各位少侠是否也收益良多呢?

感谢观赏,宝子们下次再会吧!8-)

猿族代码战记:Mutex 升级版——守护 Swift 并发的“香蕉仓库”

2025年11月8日 13:25

在这里插入图片描述

🦍 引子

旧金山废墟的猿族技术区,金属支架撑起的荧光屏泛着冷光,首席 Swift 架构师科巴的指节因攥紧终端而发白 —— 食物计数系统又出问题了。

在这里插入图片描述

刚录入的 27 根香蕉,刷新页面竟变成 29,再点一下又跳回 28,旁边年轻猿工程师紧张地挠着头:“科巴大人,不会是小猩猩偷偷改数据吧?” 科巴瞪了他一眼:“是‘并发幽灵’!自从用 Actor 保护状态,简单的计数全成了麻烦 —— 查个库存要写await,就像咱们去仓库拿根香蕉,得先找凯撒签字、找后勤登记,折腾半小时!”

在本堂猩猩课堂中,您将学到如下内容:

  • 🦍 引子
  • 🛡️ 第一章:Actor 的麻烦 —— 被异步绑架的简单需求
  • 🔧 第二章:Mutex 实战 —— 零 bug 香蕉计数器
  • 📱 第三章:适配 SwiftUI—— 让 @Observable “看见” 变化
  • ⚔️ 第四章:抉择 ——Mutex vs Actor
  • 🌟 结尾:代码丛林的生存法则

今天,他要拿出压箱底的 “轻量武器” Mutex,让代码既能挡住并发风险,又能像猿族奔袭般迅猛如潮。

🛡️ 第一章:Actor 的麻烦 —— 被异步绑架的简单需求

科巴拉过一把生锈的金属椅坐下,指尖在键盘上敲出 Actor 代码:“你们看,要改香蕉数量,必须写await counter.addBanana()—— 就一个破赋值操作,硬生生被拖进异步队列!

他顿了顿,指着屏幕上的@MainActor标签,“就算把计数器绑在主线程,其他哨站的猿想查库存,还是得等主线程‘有空’—— 这和把仓库钥匙只给主营地的猿,其他猿只能站在门口等,有啥区别?”

在这里插入图片描述

旁边的猿工程师小声问:“那以前咱们是怎么处理的?”

“以前靠 GCD 串行队列!” 科巴一拍桌子,“就像给仓库配个专属管理员,所有拿香蕉的请求都排队,谁也别插队。但队列太重了,现在 Swift 出了 Mutex—— 它是‘轻量级锁’,只护一小块状态,操作完自动解锁,还不用写一堆异步代码!”

在这里插入图片描述

🔧 第二章:Mutex 实战 —— 零 bug 香蕉计数器

科巴清了清嗓子,手指在键盘上飞快跳动,边写边讲解:“Mutex 的核心是withLock方法 —— 它会先‘抢锁’,确保当前只有一个线程能操作状态,操作完不管成功或失败,都会自动‘释放锁’,绝不会像手动加锁那样,忘了解锁导致整个系统卡死。”

在这里插入图片描述

很快,FoodCounter类的代码出现在屏幕上:

class FoodCounter {
    // 初始化 Mutex,把初始香蕉数设为0——相当于给空仓库装了把新锁
    private let mutex = Mutex(0)
    
    // 增加香蕉:开锁、给库存+1、自动锁门
    func addBanana() {
        mutex.withLock { count in
            count += 1 // 操作超简单,就像把香蕉放进仓库,一秒搞定
        }
    }
    
    // 减少香蕉:逻辑和加香蕉一样,只是把+1改成-1
    func removeBanana() {
        mutex.withLock { count in
            count -= 1
        }
    }
    
    // 读取库存:重点!读操作也要走 withLock,防止读的时候正好在写(比如刚加了半根香蕉)
    var bananaCount: Int {
        mutex.withLock { count in
            return count // 只读取,不修改,但也要保证独占访问
        }
    }
}

“千万别犯懒!” 科巴突然提高声音,“有猿觉得‘读操作不用锁’,结果读的时候正好赶上写,拿到的是‘脏数据’—— 上次有个猿没加锁读库存,以为还有 10 根香蕉,结果实际只剩 2 根,导致整个哨站的猿饿了半天!”

在这里插入图片描述

他演示了如何使用计数器,代码简洁得让猿工程师们发出惊叹:

let counter = FoodCounter()

counter.bananaCount = 10 // 直接赋值,不用等异步
print(counter.bananaCount) // 立刻输出10,没有半点延迟
counter.addBanana()

print(counter.bananaCount) // 输出11,实时更新

在这里插入图片描述

📱 第三章:适配 SwiftUI—— 让 @Observable “看见” 变化

正当猿族为新计数器欢呼时,负责 SwiftUI 仪表盘的猿跑过来:“科巴大人,计数器接入界面后,香蕉数变了,界面却一动不动!” 科巴凑过去看了眼平板 —— 屏幕上的数字始终停留在 10,哪怕点了 “加香蕉” 按钮也没反应。

在这里插入图片描述

“这是因为 @Observable ‘瞎’了!” 科巴很快找到问题,“Mutex 保护的是内部的库存数,库存变了,但 Mutex 本身没变化 ——@Observable 只能‘看见’对象属性的直接修改,看不到 Mutex 里面的小动作。”

他伸手在键盘上敲了几行代码,给bananaCountgetset加了 “传令兵”:

@Observable
final class FoodCounter: Sendable { // 加Sendable,允许计数器跨线程传递
    private let mutex = Mutex(0)
    
    var bananaCount: Int {
        get {
            // 告诉@Observable:“有人在读香蕉数量啦,记下来!”
            self.access(keyPath: \.bananaCount)
            return mutex.withLock { $0 }
        }
        set {
            // 告诉@Observable:“香蕉数量要变了,准备更新界面!”
            self.withMutation(keyPath: \.bananaCount) {
                mutex.withLock { count in
                    count = newValue
                }
            }
        }
    }
    
    // 省略addBanana和removeBanana...
}

“这俩方法是 @Observable 宏自动加的‘钩子’,” 科巴解释,“access告诉框架‘有人在读数据’,withMutation告诉框架‘数据要改了’—— 这样界面就能跟 Mutex 里的库存同步,点一下按钮,数字立刻更新,童叟无欺!”

在这里插入图片描述

⚔️ 第四章:抉择 ——Mutex vs Actor

科巴把猿族工程师召集到一起,在黑板上画了张对比表,用炭笔重重标出关键差异:

对比维度 Mutex(轻量锁) Actor(异步卫士)
代码风格 同步代码,不用写await,清爽直接 强制异步,处处要await,略显繁琐
适用场景 保护 1-2 个简单属性(如计数)、操作耗时极短 保护复杂对象(如网络管理器)、操作耗时较长(如下载图片)
线程行为 抢不到锁会 “阻塞”(等锁释放) 抢不到隔离权会 “挂起”(不阻塞线程)
学习成本 低,API 简单,上手快 高,要理解隔离域、Sendable 等概念

“选哪个不是看‘谁更强’,而是看‘谁更适合’!” 科巴敲了敲黑板,“如果你的需求像‘数香蕉’一样简单,不想写一堆异步代码,就用 Mutex—— 它是‘贴身短刀’,快准狠;如果你的需求是‘跟人类服务器同步数据’,要处理一堆异步逻辑,就用 Actor—— 它是‘坚固盾牌’,能扛住复杂并发。”

在这里插入图片描述

他顿了顿,补充道:“我通常会两种都试一下,哪个写起来顺手就用哪个。比如这次的计数器,用 Mutex 写出来的代码比 Actor 简洁一半,还不用处理异步等待,那肯定选 Mutex 啊!”

🌟 结尾:代码丛林的生存法则

科巴把最后一行代码提交到猿族的代码仓库,终端屏幕上的香蕉计数稳定跳动 —— 从 100 跳到 101,又跳到 102,那是远方哨站的猿刚入库的香蕉,正通过 Mutex 守护的代码,实时同步到主营地的仪表盘。

在这里插入图片描述

他走到窗边,看着外面:凯撒正带领年轻的猿族围着平板学习 Swift,阳光透过废墟的缝隙洒在他们身上,像给代码世界镀上了一层金光。

科巴拉过身边的年轻猿工程师,指着屏幕上的 Mutex 代码说:“咱们猿族在丛林里生存,不会拿长矛去抓兔子,也不会拿匕首去对付狮子 —— 代码世界也一样,没有‘最强的工具’,只有‘最适合当下的工具’。Mutex 是短刀,适合近距离快速解决问题;Actor 是盾牌,适合抵御大规模的并发攻击。懂取舍,会选工具,才是真 - 正的工程师。”

在这里插入图片描述

平板上的计数又跳了一下,这次是 103—— 猿族的食物储备越来越多,他们的 Swift 代码,也在 Mutex 和 Actor 的守护下,越来越稳固。

那么,各位微秃小猩猩,你们学“废”了吗?感谢观看,下次再会啦!8-)

Thread.sleep 与 Task.sleep 终极对决:Swift 并发世界的 “魔法休眠术” 揭秘

2025年11月8日 13:23

在这里插入图片描述

📜 引子:霍格沃茨的 “并发魔咒” 危机

在霍格沃茨城堡顶层的 “魔法程序与咒语实验室” 里,金色的阳光透过彩绘玻璃洒在悬浮的魔法屏幕上。哈利・波特正对着一段闪烁着蓝光的 Swift 代码抓耳挠腮,罗恩在一旁急得直戳魔杖 —— 他们负责的 “魁地奇赛事实时计分器” 又卡住了。

赫敏抱着厚厚的《Swift 并发魔法指南》凑过来,眉头紧锁:“肯定是上次加的‘休眠咒语’出了问题!我早就说过 Thread.sleep 像‘摄魂怪的拥抱’,会吸干线程的活力,你们偏不信!

在这里插入图片描述

这时,实验室的门 “吱呀” 一声开了,负责教授高阶魔法编程的菲尼亚斯・奈杰勒斯・布莱克(没错,就是那位爱吹牛的前校长幽灵)飘了进来,黑袍在空气中划出一道残影。

在本堂魔法课中,您将学到如下内容:

  • 📜 引子:霍格沃茨的 “并发魔咒” 危机
  • 🧙‍♂️ 开篇:“休眠魔咒” 的污名化 ——Task.sleep 真不是 “过街老鼠”
  • 🔍 迷雾初探:为何 Task.sleep 总出现在 “实用魔咒” 里?
  • 🧵 核心奥秘:任务与线程的 “从属关系”—— 不是 “替代”,而是 “调度”
  • 💤 危险实验:Thread.sleep 的 “沉睡诅咒”—— 吸干线程,卡住全局
  • ✨ 救赎之光:Task.sleep 的 “智能休眠”—— 只停任务,不放线程
  • 📜 终极戒律:Swift 并发的 “不可违背法则”—— 避坑指南
  • 🌟 结尾:魔法与代码的共通之道 —— 细节定成败

“一群小笨蛋,连‘休眠魔咒’的门道都没摸清,还想搞定魁地奇的实时数据?今天就给你们上一课 ——Thread.sleep 和 Task.sleep 的终极区别,搞懂了它,你们的计分器才能像火弩箭一样流畅!”


🧙‍♂️ 开篇:“休眠魔咒” 的污名化 ——Task.sleep 真不是 “过街老鼠”

在 Swift 魔法世界里,“让代码暂停执行” 这事儿,历来被视为 “禁忌操作”—— 毕竟谁也不想自己的魔法程序突然 “卡壳”,就像罗恩上次在魔药课上把坩埚炸了一样狼狈。

但菲尼亚斯的第一句话就颠覆了众人认知:“别一提‘休眠’就谈虎色变!你们总觉得 Task.sleep 和 Thread.sleep 是一丘之貉,其实前者根本没你们想的那么‘不靠谱’,今天咱们就扒掉它俩的‘魔法伪装’,看看谁才是真正的‘捣蛋鬼’。”

首先得明确一点:在 Swift 里让代码 “歇口气” 的法子不止一种,但 Thread.sleep 早就因为 “破坏力太强” 而被老法师们拉入了 “慎用清单”。而 Task.sleep 呢?虽然也常被用来实现 “防抖”(比如防止用户疯狂点击魁地奇计分按钮)或 “任务超时”(比如等待球员数据加载的时限),却总因为和 Thread.sleep 沾了 “sleep” 二字,被不少新手当成 “洪水猛兽”。

在这里插入图片描述

“这就像把‘荧光闪烁’和‘阿瓦达索命’归为一类 —— 纯属谬以千里!” 菲尼亚斯敲了敲魔法屏幕,上面立刻浮现出两行发光的文字,“关键区别,全藏在 Swift 并发世界里‘任务’和‘线程’的运作逻辑里,这可是你们之前逃课没学的重点!

🔍 迷雾初探:为何 Task.sleep 总出现在 “实用魔咒” 里?

哈利举手提问:“教授,我上次在论坛上看别人写‘魁地奇进球防抖’的代码,十篇有九篇用了 Task.sleep,这是为啥呀?”

菲尼亚斯飘到哈利身边,用魔杖一点屏幕,一段代码立刻跳了出来:

// 魁地奇进球防抖逻辑:防止用户1秒内重复点击“进球”按钮
func handleGoalTap() {
    // 先取消之前可能还在等待的任务(类似“解除旧咒语”)
    currentDebounceTask?.cancel()
    // 新建一个任务,让它“休眠”1秒后再执行真正的计分逻辑
    currentDebounceTask = Task {
        do {
            // Task.sleep 的参数是纳秒,这里1_000_000_000纳秒 = 1秒
            // 重点:这里休眠的是“任务”,不是“线程”!
            try await Task.sleep(nanoseconds: 1_000_000_000)
            // 休眠结束后,执行计分(比如格兰芬多得分+10)
            updateScore(for: .gryffindor, points: 10)
        } catch {
            // 如果任务被取消(比如用户1秒内又点了一次),就不执行计分
            print("防抖任务被取消,避免重复计分")
        }
    }
}

“看到没?” 菲尼亚斯的声音里带着得意,“这种场景下,Task.sleep 就像‘时间转换器’—— 让任务先‘暂停’一会儿,既不会耽误其他代码运行,还能精准控制逻辑触发时机。要是换成 Thread.sleep,你们的计分器早就像被施了‘石化咒’一样动不了了!”

在这里插入图片描述

🧵 核心奥秘:任务与线程的 “从属关系”—— 不是 “替代”,而是 “调度”

要搞懂两者的区别,首先得打破一个 “根深蒂固” 的误区 —— 很多新手以为 “Swift 并发里,任务取代了线程”,就像当年巫师用魔杖取代了木棍一样。

菲尼亚斯听到这话,差点笑出了幽灵特有的 “滋滋” 声:“简直是无稽之谈!任务和线程根本不是‘替代关系’,而是‘调度与被调度’的关系,就像魁地奇比赛里,球员(任务)需要骑着扫帚(线程)才能上场,你能说球员取代了扫帚吗?”

在这里插入图片描述

他用魔杖在空中划出两张魔法图,左边是 “无 Swift 并发时代”,右边是 “Swift 并发时代”:

时代 调度工具 执行载体 核心逻辑
无 Swift 并发 Dispatch Queues(飞路网信使队列) Thread(魔法信使) 用 “飞路网队列” 给 “魔法信使” 分配任务,信使跑完一个再跑下一个
Swift 并发 Task(魔法任务卷轴) Thread(魔法信使) 用 “任务卷轴” 给 “魔法信使” 分配任务,信使可以随时切换卷轴,不用等一个跑完

简单说,以前是‘一个信使只能扛一个包裹’,现在是‘一个信使能扛多个包裹,还能随时换着扛’!” 菲尼亚斯解释道,“不管有没有 Swift 并发,你们都不用直接‘创造信使’(管理线程)—— 以前靠‘飞路网队列’安排信使干活,现在靠‘任务’安排。这才是正确的‘心智模型’,要是理解错了,后面的内容就像听‘蛇佬腔’一样难懂!”

💤 危险实验:Thread.sleep 的 “沉睡诅咒”—— 吸干线程,卡住全局

为了让大家直观感受 Thread.sleep 的 “破坏力”,菲尼亚斯启动了一个 “魔法实验”:他召唤出 4 个 “魔法信使”(对应程序的 4 个线程),每个信使负责处理 3 个 “任务”(比如更新计分、播放欢呼声、记录数据等)。

“看好了,这 4 个信使就是你们程序的‘全部运力’,就像霍格沃茨只有 4 辆‘夜骐马车’负责运输一样。” 菲尼亚斯说着,给其中一个信使施了 “Thread.sleep 咒语”—— 只见那个信使立刻停下脚步,抱着包裹原地 “昏睡” 过去,不管其他任务怎么 “喊” 它,都纹丝不动。

在这里插入图片描述

“现在问题来了!” 菲尼亚斯的声音突然变得严肃起来,“原本 4 个信使能轻松搞定 12 个任务,现在少了 1 个,剩下 3 个得扛 12 个任务 —— 这就像让罗恩一个人搬 10 箱魔药材料,不累死才怪!”

更可怕的还在后面:当他给 4 个信使都施了 “Thread.sleep 咒语” 后,所有信使都昏睡过去,屏幕上的任务进度条瞬间变成了红色,魁地奇计分器的数字停在 “40:30” 不动了,连背景音乐都卡住了。

“这就是 Thread.sleep 的‘致命缺陷’!” 菲尼亚斯的魔杖指向昏睡的信使,“它会让整个‘信使’(线程)彻底休眠,期间既不能处理‘飞路网队列’的活,也不能跑其他‘任务’—— 就像被摄魂怪吸走了所有活力!

GCD 时代还好,因为它会‘临时召唤新信使’(新建线程),虽然效率低,但至少不会全卡住;可 Swift 并发不轻易‘加信使’,线程数量是固定的,要是所有信使都睡了,你们的程序就会像被施了‘统统石化’,直到信使醒来才能动 —— 这要是在魁地奇决赛上,观众不得把球场拆了才怪?!”

✨ 救赎之光:Task.sleep 的 “智能休眠”—— 只停任务,不放线程

就在哈利和罗恩倒吸一口凉气时,菲尼亚斯挥了挥魔杖,解除了 “Thread.sleep 诅咒”,然后启动了第二个实验 —— 给任务施 “Task.sleep 咒语”。

同样是 4 个信使,12 个任务。当菲尼亚斯对其中一个 “计分任务” 施咒后,神奇的事情发生了:那个任务暂时 “消失” 了,但执行它的信使没有昏睡,反而立刻拿起了下一个 “播放欢呼声” 的任务,继续干活!

在这里插入图片描述

“看到没?这就是 Task.sleep 的‘智慧’!” 菲尼亚斯的声音里满是赞叹,“它休眠的是‘任务’,不是‘线程’—— 就像让一个球员暂时下场休息,但他的扫帚不会闲着,会立刻交给另一个球员继续比赛!”

他进一步解释道:Task.sleep 本质是 “让当前任务暂时放弃线程的使用权”,线程会立刻被 “调度中心” 分配给其他等待的任务,既不会浪费 “信使资源”,也不会耽误整体进度。 就像赫敏在图书馆查资料时,会把笔记本借给哈利记笔记,而不是抱着笔记本发呆 —— 这才是 Swift 并发的 “高效精髓”!

菲尼亚斯又展示了一段对比代码,清晰标出了两者的区别:

// 🔴 危险!Thread.sleep 的错误示范:让线程昏睡1秒,期间啥也干不了
func badSleepExample() {
    Thread.sleep(forTimeInterval: 1.0) // 这里会让当前线程彻底休眠1秒
    print("1秒后才会打印这句话,但线程休眠期间,其他任务全卡住!")
}

// 🟢 安全!Task.sleep 的正确示范:只休眠任务,线程去干别的
func goodSleepExample() async throws {
    try await Task.sleep(nanoseconds: 1_000_000_000) // 1秒 = 1e9纳秒
    // 休眠期间,执行这个任务的线程已经去处理其他任务了
    print("1秒后打印这句话,但线程没闲着,效率拉满!")
}

📜 终极戒律:Swift 并发的 “不可违背法则”—— 避坑指南

实验结束后,菲尼亚斯飘到实验室中央,黑袍无风自动,活像个即将宣布 “三强争霸赛” 规则的裁判:“现在给你们立下 Swift 并发的‘第一戒律’——在 Swift 并发代码里,永远、永远、永远不要用 Thread.sleep,只用 Task.sleep!

在这里插入图片描述

他特意加重了 “永远” 两个字,眼神扫过罗恩(毕竟罗恩上次就犯过这错):“我很少说‘永远’,但这次必须说 ——Thread.sleep 就像‘未经许可使用时间转换器’,看似能解决问题,实则会引发连锁反应,把整个程序的并发逻辑搅得一团糟。而 Task.sleep 是‘被官方认可的休眠术’,既安全又高效。”

但菲尼亚斯话锋一转,表情又变得严肃:“不过,你们也别把 Task.sleep 当成‘万能解药’!要是有人写代码时说‘不加个 0.01 秒的休眠,这段逻辑就跑不通’—— 这绝对是‘治标不治本’!”

在这里插入图片描述

他举例:比如有人发现 “更新计分后立刻刷新 UI 会卡顿”,就加了 Task.sleep (0.01),看似解决了问题,实则掩盖了 “UI 更新和数据计算没在正确队列执行” 的根本问题 —— 就像罗恩为了掩盖魔药熬糊的事实,往里面加了 “香精”,闻着香,喝下去照样会拉肚子。

“真正的高手,会找到问题的根源,而不是用‘休眠’来藏拙。” 赫敏听到这话,立刻点了点头,偷偷把自己笔记里 “临时加 0.01 秒休眠” 的注释划掉了。

🌟 结尾:魔法与代码的共通之道 —— 细节定成败

当实验室的钟声响起时,哈利已经把 “魁地奇计分器” 的代码改好了 —— 他用 Task.sleep 替代了原来的 Thread.sleep,还修复了隐藏的 “队列串行化问题”。

运行代码的瞬间,屏幕上的计分器流畅地跳动着,格兰芬多的分数从 40 变成 50 时,背景立刻响起了欢呼声,没有一丝卡顿。

菲尼亚斯看着屏幕,满意地捋了捋不存在的胡子:“记住,小巫师们,魔法的真谛在于‘理解每一个咒语的本质’—— 你知道‘除你武器’是缴械,‘昏昏倒地’是催眠,才不会用错场合。编程亦如是:Thread.sleep 是‘困住信使的枷锁’,会让你的程序陷入停滞;而 Task.sleep 是‘给任务的智能休战符’,能让并发逻辑如凤凰涅槃般流畅自如。”

在这里插入图片描述

他最后一挥魔杖,魔法屏幕上浮现出一行金色的大字:“Swift 并发的战场里,选对‘休眠术’,你的代码才能像火弩箭一样,快得让对手望尘莫及;选错了,便是万丈深渊的卡顿,让用户对你的程序‘敬而远之’。”

哈利、罗恩和赫敏对视一眼,都露出了恍然大悟的笑容 —— 原来编程和魔法一样,细节里藏着成败的关键,而今天这堂 “休眠术” 课,无疑给他们的 “魔法编程手册” 添上了至关重要的一页。

那么,各位秃头魔法师,你们学“废”了吗?

感谢观看,我们下次再会吧!8-)

【大话码游之 Observation 传说】下集:破咒终局了,天眼定乾坤

2025年11月8日 13:20

在这里插入图片描述

⚡️ 引子:内存魔咒锁盘丝,旧妖狂笑待崩盘

上回说到,至尊宝用 “信号仓库” 暂时破解了旧观老妖的 “信号失踪” 计,正得意间,盘丝洞的地砖突然开始冒黑烟 —— 观气道人被 “内存魔咒” 缠上,变成了不死不休的僵尸进程,就算把月光宝盒砸成碎片,后台里的计数观测还在疯跑!

“哈哈哈!” 旧观老妖踩着黑烟狂笑,手里的破葫芦(withObservationTracking)都笑出了裂纹,“让你用新法宝!这‘强引用捆仙绳’一旦缠上,别说你这臭猴子,就算如来佛祖来了也解不开!等内存仙力耗尽,整个盘丝洞都得炸成原子!”

在这里插入图片描述

紫霞仙子急得用紫青宝剑砍代码,火花四溅却毫无用处:“亲爱的!这可怎么办?老祖的秘籍里没说这招啊!”

在本篇西游外传中,您将学到如下内容:

  • ⚡️ 引子:内存魔咒锁盘丝,旧妖狂笑待崩盘
  • 7️⃣ 破解内存魔咒:三招斩断强引用捆仙绳
  • 第一招:Task 初始化,弱引用先行
  • 第二招:循环内解包,见好就收
  • 第三招:闭包弱引用,釜底抽薪
  • 8️⃣ 天眼通的终极形态:多属性观测,一网打尽
  • 9️⃣ 终局对决:旧妖溃败,天眼定乾坤
  • 🏁 尾声:新篇待启,仙法无边

至尊宝攥着金箍棒,额头上青筋暴起:“妖魔鬼怪都给我听着!今天就算拆了这破代码,我也得把这魔咒破了!”

在这里插入图片描述


7️⃣ 破解内存魔咒:三招斩断强引用捆仙绳

就在此时,云端传来菩提老祖的洪钟之音:“痴儿!慌什么!这‘内存魔咒’看着吓人,实则有三招可破!且听我道来 ——”

在这里插入图片描述

第一招:Task 初始化,弱引用先行

老祖掷下第一道金光,照在 Task 的初始化代码上:

// 错误示范:Task强引用self,等于给观气道人戴了紧箍咒,永远摘不下来
Task { 
  // 这里的self是强引用,哪怕外面的观气道人被销毁,Task还抱着self不放
  let values = Observations { self.counter.count }
  for await value in values { /* 处理信号 */ }
}

// 正确示范:用[weak self]给Task松绑,像给捆仙绳抹了润滑油
Task { [weak self] in // 关键!弱引用self,Task不绑架观气道人
  guard let self else { return } // 先确认观气道人还在,不在就直接跑路
  let values = Observations { [weak self] in 
    self?.counter.count ?? 0 
  }
  for await value in values { /* 处理信号 */ }
}

在这里插入图片描述

“记住!” 老祖的声音震得洞顶掉灰,“Task 这东西,就像个贪财的小妖,你不给它套‘弱引用紧箍咒’,它就会把 self 死死攥在手里,就算主人(观气道人)死了,它还抱着尸体不放!”

第二招:循环内解包,见好就收

金光再闪,照向 for await 循环:

// 错误示范:循环外强解包self,等于给魔咒上了双保险
Task { [weak self] in
  guard let self = self else { return } // 在这里强解包,等于重新捆紧绳子
  for await value in values {
    // 就算观气道人后来被销毁,self还被循环攥着,内存泄漏没跑
    print(self.counter.count)
  }
}

// 正确示范:循环内按需解包,用完就扔
Task { [weak self] in
  let values = Observations { self?.counter.count ?? 0 }
  for await value in values {
    guard let self else { break } // 每次循环都检查:主人不在了?立马停手!
    print(self.counter.count)
    // 处理完就放手,绝不纠缠
  }
}

在这里插入图片描述

紫霞仙子恍然大悟:“哦!就像我给你送吃的,你吃完了就该把碗还给我,总抱着碗不放,我怎么再给别人送啊!”

第三招:闭包弱引用,釜底抽薪

最后一道金光劈向 Observations 的闭包:

// 错误示范:闭包强引用self,形成“观气道人→Task→闭包→观气道人”的死亡循环
let values = Observations { 
  self.counter.count // 这里的self是强引用,等于给魔咒加了锁
}

// 正确示范:闭包也用[weak self],从根源上断了循环
let values = Observations { [weak self] in // 闭包也弱引用,釜底抽薪
  self?.counter.count ?? 0 
}

在这里插入图片描述

“这三招齐出,” 老祖总结道,“就像给捆仙绳剪了三刀,强引用的循环链条一断,观气道人该投胎投胎,该销毁销毁,内存魔咒自然破解!”

8️⃣ 天眼通的终极形态:多属性观测,一网打尽

破解了内存魔咒,至尊宝突然一拍大腿:“对了老祖!要是我想同时盯着好几个属性变化,比如计数和宝盒的能量值,这天眼通能行吗?”

“问得好!” 老祖赞许道,“这正是天眼通比旧观气术厉害百倍的地方 —— 它能同时观测多个属性,只要你在闭包里碰过的,一个都跑不了!”

在这里插入图片描述

说着,紫霞给计数仙核加了个新属性,演示起多属性观测:

// 升级后的计数仙核,多了个能量值属性
@Observable 
class Counter {
  var count: Int
  var power: Int = 100 // 月光宝盒的能量值
}

// 天眼通同时观测count和power
let values = Observations { [weak self] in
  guard let self else { return (0, 0) }
  // 闭包里访问了两个属性,天眼通会同时盯着它们
  return (self.counter.count, self.counter.power) 
}

// 只要count或power变了,仙流就会发信号
for await (count, power) in values {
  print("次数:\(count), 能量:\(power)")
}

旧观老妖看得眼睛发直:“不可能!我那破葫芦(withObservationTracking)要同时盯两个属性,得写 twice 代码,还经常串线!这新法宝怎么能这么丝滑?”

“因为天眼通是‘属性感知雷达’,” 老祖解释道,“闭包里访问多少属性,它就自动布多少个监测点,不管你最后返回啥,只要碰过的属性变了,立马报警 —— 比哮天犬的鼻子还灵!”

在这里插入图片描述

9️⃣ 终局对决:旧妖溃败,天眼定乾坤

“不!我不甘心!” 旧观老妖见底牌被破,掏出最后一招 —— 疯狂修改计数和能量值,想让仙流过载崩溃。

可至尊宝早已用三招破解了内存魔咒,又靠着多属性观测稳稳接住所有信号。屏幕上的日志整整齐齐,没有一个遗漏,没有一丝卡顿。

在这里插入图片描述

“不可能… 我的时代… 怎么会结束…” 旧观老妖的黑气越来越淡,手里的破葫芦咔嚓一声裂成两半,“想当年,我 withObservationTracking 横行江湖的时候,你们这些小娃娃还没出生呢… 现在… 唉…”

在这里插入图片描述

随着一声叹息,旧观老妖化作一缕青烟消散,只留下一句回荡的遗言:“记住… 技术迭代如江水东流… 不跟上,就只能被拍在沙滩上…”

盘丝洞的黑烟渐渐散去,月光宝盒的计数恢复正常,内存仙力平稳流动。至尊宝搂着紫霞仙子,看着屏幕上顺畅运行的代码,嘿嘿一笑:“看来这 Xcode 26 的天眼通,还真不是盖的!”

在这里插入图片描述

🏁 尾声:新篇待启,仙法无边

紫霞仙子把玩着老祖留下的秘籍,突然发现最后一页有行小字:“天眼通初成,然仙法无穷。他日或有‘多线程仙流分流术’‘信号重放真经’问世,有缘者自得之。”

在这里插入图片描述

至尊宝凑过去一看,眼睛发亮:“多线程分流?那岂不是能让观测速度再快十倍?”

“傻猴子,” 紫霞笑着敲他的脑袋,“先把眼下的观气术练熟吧!说不定哪天,又有更厉害的妖魔鬼怪等着咱们呢!”

在这里插入图片描述

月光透过盘丝洞的窗棂,照在代码上,反射出金色的光芒。属于 Observations 的时代,才刚刚开始。而那些藏在技术深处的奥秘,还等着后来者一一揭开…

感谢各位宝子们的观看,下次我们再会吧!8-)

(全剧终)

【大话码游之 Observation 传说】上集:月光宝盒里的计数玄机

2025年11月8日 13:14
至尊宝看着屏幕上顺畅运行的 “天眼通”,得意地挠了挠头:“嘿,这神器比我的金箍棒还好用!” 可紫霞仙子却指着代码皱起眉头:“至尊宝,你看这循环接收仙流的部分,菩提老祖说这里面藏着两个大陷阱,搞不好咱们

寥寥几行代码实现 SwiftUI 超丝滑弹窗转场动画

2025年11月8日 13:08

在这里插入图片描述

概述

各位微秃小码农们是否已经厌倦了 SwiftUI 中千篇一律、愣头愣脑的 sheet 弹窗动画?我们能否换一个范儿来弹出窗口呢?

在这里插入图片描述

答案是肯定!不仅可以,而且还很容易呢!

在本篇博文中,您将学到如下内容:

概述

  1. 旧式转场的“尴尬”
  2. SwiftUI 新转场范式 总结

小伙伴们无需彷徨等待,让我们马上开始 SwiftUI 弹框转场动画的冒险之旅吧! Let‘s go!!!;)


1. 旧式转场的“尴尬”

在 SwiftUI 构建的 App 里,我们对于应用界面的总体架构来说有几种不同的组织方式。其中,弹出“模式视图”给人一种简单爽快的感觉,这是通过 sheet 修改器来实现的。

下面是通过 Cursor 提示词生成的 SheetContent 视图,用的是 chatGPT5 引擎:

// Sheet 内容视图
struct SheetContent: View {
    @Environment(\.dismiss) private var dismiss    
    var body: some View {
        NavigationView {
            VStack(spacing: 30) {
                // 顶部图标 - 与源按钮匹配
                Image(systemName: "plus.circle.fill")
                    .font(.system(size: 100))
                    .foregroundColor(.blue)
                    .padding(.top, 50)
                
                Text("Zoom Transition 成功!")
                    .font(.title)
                    .fontWeight(.bold)
                
                Text("这个界面通过 Zoom 动画从底部按钮展开")
                    .font(.body)
                    .foregroundColor(.secondary)
                    .multilineTextAlignment(.center)
                    .padding(.horizontal)
                
                // 示例内容
                VStack(spacing: 16) {
                    HStack {
                        Image(systemName: "checkmark.circle.fill")
                            .foregroundColor(.green)
                        Text("动画效果流畅")
                        Spacer()
                    }
                    .padding()
                    .background(Color.green.opacity(0.1))
                    .cornerRadius(10)
                    
                    HStack {
                        Image(systemName: "arrow.up.circle.fill")
                            .foregroundColor(.blue)
                        Text("从源元素展开")
                        Spacer()
                    }
                    .padding()
                    .background(Color.blue.opacity(0.1))
                    .cornerRadius(10)
                }
                .padding(.horizontal)
                
                Spacer()
                
                // 关闭按钮
                Button("关闭") {
                    dismiss()
                }
                .font(.title2)
                .foregroundColor(.white)
                .padding()
                .background(Color.blue)
                .cornerRadius(10)
                .padding(.bottom, 50)
            }
            .navigationTitle("详情")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("完成") {
                        dismiss()
                    }
                }
            }
        }
    }
}

生成的界面如下所示:

在这里插入图片描述

看起来 AI 生成的代码还不错吧?

接下来,再由 chatGPT 5 大脑来搞定我们的主视图:

// 主内容视图
struct MainView: View {
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                Text("Zoom Transition 演示")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                
                Text("点击底部的加号按钮")
                    .font(.title2)
                    .foregroundColor(.secondary)
                
                Spacer()
                
                // 一些示例内容
                VStack(spacing: 16) {
                    HStack {
                        Image(systemName: "star.fill")
                            .foregroundColor(.yellow)
                        Text("这是一个示例界面")
                        Spacer()
                    }
                    .padding()
                    .background(Color.gray.opacity(0.1))
                    .cornerRadius(10)
                    
                    HStack {
                        Image(systemName: "heart.fill")
                            .foregroundColor(.red)
                        Text("点击底部按钮查看 Zoom 效果")
                        Spacer()
                    }
                    .padding()
                    .background(Color.gray.opacity(0.1))
                    .cornerRadius(10)
                }
                .padding(.horizontal)
                
                Spacer()
            }
            .navigationTitle("主界面")
        }
    }
}

最后,同样让 AI 操刀生成 ZoomTransitionDemo 视图,它将会是 sheet 弹窗的绝对主宰(驱动器):

// Zoom transition 演示
struct ZoomTransitionDemo: View {    
    // 控制 sheet 的显示状态
    @State private var isPresentedNoAnim = false
    
    var body: some View {
        MainView()
            .safeAreaInset(edge: .bottom) {
                HStack {
                    Button(action: {
                        isPresentedNoAnim = true
                    }) {
                        VStack(spacing: 10) {
                            Text("旧式转场")
                            Image(systemName: "plus.circle.fill")
                                .font(.largeTitle.weight(.black))
                        }
                        .font(.title3.bold())
                        .foregroundStyle(.red.gradient)
                    }
                    
                }
            }
            .sheet(isPresented: $isPresentedNoAnim) {
                SheetContent()
            }
    }
}

现在,点击我们示例 App 主界面底部的 + 按钮,即可 sheet 蹦出急不可耐的小视图了:

在这里插入图片描述

不过,这种 sheet 弹出视图的转场方式貌似有点“腻歪”,它太其貌不扬,我们甚至有些审美疲劳了。

还好,Apple 及时为我们提供了全新的弹窗丝滑过渡方式,无需增加繁琐的撸码,我们即可实现漂亮的转场效果。

2. SwiftUI 新转场范式

从 iOS 18(SwiftUI 6.0)开始,苹果为导航转场增加了全新的 navigationTransition 修改器方法,它的功用很简单——设置视图导航的转场样式: 在这里插入图片描述

除此之外,我们还需要在导航的源视图上使用 matchedTransitionSource 修改器方法才能确保“如鱼得水”:在这里插入图片描述

让我们利用上面两个修改器方法,将 ZoomTransitionDemo 视图的原代码进行些许修改:

struct ZoomTransitionDemo: View {
    // 用于定义共享坐标空间的命名空间
    @Namespace private var namespace
    
    // 控制 sheet 的显示状态
    @State private var isPresented = false
    
    var body: some View {
        MainView()
            .safeAreaInset(edge: .bottom) {
                HStack {
                    Button(action: {
                        isPresented = true
                    }) {
                        VStack(spacing: 10) {
                            Text("顺滑转场")
                            Image(systemName: "plus.circle.fill")
                                .font(.largeTitle.weight(.black))
                        }
                        .font(.title3.bold())
                        .foregroundStyle(.blue.gradient)
                    }
                    .matchedTransitionSource(id: "transition-id", in: namespace)
                    .padding(.trailing)
                }
            }
            .sheet(isPresented: $isPresented) {
                SheetContent()
                    .navigationTransition(.zoom(sourceID: "transition-id", in: namespace))
            }
    }
}

看到了吗?我们只是在原来主视图的弹出视图和源视图上增加了对应的视图修改器方法即可大功告成!

现在,欣赏一下全新的弹窗转场动画,小伙伴们是否爱了爱了呢?❤️

棒棒哒!💯

在这里插入图片描述

总结

在本篇博文中,我们讨论了如何在 iOS 18+(SwiftUI 6)中仅用寥寥几行代码就让 sheet 弹窗转场动画有了焕然一新的进化,不禁让人眼前一亮!

感谢观赏,我们再会啦!8-)

浪浪山 iOS 奇遇记:给 APP 裹上 Liquid Glass “琉璃罩”(上集)

2025年10月7日 13:05

在这里插入图片描述

引子

浪浪山的朝阳刚爬上山头,小妖怪阿强就抱着他那台快被摸包浆的 MacBook,跟阿花蹲在老桃树下唉声叹气。

在这里插入图片描述

山大王昨天拍着青石桌下令,三天内必须给 “浪浪山访客登记 APP” 换上 iOS 26 新出的Liquid Glass设计,要是搞不定,俩人这个月的桃干俸禄就得全扣光。

在本篇奇遇记中,您将学到如下内容:

  • 引子
  • 🔍 先搞明白:Liquid Glass 这 “妖法” 该啥时候用?
  • 🛠️ 动手试试:给 UI 元素裹上 Liquid Glass “琉璃罩”
  • 🎨 给 “琉璃罩” 上色:加 tint 调背景
  • ✨ 让按钮 “活” 起来:加 interactive “互动咒”
  • 上集尾声:山大王催进度,容器 “秘招” 待解锁

可这Liquid Glass到底是啥 “妖法”?俩小妖连门儿都没摸着,只能对着屏幕抓耳挠腮,连树上掉下来的桃毛都没心思拍。

在这里插入图片描述


🔍 先搞明白:Liquid Glass 这 “妖法” 该啥时候用?

阿花翻遍了 Apple 给的 “仙册”(其实就是开发者文档),终于指着一行字喊:“阿强你看!Liquid Glass是 iOS 26 的新设计语言,说白了就是给 APP 盖一层‘琉璃罩’,但这罩子可不能乱盖!”

原来这 “琉璃罩” 的核心规矩是:只盖在 “浮” 于主界面上的元素,不能裹住整个 APP 的内容。阿强不信邪,偷偷给 APP 里的访客列表每一行都加了 “琉璃罩”,结果运行起来一看 —— 界面乱得像妖精打架,文字和背景糊在一块儿,比山大王喝醉后画的地图还难认。

“你这是犯了‘本末倒置’的错!” 阿花戳了戳屏幕,“主内容是访客信息,得清清楚楚;‘琉璃罩’该用在工具栏、标签栏、浮标按钮这些‘外挂’元素上,就像浪浪山入口的岗亭,得盖层罩子挡雨,但不能把山路都罩起来啊!”

在这里插入图片描述

说着阿花打开参考 APP “Maxine”(据说是山外神仙做的健身 APP),指着屏幕底部:“你看这默认的标签栏,就是Liquid Glass做的‘琉璃罩’,盖在列表上面不挡内容;还有那个浮标加号,也裹了层薄罩,就是背景太亮看不太清 —— 这才是正确用法!”

在这里插入图片描述

阿强摸了摸后脑勺:“原来如此!那要是实在不想用这‘琉璃罩’咋办?” 阿花又翻了翻 “仙册”:“Apple 留了个‘逃生舱’,下一个大版本前都能用,但山大王要新效果,咱躲不过咯!”

在这里插入图片描述


想要进一步了解如何在 iOS 26 中让 App 界面不适配液体玻璃效果的方法,请小伙伴们移步如下链接观赏精彩的内容:


🛠️ 动手试试:给 UI 元素裹上 Liquid Glass “琉璃罩”

既然躲不过,俩小妖决定先从一个小功能下手 —— 复刻山外早已失传的 “Path APP” 按钮,给它裹上Liquid Glass

在这里插入图片描述

阿强从 GitHub 上扒来了起始代码(据说那是山外神仙留下的 “秘籍”),代码长这样,阿花还贴心加了中文注释:

struct ContentView: View {
    // 控制按钮展开/收起的状态,就像控制桃树结果子的开关
    @State private var isExpanded = false
    
    var body: some View {
        // ZStack:把背景图和按钮叠放,类似先铺桃叶再放果子
        ZStack(alignment: .bottomTrailing) {
            Color
                .clear
                .overlay(
                    // 背景图:浪浪山的风景图,铺满整个屏幕
                    Image("bg_img")
                        .resizable()
                        .scaledToFill()
                        .edgesIgnoringSafeArea(.all)
                )

            // 四个功能按钮:首页、写字、聊天、邮件
            button(type: .home)
            button(type: .write)
            button(type: .chat)
            button(type: .email)

            // 主按钮:点一下展开/收起其他按钮,像打开果篮的开关
            Button {
                // 加动画:让按钮动起来不生硬,类似果子落地的缓冲
                withAnimation {
                    isExpanded.toggle()
                }
            } label: {
                Label("Home", systemImage: "list.bullet")
                    .labelStyle(.iconOnly) // 只显示图标,不显示文字
                    .frame(width: 50, height: 50) // 按钮大小:像个小桃儿
                    .background(Circle().fill(.purple)) // 紫色圆形背景
                    .foregroundColor(.white) // 图标白色
            }.padding(32) // 离屏幕边缘留点空,不然像贴在悬崖边
        }
    }

    // 自定义按钮方法:根据类型返回不同按钮(首页、写字等)
    private func button(type: ButtonType) -> some View {
        return Button {} label: {
            Label(type.label, systemImage: type.systemImage)
                .labelStyle(.iconOnly)
                .frame(width: 50, height:50)
                .background(Circle().fill(.white)) // 白色背景
        }
        .padding(32)
        .offset(type.offset(expanded: isExpanded)) // 按钮展开时的位置偏移
        .animation(.spring(duration: type.duration, bounce: 0.2)) // 弹簧动画,有点弹性
    }
}

“这按钮现在就是普通的‘硬疙瘩’,咱给它裹上Liquid Glass试试!” 阿强说着,在主按钮后面加了个glassEffect() 修饰符,代码变成这样:

Button {
    withAnimation {
        isExpanded.toggle()
    }
} label: {
    Label("Home", systemImage: "list.bullet")
        .labelStyle(.iconOnly)
        .frame(width: 50, height: 50)
        .background(Circle().fill(.purple))
        .foregroundColor(.white)
}
// 加上Liquid Glass“琉璃罩”!
.glassEffect()
.padding(32)

结果运行一看 —— 啥 “琉璃罩” 都没有!按钮还是原来的紫色硬疙瘩。

在这里插入图片描述

在这里插入图片描述

阿花凑过来一看,突然笑出声:“你这是把‘隐身符贴在铠甲外面’啊!按钮有个紫色背景,把glassEffect全挡住了,得把背景去掉才行哦!”

🎨 给 “琉璃罩” 上色:加 tint 调背景

阿强赶紧删掉.background(Circle().fill(.purple)),再运行 —— 按钮是透明了,但图标淡得像蒙了层雾,差点看不清。俩人对着屏幕嘀咕:“这是 beta 版的‘妖气’干扰,还是本来就这样啊?”

阿花又翻了翻 “仙册”:“还有个buttonStyle(.glass) 能试,不过这风格太‘死板’,像山大王给的统一制服,想绣个小桃花都不行。” 试了之后果然如此,自定义空间少得可怜。

“有了!” 阿花忽然拍了下手,“给glassEffect加个 tint(色调),就能给‘琉璃罩’上色,还能调透明度!” 说着就改了代码:

Button {
    withAnimation {
        isExpanded.toggle()
    }
} label: {
    Label("Home", systemImage: "list.bullet")
        .labelStyle(.iconOnly)
        .frame(width: 50, height: 50)
        .foregroundColor(.white)
}
// 给琉璃罩加紫色调,像后山的紫藤花汁
.glassEffect(.regular.tint(.purple))
.padding(32)

运行后一看 —— 嘿!按钮变成了带紫色的透明 “琉璃盏”,还自动是圆形的,不用再画背景了!

在这里插入图片描述

“Apple 这‘妖法’还挺贴心,知道圆形好看!” 阿强忍不住夸了一句。

在这里插入图片描述

但阿花觉得还不够通透:“再加点透明度,像晨雾里的琉璃盏才好看!” 于是又把颜色改成.purple.opacity(0.8),这下效果刚好 —— 既清楚又有 “玻璃感”,比山大王的琉璃酒杯还精致。

在这里插入图片描述

✨ 让按钮 “活” 起来:加 interactive “互动咒”

按钮好看了,但点下去没反应,像块死木头。

阿花又找到了 “仙册” 里的秘诀:给glassEffect加个interactive(),就能让按钮点的时候 “亮一下” 还稍微变大,像 “一碰就发光的仙果”!

改完的代码是这样:

Button {
    withAnimation {
        isExpanded.toggle()
    }
} label: {
    Label("Home", systemImage: "list.bullet")
        .labelStyle(.iconOnly)
        .frame(width: 50, height: 50)
        .foregroundColor(.white)
}
// 加了interactive,点的时候会有 shimmer 效果还会变大
.glassEffect(.regular.tint(.purple.opacity(0.8)).interactive())
.padding(32)

在这里插入图片描述

俩人轮番点了点,都笑了:“这下有那味儿了!像摸了会发光的萤火虫,比山大王的夜明珠还灵!”

在这里插入图片描述

可高兴没多久,阿强又皱起眉:“虽然互动有了,但这些按钮还是各自独立的,像散落在石桌上的果子,没‘液态’那感觉 ——Apple 说的‘液体玻璃’,得像流水似的融在一起才对呀!”

上集尾声:山大王催进度,容器 “秘招” 待解锁

阿花盯着屏幕忽然眼睛一亮:“‘仙册’里提了个GlassEffectContainer!把所有按钮放进这个‘容器’里,它们靠近时就会像融在一起的糖浆,动起来也会像流水似的!”

在这里插入图片描述

俩人刚要动手写代码,就听见山大王的大嗓门从远处传来:“俩小妖!APP 改得咋样了?再磨蹭午饭的肉干也没了!”

阿强赶紧把 MacBook 合上,阿花攥着写满笔记的桃叶小声说:“别急,下晌咱们就试这个GlassEffectContainer,肯定能让这些按钮像浪浪河的水似的,流着动起来!”

在这里插入图片描述

到底这 “容器” 咋用?按钮能不能真的 “液态” 起来?山大王的肉干能不能保住?咱们下集接着唠!

❌
❌