模仿Dcard

Dcard是很多大學生在用的討論平台,雖然還是比較喜歡用PTT,但Dcard介面比較好看就來練習做出他們的畫面及使用Json抓取資料囉!

畫面都是使用storyboard來製作,直接來看程式碼

這次製作的為抓取最新文章及點擊後進到對應文章的內容裡

附上最新文章的API格式

https://dcard.tw/_api/posts

文章內容API格式

https://dcard.tw/_api/posts/文章id

附上需要呈現資料的程式碼,也可以根據畫面新增其他種類

struct Post: Codable {
let id: Int
let title: String
let excerpt: String
let commentCount: Int
let likeCount: Int
let forumName: String
let gender: String
var school: String?
var mediaMeta: [MediaMeta]
}struct MediaMeta: Codable {
var url: URL

}

型別遵從Codable表示可以解碼或編碼,Decodable 表示可解碼,Encodable表示可以解碼,並將mediaMeta型別宣告為[MediaMeta],說明為Array。school宣告為 optional表示Json不一定每一筆都有這個資料。

抓取 JSON 資料,將它變成自訂型別 Post

let urlStr = "https://dcard.tw/_api/posts"
if let url = URL(string: urlStr) {
URLSession.shared.dataTask(with: url) { (data, response, error) in
let decoder = JSONDecoder()
if let data = data {
do {
let posts = try decoder.decode([Post].self, from: data)
self.posts = posts
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch {
print(error)
}
}
}.resume()
}

由於Json第一層為Array,所以在 decode 時傳入的型別加上 [ ] ,例如上方的[Post]。

抓取 JSON 資料的圖片url

let imageUrl = post.mediaMeta[0].url
URLSession.shared.dataTask(with: imageUrl) { (data, response, error) in
if let data = data {
DispatchQueue.main.async {
cell.postImage.isHidden = false
cell.postImage.image = UIImage(data: data)
}
}
}.resume()
} else {
cell.postImage.isHidden = true
}
return cell

在畫面設計上 cell右邊放上imageView,但不是每篇文章都會有圖片,所以在抓資料時做判斷,如果沒有就將它隱藏起來。

cell.postImage.isHidden = true

PostDtail頁面

var post: Post!var postDetail: PostDetail?

加上這個後表示近來這頁面表示依訂有這個最新文章,所以Post可以直接加!,但不一定會點進來,所以加上?代表可以為nil。

由前一頁傳資料進來

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let row = tableView.indexPathForSelectedRow?.row, let controller = segue.destination as? PostDetailViewController {
let post = posts[row]
controller.post = post
}

}

解析ISO8601日期格式

頁面上會顯示po文的日期,所以使用ISO8601DateFormatter 解析 ISO 8601 格式的字串,但 ISO 8601 帶有小數的秒時,就無法解析了,要額外在formatOptions 設定 ISO 8601 的格式。

設定 [.withInternetDateTime, .withFractionalSeconds],withInternetDateTime 指的是標準的 ISO 8601 格式,而 withFractionalSeconds 則包含小數的秒。

if let url = URL(string: "https://dcard.tw/_api/posts/\(post.id)") {

print(post.id)

URLSession.shared.dataTask(with: url) { (data, response, error) in
let decoder = JSONDecoder()
let formatter = ISO8601DateFormatter()
// 解析時間ISO8601
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let data = try decoder.singleValueContainer().decode(String.self)
// 回傳接到的時間,否則回傳目前時間
return formatter.date(from: data) ?? Date()
})

文章內容是使用TextView來呈現,但是Json的文章內容裡是文字夾帶著url,所以要另外做處理才可以讓內容可以依照實際App那樣文字中夾圖片的樣子。

用程式生成圖片

extension UIImage {
static func image(from url: URL, handel: @escaping (UIImage?) -> ()) {

guard let data = try? Data(contentsOf: url), let image =
UIImage(data: data) else {
handel(nil)
return

}
handel(image)

}

func scaled(with scale: CGFloat) -> UIImage? {
let size = CGSize(width: floor(self.size.width * scale), height: floor(self.size.height * scale))
UIGraphicsBeginImageContext(size)
draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}

設定圖片大小及字體大小

extension NSMutableAttributedString {
func append(string: String) {
// 調整textView上字體大小
self.append(NSAttributedString(string: string + "\n", attributes: [.font: UIFont.systemFont(ofSize: 16)]))
}

func append(imageFrom: String, textView: UITextView) {
guard let url = URL(string: imageFrom) else { return }
UIImage.image(from: url) { (image) in
guard let image = image else { return }

// 設定螢幕寬度的0.8 長寬比不變
let scaledImg = image.scaled(with: UIScreen.main.bounds.width / image.size.width * 0.8)
let attachment = NSTextAttachment()
attachment.image = scaledImg
self.append(NSAttributedString(attachment: attachment))
self.append(NSAttributedString(string: "\n"))
}

}
}

拿到每一句文字,並把文字用 \n做分隔,例如”asd\nzxc” 會變成 [asd, zxc],for這個array,只要這一句包含 http代表它是圖片

let contentArray = postDetail.content.split(separator: "\n").map(String.init)
let mutableAttributedString = NSMutableAttributedString()
contentArray.forEach {row in
if row.contains("http") {
mutableAttributedString.append(imageFrom: row, textView: self.contentTextView)
} else {
mutableAttributedString.append(string: row)
}
}

附上github

--

--