今天,我們的目標就是透過優化載入流程與加入動畫,讓整個體驗更加流暢。
困惑點:我的 App 現在能用了,但體驗很差。當我點擊「生成」按鈕後,UI 會卡住好幾秒,使用者不知道發生了什麼事,也不知道要等多久。當結果出現時,又很突兀地跳出來。
AI 協作
我的 AI 應用在呼叫 API 時需要等待好幾秒,這期間 UI 會沒有回應,使用者體驗很差。請問在 iOS 開發中,處理這種長時間非同步任務時,有哪些提升使用者體驗的最佳實踐?我想知道如何提供清晰的載入反饋,以及如何用動畫讓結果的呈現更柔順。
AI 的回答:
您提出的正是在 App 開發中從「能用」邁向「好用」的關鍵一步!處理長時間非同步任務的 UX 是決定 App 專業度的核心。以下是幾個業界公認的最佳實踐:
1. 提供即時且清晰的載入反饋 (Loading Feedback)
使用者最害怕的就是「未知的等待」。您的首要任務就是打破這種未知感,讓使用者隨時知道 App 的狀態。
- 禁用輸入元件:一旦開始執行任務,應立刻禁用觸發按鈕(
UIButton
)和輸入框(UITextView
/UITextField
)。這可以防止使用者因不耐煩而重複點擊,造成多次 API 請求。- 顯示狀態指示器:使用
UILabel
來顯示多階段的狀態文字。由於您的任務分為「文字解析」和「圖像生成」,您可以依序更新標籤的文字,如「正在為您深度解析夢境...」、「正在將您的夢境轉化為視覺藝術...」。這讓使用者感覺到進度正在推進,而不只是一個漫長的等待。- 使用活動指示器 (Spinner):除了文字,您也可以加入一個
UIActivityIndicatorView
(俗稱轉轉轉的菊花圖),給予一個動態的「正在處理中」的視覺信號。2. 使用動畫優雅地呈現結果 (Elegant Animation)
當數據終於回來時,避免讓它生硬地「蹦」出來。一個簡單的過渡動畫就能極大地提升質感。
- 淡入淡出 (Fade In/Out):這是最常用也最有效的效果之一。在結果頁面(
ResultViewController
)的viewDidLoad
中,先將要顯示的UIImageView
和UITextView
的透明度(alpha
)設為 0。然後,在viewDidAppear
(此時畫面已在螢幕上,是執行動畫的最佳時機)中,使用UIView.animate
將它們的alpha
在 0.5 秒內變回 1。這個簡單的淡入效果會讓結果像是輕柔地浮現在眼前,感覺非常高級。3. 優雅地處理錯誤 (Graceful Error Handling)
網路隨時可能中斷,API 也可能出錯。一個專業的 App 絕不會在出錯時閃退或毫無反應。
- 顯示友善的錯誤訊息:在
catch
區塊中,應更新狀態標籤UILabel
,顯示一個使用者能看懂的錯誤訊息,例如「連線失敗,請檢查網路後再試一次」,而不是直接顯示技術性的錯誤碼。- 恢復 UI 狀態:最重要的一點是,無論成功或失敗,任務結束後,一定要記得將之前禁用的 UI 元件重新啟用。這讓使用者有機會可以修正問題(例如開啟 Wi-Fi)後重試。
總結來說,一個好的非同步流程 UX = 開始時鎖定 UI 並給予清晰進度 + 結束時用動畫優雅呈現結果 + 失敗時提供明確指引並恢復 UI。做到這三點,您的 App 將會給人一種非常可靠且精緻的感覺。
根據 AI 的建議,我把詳細的實作步驟整理如下:
InputViewController
中,完善 setLoading
方法,並在 Task
執行的各個階段更新狀態,給予使用者明確的反饋。ResultViewController
中,利用 viewDidAppear
和 UIView.animate
實現結果內容的淡入效果。InputViewController
中的 catch
區塊,確保在發生錯誤時,能夠向使用者顯示有意義的訊息,並恢復 UI 的可操作性。我們首先聚焦於 InputViewController
。核心是一個名為 setLoading
的輔助方法,它像一個開關,集中管理所有 UI 在「載入中」和「空閒」兩種狀態下的行為。
檔案:InputViewController.swift
// ...
// 修改 setLoading 方法以接收狀態文字
private func setLoading(_ isLoading: Bool, status: String) {
btnSent.isEnabled = !isLoading
txfDream.isEditable = !isLoading
lbStatus.text = status
}
@IBAction func btnSent(_ sender: UIButton) {
dismissKeyboard()
guard let dreamDescription = txfDream.text, !dreamDescription.isEmpty else {
return
}
// 【第 1 步】任務開始,立刻鎖定 UI 並顯示初始狀態
setLoading(true, status: "AI 準備中...")
Task {
do {
// 【第 2 步】在主執行緒更新狀態文字,讓使用者知道進度
await MainActor.run { lbStatus.text = "正在為您深度解析夢境..." }
// 呼叫文字生成 API
let prompt = PromptFactory.createDreamInterpretationPrompt(from: dreamDescription)
let geminiResponse: GeminiResponse = try await geminiService.generateContent(prompt: prompt)
// 解析文字回應
let dreamResponse = try JSONDecoder().decode(DreamInterpretationResponse.self, from: jsonData)
// 【第 3 步】再次更新狀態文字
await MainActor.run { lbStatus.text = "正在將您的夢境轉化為視覺藝術..." }
let generatedImage = try await geminiService.generateImage(prompt: dreamResponse.imagePrompt)
// 成功:傳遞並跳轉
await MainActor.run {
navigateToResult(interpretation: dreamResponse.interpretation, image: generatedImage)
}
} catch {
// (錯誤處理將在步驟三詳述)
}
}
}
private func navigateToResult(interpretation: String, image: UIImage?) {
let resultVC = ResultViewController()
resultVC.interpretationText = interpretation
resultVC.dreamImage = image
navigationController?.pushViewController(resultVC, animated: true)
// 【第 4 步】成功跳轉後,重設輸入頁的 UI 狀態,以便使用者返回後可以再次使用
setLoading(false, status: "")
}
// ...
透過在 Task
的不同 await
階段之間插入 MainActor.run
來更新 UI,我們為使用者提供了一個清晰的進度條,大大降低了等待的焦慮感。
現在,我們將目光轉向 ResultViewController
。我們要在這裡實現 AI 建議的淡入動畫,讓夢境的解析與圖像如魔法般浮現。
檔案:ResultViewController.swift
import UIKit
class ResultViewController: UIViewController {
// MARK: - IBOutlet
@IBOutlet weak var imgvResult: UIImageView!
@IBOutlet weak var txfResult: UITextView!
// MARK: - Property
var interpretationText: String?
var dreamImage: UIImage?
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
title = "夢境分析結果"
// 【第 1 步】在畫面載入時,先將結果設為完全透明
imgvResult.alpha = 0
txfResult.alpha = 0
// 將資料設定給 UI 元件
txfResult.text = interpretationText
imgvResult.image = dreamImage
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// 【第 2 步】在畫面已經完全顯示在螢幕上後,執行動畫
// 使用 UIView.animate 執行一個 0.5 秒的淡入動畫
UIView.animate(withDuration: 0.5) {
self.imgvResult.alpha = 1
self.txfResult.alpha = 1
}
}
}
最後,我們回到 InputViewController
,確保在失敗時,App 能夠告知使用者,而不是崩潰或卡死。
檔案:InputViewController.swift
(btnSent
的 catch
區塊)
// ... 在 btnSent 的 Task 中 ...
} catch let decodingError as DecodingError {
// 處理 JSON 解析失敗的特定錯誤
await MainActor.run {
setLoading(false, status: "JSON 解析失敗,請重試")
}
} catch let error as APPError {
// 處理我們自訂的應用程式錯誤
await MainActor.run {
setLoading(false, status: "應用程式錯誤: \(error.localizedDescription)")
}
} catch {
// 處理所有其他未知錯誤
await MainActor.run {
setLoading(false, status: "發生錯誤: \(error.localizedDescription)")
}
}
// ...
我發現自己赤著腳,站在一片柔軟如絲絨的草地上。草葉的尖端都點綴著露珠,在溫柔的暮色中閃爍著微光。空氣中瀰漫著一股淡淡的花香與泥土的清新氣息,溫暖的微風輕輕拂過我的臉龐,帶走所有現實中的疲憊。
我抬起頭,看見天空不是單純的黑夜,而是一幅由深藍與紫色渲染而成的畫布。天上沒有月亮,取而代之的是億萬顆如鑽石般明亮的星星,它們緩慢地、有節奏地閃爍,彷彿在對我眨眼。其中幾顆星星甚至會偶爾脫離軌道,像頑皮的精靈一樣,在空中劃出一道道絢爛的金色弧線。
我的目光被前方吸引,那裡有一條由發光鵝卵石鋪成的小徑,蜿蜒地伸向遠方。我沿著小徑前行,腳步輕盈得幾乎感覺不到重量。小徑兩旁,是會發光的奇異花朵,它們的花瓣在黑暗中散發出柔和的粉色、藍色和金色光芒。成群的螢火蟲圍繞著我飛舞,像是一群提著小燈籠的嚮導,引領我前進。
路的盡頭,是一片廣闊的湖泊,湖面平靜如鏡,完美地倒映著天上的星河,讓我一時分不清哪裡是天,哪裡是水。湖的中央,漂浮著一座開滿了櫻花樹的浮島。我毫不費力地,一步步踏空而行,像是走在一座無形的橋上,輕鬆地登上了浮島。
島上,我最想念的親人、摯友,甚至是許久不見的寵物,都帶著溫暖的笑容在那裡等著我。一張由發光藤蔓編織而成的長桌上,擺滿了我最愛吃的食物和甜點,它們都冒著誘人的香氣。不需要言語,一個眼神、一個微笑,我們便開始分享這場盛宴。歡聲笑語在島上迴盪,與風吹過櫻花樹的沙沙聲,交織成一首最悅耳的交響樂。
吃飽喝足後,有人提議放天燈。我們在散發著溫暖光芒的天燈上,寫下最真誠的願望。隨著天燈冉冉升空,它們化作新的星星,加入了天上那片璀璨的星河。
我躺在櫻花樹下,身邊圍繞著所有我愛的人,感受著前所未有的平靜與歸屬感。我知道在這個夢裡,沒有煩惱,沒有壓力,只有純粹、圓滿的幸福。當睡意再次溫柔地將我包圍時,我的嘴角掛著一抹安心的微笑,準備迎接一個充滿活力的早晨。
我們的 AI 應用實戰暫告一段落。明天,我們將探索一個 AI 概念:Function Calling。我們將學習如何不再只是從 AI 獲取資訊,而是賦予 AI 「調用」我們 App 內建工具的能力。