16. %d\n Xcode Study — About Design Pattern 由繁入簡(二)

春麗 S.T.E.M.
14 min readAug 22, 2021

--

Massive VC?MVC/MVP/MVVM/VIPER

上篇傳值到最後一頁還有幾個重點,一個是轉換時間的formatter,另一個則是比對session跟attendee,在session列出會出席的人名,在attendee列出個人會參加哪幾個會議,像下面這樣

所以在前一頁,session程式碼的地方,一樣加入了fetchOnlineData( ),將之放到viewWillAppear裡呼叫。

那麼它也需要下面這兩個變數

var attendee = Attendee(records: [])
let apiKey = "****"

在segueAction裡改成

let controller = DetailViewController(coder: coder)
guard let row = tableView.indexPathForSelectedRow?.row else { return nil }

let session = sessions[row]
var detailsString = session.description
detailsString += "\n\nAttendees:\n"
for i in 0...(attendee.records.count - 1) {
if session.attendeesBadges.contains( UInt8(attendee.records[i].fields.badgeNumber) ?? 0 ) {
detailsString +=
"\n - \(attendee.records[i].fields.name)"
}
}

controller?.details = detailsString

return controller

藉由遍歷,讓session這個陣列去檢查是否有包含
UInt8(attendee.records[i].fields.badgeNumber) ,遍歷範圍是網路抓取資料records這個陣列的長度,
UInt8(attendee.records[I].fields.badgeNumber)即是將讀取到的badgeNumber轉成UInt8,為避免nil,用了一個不可能的0 為default值。

如此便可將會議出席人列在下面,即是方才看到的結果。

那在attendee那邊則相似,最後看到右邊的結果

上圖右表示,點選到的這個人會出席哪些會議。

到此為止,要開始進到下一個階段,不過還有一件事,如果request預設是GET,那麼應該有方法可以更簡潔地去獲取網路資料,看到airTable裡API的說明下面有一項要我們這樣做

EXAMPLE USING QUERY PARAMETER

$ curl https://api.airtable.com/v0/appu4rLL2e1w9u848/Attendees?api_key=YOUR_API_KEY

即是可通過後面的query設置你的API_KEY以取得連線,所以fetchOnlineData可以改成這樣

let urlStr = "https://api.airtable.com/v0/appu4rLL2e1w9u848/Attendees?api_key=\(apiKey)"
guard let url = URL(string: urlStr) else { return }
下面就直接可使用urlURLSession.shared.dataTask(with: url) { data, response, error in不需要再用request繞路了

那麼,pushDataOnline可以改成這樣

let urlStr = "https://api.airtable.com/v0/appu4rLL2e1w9u848/Attendees?api_key=\(apiKey)"
guard let url = URL(string: urlStr) else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let encoder = JSONEncoder()
request.httpBody = try? encoder.encode(attendee)

簡而言之,即是把下面這兩行

        let urlStr = "https://api.airtable.com/v0/appu4rLL2e1w9u848/Attendees"        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")

合併為一行

let urlStr = "https://api.airtable.com/v0/appu4rLL2e1w9u848/Attendees?api_key=\(apiKey)"

接著還有值得做的事,重新審視model,將之改為

struct Attendee: Codable {
let records: [Record]
}
struct Record: Codable {
let fields: Fields
}

struct Fields: Codable {
let name: String
let nationality: String
let badgeNumber: String
let age: String
let isFirstTimeAttending: String

var description: String {
return """
Name: \(name)
Nationality: \(nationality)
Age: \(age)
Badge Number: \(badgeNumber)
Attending For The First Time: \(isFirstTimeAttending)
"""
}
}

如此便可宣告一個變數如下,如此寫成閉包方便寫出型別

var records = [Record]()原為
var attendee = Attendee(records: [])
不易讀
你覺得還好,如果要寫在閉包裡作為型別呢?我們繼續往下看

重頭戲就是改寫fetchOnlineData,以閉包的方式

func fetchOnlineDatas(completion: @escaping ([Record]?) -> Void) {
let apiKey = "keyyyaSglzifqkOXW"
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "api.airtable.com"
urlComponents.path = "/v0/appu4rLL2e1w9u848/Attendees"
urlComponents.query = "api_key=\(apiKey)"
guard let url = urlComponents.url else { return }


URLSession.shared.dataTask(with: url) { data, response, error in
let decoder = JSONDecoder()
if let data = data {

do {
let attendee = try decoder.decode(Attendee.self, from: data)
print(attendee.records)
completion(attendee.records)
// self.records = attendee.records
// DispatchQueue.main.async {
//
// self.tableView.reloadData()
// }

} catch {
print(error)
}
} else if let error = error {
print(error)
completion(nil)
}
}.resume()

}

前面那坨url已改成component的寫法,後面else if抓出去錯誤則暫且不管,但閉包裡面的completion皆代入參數,attendee.records是解析成功時丟到completion裡面,讓這個數值在外面還能使用。

這邊發現dispatchQueue的reloadData( )已經註解掉,因為改成下面的寫法

在viewDidLoad裡面fetchOnlineDatas { attendRecords in
if let records = attendRecords {
self.updateUI(with: records)
}
}
在viewDidLoad外面func updateUI(with datas: [Record]) {
DispatchQueue.main.async {
self.records = datas
self.tableView.reloadData()
}
}

大抵順序為,呼叫了抓取方法,resume了方法解析資料,得到閉包資料逃逸給外面用,所以viewDidLoad裡的呼叫,將逃逸的閉包代入updateUI,接著reloadDataupdate tableview

只不過updataUI的撰寫,不是那麼必要,所以也可以直接在viewDidLoad裡

fetchOnlineDatas { attendRecords in
if let records = attendRecords {
DispatchQueue.main.async {
self.records = records
self.tableView.reloadData()
}
}
}

接著,將url位址整理整理,將原先

let apiKey = "****"
let urlPath = "/v0/appu4rLL2e1w9u848/Attendees"
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "api.airtable.com"
urlComponents.path = "/v0/appu4rLL2e1w9u848/Attendees"
urlComponents.query = "api_key=\(apiKey)"
guard let url = urlComponents.url else { return }

改成這樣

let apiKey = "****"
let urlPath = "/v0/appu4rLL2e1w9u848/Attendees"
var urlComponents = URLComponents(string: "https://api.airtable.com/\(urlPath)")

urlComponents?.queryItems = [
URLQueryItem(name: "api_key", value: "\(apiKey)")
]
guard let url = urlComponents?.url else { return }

或透過URL的extension,寫成更好看( 炫技? )的

let apiKey = "****"
let urlPath = "/v0/appu4rLL2e1w9u848/Attendees"
let queries = [
"api_key": apiKey
]
let baseURL = URL(string: "https://api.airtable.com")?.appendingPathComponent("\(urlPath)").withQueries(queries)guard let url = baseURL else { return }

至此,由於上傳資料僅做測試用,暫時不採用閉包寫法,但本地資料呢?如下:

func fetchLocalData(completion: @escaping ([Session]?) -> Void) {
guard let url = Bundle.main.url(forResource: "WWDCSessions", withExtension: "json") else {
print("Can't find this url.")
return
}

do {
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)

let data = try Data(contentsOf: url)
let sessions = try decoder.decode([Session].self, from: data)
completion(sessions)
print("sessions一共 \(sessions.count)項")

} catch {
print("Can't prase jsonData.")
}
}

如此便完成了。

前兩個controller來統計並回顧一下

  1. sessions:上方,自設計cell(3個元件)、tableview的outlet、viewDidLoad呼叫了兩個方法( fetchOnlineDatas/fetchLocalData )、segueAction、兩個方法、tableview datasource,再加上一點雜的共166行代碼。
  2. attendees:原設計cell (2個元件,免拉outlet)、viewDidLoad呼叫了三個方法( fetchOnlineDatas/fetchLocalData/pushDataOnline )、一個updateUI、segueAction、三個方法、tableview datasource,再加上一點雜的共228行代碼。

天哪——!這不過是簡單的表格顯示就已經讓vc看起來有點雜了。

並且比對以上兩個vc,可以看到許多重複做的地方,是的,抓取與上傳資料的地方,把它移到新建的swift file,Network裡吧。

--

--

春麗 S.T.E.M.
春麗 S.T.E.M.

Written by 春麗 S.T.E.M.

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.

No responses yet