如何用 SwiftUI绪氛、Combine、Swift Concurrency Aysnc Await Actor 歡暢開發(fā)

先說兩句廢話(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)容:

image
image

完整內(nèi)容見:https://mp.weixin.qq.com/s/b5fj2b65xRv4mhFpftwNcg

視頻可見這條微博地址:https://weibo.com/1351051897/KEdu5Fi1x?pagetype=profilefeed

視頻有六十多萬播放量车份,兩百多評論和一千多轉(zhuǎn)發(fā)。

image.png

話題還上了微博熱搜牡彻,有六百多萬閱讀和三千多討論扫沼。

image

你肯定會覺得很奇怪,我怎么會接受時(shí)尚雜志采訪庄吼,其實(shí)我早在2006年就跟時(shí)尚娛樂圈有染了缎除,那年張紀(jì)中版《神雕俠侶》剛熱播完,劉亦菲演的小龍女总寻,我特別的喜歡伴找。有幸在一次活動(dòng)中我成為她的御用攝影師,由于過于激動(dòng)手抖废菱,拍糊了好多張技矮,蠻可惜的抖誉。私存這批里還是有些清晰的,這些照片最近在找資料時(shí)不小心被我翻了出來衰倦。挑幾張看看十六年前的劉亦菲和我是什么樣的吧袒炉。

image
image
image
image

我還很用心的置辦了新家。也是希望能夠讓自己能夠開心些樊零。

image
image.png
image
image
image
image
image

那么我磁,怎樣高效開發(fā),帶來愉悅的呢驻襟?

看看做出來的樣子

這是個(gè) macOS 應(yīng)用(https://github.com/ming1016/SwiftPamphletApp)《戴銘的開發(fā)小冊子》夺艰,能夠方便的查看 Swift 語法,還有一些主要庫的使用指南沉衣,內(nèi)容還在完善中郁副,選擇的庫主要就是開發(fā)小冊子應(yīng)用使用到的 SwitUI、Combine豌习、Swift Concurrency存谎。

image

除了這些速查和庫的使用內(nèi)容外,這個(gè)應(yīng)用還有一些開發(fā)者的動(dòng)態(tài)肥隆,當(dāng)他們有新的動(dòng)作既荚,比如提交了代碼、star 了什么項(xiàng)目栋艳,提交和留言了議題都會直接在程序塢中提醒你恰聘。

image

我對一些庫做了分類,方便按需查找吸占,庫有新的提交也會在程序塢中提醒晴叨。

image

還能方便的查看庫的議題。比如在阮一峰的《科技愛好者周刊》的議題中可以看到有很多人推薦和自薦了一些信息旬昭。保留議題有一千六百多個(gè)篙螟。

image

這個(gè)元旦假期菌湃,我又添加了博客動(dòng)態(tài)的功能问拘,可以跟進(jìn)一些博客內(nèi)容的更新。

image

由于 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 寫的布局界面效果如下:

image

界面主體是 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)效果如下圖:

image

指南內(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ā)者介紹信息頁面如下:

image

界面中的數(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 代碼就好了万俗。

image

原推見 https://twitter.com/fcbunn/status/1259078251340800000湾笛。

開發(fā)者接受事件和事件類似,只是會多顯示事件的 actor 字段內(nèi)容闰歪,表明開發(fā)者接受的是誰發(fā)出的事件嚎研。事件界面如下所示:

image

倉庫整體處理和開發(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ù),如圖:

image

更新未讀數(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ù)了类缤,效果如下:

image

Dock 欄提示設(shè)置需要用到系統(tǒng)的 NSApp,代碼如下:

NSApp.dockTile.showsApplicationBadge = true
NSApp.dockTile.badgeLabel = "\(count)"
image

小冊子里還可以查看 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)
    }
}

效果如下圖:

image

云打包

工程如果是本地編譯,在 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。

image
image

推薦可以學(xué)習(xí)的開源倉庫

為了避免閉門造車锭汛,可以多關(guān)注些開源項(xiàng)目笨奠,以下這些倉庫是我放在小冊子里可以關(guān)注到更新動(dòng)態(tài)的項(xiàng)目袭蝗,這里作為附錄列下,也可以直接在小冊子里查看般婆。除了 Swift 也有些非常有趣的項(xiàng)目到腥,希望可以豐富到你的開發(fā)生活。

好庫

官方

新鮮事

封裝易用功能

網(wǎng)絡(luò)

圖片

文字處理

動(dòng)畫

持久化存儲

編程范式

路由

靜態(tài)檢查

系統(tǒng)能力

接口

macOS程序

性能和工程構(gòu)建

  • tuist 創(chuàng)建和維護(hù) Xcode projects 文件
  • vscode-swift VSCode 的 Swift 擴(kuò)展

音視頻

服務(wù)器

探索庫

SwiftUI擴(kuò)展

接口應(yīng)用

macOS

應(yīng)用

  • Clendar SwiftUI 寫的日歷應(yīng)用

游戲

新技術(shù)展示

新鮮事

  • weekly 科技愛好者周刊

聚合

知識管理

  • logseq 更好的知識管理工具

性能和工程構(gòu)建

網(wǎng)絡(luò)

圖形

系統(tǒng)

  • swift-project1 Swift編寫內(nèi)核,可在 Mac 和 PC 啟動(dòng)

Apple

待分類

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市索赏,隨后出現(xiàn)的幾起案子盼玄,更是在濱河造成了極大的恐慌,老刑警劉巖潜腻,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件埃儿,死亡現(xiàn)場離奇詭異,居然都是意外死亡融涣,警方通過查閱死者的電腦和手機(jī)童番,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來威鹿,“玉大人剃斧,你說我怎么就攤上這事『瞿悖” “怎么了幼东?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長科雳。 經(jīng)常有香客問我根蟹,道長,這世上最難降的妖魔是什么糟秘? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任娜亿,我火速辦了婚禮,結(jié)果婚禮上蚌堵,老公的妹妹穿的比我還像新娘买决。我一直安慰自己沛婴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布督赤。 她就那樣靜靜地躺著嘁灯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪躲舌。 梳的紋絲不亂的頭發(fā)上丑婿,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音没卸,去河邊找鬼羹奉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛约计,可吹牛的內(nèi)容都是我干的诀拭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼煤蚌,長吁一口氣:“原來是場噩夢啊……” “哼耕挨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起尉桩,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤筒占,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蜘犁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翰苫,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年这橙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了奏窑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡析恋,死狀恐怖良哲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情助隧,我是刑警寧澤筑凫,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站并村,受9級特大地震影響巍实,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜哩牍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一棚潦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧膝昆,春花似錦丸边、人聲如沸叠必。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纬朝。三九已至,卻和暖如春骄呼,著一層夾襖步出監(jiān)牢的瞬間共苛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工蜓萄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留隅茎,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓嫉沽,卻偏偏與公主長得像辟犀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子耻蛇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容