先說兩句廢話(Don't blame me about my calculation)
為啥寫這篇文章窃判,簡單說,這些日子以來喇闸,總覺著做事還是專注些好袄琳,于是也逐步減少了很多信息消費(fèi),縮減了些欲望吧燃乍。目前更加關(guān)注怎么能夠讓開發(fā)更快樂些唆樊,相信有了這個(gè)方向,其他事情就更容易見招拆招了刻蟹,面對的挑戰(zhàn)也不再是挑戰(zhàn)逗旁,而是激發(fā)自己斗志的輔助工具,其實(shí)不用在乎那些看似權(quán)威的做法和打法舆瘪,只要是沒讓你開心的片效,肯定是有改進(jìn)空間的。思路和方向才是最重要的英古,比如《大偵探波洛》淀衣,每次破案之前波洛就已經(jīng)通過利害關(guān)系找好了方向,他的推理都是基于認(rèn)定的方向去尋找素材召调。
開心不是因?yàn)闆]有挑戰(zhàn)膨桥,沒有困難蛮浑,沒有煎熬,而是因?yàn)檎业搅朔较蛑幌@個(gè)方向就是沮稚,快樂的 Coding,開心的工作册舞,為了達(dá)成這個(gè)目標(biāo)那些艱難挑戰(zhàn)也就不算什么了蕴掏。對于 Coding,經(jīng)過實(shí)操环础,我覺得聲明式 UI 響應(yīng)式編程范式就是很好的提升工作愉悅程度的方式囚似。代碼在 GitHub 上,鏈接 线得。后面我會詳細(xì)跟你說說這個(gè)應(yīng)用如何開發(fā)的及相關(guān)知識點(diǎn)饶唤,希望你也能夠感受下這種 Happy 的開發(fā)模式。
這之前贯钩,我想先說下為什么我覺得快樂是很件重要的事情募狂。這段時(shí)間,我接受了好幾次采訪角雷,有關(guān)于工程師文化方面的祸穷,還有《時(shí)尚COSMOPOLITAN》雜志的采訪,記者會問到一些以前的事情勺三,在聊過往事情時(shí)我發(fā)現(xiàn)原來快樂才是每天自己存在著的最根本的原動(dòng)力雷滚。為了能夠讓自己能夠一直活著,就不要偏離快樂吗坚。攝影師是任欣羽祈远,參與過《一代宗師》的拍攝,還是《時(shí)尚芭莎》的模特商源。以下是時(shí)尚 COSMOPOLITAN 的采訪內(nèi)容:
完整內(nèi)容見:https://mp.weixin.qq.com/s/b5fj2b65xRv4mhFpftwNcg
視頻可見這條微博地址:https://weibo.com/1351051897/KEdu5Fi1x?pagetype=profilefeed
視頻有六十多萬播放量车份,兩百多評論和一千多轉(zhuǎn)發(fā)。
話題還上了微博熱搜牡彻,有六百多萬閱讀和三千多討論扫沼。
你肯定會覺得很奇怪,我怎么會接受時(shí)尚雜志采訪庄吼,其實(shí)我早在2006年就跟時(shí)尚娛樂圈有染了缎除,那年張紀(jì)中版《神雕俠侶》剛熱播完,劉亦菲演的小龍女总寻,我特別的喜歡伴找。有幸在一次活動(dòng)中我成為她的御用攝影師,由于過于激動(dòng)手抖废菱,拍糊了好多張技矮,蠻可惜的抖誉。私存這批里還是有些清晰的,這些照片最近在找資料時(shí)不小心被我翻了出來衰倦。挑幾張看看十六年前的劉亦菲和我是什么樣的吧袒炉。
我還很用心的置辦了新家。也是希望能夠讓自己能夠開心些樊零。
那么我磁,怎樣高效開發(fā),帶來愉悅的呢驻襟?
看看做出來的樣子
這是個(gè) macOS 應(yīng)用(https://github.com/ming1016/SwiftPamphletApp)《戴銘的開發(fā)小冊子》夺艰,能夠方便的查看 Swift 語法,還有一些主要庫的使用指南沉衣,內(nèi)容還在完善中郁副,選擇的庫主要就是開發(fā)小冊子應(yīng)用使用到的 SwitUI、Combine豌习、Swift Concurrency存谎。
除了這些速查和庫的使用內(nèi)容外,這個(gè)應(yīng)用還有一些開發(fā)者的動(dòng)態(tài)肥隆,當(dāng)他們有新的動(dòng)作既荚,比如提交了代碼、star 了什么項(xiàng)目栋艳,提交和留言了議題都會直接在程序塢中提醒你恰聘。
我對一些庫做了分類,方便按需查找吸占,庫有新的提交也會在程序塢中提醒晴叨。
還能方便的查看庫的議題。比如在阮一峰的《科技愛好者周刊》的議題中可以看到有很多人推薦和自薦了一些信息旬昭。保留議題有一千六百多個(gè)篙螟。
這個(gè)元旦假期菌湃,我又添加了博客動(dòng)態(tài)的功能问拘,可以跟進(jìn)一些博客內(nèi)容的更新。
由于 Swift 語言的簡潔惧所,這些庫的先進(jìn)骤坐,最近有同學(xué)做實(shí)驗(yàn),5.5版本還有瘦體積的效果下愈。這樣的一個(gè)小冊子應(yīng)用程序累積開發(fā)的時(shí)間不多纽绍,就是很高效的嘛。特別是最后博客動(dòng)態(tài)這個(gè)功能势似,七年前我用 Objective-C 做的一個(gè)RSS閱讀器耗費(fèi)了我兩三周的時(shí)間拌夏。同樣的功能用 Swift 這套來做元旦假期兩天就完成了僧著。聲明式 UI 響應(yīng)式范式配合上 Swift 簡潔的語法真是蠻 Cool 的。
基礎(chǔ)網(wǎng)絡(luò)能力
小冊子應(yīng)用會大量使用網(wǎng)絡(luò)障簿,先看看怎么用 Swift Concurrency 來做吧盹愚。
func RSSReq(_ urlStr: String) async throws -> String? {
guard let url = URL(string: urlStr) else {
fatalError("wrong url")
}
let req = URLRequest(url: url)
let (data, res) = try await URLSession.shared.data(for: req)
guard (res as? HTTPURLResponse)?.statusCode == 200 else {
fatalError("wrong data")
}
let dataStr = String(data: data, encoding: .utf8)
return dataStr
}
如上嘿歌,通過 url 可以獲取到 data 和 response芍躏,和其他網(wǎng)絡(luò)請求的方式不同的是,使用 await 后就不用繁瑣的代理或閉包來進(jìn)行后續(xù)的處理值戳,代碼變得更好理解西篓,即字面意思上的 await 后執(zhí)行后面的行愈腾。舉個(gè)例子,獲取博客 RSS 時(shí)岂津,如果希望處理完一個(gè) RSS 后再處理后面一個(gè) RSS虱黄,使用 await 語法看起來就非常簡潔清爽易于理解了。
Task {
do {
let rssFeed = SPC.rssFeed() // 獲取所有 rss 源的模型
for r in rssFeed {
let str = try await RSSReq(r.feedLink)
guard let str = str else {
break
}
RSSVM.handleFetchFeed(str: str, rssModel: r)
// 在 Main Actor 更新通知數(shù)
await rssUpdateNotis()
}
} catch {}
}
如上寸爆,當(dāng)出現(xiàn)數(shù)據(jù)獲取錯(cuò)誤就跳過后面邏輯直接去請求下個(gè) RSS礁鲁,獲取成功會更新 Main Actor 處理通知邏輯,不同隊(duì)列之間切換就是這么自然赁豆,短短幾行代碼就都講清楚了仅醇。
Combine 來處理網(wǎng)絡(luò)的優(yōu)勢就是能夠?qū)⒕W(wǎng)絡(luò)請求到數(shù)據(jù)處理,最后到數(shù)據(jù)綁定都負(fù)責(zé)了魔种。也就是發(fā)布者析二、操作符和訂閱者的組合。下面我通過開發(fā)指南功能的過程說明下 Combine 的用法节预。
怎么開發(fā)指南功能
指南的列表結(jié)構(gòu)使用的是 JSON叶摄,我把列表的數(shù)據(jù)保存在倉庫的議題中,通過 GitHub 的 REST API 獲取議題進(jìn)行展示安拟,這樣對于指南列表的內(nèi)容修改豐富可以通過直接在議題中進(jìn)行編輯即可蛤吓,無需升級應(yīng)用。
Combine 網(wǎng)絡(luò)請求我寫在 APIRequest.swift 文件里糠赦,主要代碼如下:
final class APISev: APISevType {
private let rootUrl: URL
init(rootUrl: URL = URL(string: "https://api.github.com")!) {
self.rootUrl = rootUrl
}
func response<Request>(from req: Request) -> AnyPublisher<Request.Res, APISevError> where Request : APIReqType {
let path = URL(string: req.path, relativeTo: rootUrl)!
var comp = URLComponents(url: path, resolvingAgainstBaseURL: true)!
comp.queryItems = req.qItems
var req = URLRequest(url: comp.url!)
req.addValue("token \(SPC.gitHubAccessToken)", forHTTPHeaderField: "Authorization")
req.addValue("SwiftPamphletApp", forHTTPHeaderField: "User-Agent")
let de = JSONDecoder()
de.keyDecodingStrategy = .convertFromSnakeCase
let sch = DispatchQueue(label: "GitHub API Queue", qos: .default, attributes: .concurrent)
return URLSession.shared.dataTaskPublisher(for: req)
.retry(3)
.subscribe(on: sch)
.receive(on: sch)
.map { data, res in
return data
}
.mapError { _ in
APISevError.resError
}
.decode(type: Request.Res.self, decoder: de)
.mapError { _ in
APISevError.parseError
}
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}
如上会傲,Combine 有 decode 的操作符,能夠直接指定 JSON 模型數(shù)據(jù)類型和 JSONDecoder 對象拙泽。還有重試淌山、隊(duì)列指定以及拋錯(cuò)誤的操作符。
一個(gè)應(yīng)用的生命周期內(nèi)顾瞻,相同的請求會發(fā)布很多次泼疑,需要定義一個(gè)發(fā)起請求的 Subject,還有請求完成響應(yīng)的 Subject荷荤。定義如下:
private let apCustomIssuesSj = PassthroughSubject<Void, Never>()
private let resCustomIssuesSj = PassthroughSubject<IssueModel, Never>()
apCustomIssuesSj 會發(fā)起網(wǎng)絡(luò)請求退渗,代碼如下:
let resCustomIssuesSm = apCustomIssuesSj
.flatMap { [apiSev] in
apiSev.response(from: reqCustomIssues)
.catch { [weak self] error -> Empty<IssueModel, Never> in
self?.errSj.send(error)
return .init()
}
}
.share()
.subscribe(resCustomIssuesSj)
上面 .catch 里errSj 發(fā)布者就是嵌套發(fā)布者移稳,.flatMap 會讓每次返回都是新發(fā)布者。apiSev.response 返回的是被類型擦除到 AnyPublisher 上会油,這樣不同類型的發(fā)布者能夠被 .flatMap 處理秒裕。閉包內(nèi)的 .catch 處理能區(qū)分發(fā)布者,僅對當(dāng)前發(fā)布者有效钞啸,不會影響后面發(fā)布者几蜻,導(dǎo)致整個(gè)管道被取消。發(fā)布者失敗類型是 Never体斩,失敗本身會被連貫的處理梭稚。
.flatMap 除了從它 map 函數(shù)里生產(chǎn)發(fā)布者,還有個(gè)可選參數(shù) maxPublishers絮吵,通過這個(gè)參數(shù)可以限制一次生產(chǎn)的最大發(fā)布者數(shù)量弧烤,也就是你可以通過 .flatMap 對管道上游的發(fā)布者進(jìn)行反壓(Backpressure),maxPublishers 能有效的節(jié)流管道蹬敲,按照管道內(nèi)部實(shí)際上的發(fā)布速度進(jìn)行反壓暇昂,這個(gè)也是 Combine 相較于 RxSwift 來說的一個(gè)優(yōu)勢。比如當(dāng)網(wǎng)絡(luò)請求多時(shí)伴嗡,你可以通過設(shè)置 .max(1) 來減輕請求對服務(wù)的壓力急波,同時(shí)還能夠保證結(jié)果到達(dá)的順序和請求順序的一致。
resCustomIssuesSj 會去處理網(wǎng)絡(luò)請求成功的數(shù)據(jù)瘪校,最后通過 .assign 將處理的數(shù)據(jù)分配給遵循 ObservableObject 協(xié)議類的 @Published 屬性包裝的屬性 customIssues澄暮,用于響應(yīng)式的更新 SwiftUI 布局?jǐn)?shù)據(jù)。實(shí)現(xiàn)代碼如下:
let repCustomIssuesSm = resCustomIssuesSj
.map({ issueModel in
let str = issueModel.body?.base64Decoded() ?? ""
let data: Data
data = str.data(using: String.Encoding.utf8)!
do {
let decoder = JSONDecoder()
return try decoder.decode([CustomIssuesModel].self, from: data)
} catch {
return [CustomIssuesModel]()
}
})
.assign(to: \.customIssues, on: self)
如上阱扬,你會發(fā)現(xiàn)在 .map 中還會對數(shù)據(jù)進(jìn)行 base64 decode泣懊,這是因?yàn)槲以趥}庫議題中保存的是 base64 encode 的數(shù)據(jù),decode 成 JSON 數(shù)據(jù)再用 JSONDecoder 轉(zhuǎn)為 [CustomIssuesModel] 模型 數(shù)據(jù)分配給 customIssues麻惶。
使用 SwiftUI 寫的指南列表視圖馍刮,代碼如下:
struct IssuesListFromCustomView: View {
@StateObject var vm: IssueVM
var body: some View {
List {
ForEach(vm.customIssues) { ci in
Section {
ForEach(ci.issues) { i in
NavigationLink {
IssueView(vm: IssueVM(repoName: SPC.pamphletIssueRepoName, issueNumber: i.number))
} label: {
Text(i.title)
.bold()
}
}
} header: {
Text(ci.name).font(.title)
}
}
}
.alert(vm.errMsg, isPresented: $vm.errHint, actions: {})
.onAppear {
vm.doing(.customIssues)
}
}
}
代碼中的屬性包裝 @StateObject 會在當(dāng)前視圖生命周期中保持 vm 這個(gè)屬性的數(shù)據(jù),vm 需要遵循 ObservableObject 協(xié)議窃蹋,其 @Published 發(fā)布屬性的值會被 SwiftUI 自動(dòng)進(jìn)行管理卡啰,屬性 vm 的發(fā)布屬性數(shù)據(jù)變化時(shí)會自動(dòng)觸發(fā)布局依據(jù)新數(shù)據(jù)的更新。
上面代碼中的 SwiftUI 寫的布局界面效果如下:
界面主體是 List 視圖脐彩,根據(jù) List 的定義碎乃,要求的輸入是一個(gè)數(shù)組姊扔,數(shù)組內(nèi)元素需要遵循 Identifiable惠奸,每行的返回是被 @ViewBuilder 標(biāo)記的 View。ForEach 根據(jù)數(shù)組中的元素會創(chuàng)建能夠重復(fù)使用的視圖恰梢,性能接近大家熟悉的 UITableView佛南,但是寫法上簡潔的不要太多梗掰,真實(shí)完美解痛點(diǎn)案例,????嗅回。
指南的內(nèi)容也會以 markdown 格式存在議題中及穗,通過調(diào)用 GitHub API 的接口進(jìn)行指南內(nèi)容的讀取。一個(gè)接口是議題接口绵载,請求結(jié)構(gòu)體定義如下:
struct IssueRequest: APIReqType {
typealias Res = IssueModel
var repoName: String
var issueNumber: Int
var path: String {
return "/repos/\(repoName)/issues/\(issueNumber)"
}
var qItems: [URLQueryItem]? {
return nil
}
}
另一個(gè)是議題留言的接口埂陆,定義如下:
struct IssueRequest: APIReqType {
typealias Res = IssueModel
var repoName: String
var issueNumber: Int
var path: String {
return "/repos/\(repoName)/issues/\(issueNumber)"
}
var qItems: [URLQueryItem]? {
return nil
}
}
實(shí)現(xiàn)效果如下圖:
指南內(nèi)容放在議題中,也是希望能夠通過議題留言功能娃豹,讓反饋和大家經(jīng)驗(yàn)的補(bǔ)充被更多人看到焚虱。
除了語法速查的內(nèi)容,關(guān)于 Swift 的一些特性懂版,專題鹃栽,還有 Combine、Concurrency躯畴、SwiftUI 這些庫的使用指南內(nèi)容都是采用的 GitHub API 接口讀取議題方式獲取的民鼓。
讀取議題接口獲取指南列表的模式,也用在了開發(fā)者和倉庫動(dòng)態(tài)列表中蓬抄。接下來我跟你說下開發(fā)者和倉庫動(dòng)態(tài)怎么開發(fā)的吧丰嘉。
開發(fā)者和倉庫動(dòng)態(tài)
顯示開發(fā)者信息的頁面代碼在 UserView.swift 里,開發(fā)者介紹信息頁面如下:
界面中的數(shù)據(jù)都來自 /users/(userName) 接口嚷缭,獲取數(shù)據(jù)邏輯在 UserVM.swift 里供嚎。數(shù)據(jù)多,但情況不復(fù)雜峭状,布局上只要注意進(jìn)行數(shù)據(jù)是否有的區(qū)分即可克滴,布局代碼如下:
HStack {
VStack(alignment: .leading, spacing: 10) {
HStack() {
AsyncImageWithPlaceholder(size: .normalSize, url: vm.user.avatarUrl)
VStack(alignment: .leading, spacing: 5) {
HStack {
Text(vm.user.name ?? vm.user.login).font(.system(.title))
Text("(\(vm.user.login))")
Text("訂閱者 \(vm.user.followers) 人,倉庫 \(vm.user.publicRepos) 個(gè)")
}
HStack {
ButtonGoGitHubWeb(url: vm.user.htmlUrl, text: "在 GitHub 上訪問")
if vm.user.location != nil {
Text("居子糯病:\(vm.user.location ?? "")").font(.system(.subheadline))
}
}
} // end VStack
} // end HStack
if vm.user.bio != nil {
Text("簡介:\(vm.user.bio ?? "")")
}
HStack {
if vm.user.blog != nil {
if !vm.user.blog!.isEmpty {
Text("博客:\(vm.user.blog ?? "")")
ButtonGoGitHubWeb(url: vm.user.blog ?? "", text: "訪問")
}
}
if vm.user.twitterUsername != nil {
Text("Twitter:")
ButtonGoGitHubWeb(url: "https://twitter.com/\(vm.user.twitterUsername ?? "")", text: "@\(vm.user.twitterUsername ?? "")")
}
} // end HStack
} // end VStack
Spacer()
}
上面代碼可以看到劝赔,對于數(shù)據(jù)是否存在,SwiftUI 是可以使用 if 來進(jìn)行判斷是否展示視圖的胆敞,這個(gè)條件判斷也會存在于整個(gè)視圖結(jié)構(gòu)類型中被編譯生成着帽,因此更好的方式是將數(shù)據(jù)判斷放到 ViewModifier 中,因?yàn)?ViewModifier 處理時(shí)機(jī)是在運(yùn)行時(shí)移层,可以減少布局初始創(chuàng)建邏輯運(yùn)算仍翰。
開發(fā)者的事件和接受事件部分的數(shù)據(jù)就比介紹部分復(fù)雜些,使得界面變化也多些观话,事件接口是 /users/(userName)/events予借,接受事件接口是 /users/(userName)/received_events 。數(shù)據(jù)的復(fù)雜體現(xiàn)在類型上,類型種類較多灵迫,我采用的是直接處理 payload 里的字段秦叛,如果其 issue.number 字段不為空,那么就表示這個(gè)開發(fā)者事件是和議題相關(guān)瀑粥,會顯示 issue.title 標(biāo)題挣跋,有內(nèi)容的話,也就是 issue.body 不為空狞换,繼續(xù)顯示議題的內(nèi)容避咆。如果字段是 comment,就表示事件是議題的留言修噪。如果字段是 commits牌借,表示需要列出這個(gè)事件中所有的 commit 提交及標(biāo)題和描述。pullRequest 字段不為空就顯示這個(gè) PR 的標(biāo)題和內(nèi)容描述割按。字段處理邏輯代碼實(shí)現(xiàn)如下:
if event.payload.issue?.number != nil {
if event.payload.issue?.title != nil {
Text(event.payload.issue?.title ?? "").bold()
}
if event.payload.issue?.body != nil && event.type != "IssueCommentEvent" {
Markdown(Document(event.payload.issue?.body ?? ""))
}
if event.type == "IssueCommentEvent" && event.payload.comment?.body != nil {
Markdown(Document(event.payload.comment?.body ?? ""))
}
}
if event.payload.commits != nil {
ListCommits(event: event)
}
if event.payload.pullRequest != nil {
if event.payload.pullRequest?.title != nil {
Text(event.payload.pullRequest?.title ?? "").bold()
}
if event.payload.pullRequest?.body != nil {
Markdown(Document(event.payload.pullRequest?.body ?? ""))
}
}
if event.payload.description != nil {
Markdown(Document(event.payload.description ?? ""))
}
上面代碼中膨报,對于不定數(shù)量的 commit 視圖寫在了一個(gè)單獨(dú)的 ListCommits 視圖中。只要是遵循了 View 協(xié)議适荣,就可以作為自定義視圖在其他視圖中直接使用现柠。ListCommits 代碼如下:
struct ListCommits: View {
var event: EventModel
var body: some View {
ForEach(event.payload.commits ?? [PayloadCommitModel](), id: \.self) { c in
ButtonGoGitHubWeb(url: "https://github.com/\(event.repo.name)/commit/\(c.sha ?? "")", text: "提交")
Text(c.message ?? "")
}
}
}
上面代碼你會發(fā)現(xiàn)一個(gè) ButtonGoGitHubWeb的視圖,進(jìn)入看會發(fā)現(xiàn)用到了一個(gè)自定義的 ButtonStyle:
.buttonStyle(FixAwfulPerformanceStyle())
FixAwfulPerformanceStyle() 的實(shí)現(xiàn)如下:
/// 列表加按鈕性能問題弛矛,需觀察官方后面是否解決
/// https://twitter.com/fcbunn/status/1259078251340800000
struct FixAwfulPerformanceStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.font(.body)
.padding(EdgeInsets.init(top: 2, leading: 6, bottom: 2, trailing: 6))
.foregroundColor(configuration.isPressed ? Color(nsColor: NSColor.selectedControlTextColor) : Color(nsColor: NSColor.controlTextColor))
.background(configuration.isPressed ? Color(nsColor: NSColor.selectedControlColor) : Color(nsColor: NSColor.controlBackgroundColor))
.overlay(RoundedRectangle(cornerRadius: 6.0).stroke(Color(nsColor: NSColor.lightGray), lineWidth: 0.5))
.clipShape(RoundedRectangle(cornerRadius: 6.0))
.shadow(color: Color.gray, radius: 0.5, x: 0, y: 0.5)
}
}
這是社區(qū) @Kam-To 提的一個(gè) PR够吩,是解的 macOS 上的一個(gè)性能問題,也就是在 List 中直接使用 Button丈氓,在列表快速滾動(dòng)時(shí)周循,流暢度會有損傷,加上上面的 ButtonStyle 代碼就好了万俗。
原推見 https://twitter.com/fcbunn/status/1259078251340800000湾笛。
開發(fā)者接受事件和事件類似,只是會多顯示事件的 actor 字段內(nèi)容闰歪,表明開發(fā)者接受的是誰發(fā)出的事件嚎研。事件界面如下所示:
倉庫整體處理和開發(fā)者類似,只是多了議題和 README 內(nèi)容库倘,數(shù)據(jù)復(fù)雜度比開發(fā)者要低临扮。接下來我要跟你說的是如果開發(fā)者或倉庫有新的提交,怎么能夠獲取到教翩,并提示有更新杆勇。
動(dòng)態(tài)有更新,怎么提醒的
我的思路是通過本地定時(shí)器饱亿,定期獲取數(shù)據(jù)蚜退,本地記錄上次瀏覽的位置闰靴,通過對比,看有多少新的動(dòng)態(tài)沒有查看关霸,并通過 .badge 這個(gè) ViewModifier 和 NSApp.dockTile.badgeLabel 來進(jìn)行端內(nèi)端外的提醒。
定時(shí)器
在 SwiftUI 中杰扫,可以使用 Combine 的 Timer.publish 發(fā)布器來設(shè)置一個(gè)定時(shí)屬性队寇,Timer.publish 指定好時(shí)間周期和隊(duì)列模式等參數(shù)。比如設(shè)置一個(gè)開發(fā)者動(dòng)態(tài)定時(shí)器屬性章姓,代碼如下:
let timerForRepos = Timer.publish(every: SPC.timerForReposSec, on: .main, in: .common).autoconnect()
然后再在 .onReceive 中執(zhí)行網(wǎng)絡(luò)數(shù)據(jù)獲取操作佳遣,就可以定時(shí)獲取數(shù)據(jù)了。
.onReceive(timerForRepos, perform: { time in
if let repoName = appVM.timeForReposEvent() {
let vm = RepoVM(repoName: repoName)
vm.doing(.notiRepo)
}
})
獲取到的數(shù)據(jù)會跟本地已經(jīng)存儲的數(shù)據(jù)進(jìn)行對比凡伊。
本地存儲
本地?cái)?shù)據(jù)存儲零渐,我用的是 SQLite.swift,這個(gè)庫是使用 Swift 對 SQLite 做了一層封裝系忙,使用很簡便诵盼,在 DBHandler.swift 里有數(shù)據(jù)庫初始化和表的創(chuàng)建相關(guān)代碼,DBDevNoti.swift 中的 DevsNotiDataHelper 有對數(shù)據(jù)操作的代碼银还,DBDevNoti 定義了數(shù)據(jù)表的結(jié)構(gòu)风宁。如何使用可以參考 SQLite.swift 官方的指南,里面講得非常詳細(xì)清楚蛹疯。
用 DB Browser for SQLite 應(yīng)用可以查看本地的數(shù)據(jù)庫戒财。下面是用它查看記錄的 RSS 的數(shù)據(jù),如圖:
更新未讀數(shù)的判斷邏輯捺弦,我封到了一個(gè)函數(shù)里饮寞,代碼如下:
func updateDBDevsInfo(ems: [EventModel]) {
do {
if let f = try DevsNotiDataHelper.find(sLogin: userName) {
var i = 0
var lrid = f.lastReadId
for em in ems {
if i == 0 {
lrid = em.id
}
if em.id == f.lastReadId {
break
}
i += 1
}
i = f.unRead + i
do {
let _ = try DevsNotiDataHelper.update(i: DBDevNoti(login: userName, lastReadId: lrid, unRead: i))
} catch {}
} // end if let f
} catch {}
} // end func updateDBDevsInfo
如上面代碼所示,入?yún)?ems 是獲取到的最新數(shù)據(jù)列吼,先從本地?cái)?shù)據(jù)庫中取到上次最新的閱讀編號 lastReadId幽崩,迭代 ems,如果第一個(gè) ems 的編號就和本地?cái)?shù)據(jù)庫 lastReadId 一樣寞钥,那表示無新動(dòng)態(tài)歉铝,如果沒有就開始計(jì)數(shù),直到找到相等的 lastReadId 位置凑耻,記了多少數(shù)就表示有多少新動(dòng)態(tài)太示。
提醒
列表、Sidebar 還有 macOS 系統(tǒng)的 Dock 上都可以顯示新狀態(tài)數(shù)的提醒香浩。列表和 Sidebar 直接使用 .badge ViewModifier 就可以展示未讀數(shù)了类缤,效果如下:
Dock 欄提示設(shè)置需要用到系統(tǒng)的 NSApp,代碼如下:
NSApp.dockTile.showsApplicationBadge = true
NSApp.dockTile.badgeLabel = "\(count)"
小冊子里還可以查看 Swift 社區(qū)里博主們博客更新動(dòng)態(tài)邻吭。我接著跟你說說我怎么做的餐弱。
博客 RSS 更新動(dòng)態(tài)
博客 RSS 的數(shù)據(jù)獲取我在前面基礎(chǔ)網(wǎng)絡(luò)能力中已經(jīng)說了。所有解析邏輯我都寫在了工程 RSSReader/Parser/ 目錄下的 ParseStandXMLTagTokens.swift、ParseStandXMLTags.swift膏蚓、ParseStandXML.swift 三個(gè)文件中瓢谢,實(shí)現(xiàn)思路我在先前《如何對 iOS 啟動(dòng)階段耗時(shí)進(jìn)行分析》文章的“優(yōu)化后如何保持?”章節(jié)有詳細(xì)說明驮瞧。
根據(jù) RSS 的 XML 結(jié)構(gòu)氓扛,定義 Model 結(jié)構(gòu)如下:
struct RSSModel: Identifiable {
var id = UUID()
var title = ""
var description = ""
var feedLink = ""
var siteLink = ""
var language = ""
var lastBuildDate = ""
var pubDate = ""
var items = [RSSItemModel]()
var unReadCount = 0
}
struct RSSItemModel: Identifiable {
var id = UUID()
var guid = ""
var title = ""
var description = ""
var link = ""
var pubDate = ""
var content = ""
var isRead = false
}
根據(jù)這個(gè)結(jié)構(gòu),也會在本地?cái)?shù)據(jù)庫設(shè)計(jì)對應(yīng)的兩個(gè)表论笔,兩個(gè)表的增刪改代碼分別在 DBRSSFeed.swift 和 DBRSSItems.swift 里采郎。表的結(jié)構(gòu)和 Model 的結(jié)構(gòu)基本一致,方便內(nèi)存和磁盤進(jìn)行切換狂魔。更新提醒邏輯和前面說的開發(fā)者動(dòng)態(tài)更新邏輯區(qū)別在于蒜埋,RSS 使用 isRead 標(biāo)記有沒有閱讀過,直接在本地?cái)?shù)據(jù)里 count 出 isRead 字段值為 false 的數(shù)量就是需要提醒的數(shù)最楷。
新 RSS 的添加會先在本地?cái)?shù)據(jù)庫中查找是否有存在整份,依據(jù)的是文章的 url,如果不存在就會添加到數(shù)據(jù)庫中設(shè)置為未讀作為提醒籽孙。
RSS 里文章的內(nèi)容是 HTML皂林,顯示內(nèi)容使用的是 WebKit 庫,要在 SwiftUI 中使用蚯撩,需要封裝下础倍,代碼如下:
import SwiftUI
import WebKit
struct WebUIView : NSViewRepresentable {
let html: String
func makeNSView(context: Context) -> some WKWebView {
return WKWebView()
}
func updateNSView(_ nsView: NSViewType, context: Context) {
nsView.loadHTMLString(html, baseURL: nil)
}
}
效果如下圖:
云打包
工程如果是本地編譯,在 SwiftPamphletAppConfig.swift 的 gitHubAccessToken 中添上 token 就可以了胎挎,如果想快速打包使用小冊子沟启,使用 Github Action Workflow 編譯,無需在本地操作犹菇、也無需開啟 Xcode 設(shè)置個(gè)人開發(fā)帳號德迹,只需設(shè)置 personal access token(PAT) 在 repository 設(shè)定中 action secrets,并命名為 PAT揭芍。Frok 此 repository胳搞,設(shè)置 PAT,手動(dòng)啟用 action称杨,等候約3分鐘即可下載檔案肌毅,往后專案更新時(shí),只需 fetch and merge姑原,action 會自動(dòng)進(jìn)行悬而。非常感謝社區(qū) @powenn 開發(fā)的這個(gè) Github Action。
推薦可以學(xué)習(xí)的開源倉庫
為了避免閉門造車锭汛,可以多關(guān)注些開源項(xiàng)目笨奠,以下這些倉庫是我放在小冊子里可以關(guān)注到更新動(dòng)態(tài)的項(xiàng)目袭蝗,這里作為附錄列下,也可以直接在小冊子里查看般婆。除了 Swift 也有些非常有趣的項(xiàng)目到腥,希望可以豐富到你的開發(fā)生活。
好庫
官方
- swift
- swift-evolution 提案
- llvm-project 編譯器
新鮮事
- iOS-Weekly 老司機(jī) iOS 周報(bào)
- awesome-swift
- SwiftPamphletApp 戴銘的開發(fā)小冊子
封裝易用功能
- SwifterSwift Handy Swift extensions
網(wǎng)絡(luò)
圖片
文字處理
動(dòng)畫
持久化存儲
編程范式
- RxSwift 函數(shù)響應(yīng)式編程
- swift-composable-architecture
- awesome-ios-architecture
路由
靜態(tài)檢查
系統(tǒng)能力
接口
macOS程序
- open-source-mac-os-apps 開源 macOS 程序合集
- NetNewsWire
- TelegramSwift
性能和工程構(gòu)建
- tuist 創(chuàng)建和維護(hù) Xcode projects 文件
- vscode-swift VSCode 的 Swift 擴(kuò)展
音視頻
- iina
- HaishinKit.swift RTMP, HLS
- AudioKit
服務(wù)器
探索庫
SwiftUI擴(kuò)展
- SwiftUIX 擴(kuò)展 SwiftUI
- SDWebImageSwiftUI
- ASCollectionView SwiftUI collection
- SwiftUI-Introspect SwiftUI 引入 UIKit
- SwiftUIKitView 在 SwiftUI 中 使用 UIKit
接口應(yīng)用
- Weather 天氣應(yīng)用
- MovieSwiftUI 電影 MovieDB 應(yīng)用
- NotionSwift
- RedditOS SwiftUI 寫的 Reddit客戶端
- reddit-swiftui SwiftUI 寫的 Reddit客戶端
- SwiftHN Hacker News 閱讀
- EhPanda
- MortyUI GraphQL + SwiftUI 開發(fā)的瑞克和莫蒂應(yīng)用
- V2ex-Swift V2EX 客戶端
- iOS V2EX 客戶端
- weibo_ios_sdk
- MNWeibo Swift5 + MVVM 微博客戶端
- octokit.swift Swift API Client for GitHub
- GitHawk iOS app for GitHub
- free-api
- Graphaello SwiftUI 中使用 GraphQL 的工具
- tmdb GraphQL 包裝電影數(shù)據(jù)接口
macOS
- FileWatcher macOS 上監(jiān)聽文件變化
- XcodeCleaner-SwiftUI 清理 Xcode
- eul SwiftUI 寫的 macOS 狀態(tài)監(jiān)控工具
- ACHNBrowserUI SwiftUI 寫的動(dòng)物之森小助手程序
- RegExPlus 正則表達(dá)式
應(yīng)用
- Clendar SwiftUI 寫的日歷應(yīng)用
游戲
- isowords 單詞搜索游戲
- awesome-games-of-coding 教你學(xué)編程的游戲收集
- OpenEmu 視頻游戲模擬器
- swiftui-2048
- gb-studio 拖放式復(fù)古游戲創(chuàng)建器
新技術(shù)展示
- Moments-SwiftUI SwiftUI蔚袍、Async乡范、Actor
新鮮事
- weekly 科技愛好者周刊
聚合
- chinese-independent-blogs
- awesome-swiftui
- SwiftUI
- GitHub-Chinese-Top-Charts GitHub中文排行榜
- awesome-swiftui
- About-SwiftUI 匯總 SwiftUI 的資料
知識管理
- logseq 更好的知識管理工具
性能和工程構(gòu)建
- periphery 檢測 Swift 無用代碼
- ViewInspector SwiftUI Runtime introspection 和 單元測試
網(wǎng)絡(luò)
- Knot 使用 SwiftNIO 實(shí)現(xiàn) HTTPS 抓包
- async-http-client 使用 SwiftNIO 開發(fā)的 HTTP 客戶端
- Get
- awesome-selfhosted 網(wǎng)絡(luò)服務(wù)及上面的應(yīng)用
- Starscream WebSocket
- ShadowsocksX-NG
- swift-request 聲明式的網(wǎng)絡(luò)請求
圖形
- SwiftSunburstDiagram SwiftUI 圖表
系統(tǒng)
- swift-project1 Swift編寫內(nèi)核,可在 Mac 和 PC 啟動(dòng)
Apple
- swift-corelibs-foundation
- swift-package-manager
- swift-markdown
- sourcekit-lsp
- swift-nio
- swift-syntax 解析页响、生成篓足、轉(zhuǎn)換 Swift 代碼
- swift-crypto CryptoKit 的開源實(shí)現(xiàn)
待分類
- public-apis
- WWDC
- Actions
- the-book-of-secret-knowledge
- awesome-math
- AltSwiftUI 類 SwiftUI
- ios-stack-kit 類 SwiftUI
- OpenCombine Combine 的開源實(shí)現(xiàn)
- CombineExt 對 Combine 的補(bǔ)充
- ReSwift 單頁面狀態(tài)和數(shù)據(jù)管理
- DeviceKit UIDevice 易用封裝
- SwiftCharts
- FileKit 文件操作
- Files 文件操作
- PathKit 文件操作
- Publish 靜態(tài)站點(diǎn)生成器
- IceCream CloudKit 同步 Realm 數(shù)據(jù)庫
- RichTextView
- edhita
- MarkdownView
- Down fast Markdown
- SwiftDown Swift 寫的可換主題的 Markdown 編輯器組件
- Komondor Git Hooks for Swift projects
- SwiftGen 代碼生成
- netfox 獲取所有網(wǎng)絡(luò)請求
- iOS-Developer-Roadmap
- ios-oss
- WordPress-iOS
- VersaPlayer
- firefox-ios
- PostgresApp
- Moya
- BlueSocket
- BluetoothKit
- BiometricAuthentication FaceID or TouchID authentication
- CryptoSwift
- Advance Physics-based animations
- Spring 動(dòng)畫
- UIImageColors 獲取圖片主次顏色
- GPUImage3 Metal 實(shí)現(xiàn)
- Macaw SVG
- Magnetic SpriteKit氣泡支持SwiftUI
- Swift-Radio-Pro 電臺應(yīng)用
- SKPhotoBrowser 圖片瀏覽
- swift-algorithm-club
- Cache
- SwiftyUserDefaults
- MonitorControl 亮度和聲音控制
- Commander 命令行
- ReactiveCocoa
- Carthage
- Charts
- Quick 測試框架
- ijkplayer 播放器
- dosbox-pure DOS 游戲模擬器
- HackingWithSwift 示例代碼
- fsnotes
- CotEditor
- JKSwiftExtension Swift常用擴(kuò)展段誊、組件闰蚕、協(xié)議
- iOS-Nuts-And-Bolts
- ExtensionKit
- publish 用 swift 來寫網(wǎng)站
- awesome-software-architecture 軟件架構(gòu)
- hacker-scripts 程序員的活都讓機(jī)器干的腳本(真實(shí)故事)
- clean-architecture-swiftui 干凈完整的SwiftUI+Combine例子,包含網(wǎng)絡(luò)和單元測試等
- CareKit 使用 SwiftUI 開發(fā)健康相關(guān)的庫
- awesome-result-builders Result Builders
- SwiftSpeech 蘋果語言識別封裝庫连舍,已適配 SwiftUI
- NextLevel 相機(jī)
- MaLiang 基于 Metal 的涂鴉繪圖庫
- awesome-blockchain-cn 區(qū)塊鏈 awesome
- XcodesApp Xcode 多版本安裝
- Mocker Mock Alamofire and URLSession
- ReduxUI SwiftUI Redux 架構(gòu)
- 5GUIs 可以分析程序用了哪些庫没陡,用了LLVM objdump
- episode-code-samples
- PackageList
- awesome 內(nèi)容廣
- open-source-ios-apps 開源的完整 App 例子
- Model3DView 毫不費(fèi)力的使用 SwiftUI 渲染 3d models
- ios-crash-dump-analysis-book iOS Crash Dump Analysis Book
- SVGView 支持 SwiftUI 的 SVG 解析渲染視圖
- Sourcery Swift 元編程
- TIL 學(xué)習(xí)筆記
- ipatool 下載 ipa
- Ink Markdown 解析器