跳到正文
Chandler Nguyen
AI阅读时间1分钟

AI

Native 不是换层皮:重做 DIALØGUE 的 iOS App

几个月前,我把 DIALØGUE 的 iOS app 当成网页版的移植直接发了出去,后来又几乎全部重做成了真正的原生 app——三个 tab、锁屏音频、同步字幕、扛得住糟糕网络的离线、还有 Siri——因为一个被压缩进手机的网页,说到底还是网页。

几个月前,我把 DIALØGUE(对话)的 iOS app 当成网页版的移植发了出去。后来,我几乎把它全部重做成了一个真正的原生 app。

它现在已经上架 App Store 了——如果你好奇,欢迎去戳一戳。这篇剩下的内容,写的是到底改了什么,以及为什么。

这两者的差别,听上去像是在抠字眼,直到你把两个版本同时拿在手里。第一个版本就是把网页压缩进手机:一样的 tab、一样的多步创建向导、一样的 dashboard。它能编译、能跑、也通过了审核。可它给人的感觉,就像一个网站套了一身 app 的戏服。

一个被压缩进手机的网页,说到底还是网页。Native 不是在你网页布局上面刷一层漆——它是和设备之间一份完全不同的契约。 而这份契约,正是我当初跳过的东西。为了认真履行它去重做,结果改动了 tab、音频、收听界面、离线行为,连 app 怎么启动都变了。下面就一屏一屏地说,这到底意味着什么。

那个移植版本,本质上是个 dashboard

第一个版本默认 app 的工作就是把生成器摆出来。DIALØGUE 是做 AI 播客的,所以当时的想法是:把每一个网页界面都搬到屏幕上,事情就算做完了。

这么做的结果,就是一个 dashboard。五个 tab、一个多步向导、什么都给一块面板。没人打开一个播客 app 是为了操作 dashboard。他们打开它,是为了把一个想法变成声音,是为了听自己做出来的东西,或者是为了重新开始一档之前跑过的节目。那个移植来的界面,护住的是网页的布局,护不住的恰恰是这三件事。

所以这次重做,从你看到的第一样东西开始。

三个 tab,因为手机会惩罚 tab

旧 app 打开是五个 tab:Library、Studio、Create、Credits、Profile。移植网页就会得到这种结果——每一个网页界面都要争一个 tab。

重做后的 app 只有三个:收听(Listen)、创建(Create)、你(You)

有两个 tab 没能活下来。Credits 不再是一个目的地——没人会打开一个 app 专门去盯着余额看。它被并进了"你"里面,而唯一真正重要的那个时刻——正要去生成时余额刚好不够——现在会恰好在那一步弹出一张购买卡片。Studio 也不再是一个"地方"了。 它变成了 系列(Series)——一套保存好的设定(主持人、语气、格式、语言、来源模式),它住在"创建"里面,并在"收听"里以一个合集的形式出现,而不是装成一间控制室的 tab。Library 和 Profile 则变成了更朴素的"收听"和"你"。

在桌面端,多一个 tab 是免费的。在手机上,每一个 tab 都是对注意力征的税。这次重做,把这笔税还掉了。

DIALØGUE iOS 的「系列」界面,用来保存一套可重复使用的播客设定
系列——一套住在「创建」里的保存好的设定,而不是独占一个 tab 的控制室。

按 iOS 期待的方式去做音频

就是在这里,"native 是一份契约"这句话不再抽象。

那个移植版本并不是完全不懂音频。它已经能出现在锁屏上,带着播放、暂停、跳过,来电时会暂停,拔掉耳机时会停下来。这些都是基本盘,它确实做到了。

它做不到的,是一旦屏幕锁上,还能像一个真正的音频 app 那样表现。你没法从锁屏拖动播放进度条。锁屏上没有封面图——只有文字。没有 AirPlay 按钮,没有睡眠定时器,跳过永远是固定的十五秒,不管你喜不喜欢。

这次重做,是顺着平台去补上这道缺口,而不是绕开它。现在锁屏上会显示这一集的动态封面图,还有一个可以拖到任意位置的进度条。有 AirPlay,有一个会把音量慢慢淡出、而不是直接掐断的睡眠定时器,还有一个可以从十秒到六十秒自由设定的跳过间隔——而这个设定还会同时驱动锁屏上的按钮,因为系统控件就该和 app 里保持一致。音频会话把自己声明成"语音类音频",所以系统会把它当成说话来对待,而不是音乐。

这些都不光鲜。这正是重点。在手机上,一个无视锁屏和 AirPlay 的音频,不叫"极简"。它恰恰是在人们真正会听的那些时刻坏掉了——走路时、开车时、手机揣在口袋里时。

一份只有 AI 播客才做得出来的字幕

接下来这部分,是真正属于我们自己的东西,而不只是良好的 iOS 习惯。

播放一集的时候,app 会显示一份同步的字幕:当前那句会高亮,画面自动滚动让它保持在中间,你点任意一句,就能直接跳到那个时刻。一个通用的播客 app 其实做不到这一点,因为它不知道哪句话是在什么时间说的。DIALØGUE 知道——是它生成了脚本,所以它早就知道这一集的结构。

诚实地讲一个工程细节:一句话只有在音频带有精确的逐段时间戳时才可以点。当时间戳只是大概的,那句话照样会显示出来,只是不能跳转,而且 app 不会假装它能。同一套分段时间戳还会直接在进度条上画出章节刻度,所以你可以像快速浏览音频一样略读一集,但手里还多了一张"系统到底做了什么"的地图。字幕甚至会标出哪句话是哪位主持人说的,因为脚本本来就是按双主持人对话写的。

这就是"在播放器上硬装一份字幕"和"把生成的脚本当成整个收听体验的事实来源"之间的差别。

有两样东西让这件事成立,而它们都不会出现在屏幕上。后端会给每一段都盖上开始和结束的时间戳,并标记这个时间是精确的还是只是估算——这样 app 才能让你点一句精确计时的话,又能悄悄地拒绝在一句估算的话上弄虚作假。还有,你的播放进度是存在服务器上的,不只是存在设备上,所以"从上次的地方接着听"在你网页上开了一集、手机上接着听完时也照样成立。

DIALØGUE iOS 的同步字幕界面,字幕卡片可点击跳转
同步字幕——点一句就能跳过去。这之所以可能,是因为这一集的脚本本来就是这个 app 生成的,它知道时间戳。

扛得住一次真实通勤的离线

这个 app 从一开始就能下载单集。这不是新东西,我想说得准确一点。这次重做加上去的,是韧性——是"一个下载功能"和"一份你信得过的离线"之间的差别。

现在一个被中断的下载会从断点继续,而不是从头再来,因为 app 会按集保存系统的恢复数据,并从那里重新开始。有一个"仅 Wi-Fi"的选项,它会真的禁掉蜂窝网络,所以一个排队中的下载会乖乖等 Wi-Fi,而不是悄悄烧掉你的流量套餐。下载最多同时跑三个,排成一个先进先出的队列,而不是一窝蜂去挤网络。有一个存储界面,让你能看到、也能删掉设备上的东西。那些在网络不稳时会失败的临时请求,会带着退避重试——试三次,从半秒开始往上加直到一个上限,并且绝不会去重试一个用户主动取消的操作。

重试很容易;难的是重试时不去和一个刚按了取消的用户对着干——正是这一点,让离线在糟糕的地铁信号下感觉是稳的,而不是在那里干转圈。

DIALØGUE iOS 的离线收听界面,带有已下载单集的控制
离线一直都在。这次重做让它变得有韧性——断点续传、仅 Wi-Fi、存储控制、有上限的队列。

即时,因为转圈圈会被读成"坏了"

在手机上,延迟是一种感受,不是一个数字。冷启动时的一个转圈圈,会被读成"这个 app 坏了",哪怕其实什么问题都没有。

那个移植版本每次冷启动都重新下载每一张封面图,所以打开 Library 看到的是满屏的转圈圈。这次重做加了一层封面图的内存加磁盘共享缓存——磁盘那层能挺过重启,内存那层让滚动保持顺滑,连锁屏也复用同一份缓存来显示它的封面图。重新打开一集以前要等一次网络往返;现在每一集的分段和字幕都被缓存起来,会立刻渲染出来,再在后台悄悄刷新。我还把那个每半秒一次的播放计时从列表行里抽了出来,这样这个计时器就不会再逼着整个 Library 重画一遍。

这不是一个你能截图的功能。它是 app 和网站之间的那道沟。

集做好了,它会提醒你

生成一档播客要的是几分钟,不是几秒钟——要做研究、列大纲、写脚本,然后才是音频。那个移植版本让你从头到尾盯着进度条。这个原生 app 不会。

在你允许的前提下,你的这一集一做好,它就发一条推送通知给你,所以你大可以锁上手机、去做别的事,等它一震你再回来。设备 token 存在服务器端,只有那个负责发通知的任务能读到它,而且你可以在设置里把整套东西关掉。这只是一小段管道——一张表、一个 worker、苹果的推送服务——但它把产品给人的感受,从"在这个屏幕前面等着"变成了"做好了我会告诉你"。

工作流可以从 app 之外启动

一个原生 app 不只活在自己的窗口里。这次重做通过 App Intents 加上了 Siri 和快捷指令的支持,所以"创建一档播客""继续收听""打开我的播客"都能作为说出来的短语、在快捷指令 app 里、以及在 Spotlight 里用上——不需要任何特殊授权。至于"继续"到底该做什么(接着播当前这一集,还是在什么都没加载时打开 Library),这个决定是一个极小的纯函数,我可以单独给它写单元测试——正是这种东西,能让 Siri 的行为不至于跑偏。

第一次打开还有一段克制的三页欢迎——创建、声音、随处收听——只显示一次;还有 deep link,让被点开的链接打开正确的界面,而不是把你丢在首页那个 tab 上。都是些小事。但它们正是"一个停在手机上的 app"和"一个属于这部手机的 app"之间的差别。

也说一句我在哪里停手了:目前还没有主屏幕小组件、没有实时活动(Live Activities)、也没有 CarPlay——每一样都需要它自己的扩展,或者一份苹果发放的授权,而我选择先把收听这个核心发出去。这里的"Native"是一个方向,不是一张打完勾的清单。

我从这件事里带走的一课

如果你正在把某样东西移植到一个新平台,最诱人的做法,就是让它能在那上面跑起来,然后就当做完了。它确实会跑。但它也会让人感觉是借来的。

Native 是和设备之间的一份契约:尊重锁屏、尊重音频输出路径的变化、尊重离线的现实、尊重人们早已在用的那些系统界面。一个移植版本,护的是你旧的布局。一个原生 app,护的是平台的惯例——哪怕这意味着要删掉 tab、重写你已经发出去的界面。

我手上没有什么惊人的安装量或留存数字可以拿出来挥舞——当前这个版本就在 App Store 上,这就是它诚实的状态。真正的考验从来都不是截图看上去够不够 native。它是:有没有人做了一集,在锁着屏的散步路上听完,然后回来再做一集。

如果你在做移动端,我很好奇你把那条线画在哪里:什么时候"它能在手机上跑"就够了,什么时候平台的契约逼着你不得不重做?

常见问题

DIALØGUE 的 iOS 重做到底改了什么?

这个 app 从网页版的一次移植,变成了一次原生重做。信息架构从五个 tab 收到了三个(收听、创建、你)。收听体验加上了可点击跳转的同步字幕、章节标记、锁屏拖动进度和封面图、AirPlay、睡眠定时器,以及可配置的跳过间隔。离线下载变得有韧性,app 会缓存封面图和分段以获得即时的感受,Siri 和快捷指令让核心操作能从 app 之外启动,还有一条"集做好了"的推送通知会告诉你什么时候完成。

为什么五个 tab 变成了三个?

因为手机会惩罚每一个多出来的 tab。Credits 不是任何人想去的地方,所以它被并进了"你",再加上一张在你真的余额不够时才出现的购买卡片。Studio 变成了系列——一套住在"创建"里的保存好的设定,而不是它自己的一个 tab。Library 和 Profile 则变成了更朴素的"收听"和"你"。

这次重做加了哪些原生 iOS 功能?

锁屏拖动进度和动态封面图、AirPlay、带音量淡出的睡眠定时器、可配置的跳过间隔(10–60 秒,并且会同时驱动锁屏控件)、进度条上的章节刻度、可点击跳转的同步字幕、有韧性的离线下载(断点续传、仅 Wi-Fi、存储管理、有上限的队列)、封面图和分段的共享缓存以实现即时渲染、Siri 和快捷指令意图、"集做好了"的推送通知、存在服务器端、能在网页和手机之间接续的播放进度、deep link,以及首次打开的引导。

这份同步字幕对每一集都好用吗?

只有当音频带有精确的逐段时间戳时,字幕里的某一句才可以点击跳转。当时间戳只是大概的,那句话照样会显示出来,但不能跳转,而且 app 不会假装它能。这份字幕之所以可能,是因为脚本本来就是 DIALØGUE 生成的,所以它知道结构、也知道谁说了什么。

Studio 和系列(Series)有什么区别?

Studio 暗示的是一间满是旋钮的控制室。系列就只是一套保存好的设定——主持人、语气、格式、语言、来源模式——它让你不用重新配置一切,就能开始下一集。它是住在"创建"里的一个预设,不是一个独立的 tab。

这和 NotebookLM 的音频概览有什么不同?

NotebookLM 确实很有用,也是免费的,能把你的资料变成一段快速的音频概览。DIALØGUE 想做的,是在"生成"之上做一个完整的原生收听产品:在出音频之前先审大纲和脚本、选声音、一份可点击跳转的同步字幕、章节、锁屏和 AirPlay 控制、离线下载、Siri,还有给重复节目用的系列。诚实地说,这个区别与其说是"谁生成的音频更好",不如说是"这一集存在之后,接下来会发生什么"。

这次重做是把离线收听加上去的吗?

离线下载早就有了。这次重做让它变得有韧性:被中断的下载会续传而不是重来,有一个"仅 Wi-Fi"选项和一个存储界面,下载最多同时跑三个,临时的网络失败会带着退避重试,又不去和一个按了取消的用户对着干。

我这边暂时就这些了。

祝好,Chandler