阅读视图

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

微信小程序同声传译插件深度应用:语音合成与长文本播放优化

之前的文章 微信小程序同声传译插件接入实战:语音识别功能完整实现指南介绍如何使用同声传译插件进行语音识别,这篇将会讲述同声传译的另一个功能语音合成。

功能概述

微信小程序同声传译插件的语音合成(TTS)功能能将文字内容转换为语音播放,适用于内容朗读、语音提醒、无障碍阅读等场景。

核心实现架构

状态管理

const textToSpeechContent = ref("")
const textToSpeechStatus = ref(0)  // 0 未播放 1 合成中 2 正在播放

核心功能实现

语音合成主函数

function onTextToSpeech(text = "") {
  // 如果正在播放,先停止
  if(textToSpeechStatus.value > 0) {
    uni.$emit("STOP_INNER_AUDIO_CONTEXT")
  }
  
  textToSpeechStatus.value = 1
  uni.showLoading({
    title: "语音合成中...",
    mask: true,
  })
  
  // 处理文本内容
  if(text.length) {
    textToSpeechContent.value = text
  }
  
  // 分段处理长文本(微信限制每次最多200字)
  let content = textToSpeechContent.value.slice(0, 200)
  textToSpeechContent.value = textToSpeechContent.value.slice(200)
  
  if(!content) {
    uni.hideLoading()
    return
  }
  
  // 调用合成接口
  plugin.textToSpeech({
    lang: "zh_CN",
    tts: true,
    content: content,
    success: (res) => {
      handleSpeechSuccess(res)
    },
    fail: (res) => {
      handleSpeechFail(res)
    }
  })
}

合成成功处理

function handleSpeechSuccess(res) {
  uni.hideLoading()
  
  // 创建音频上下文
  innerAudioContext = uni.createInnerAudioContext()
  innerAudioContext.src = res.filename
  innerAudioContext.play()
  textToSpeechStatus.value = 2
  
  // 播放结束自动播下一段
  innerAudioContext.onEnded(() => {
    innerAudioContext = null
    textToSpeechStatus.value = 0
    onTextToSpeech() // 递归播放剩余内容
  })
  
  setupAudioControl()
}

音频控制管理

function setupAudioControl() {
  uni.$off("STOP_INNER_AUDIO_CONTEXT")
  uni.$on("STOP_INNER_AUDIO_CONTEXT", (pause) => {
    textToSpeechStatus.value = 0
    if(pause) {
      innerAudioContext?.pause()
    } else {
      innerAudioContext?.stop()
      innerAudioContext = null
      textToSpeechContent.value = ""
    }
  })
}

错误处理

function handleSpeechFail(res) {
  textToSpeechStatus.value = 0
  uni.hideLoading()
  toast("不支持合成的文字")
  console.log("fail tts", res)
}

关键技术点

1. 长文本分段处理

由于微信接口限制,单次合成最多200字,需要实现自动分段:

let content = textToSpeechContent.value.slice(0, 200)
textToSpeechContent.value = textToSpeechContent.value.slice(200)

2. 播放状态管理

通过状态值精确控制播放流程:

  • 0:未播放,可以开始新的合成
  • 1:合成中,显示loading状态
  • 2:播放中,可以暂停或停止

3. 自动连续播放

利用递归实现长文本的自动连续播放:

innerAudioContext.onEnded(() => {
  onTextToSpeech() // 播放结束继续合成下一段
})

完整代码

export function useTextToSpeech() {
  const plugin = requirePlugin('WechatSI')
  let innerAudioContext = null
  const textToSpeechContent = ref("")
  const textToSpeechStatus = ref(0)
  
  function onTextToSpeech(text = "") {
    if(textToSpeechStatus.value > 0) {
      uni.$emit("STOP_INNER_AUDIO_CONTEXT")
    }
    textToSpeechStatus.value = 1
    uni.showLoading({
      title: "语音合成中...",
      mask: true,
    })
    
    if(text.length) {
      textToSpeechContent.value = text
    }
    
    let content = textToSpeechContent.value.slice(0, 200)
    textToSpeechContent.value = textToSpeechContent.value.slice(200)
    
    if(!content) {
      uni.hideLoading()
      return
    }
    
    plugin.textToSpeech({
      lang: "zh_CN",
      tts: true,
      content: content,
      success: (res) => {
        uni.hideLoading()
        innerAudioContext = uni.createInnerAudioContext()
        innerAudioContext.src = res.filename
        innerAudioContext.play()
        textToSpeechStatus.value = 2
        
        innerAudioContext.onEnded(() => {
          innerAudioContext = null
          textToSpeechStatus.value = 0
          onTextToSpeech()
        })
        
        uni.$off("STOP_INNER_AUDIO_CONTEXT")
        uni.$on("STOP_INNER_AUDIO_CONTEXT", (pause) => {
          textToSpeechStatus.value = 0
          if(pause) {
            innerAudioContext?.pause()
          } else {
            innerAudioContext?.stop()
            innerAudioContext = null
            textToSpeechContent.value = ""
          }
        })
      },
      fail: (res) => {
        textToSpeechStatus.value = 0
        uni.hideLoading()
        toast("不支持合成的文字")
        console.log("fail tts", res)
      }
    })
  }
  
  return {
    onTextToSpeech,
    textToSpeechContent,
    textToSpeechStatus
  }
}

【uniapp】体验优化:开源工具集 uni-toolkit 发布

背景

最近在做一些 uniapp 小程序 相关的 体积优化功能补充 工作,写了几个插件觉得太分散,不好梳理和归类,于是就创建一个 github 组织 来整理我的一些工具和插件,一方面方便我的日常工作,另一方面可以搜集来自社区的想法或者建议,可以首先考虑加到 uniapp 官方仓库 中,不方便加的再通过插件等形式实现。

插件列表

目前该仓库下已经有了三个插件,如下所示

功能

名称 描述 地址
@uni_toolkit/vite-plugin-component-config 一个用于处理 Vue 文件中的 <component-config> 标签的 vite插件,将配置提取并合并到对应的 JSON 文件 中,弥补组件无法自定义 JSON 配置 的缺陷 vite-plugin-component-config
@uni_toolkit/webpack-plugin-component-config 一个用于处理 Vue 文件中的 <component-config> 标签的 webpack插件,将配置提取并合并到对应的 小程序 JSON 文件 中,弥补组件无法自定义 JSON 配置 的缺陷 webpack-plugin-component-config

性能

名称 描述 地址
@uni_toolkit/unplugin-compress-json 一个用于压缩 JSON 文件的 unplugin 插件,支持 Vite 和 Webpack。自动压缩 JSON 文件 ,减小文件体积。 unplugin-compress-json

结语

如果这个库的插件帮助到了你,可以点个 star✨ 鼓励一下。

如果你有什么好的想法或者建议,欢迎在 github.com/uni-toolkit… 提 issue 或者 pr

Uniapp UTS插件开发实战:引入第三方SDK

创建uts 插件

  1. 创建uni_modules目录。

image.png

  1. 新建uts插件

image.png

image.png

  1. 放置第三方依赖在 libs中,如图所示

image.png

引入第三方SDK

  1. 引入格式; import 类名 from 包;

    如:import UHFRManager from "com.handheld.uhfr.UHFRManager";

  2. 使用

// app-android/index.uts
import UHFRManager from "com.handheld.uhfr.UHFRManager";

export const myApi : MyApi = function (options : MyApiOptions) {
    console.log(options.paramA)
    const instance = UHFRManager.getInstance()
    options.success?.(instance);
    options.complete?.(res);
}
// pages/test/test.vue
<template>
<view>
    <wd-button @click="testClick"> test</wd-button>
</view>
</template>

<script setup lang="ts">
// @ts-ignore
import {myApi} from "@/uni_modules/test-uts"

function testClick(){
  myApi({
    paramA: false,
    success:(res:any)=> {
      console.log(res);
    }
  })
}
</script>
  1. 打自定义调试基座

image.png 6. uniapp使用

插件使用示例

调试

事件管理

1、uts中

广播事件

  public static registerKeyReceiver() {
    let context = UTSAndroid.getAppContext(); // 获取 Application 上下文
    let intentFilter = new IntentFilter();
    intentFilter.addAction("android.rfid.FUN_KEY");
    intentFilter.addAction("android.intent.action.FUN_KEY");
    if (this.keyReceiver == null) {
      this.keyReceiver = new KeyReceiver();
    }
    context!!.registerReceiver(this.keyReceiver, intentFilter); // 用 Application Context 进行注册
    console.log("已注册按键广播接收器3");
  }

注销广播事件

  public static unregisterKeyReceiver() {
    let context = UTSAndroid.getAppContext();
    context!!.unregisterReceiver(this.keyReceiver);
    console.log("已取消注册按键广播接收器1");
  }

registerReceiver方法需要传入BroadcastReceiver类,新建一个类并重写onReceive方法

class KeyReceiver extends BroadcastReceiver {
  constructor() {
    super();
  }

  override  onReceive(context : Context, intent : Intent) : void {
    let keyCode = intent.getIntExtra("keyCode", 0);
    console.log("keyCode = " + keyCode);

    if (keyCode === 0) {
      keyCode = intent.getIntExtra("keycode", 0);
    }

    let keyDown = intent.getBooleanExtra("keydown", false);

    if (keyDown) {
      // 按键按下时的处理逻辑(可自行添加)
    } else {
      // 按键松开时的处理逻辑
      switch (keyCode) {
        case KeyEvent.KEYCODE_F1:
          break;
        case KeyEvent.KEYCODE_F2:
          break;
        case KeyEvent.KEYCODE_F5:
          break;
        case KeyEvent.KEYCODE_F3: // C510x
        case KeyEvent.KEYCODE_F4: // 6100
        case KeyEvent.KEYCODE_F7: // H3100
          this.onReadTag();
          break;
      }
    }
  }
  //这里是事务处理
  private onReadTag():void{
    console.log("按键触发读取标签操作");
    try{
     rfidHelper.startScan().then(res=>{

       let intent = new Intent("com.example.RFID_SCAN");
       intent.putExtra("scannerdata", res.join(','));
       console.log("发送数据",res);
       console.log("--------------")
         let context = UTSAndroid.getAppContext();
       context!!.sendBroadcast(intent);
     });
    }catch(e){
      console.log("rfid读取失败");
    }
  }
}

2、uniapp 中

接收广播 并使用eventbus转发消息给订阅者

import { EventBus, EventKey } from './EventBus'

let main: any
let filter: any
let receiver: PlusAndroidInstanceObject
let tag: boolean = false
/**
 * 开始广播监听扫码
 * that:传this;
 */
function start() {
  /* #ifdef APP-PLUS */
  main.registerReceiver(receiver, filter)
  /* #endif */
}

/**
 * 停止广播监听扫码
 * that:传this;
 */
function stop() {
  /* #ifdef APP-PLUS */
  main.unregisterReceiver(receiver)
  /* #endif */
}

/**
 *  剩余下个变量已经做了全局变量
 *
 * 定义广播
 * that:传this;
 */
function init(onReceive?: any) {
  /* #ifdef APP-PLUS */
  // 获取activity
  main = plus.android.runtimeMainActivity()
  const IntentFilter = plus.android.importClass('android.content.IntentFilter') as any
  // console.log(IntentFilter)
  filter = new IntentFilter()
  // console.log(filter)
  // 扫描设置的广播名称
  filter.addAction('com.android.server.scannerservice.broadcast') // 测试用
  filter.addAction('com.example.RFID_SCAN')// rfid uts插件自定义
  filter.addAction('com.rfid.SCAN')// 扫码

  receiver = plus.android.implements('io.dcloud.feature.internal.reflect.BroadcastReceiver', {

    onReceive(context: any, intent: any) {
      console.log('广播接收')
      // console.log(context, intent)
      plus.android.importClass(intent)
      // 扫描设置的开发者选项--键值名称 scannerdata
      const code = intent.getStringExtra('scannerdata')

      //获取广播源
      const action = intent.getAction();
      switch(action){
        case 'com.android.server.scannerservice.broadcast':
          EventBus.emit(EventKey.TEST, code)
          break;
        case 'com.example.RFID_SCAN':
          EventBus.emit(EventKey.RFIDEvent, code)
          break;
        case 'com.rfid.SCAN':
          EventBus.emit(EventKey.ScanEvent, code)
          break;
      }

      // 业务
      // 防重复
      if (tag) {
        return
      }
      tag = true
      setTimeout(() => {
        tag = false
      }, 150)
      console.log(code)
      // 到这里扫描成功了,可以调用自己的业务逻辑,code就是扫描的结果    return出code进行业务处理
      if (onReceive) {
        onReceive(code)
      }
    },
  })
  /* #endif */
}

function test() {
  const Intent = plus.android.importClass('android.content.Intent') as any
  const intent = new Intent('com.android.server.scannerservice.broadcast')
  main = plus.android.runtimeMainActivity()
  intent.putExtra('scannerdata', '123456789')
  main.sendBroadcast(intent)
}
export const broadcastScan = {
  init,
  start,
  stop,
  test,
}

苹果截胡马斯克抢到 AI 人才,想给 HomePod 加个「智慧眼」

没想到在 AI 上慢半拍的苹果,最近也加入到了如火如荼的「AI 抢人大战」中,还抢到了马斯克的头上。

CNBC 报道,苹果正在收购视觉 AI 初创公司 Prompt AI 的工程师和技术,目前已经推进到后期谈判,而这家公司此前也曾与埃隆 · 马斯克旗下的 xAI 和 Neuralink 接触。

▲ 苹果 CEO 蒂姆 · 库克

Prompt AI,什么来头?

一段内部录音显示,Prompt AI 的领导层在一场全体会议上通报了这次收购交易,并表示那些没有加入苹果的员工将会被降薪,鼓励他们去申请苹果的空缺职位。

这些小型 AI 初创公司一直是科技巨头这几年青睐的收购对象,既能避免反垄断审查,也能有针对性地快速补充公司本身的 AI 和技术积累。除了苹果,公司员工仅有 11 人的 Prompt AI 此前也接触了像埃隆 · 马斯克旗下 xAI 和 Nerualink 公司这样的潜在买家。

对于当初的投资者,Prompt AI 也表示会在交易完成后提供一部分资金回报,但不会全额返还投资。

那么,这家公司究竟是什么来头?

Prompt AI 于 2023 年在旧金山成立,当年获得了 500 万美元种子轮融资,创始人包括现任 CEO,北京大学和加州大学伯克利分校毕业的博士 Tete Xiao,以及伯克利 AI 研究实验室创始人 Trevor Darrell。

值得一提的是,从公司展示的团队页面来看,除了 CEO 之外,Prompt AI 还有不少成员也是华人。

这家公司的旗舰产品名为「Seemour」,官方称其为「具有家庭空间理解能力的环境人工智能(Ambient AI)」。

具体来说,Seemour 是一个智能安防摄像头 AI 系统,能够智能识别家庭中的特定成员、宠物和其他物体,针对他们的具体行为生成文字提醒,还能用于警告用户潜在的可疑人员和野生动物,也可以用在办公室来识别员工的上下班情况。

Prompt AI 的核心技术就在于这个识别系统,能够在一秒不到处理数千万像素,从中获得多特征的视觉线索,因此能实现相对可靠的人物和动物识别,系统还会在不同的条件下不断提升识别准确率。

除此之外,Prompt AI 还有专门用来处理复杂视频的多模态大语言模型,摄像头会记录大量的日常生活视频素材,但用户不需要逐个点开查看,Seemour 能够理解这些视频中的行为和背景,选出其中需要用户注意的可疑片段和人物。

很明显,苹果收购 Prompt AI,就是在为自己的智能家居战略「招兵买马」。此前彭博社已经爆料了一系列苹果正在酝酿的智能家居新品,其中有一个就是「智能安防摄像头」,能力和 Seemour 高度重合——精确识别每个进入房间的家庭成员。

在被苹果收购之前,Seemour 就一直支持与 Amazon 的 Ring 智能摄像头和门铃配合使用。

和 Seemour 这种相对孤立的家庭安防解决方案不同,苹果拥有一个更加完整的 HomeKit 智能家居生态,因而能实现更丰富的自动化功能:如果摄像头检测到你回家,它会自动点亮你喜欢的灯光、播放你常听的歌单,或者给你推荐喜欢的剧;但如果是家里的小孩开电视,那么 Apple TV 可能就会播放适合儿童观看的内容;要是空无一人的时候,家里的灯还亮着,那么它也会贴心地帮你关掉。

▲ 传闻苹果正在打造带屏幕 HomePod 家庭中枢

视频识别的大模型能力,也有望能整合进苹果的「视觉智能」功能之中,增强 Apple 智能理解视频的能力。

Prompt AI 内部表示,Seemour 的整个方案以及公司的技术都运行良好,但他们只负责为摄像头提供 AI 方案,Seemour 不向用户收取授权费,很难形成良好的商业模式,被苹果这样的大公司吸收,或许是更好结局。

至于他们的 Seemour 应用,目前已经从 App Store 下架,Prompt AI 也已经通知用户相关数据将被删除,确保隐私安全。

苹果瞄准「小而美」AI 公司

比起 AI 巨头们动辄数十上百亿美元的大规模并购,苹果则更青睐小型化的 AI 初创公司。去年苹果也收购了一家很小型的 AI 初创公司 Dataklaba,同样聚焦在 AI 面部识别和情绪捕捉技术,相关技术很可能也将用于新的屏幕版 HomePod 以及家庭摄像头。

面对目前在 AI 方面持续落后的局面,苹果正在不断从外部引入一些新的技术和团队,以实现快速的补强,聚焦在小型公司上则是其一贯的做法:规模太大的公司买回来总是需要磨合很久,像是 Beats 和英特尔的基带团队,苹果都花了相当一段时间整合新的技术,和解决新员工带来的文化冲突问题。

苹果更喜欢根据公司产品功能上的需要,有针对性地采购已经成熟的技术,并整合进公司的产品之中。

比如当年的 iPhone X,苹果为了实现 3D 结构光的人脸识别,收购了 PrimeSense 这家 3D 测感技术和解决方案公司,最终成就了 iPhone 沿用至今的 Face ID 。

近期苹果关于苹果收购 AI 公司的传闻,最引人关注的还是 Perplexity 。

根据彭博社,苹果内部正在构建一个名为「答案引擎」的聊天机器人,能够爬取网络数据来回答常识性问题,不仅会有一个独立的 app,还会作为基础技术,为未来的 Siri、Spotlight 聚焦搜索以及 Safari 浏览器提供搜索的功能。

这刚好也是 Perplexity 所擅长的,苹果服务高管 Eddy Cue 在 Google 反垄断的证词中已经公开表示了对 Perplexity 「印象深刻」,而彭博社爆料称苹果内部已经讨论过收购 Perplexity 是否合理,并且约见了其领导团队,不过目前两家公司还未展开正式谈判。

▲ 用 Perplexity 搜索 Prompt AI

Perplexity 估值 140 亿美元,只有约 250 名员工,比起 OpenAI 和 Anthropic 这样估值几百甚至上千亿美元的巨头,收购难度会更低,也不用担心严格的反垄断调查。

不过,即使在 AI 初创公司中属于中型体积的 Perplexity,如果最终真的被苹果收购,也将会超越 Beats 成为苹果史上最大的一笔收购交易,妥妥也属于一次大型的收购,苹果内部当然会谨慎评估各种风险和可能性。

在上个季度的财报会议上,CEO 蒂姆 · 库克表示公司接下来会加大 AI 支出,并且会收购更多的大型 AI 企业,AI 布局将会进一步积极扩张。

Perplexity 的收购八字还没一撇,但我们接下来肯定会看到越来越多像 Prompt AI 这样的小型公司不断被苹果收入囊中,他们的技术将成为苹果正在重点发展的 AI 以及智能家居下一块拼图。

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

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


uniapp 用css实现圆形进度条组件

一个基于 UniApp + Vue2 的圆形进度条组件,支持自定义进度、颜色、文本和内圈样式。 效果预览 功能特性 圆形进度条显示 完全可自定义的颜色和样式 支持数值和描述文本 灵活的内圈配置 响应式

分享一个4.3(a)瓜,一个操作毁了公司3个月的成果。

背景

对Appstore来说,4.3(a)是最让人头疼的事情。

有些是故意而为之,为了快速抢占市场、做备用包或者内部赛马等等

有些是无奈之举,莫名其妙的被3.2f,又想恢复产品的权重,重新夺回阵地

简单来说:

4.3(a)并非一蹴而就,是一个综合判定后的结论! 俗称:叠buffer!

事情梗概

今天跟往常一样,有粉丝添加好友询问关于4.3的问题。简单沟通之后,分析了大概可能得原因。

略提一下,基本上备注了有偿的粉丝都会多聊几句。你懂付费的价值,我懂你的不易!

从元数据到代码,把可能踩雷的地方维度均考量了一圈。最后基本可以锁定为代码层面!

事情详述

领导给员工B,提了一个节前上架的版本。站在员工的角度来看,任务重时间紧,但是都是打工的思维。肯定是想怎么简单的解决问题,怎么来

在这期间还有一个小插曲,该员工B想提前调休。在确保工作不影响的情况下。但是领导因必须提供稳定版本为由,直接拒绝了,并且还吐槽其没有团队意识等等破其道心的话。(其实身份不一样,角度不一样,责任也不一样,都是打工人当面吐槽属实过分了。

该员工B为了节前调休,采用了合并代码的操作。为了快速符合测试预期。

好消息:提前完成了!

坏消息:4.3了!

事情后续

该员工B,在节后主动辞职。顺利混完国庆带薪假期。对公司而言亏损修炼了3个多月成果,导致现有版本沦为鸡肋。

食之无味,弃之可惜!

所以,都是为了讨生活没有必要苦苦相逼。领导者该有的气度要有,毕竟雇佣的员工和合伙人心境是不一样。除非钱到位了,用了钞能力!

老板以为的是合伙创业,在员工看来不过是个饭票,没了再找罢了。

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# Pingpong和连连的平替,让AppStore收款无需新增持有人。

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

知识星球

更多Appstore咨询问题,请关注知识星球。「提供1v1上架指导,帮助开发者解决Appstore的疑难杂症,助力每一位开发者!」

【uniapp】小程序体积优化,JSON文件压缩

背景

2025年9月30号下午,uniapp社区 有开发者发布了一个帖子 ask.dcloud.net.cn/question/21… ,希望能支持压缩小程序编译后的 JSON文件 以缓解包体积越来越大的问题,于是这个插件 github.com/chouchouji/… 便由此而生。

功能特性

  • 🗜️ 自动压缩 - 自动移除 JSON 文件中的空白字符和换行符
  • 🔧 多构建工具支持 - 支持 Vite、Webpack、Rollup 等构建工具
  • 零配置 - 开箱即用,无需额外配置
  • 🎯 精确匹配 - 只处理 .json 文件,不影响其他资源

下面是一张测试效果图,6.21KB -> 4.54KB,越大的 JSON文件 插件效果越明显。

cjs.png

安装

# npm
npm install @binbinji/unplugin-compress-json -D

# yarn
yarn add @binbinji/unplugin-compress-json -D

# pnpm
pnpm add @binbinji/unplugin-compress-json -D

使用方法

Vite

// vite.config.js
import { defineConfig } from 'vite'
import CompressJson from '@binbinji/unplugin-compress-json/vite'
import uni from '@dcloudio/vite-plugin-uni'

export default defineConfig({
  plugins: [
    uni(),
    CompressJson(),
  ],
})

Vue CLI

// vue.config.js
const CompressJson = require('@binbinji/unplugin-compress-json/webpack')

module.exports = {
  configureWebpack: {
    plugins: [
      CompressJson(),
    ],
  },
}

工作原理

插件会在构建过程中自动检测所有 .json 文件,并移除其中的:

  • 空格
  • 制表符
  • 换行符
  • 其他空白字符

压缩前:

{
  "name": "example",
  "version": "1.0.0",
  "description": "这是一个示例"
}

压缩后:

{"name":"example","version":"1.0.0","description":"这是一个示例"}

iOS App包大小由浅入深优化之路

App在迭代过程中,随着新需求的不断丰富,包体积逐渐增大。早期功能较简单,优化空间有限,用户感知不明显,包大小问题未受重视。随着功能多元化,包大小水涨船高,大到一定值后,会产生一些消极影响。

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

在这里插入图片描述

引子

浪浪山的朝阳刚爬上山头,小妖怪阿强就抱着他那台快被摸包浆的 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,肯定能让这些按钮像浪浪河的水似的,流着动起来!”

在这里插入图片描述

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

国庆假期 iOS 开发者守好邮箱 “防线”,严防恶意投诉避免产品下架

背景

国庆长假临近,当大家准备暂别工作享受假期时,务必提前绷紧 “产品防护” 这根弦 —— 历年假期都是恶意投诉的高发期,而开发者邮箱作为苹果审核沟通的唯一官方渠道,一旦疏忽错过关键通知,极可能导致辛苦打造的产品遭遇下架危机。

恶意行为

为何假期风险陡增?

不少竞品会利用团队休假、响应延迟的 “时间差”,通过多种不正当手段发起攻击:或是伪造 “侵权”“违规收集信息” 等虚假举报,或是组织恶意刷评、批量提交不实用户投诉,甚至故意曲解产品功能触发审核红线。苹果审核机制对投诉响应时效要求严苛,若 24 小时内未查看审核邮件、未提交澄清材料,系统可能直接判定 “默认违规”,导致产品临时下架、核心功能封禁;更严重的是,多次未及时响应还会影响账号历史信誉,后续恢复上架不仅需要反复沟通举证,流程长达 1-2 周,期间错过假期流量窗口事小,用户因无法访问流失、营收断崖式下跌才是致命打击。

防御措施

为守住产品成果,建议大家提前做好两手方面准备:

一是给开发者邮箱设置短信、APP 双重推送提醒,确保 “审核通知”“投诉预警” 类邮件实时触达,避免被淹没在垃圾邮件中;

二是团队内明确 “审核轮值机制”,每天安排 1 名成员固定花 15 分钟查看邮箱,同步审核动态到工作群,遇到紧急投诉第一时间启动响应流程。

每款产品从立项到上架,都凝聚着团队无数个日夜的心血,假期放松的同时,千万别让 “一时疏忽” 给恶意竞争可乘之机。愿大家既能安心享受假期,也能守住产品成果

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# Pingpong和连连的平替,让AppStore收款无需新增持有人。

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

知识星球

更多Appstore咨询问题,请关注知识星球。「提供1v1上架指导,帮助开发者解决Appstore的疑难杂症,助力每一位开发者!」

苹果海外老账号续费,踩了个大坑!

我们有个香港苹果公司老账号要续费了,我像以前一样,用招行的VISA卡去网页端续费,可能无论怎么输,就是各种报错。 地区选“中国大陆”,卡号会飘红字“您输入的信用卡在中国大陆无效,请提供在中国大陆有效的
❌