iT邦幫忙

2023 iThome 鐵人賽

DAY 9
0
Mobile Development

用 SwiftUI 魔法變出 Leetcode 刷題知識學習 App!系列 第 9

Day 9: 串接 LeetCode 題目 API,顯示在 SwiftUI 的 List 上

  • 分享至 

  • xImage
  •  

昨天我們已經將 LeetCode 題目呈現在 SwiftUI 的 List 上了,而我們今天要挑戰的是,資料取得改從 Network API 請求去拿資料,並且成功顯示在頁面上,所以主要是網路連線,JSON 資料轉換成物件,最後塞入 UI 元件的過程

API 是什麼

API 全名是 Application Programming Interface,翻譯成中文是應用程式介面,聽起來好像是中文的手機裝置畫面,但並不是,它的概念比較通用廣泛,它是提供一個溝通媒介,讓兩端溝通時用統一的格式語言,而兩邊的程式不需要知道對方實作細節,只需要拿到他想要的資料即可,不過這是我的理解後所定義,更專業的定義可以去維基百科查詢

API 取得

LeetCode 其實沒有一個正式的 API 文件可以讓開發者去使用,但是卻有 API 可以獲得所有題目的資料,需要透過網路取得的 API 我們稱為 Web API,如果有符合 Restful API 風格,則稱為 Restful API,如下網址

https://leetcode.com/api/problems/algorithms/

API 獲得的 JSON 資料

https://i.imgur.com/LCgDv7jl.png

實際上拿到密密麻麻亂七八糟的文字,我們將這些文字貼到 http://json.parser.online.fr/ 這個網站,它會幫我們整理出比較漂亮的且有縮行的 JSON 格式,方便我們檢視每個資料代表的意義,並轉換成 Swift 物件

定義 Swift 物件結構

接下來就是要把 JSON 資料轉換成 Swift 物件了!而這個 API 複雜度實在非常之高,身為工程師實在很懶得一個一個拼揍,於是藉由工具的力量產生,網站在此:https://quicktype.io/

讓我們把這個密密麻麻 LeetCode API JSON 資料輕鬆轉換成 Swift 物件,程式碼如下

// MARK: - Welcome
struct Welcome: Codable, Identifiable {
    let id = UUID()
    let userName: String
    let numSolved, numTotal, acEasy, acMedium: Int
    let acHard: Int
    let statStatusPairs: [StatStatusPair]
    let frequencyHigh, frequencyMid: Int
    let categorySlug: String

    enum CodingKeys: String, CodingKey {
        case userName = "user_name"
        case numSolved = "num_solved"
        case numTotal = "num_total"
        case acEasy = "ac_easy"
        case acMedium = "ac_medium"
        case acHard = "ac_hard"
        case statStatusPairs = "stat_status_pairs"
        case frequencyHigh = "frequency_high"
        case frequencyMid = "frequency_mid"
        case categorySlug = "category_slug"
    }
}

// MARK: - StatStatusPair
struct StatStatusPair: Codable, Identifiable {
    let id = UUID()
    let stat: Stat
    let status: Status?
    let difficulty: Difficulty
    let paidOnly, isFavor: Bool
    let frequency, progress: Int

    enum CodingKeys: String, CodingKey {
        case stat, status, difficulty
        case paidOnly = "paid_only"
        case isFavor = "is_favor"
        case frequency, progress
    }
}

// MARK: - Difficulty
struct Difficulty: Codable {
    let level: Int
}

// MARK: - Stat
struct Stat: Codable {
    let questionID: Int
    let questionArticleLive: Bool?
    let questionArticleSlug: String?
    let questionArticleHasVideoSolution: Bool?
    let questionTitle, questionTitleSlug: String
    let questionHide: Bool
    let totalAcs, totalSubmitted, frontendQuestionID: Int
    let isNewQuestion: Bool

    enum CodingKeys: String, CodingKey {
        case questionID = "question_id"
        case questionArticleLive = "question__article__live"
        case questionArticleSlug = "question__article__slug"
        case questionArticleHasVideoSolution = "question__article__has_video_solution"
        case questionTitle = "question__title"
        case questionTitleSlug = "question__title_slug"
        case questionHide = "question__hide"
        case totalAcs = "total_acs"
        case totalSubmitted = "total_submitted"
        case frontendQuestionID = "frontend_question_id"
        case isNewQuestion = "is_new_question"
    }
}

enum Status: String, Codable {
    case ac = "ac"
    case notac = "notac"
}

但是我們的 SwiftUI 列表 List 不需要這麼過度複雜的資料,只需要拿取部分的標題跟 LeetCode 題目等級的資料,所以我們製作了一個專為 UI 資料顯示的資料,程式碼如下

struct LeetCodeProblem: Identifiable {
    var id: UUID?
    var title: String?
    var description: String?
}

而這兩種資料格式你都會發現後面多了一個 Identifiable 因為 SwiftUI 的列表需要識別獨立的 ID,好讓它能夠認得列表中每個不同的資料元素,我們也多設定了一個 var id: UUID? 這是原本 JSON 資料沒有的,由我們自己程式自己產獨立的 UUID 識別

URLSession 獲取 API

利用 Swift 的 URLSession 工具去取得 API 請求資料,如下程式碼,因為網路有可能失敗或是JSON 解析拿不到資料,所以有返回 nil 的可能性,且我們將從 Web API 的請求資料用 JSONDecoder 去轉換成 Welcome 物件,成功轉換後,我們在將 UI 要顯示的資料對應到 LeetCodeProblem 物件,並且組成 var problems: [LeetCodeProblem] 這個陣列返回給 SwiftUI 的 List

func fetchProblems(completion: @escaping ([LeetCodeProblem]?) -> Void) {
    guard let url = URL(string: "https://leetcode.com/api/problems/all/") else {
        completion(nil)
        return
    }
    
    URLSession.shared.dataTask(with: url) { data, response, error in
        guard let data = data, error == nil else {
            completion(nil)
            return
        }
        
        do {
            let leetCodeData = try JSONDecoder().decode(Welcome.self, from: data)
            
            var problems: [LeetCodeProblem] = []
            leetCodeData.statStatusPairs.forEach { statStatusPair in
                if(!statStatusPair.paidOnly) {
                    var p = LeetCodeProblem()
                    p.id = statStatusPair.id
                    p.title = statStatusPair.stat.questionTitle
                    
                    switch(statStatusPair.difficulty.level) {
                        case 1:
                            p.description = "Level: Easy"
                        case 2:
                            p.description = "Level: Medium"
                        case 3:
                            p.description = "Level: Hard"
                        default:
                            p.description = ""
                    }
                    
                    problems.append(p)
                }
            }
            completion(problems)
        } catch {
            print(error)
            completion(nil)
        }
    }.resume()
}

順帶一提,我們只顯示不用付費的 LeetCode 題目哦!所以有用 !statStatusPair.paidOnly 去過濾判斷非付費題目才會顯示於列表上

顯示在 SwiftUI 的 List 上

我們利用 .onAppear 的閉包去取得網路請求 API 資料,並且通知屬於 @Stateproblems 陣列,讓他去更新 UI 顯示相關的 LeetCode 題目資料

struct ContentView: View {
    
    @State private var problems: [LeetCodeProblem] = []
    
    var body: some View {
        List(problems) { problem in
            VStack(alignment: .leading) {
                if let title = problem.title {
                    Text(title)
                        .font(.headline)
                }
                
                
                Text(problem.description ?? "")
                    .font(.subheadline)
                
                
            }
        }
        .onAppear {
            fetchProblems { fetchedProblems in
                if let fetchedProblems = fetchedProblems {
                    self.problems = fetchedProblems
                }
            }
        }
        
    }
}

最終我們的畫面成功依照 LeetCode API 顯示 LeetCode 題目囉!顯示的當下十分有成就感,邁進了一大步的感覺

注意 Swift 寫法

在撰寫 SwiftUI 的過程中,本來想做防呆判斷所以原本的寫法是如下,但是 Text 吃的字串不可以是 nil,即使你判斷了它不是 nil

VStack(alignment: .leading) {
    // NG 寫法                        
    if(problem.title != nil) {
        Text(problem.title)
           .font(.headline)
    }                                                        
}

這個寫法是拆包確定不是 nil 後,塞值到 title,如果是 nil 這個 Text 就不會生成出現

VStack(alignment: .leading) {
    // OK 寫法                        
    if let title = problem.title {
         Text(title)
             .font(.headline)
    }                                                      
}

而本文範例寫法是即使是 nil 仍給他一個空字串生成 Text 字串,這沒有正確答案,只有看需求顯示當下最合宜的防呆

Text(problem.title ?? "")
        .font(.headline)

總結

本日成功的讓我們的資料不在寫死在 App 端,而是透過網路請求獲得 LeetCode 題目資料,這代表只要 LeetCode 題目增加,我們的 App 可以隨之即時更新,不需要再透過 App 程式碼的更改,又要再重新上架 App 過送審的動作,讓 App 更靈活呈現在使用者的畫面上!


上一篇
Day 8: SwiftUI 顯示 LeetCode 題目列表,使用 List 和 NavigationLink
下一篇
Day 10: LeetCode 題目詳情頁面,使用 SwiftUI Text 和 ScrollView
系列文
用 SwiftUI 魔法變出 Leetcode 刷題知識學習 App!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言