iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
生成式 AI

iOS? AI-Yes!:用 Vide Coding 加速我的 Swift 學習曲線系列 第 23

Day 23 - 【AI 專案 II】AI 夢境視覺化師:體驗升級與動畫

  • 分享至 

  • xImage
  •  

今天,我們的目標就是透過優化載入流程與加入動畫,讓整個體驗更加流暢。

  • 困惑點:我的 App 現在能用了,但體驗很差。當我點擊「生成」按鈕後,UI 會卡住好幾秒,使用者不知道發生了什麼事,也不知道要等多久。當結果出現時,又很突兀地跳出來。

  • AI 協作

    • 我的 Prompt我的 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 中,先將要顯示的 UIImageViewUITextView 的透明度(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 的建議,我把詳細的實作步驟整理如下:

    1. 打造清晰的載入流程:在 InputViewController 中,完善 setLoading 方法,並在 Task 執行的各個階段更新狀態,給予使用者明確的反饋。
    2. 實現登場動畫:在 ResultViewController 中,利用 viewDidAppearUIView.animate 實現結果內容的淡入效果。
    3. 完善錯誤處理機制:檢查 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 (btnSentcatch 區塊)

// ... 在 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)")
            }
        }
// ...

夢境測試

我發現自己赤著腳,站在一片柔軟如絲絨的草地上。草葉的尖端都點綴著露珠,在溫柔的暮色中閃爍著微光。空氣中瀰漫著一股淡淡的花香與泥土的清新氣息,溫暖的微風輕輕拂過我的臉龐,帶走所有現實中的疲憊。

我抬起頭,看見天空不是單純的黑夜,而是一幅由深藍與紫色渲染而成的畫布。天上沒有月亮,取而代之的是億萬顆如鑽石般明亮的星星,它們緩慢地、有節奏地閃爍,彷彿在對我眨眼。其中幾顆星星甚至會偶爾脫離軌道,像頑皮的精靈一樣,在空中劃出一道道絢爛的金色弧線。

我的目光被前方吸引,那裡有一條由發光鵝卵石鋪成的小徑,蜿蜒地伸向遠方。我沿著小徑前行,腳步輕盈得幾乎感覺不到重量。小徑兩旁,是會發光的奇異花朵,它們的花瓣在黑暗中散發出柔和的粉色、藍色和金色光芒。成群的螢火蟲圍繞著我飛舞,像是一群提著小燈籠的嚮導,引領我前進。

路的盡頭,是一片廣闊的湖泊,湖面平靜如鏡,完美地倒映著天上的星河,讓我一時分不清哪裡是天,哪裡是水。湖的中央,漂浮著一座開滿了櫻花樹的浮島。我毫不費力地,一步步踏空而行,像是走在一座無形的橋上,輕鬆地登上了浮島。

島上,我最想念的親人、摯友,甚至是許久不見的寵物,都帶著溫暖的笑容在那裡等著我。一張由發光藤蔓編織而成的長桌上,擺滿了我最愛吃的食物和甜點,它們都冒著誘人的香氣。不需要言語,一個眼神、一個微笑,我們便開始分享這場盛宴。歡聲笑語在島上迴盪,與風吹過櫻花樹的沙沙聲,交織成一首最悅耳的交響樂。

吃飽喝足後,有人提議放天燈。我們在散發著溫暖光芒的天燈上,寫下最真誠的願望。隨著天燈冉冉升空,它們化作新的星星,加入了天上那片璀璨的星河。

我躺在櫻花樹下,身邊圍繞著所有我愛的人,感受著前所未有的平靜與歸屬感。我知道在這個夢裡,沒有煩惱,沒有壓力,只有純粹、圓滿的幸福。當睡意再次溫柔地將我包圍時,我的嘴角掛著一抹安心的微笑,準備迎接一個充滿活力的早晨。

IMG_1325 (1)

我們的 AI 應用實戰暫告一段落。明天,我們將探索一個 AI 概念:Function Calling。我們將學習如何不再只是從 AI 獲取資訊,而是賦予 AI 「調用」我們 App 內建工具的能力。


上一篇
Day 22 - 【AI 專案 II】AI 夢境視覺化師:呼叫 Gemini 圖像生成模型
下一篇
Day 24 - 【AI 超能力】Function Calling 是什麼?讓 AI 控制你的 App
系列文
iOS? AI-Yes!:用 Vide Coding 加速我的 Swift 學習曲線24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言