功能說明
串接 Random User API 取得亂數 user 清單,並且用 scroll view 搭配 container view 達到左右滑動換頁顯示每個 user 的功能。
Data 下載以及 JSON 格式的解析
先將 JSON 資料裡所需要的 property 整理,並自訂義 struct 且遵從 Decodable,之後供 JSONDecoder 使用
struct SearchResult: Decodable {
let results: [User]
let info: Info
}struct User: Decodable {
let name: Name
let location: Location
let email: String
let dob: Dob
let phone: String
let picture: Picture
struct Name: Decodable{
let title: String
let first: String
let last: String
}
struct Location: Decodable {
let street: Street
let city: String
struct Street: Decodable {
let number: Int
let name: String
}
}
struct Dob: Decodable {
let date: Date
let age: Int
var dateString: String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yyyy"
return dateFormatter.string(from: date)
}
}
struct Picture: Decodable {
let large: URL
let medium: URL
let thumbnail: URL
}
}struct Info: Decodable {
let seed: String
let results: Int
let page: Int
let version: String
}
{} 是 object,自定義型別、[] 是 array,其他的就 String、 Int、URL、Date 等等。
採用不會卡住 UI 的 URLSession 來取得 http 下載來的 Data,並且使用 JSONDecoder 解析,取得 user 資料後在 main thread 更新畫面
let urlStr = "https://randomuser.me/api/?results=3&inc=name,location,email,dob,phone,picture"if let url = URL(string: urlStr) {
URLSession.shared.dataTask(with: url) { [self] data, urlResponse, error in let decoder = JSONDecoder() let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
if let date = dateFormatter.date(from: dateString) {
return date
}else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "")
}
}) if let data = data,
let urlResponse = urlResponse as? HTTPURLResponse,
error == nil {
print("response statusCod = \(urlResponse.statusCode)")
do {
let searchResult = try decoder.decode(SearchResult.self, from: data)
users = searchResult.results
DispatchQueue.main.async {
setupUserInfo()
}
} catch {
print("JSONDecoder 解析失敗")
}
} }.resume()
}
dateDecodingStrategy 可以設定 JSONDecoder 解析時間的格式
上圖是 Random User API 取得的時間格式,此為帶有秒數小數點的 ISO8601 格式。如果是單純的 ISO8601 沒有小數點的話,直接在 dateDecodingStrategy 設定 .iso8601 即可
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
但有小數點的 ISO8601,iOS SDK提供了 ISO8601DateFormatter 來幫忙解析。但 ISO8601DateFormatter 不是 DateFormatter 所以不能在 dateDecodingStrategy 的 .formatted() 傳入。
因此這裡透過 dateDecodingStrategy 的 .custom 自己在 closure 裡頭撰寫解析方法
let decoder = JSONDecoder()let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
if let date = dateFormatter.date(from: dateString) {
return date
}else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "")
}
})
ISO8601DateFormatter().formatOptions
傳入 .withInternetDateTime
表示是標準的 ISO8601 格式,傳入 .withFractionalSeconds
表示是帶小數的秒。
decoder.singleValueContainer()
取得解析 JSON 欄位遇到的 date 欄位的內容,然後解析取得字串,再透過 ISO8601DateFormatter().date(from: dateString)
把字串變 Date。
水平滑動切換頁面
總共有一個主 ViewController (UsersViewController) 以及擺放三個 user 的三個可供滑動的 ViewController (UserFirstViewController、UserSecondViewController、UserThirdViewController)。
UsersViewController 畫面上的元件架構為,ScrollView 裡頭加入 Horizontal StackView,StackView 裡頭加入三個 ContainerView。
Auto Layout 為,
StackView 上下左右對齊 ScrollView 的 content layout guide,高度等於 frame layout guide 的高度。
第一個 ContainerView 寬度等於 frame layout guide 的寬度。
並且,StackView 的 Alignment 設為 fill,Distribution 設為 Fill Equally。
從三個 ContainerView 各自拉線到 UserFirstViewController、UserSecondViewController、UserThirdViewController,選擇 Embed。
ScrollView 與 SegmentedControl 之間的連動
滑動頁面更新 SegmentedControl
@IBOutlet weak var usersSegmentedControll: UISegmentedControl!extension UsersViewController: UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let index = Int(scrollView.contentOffset.x / scrollView.bounds.width)
usersSegmentedControll.selectedSegmentIndex = index
}
}
點選 SegmentedControl 切換分頁
@IBOutlet weak var scrollView: UIScrollView!@IBAction func changePage(_ sender: UISegmentedControl) {
let x = CGFloat(sender.selectedSegmentIndex) * scrollView.bounds.width
let offset = CGPoint(x: x, y: 0)
scrollView.setContentOffset(offset, animated: true)
}
存取 ContainerView 的 controller
為了在 ContainerView 關聯到的 controller 的畫面上呈現 API 取得的資料,我們可以透過 children,在主要 controller 存取各個 ContainerView 關聯到的 controller。(相反的也可以透過 parent,讀取主要 controller)
func setupUserInfo() {
for (i, child) in children.enumerated() {
let firstName = users[i].name.first
usersSegmentedControll.setTitle(firstName, forSegmentAt: i)
let lastName = users[i].name.last
let email = users[i].email
let birthday = users[i].dob.dateString
let addressStreetNum = users[i].location.street.number.description
let addressStreetName = users[i].location.street.name
let phone = users[i].phone
let picture = users[i].picture.large
if let controller = child as? UserFirstViewController {
//傳值給controller並下載圖檔以及更新畫面
} else if let controller = child as? UserSecondViewController {
//傳值給controller並下載圖檔以及更新畫面
} else if let controller = child as? UserThirdViewController {
//傳值給controller並下載圖檔以及更新畫面
}
}
}
從 JSON 解析得到圖檔網址後,就可以根據此網址下載圖檔
@IBOutlet weak var imageView: UIImageView!func setupImage (picture: URL) {
URLSession.shared.dataTask(with: picture) { data, urlResponse, error in
if let data = data,
let image = UIImage(data: data) {
DispatchQueue.main.async {
self.imageView.image = image
}
}
}.resume()
}
Demo