客户端功能 & 设计

每一项设计都解一个具体问题。下面是 iOS 客户端值得说的功能 + 对应的「为什么这么做」。

浏览

三轴艺人导航

页面顶部一条艺人横滑列表,下面是这位艺人的专辑 grid,再下面是专辑里的曲目。三层一目了然,不用点进点出三层导航。

为什么这么设计:

选中艺人自动滚到视野中心

搜索结果跳过来、初次进 App 加载上次留的艺人,被选中的头像会自动滚到艺人 row 的中间,加 1.05× 放大 + 紫色描边 + 阴影。

为什么: 不滚的话你大概率看不到被选中的人,因为它可能在屏幕外。视觉反馈解决「我点了,但没看到反应」这种焦虑。

搜索

递进字符搜索

输入框打一个字,下面立刻出「这字后面是哪个字?」的 chips。点 chip 接力一字一字筛,匹配 ≤ 8 项时直接列结果。

为什么这么设计:

后退一字 vs 全清空

输入框右边的按钮不是 ✗(全清),是 (删一字)。

为什么: 搜不到时第一反应是「太窄了,回退一字接着试」。全清会把上下文也清掉,反而要重新打。后退一字 = 优化你下一次尝试,不是惩罚你这一次失败。

保留上次搜索

App 关掉再打开,搜索框里上次打的字还在,下面的结果也立刻刷出来。

为什么: 你关掉 App 通常不是「搜完了」,而是「先去做别的,等下回来接着搜」。让 App 替你记住状态,比你又敲一遍要尊重你的时间。

封面 / 元数据

4 源切换换封面

长按专辑封面弹封面选择器,4 种来源:

为什么:

手动指认曲目(含本地指纹库)

长按曲目 → 弹 MusicBrainz 搜索 → 选对的 recording。绑完,MusicTidy 自动算这首歌的 chromaprint 存到你本地指纹库

为什么:

长按 + 滑动:物理操作的轻重区分

为什么: 不同破坏性的动作配不同物理强度,符合直觉,手滑成本低。

队列 / 播放

队列追加不替换

进新专辑点第一首,不是把当前队列清掉重排。是把整张专辑追加到队列末尾,跳到该首开始播。老队列保留作 history。

为什么:

队列页两级 + 保存为播放列表

队列页按专辑分组,每组可展开 / 收起。点专辑头切收起;点曲目跳到该首播。当前队列可一键保存为命名播放列表,存本地 UserDefaults。

为什么:

Tab 自切换:再点队列 → 当前曲目滚入视野

已经在队列 tab 时再点一次「队列」标题,自动展开当前播的专辑组 + 滚到该首。

为什么: 最常见的操作是「我刚加了一首,它在哪?」自动滚比手动翻整个队列快十倍。

播放器

顶部 tab 只能从标题滑

曲库 / 队列两个 tab,左右滑切换的手势只绑在 title 上,不绑内容区。

为什么: 内容区里到处都是可横滑的元素(艺人 row、专辑 grid)。绑在内容上的切换手势会跟内容滑动打架,造成”我想滑横向 chip 结果跳页了”。标题区是清晰、专属的切换入口。

锁屏控制 / CarPlay / 蓝牙耳机

完整支持 iOS 标准 MPRemoteCommandCenter:锁屏、控制中心、AirPods、车载方向盘按钮全都能 play / pause / next / prev。Now Playing 信息里有专辑封面(异步加载)。

为什么: 和系统集成不是可选项,是默认期待。

别的 app 抢音频也能正确恢复

YouTube / 电话 / Siri / 闹钟接管音频后,MusicTidy 的 UI 状态正确切到 paused;外部结束后如果系统暗示 .shouldResume,自动继续。

为什么: 长期手机 App 最容易出问题的就是「状态跟实际不一致」—— UI 显示在播但 AVPlayer 早就被踢停了。3 层保护:KVO timeControlStatusAVAudioSessionInterruptionrouteChangeNotification,保证状态始终是真的。

服务器 / 配置

服务器地址 3 字段分开(不是一个 URL 框)

scheme(https / http 下拉)+ host + port,三个独立字段。port 默认值跟着 scheme 走(https → 443 / http → 8765)。

为什么:

二维码扫描配置

服务器设置页有「扫二维码」按钮,从相册选一张包含 musictidy://server?scheme=...&host=...&port=... 的二维码图,App 用 Vision framework 解析后三字段自动填好。

为什么: 朋友推荐 / 网站 demo 这种场景,扫码比照着输 URL 友好太多。

服务器指纹握手

测试连接时,App 调 /healthz 检查响应里 "app": "MusicTidy" 字段。不是 MusicTidy 服务器会明确报错:「能连上,但不是 MusicTidy 服务器」。

为什么: 有时候 host 输错了打到别的 HTTP 服务上,普通连通性测试会绿勾欺骗你。指纹握手避免”看着像通了实际通不了”。

多服务器随时切换

设置页可以随时改服务器 URL(包括登录界面也有「更换服务器」按钮)。

为什么: 家里一个 NAS、出差时一个 VPS demo、试朋友的服务器 —— 不应该被锁死在一个。登录页加更换入口是关键 —— 不然你忘记密码 + 服务器换了就彻底死锁。

安全 / 登录

Face ID / Touch ID 登录

设置里开关一键启用。token 从 UserDefaults 搬进 Keychain(带 .biometryCurrentSet),下次打开 App 自动弹 Face ID 解锁。失败可以「改用密码登录」回退。

为什么:

退出登录会停掉正在播放

退出 → 先 player.stop() → 再清 token。

为什么: 不停的话退出之后还在偷偷播,下个用户启动 App 时尴尬。「退出」=「彻底结束这个会话」的物理直觉应该被尊重。

离线缓存

自动 LRU 5GB(默认开)

边听边自动落盘,超过 5GB 按 mtime 最旧的淘汰。不用任何手动操作。

为什么: 通勤一进地铁 / 飞行模式开启时,最近听的歌应该还能继续放。自动比要求用户「下载」直觉得多。

默认仅 WiFi

蜂窝时不被动缓存,省流量。

为什么: 不少用户流量套餐有限。默认仅 WiFi 是友好的,可在设置关掉。

主动整张下载 + 长按选码率

专辑详情页 ⬇️ 按钮:

为什么:

缓存详情按专辑分组 + 左滑删

设置 → 当前用量 → 弹出列表,按专辑分组 + 总大小,左滑某行删整张。

为什么:

清缓存后 UI 实时刷新

清空 / 删某张专辑后,AlbumDetailView 上的绿色对号立即消失,不需要重新进页面才看到。

为什么: 状态和真实磁盘要绝对一致。否则用户看到对号还在会怀疑「删了没?」

微交互(每一次点击都是设计)

点底部 PlayerBar → 跳到正在播的专辑详情,自动滚到当前曲

不是弹一个浮窗,是直接 navigate 到那张专辑页 + 把当前曲目滚到列表中间。

为什么:

PlayerBar 跳过去时,如果你刚好在那张专辑页 → 弹 FullPlayer

聪明的兜底:当前 NavigationStack 已经在播放的那张专辑了,再点 PlayerBar 不重复 navigate,直接弹全屏播放器。

为什么: 不该让”已经达成目的的点击”反而变成 noop 或重复 push。

在专辑详情页点 PlayerBar 时切歌的话也保留

切歌后专辑详情页里的 EqualizerBars 动画跟着移动到新曲那行,封面、按钮”在播”状态都跟着新曲。

为什么: 专辑页是个连续观察界面,应该反映实时状态,不应该是静态截图。

切歌时自动滚到该曲那一行

ScrollViewReader .onChange(of: player.current?.id) 触发,动画把新当前曲目滚到屏幕中间。同时顺手刷一遍下载状态(被动缓存可能刚把上一首落盘,绿对号要亮起来)。

为什么: 肉眼能看到”现在在播第几首” + 顺手验证缓存状态 = 一举两得。

再点已选 tab → 触发上下文动作

为什么: 原生 iOS Tab Bar 就是这种行为 —— 重复点击是”回到这个 tab 的根/最重要状态”。我们的 custom pager 也跟着这套直觉。

长按 vs 短按的语义对照

触发短按长按
曲目加入队列接着播弹手动指认 sheet
专辑封面(无)换封面 4 源选择器
下载 ⬇️ 按钮源码下载(蜂窝时 confirm)选码率(5 档)
艺人头像选中这位艺人 + 加载专辑(无)

为什么: 长按 = “我知道我在做不可逆/重操作”。给手指 0.5 秒收回的时间,符合所有手机 OS 的肌肉记忆。

搜索结果点击的明确含义

你点的是跳到哪
艺人那位艺人的专辑页
专辑那位艺人的专辑页(专辑作为入口,而不是直接播)
曲目直接播这首(你点曲目就是想听)

为什么: 点专辑直接播不合理 —— 你可能只是想看看里面有什么。曲目反过来:搜出来点一下就是想听,不绕弯。

“队列追加 vs 替换” 严格区分

为什么: 你的播放历史不应该因为你「想听点别的」就被清掉。听完想回去再听刚才那首 → 队列里还在。

拉刷新 = 触发服务器 scan

曲库页下拉 → 服务器跑一次 scan,等几秒再刷艺人列表。

为什么: 你刚把新文件 rsync 进服务器,下拉刷新是最直觉的”现在去看一下”动作。比专门进设置点按钮短得多。

退出登录前先停播

退出 → player.stop()await api.logout() → 清 token。顺序固定。

为什么: 不停的话 token 都清了,AVPlayer 还在用旧 token 拉下一首流 → 401 → 但歌还放完已下载的部分。这种「半生不死」状态很违背直觉。

Toast 是非破坏性反馈

成功的操作(保存为播放列表 / 自定义封面成功 / 等等)用底部 toast 提示,2 秒自动消失。失败的操作用红字 inline 显示,不消失直到你重试。

为什么: 成功操作不需要用户做任何事,toast 自动消失最不打扰。失败需要用户决定下一步,所以要 inline + 保留信息直到处理。


调试 & 透明

模拟蜂窝 toggle(DEBUG 版本)

设置最下面调试 section(Release 构建里没有):勾选后强制把网络判定成蜂窝。

为什么: 模拟器 NWPathMonitor 永远报 WiFi,没这开关没法测蜂窝分支的所有 UX 流程(流量警告 / AAC 转码 / 下载 confirm 等)。

Bullhead 错误透明

服务器返回错误时不是黑屏,给具体的 toast / 红字:「连不上 xxx」/ “能连上但不是 MusicTidy 服务器” / “密码错误”等等。

为什么: 自托管用户经常调环境,错误信息能让他们一步定位问题。


不在这里看到的功能 + 设计原由可以问 GitHub DiscussionsIssues