SwiftUI 利用本机 “AI” 让 App 懂你的喜怒哀乐(三)

21 阅读5分钟

在这里插入图片描述

概述

大家都知道,要想一款 App 真正的引人入胜、夺人心魄,最重要的是站在用户的立场投其所爱。如何更加贴心的懂得用户的小心思呢?诚然,大家可以动用市面上诸多 AI 大模型来让我们得偿所愿,不过这些对于秃头初学小码农们来说门槛略微有些“高不可攀”。

在这里插入图片描述

所幸的是,Apple 早就在本地集成了一些“轻”AI 框架,利用它们大家起码可以小试牛刀来预先验证一下我们的设计构想。好消息是:这些框架使用起来都相当简单,值得我们进一步“潜精研思”。

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

  1. 迂回曲折:好事多磨!
  2. 让“讨厌”的模拟器和预览乖乖“闭嘴”

闲言少叙,让我们马上开始“情感剖析”大冒险吧!

Let's go!!!;)


5. 迂回曲折:好事多磨!

上回说到,苹果的自然语言框架目前对中文支持并不好。除了使用另外第三方库以外,为了达到评估中文文本情绪价值的目的,我们需要采用迂回战术。

一种思路是先将中文翻译为英文,然后再评估翻译后的英文文本。让人欣慰的是,为了应付这种苦差事,苹果也为移动设备做好了准备。

从 iOS 17.4(macOS 14.4)开始,苹果果断推出了原生翻译(Translation)框架,使用它我们可以在真机上体验离线的翻译功能了。

在这里插入图片描述

为了让翻译功能与 UI 的协作更加“心有灵犀”,苹果从 iOS 18(macOS 15)开始又增加了 TranslationSession 类,该类的核心功能只有一个:那就是全心全意专注于两种语言之间的翻译。

在这里插入图片描述

更妙的是,该类对 SwiftUI 也做出了更好的支持,我们现在可以借助 translationTask 视图修改器直接将翻译功能与具体视图相绑定啦:

在这里插入图片描述

我们的思路很简单:先将成语的释义用 Translation 框架翻译成英文,然后再用自然语言框架评估它们的情绪价值。

我们为成语释义的翻译与评估实现了一个 IdiomView 视图,如下代码所示:

import SwiftUI
import NaturalLanguage
import Translation

struct IdiomView: View {
    
    @Bindable var idiom: Idiom
    @State private var englishMeaning: String?
    
    @Environment(\.modelContext) var modelContext
    
    var body: some View {
        VStack {
            
            HStack {
                
                Text("\(idiom.pinyin.joined(separator: " "))")
                    .foregroundStyle(.gray)
                    .font(.title)
                
                Spacer(minLength: 0)
                
                if idiom.score != -11 {
                    Text("\(idiom.score, specifier: "%0.1f")\(Emotion.emotion4Score(idiom.score) ?? "")")
                            .font(.largeTitle.bold())
                }
            }
            
            
            if let meaning = idiom.meaning {
                GroupBox("释义") {
                    Text(meaning)
                }
            }
            
            if let englishMeaning {
                GroupBox("英文释义") {
                    Text(englishMeaning)
                }
            }
            
            if let source = idiom.source {
                GroupBox("出处") {
                    Text(source)
                }
            }
            
            if let example = idiom.example, example.count > 1 {
                GroupBox("示例") {
                    Text(example)
                }
            }
            
            Spacer()
            
            Toggle(idiom.like ? "❤️" : "喜欢", isOn: $idiom.like)
        }
        .padding()
        .navigationTitle(idiom.name)
        .translationTask { session in
            guard let meaning = idiom.meaning, meaning.count > 1, let response = try? await session.translate(meaning) else { return }
            
            englishMeaning = response.targetText
             
        }
        .onChange(of: englishMeaning) { _, new in
            guard idiom.englishMeaning == nil, let new, !new.isEmpty else { return }
            
            idiom.englishMeaning = englishMeaning
            let tagger = NLTagger(tagSchemes: [.sentimentScore])
            tagger.string = new
            
            if let sentiment = tagger.tag(at: new.startIndex, unit: .paragraph, scheme: .sentimentScore).0, let score = Double(sentiment.rawValue) {
                idiom.score = Float(score)
            }
            
            do {
                try modelContext.save()
            } catch {
                print("保存英文释义和情绪评分失败:\(error.localizedDescription)")
            }
        }
    }
}

在上面的代码中,我们主要做了这样两件事:

  1. 调用 translationTask 方法翻译成语的中文释义到英文;
  2. 在翻译完毕后,利用 NaturalLanguage 评估英文释义所代表的情绪价值;

因为 Xcode 预览和模拟器都不支持翻译功能,所以我们需要将 App 部署到真机上以“投石问路”:

在这里插入图片描述

公平来说,我们的实现在真机上表现的并不太理想。对成语评估效果不佳的原因可能是由于两个“不准确”的“双重放大”:

  1. 中文翻译到英文不准确;
  2. 英文本身评估的不准确;

不过,本文旨在使用本地且原生的方法来小试身手,只是一种抛砖引玉。 下一步大家可以充分甩开思想的束缚,继续使用第三方 AI 模型来更好的评估用户的多愁善感。

6. 让“讨厌”的模拟器和预览乖乖“闭嘴”

在文章的最后,我们再略微谈谈上述实现里的一个“小麻烦”:即翻译功能无法在 Xcode Preview 以及模拟器中执行,每到此处我们都需要关闭一个善意但却有些讨厌的提示:

在这里插入图片描述

为了调试时少掉几根头发,我们可以有选择的执行翻译功能。这是通过检查 ProcessInfo 实例对象中的环境值来完成的:

extension ProcessInfo {
    public var isRunningInPreview: Bool {
        environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
    }
    
    public var isRunningInSimulator: Bool {
        // 在预览中也会返回 true,因为预览也是一种模拟器环境
        environment["SIMULATOR_DEVICE_NAME"] != nil
    }
}

extension EnvironmentValues {
    @Entry var isPreviewing = ProcessInfo.processInfo.isRunningInPreview
    @Entry var isRunningInSimulator = ProcessInfo.processInfo.isRunningInSimulator
}

在上面的代码中,我们分别用了 isPreviewing 和 isRunningInSimulator 两个环境状态来探查当前的运行环境。

有了上面的铺垫,我们可以将原来 IdiomView 视图的代码做如下升级了:

struct IdiomView: View {
    
    @Bindable var idiom: Idiom
    @State private var englishMeaning: String?
    
    @Environment(\.modelContext) var modelContext
    @Environment(\.isPreviewing) var isPreviewing
    @Environment(\.isRunningInSimulator) var isRunningSimulator

    var body: some View {
        VStack {
            //...
        }
        .padding()
        .navigationTitle(idiom.name)
        .translationTask { session in
        	// 只在真机中执行翻译功能
            guard !isPreviewing, !isRunningSimulator, let meaning = idiom.meaning, meaning.count > 1, let response = try? await session.translate(meaning) else { return }
            
            englishMeaning = response.targetText
             
        }
        .onChange(of: englishMeaning) { _, new in
            //...
        }
    }
}

现在,我们在 Xcode 预览或模拟器中测试 SwiftUI 视图的核心功能时,终于可以摆脱这些“雀喧鸠聚”,专注于我们的小目标啦!棒棒哒!💯

总结

在本篇博文中,我们讨论了如何使用 Apple 提供的本地离线翻译功能实现成语释义的英文翻译以便更好地完成文本情绪价值的评估。我们最后还顺便聊了聊如何摆脱 Xcode 预览和模拟器中的烦人提示。

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