前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从零开始的 Swift UI (二)

从零开始的 Swift UI (二)

作者头像
Innei
发布2021-12-28 11:01:59
1.4K0
发布2021-12-28 11:01:59
举报
文章被收录于专栏:静之森

接上文:

从零开始的 Swift UI (一)

在上一篇文章中,我们完成了 HomeView 的基本布局。接下来我们来编写一下数据层(Model ViewModel)。

大概包括两个方面:数据的获取(JSON URLSession) 和 UI ViewModel 的数据同步。

数据的获取

首先我们使用的 Api 是

Hikotoko

。随机获取一条 Hikotoko 的 JSON 如下。

json

代码语言:javascript
复制
1{
2"id": 5716,
3"uuid": "71396790-6d06-49dd-bc72-2568311cdd7b",
4"hitokoto": "粗缯大布裹生涯,腹有诗书气自华。",
5"type": "i",
6"from": "和董传留别",
7"from_who": "苏轼",
8"creator": "a632079",
9"creator_uid": 1044,
10"reviewer": 4756,
11"commit_from": "web",
12"created_at": "1586333487",
13"length": 16
14}

COPY

使用工具 JSON2Swift 将 JSON Model 转化为 Swift Struct。工具推荐使用:

https://app.quicktype.io/

右侧选项根据需要修改。仅参考。

使用此工具的好处是,他把 URLSession 也自动构建好了。并给出了实例。

新建一个 Swift 文件,命名为 Model.swift 将生成的代码复制到新文件。

再新建一个 Swift 文件,命名为 ViewModel.swift,写入以下代码。

swift

代码语言:javascript
复制
1import Foundation
2
3class HitokotoViewModel {
4    static func fetch(completion: @escaping (HitokotoModel) -> Void) {
5        let task = URLSession.shared.hitokotoModelTask(with: URL(string: "https://v1.hitokoto.cn/")!) { hitokotoModel, _, _ in
6            if let hitokotoModel = hitokotoModel {
7                DispatchQueue.main.async {
8                    completion(hitokotoModel)
9                }
10            }
11        }
12
13        task.resume()
14    }
15}

COPY

在 HomeView 中调用此方法。修改 HomeView 的代码为

swift

代码语言:javascript
复制
1//
2//  HomeView.swift
3//  Meet
4//
5//  Created by Innei on 2020/12/28.
6//
7
8import SwiftUI
9
10struct HomeView: View {
11    @State var model: HitokotoModel? = nil
12
13    func fetch() {
14        HitokotoViewModel.fetch {
15            self.model = $0
16        }
17    }
18
19    var body: some View {
20        GeometryReader { reader in
21            ZStack {
22                VStack {
23                    Text(model?.hitokoto ?? "")
24                        .foregroundColor(.blue)
25                        .padding(.vertical)
26
27                    HStack {
28                        Spacer()
29
30                        Text(model?.creator ?? "")
31                    }
32                }.padding()
33
34                ActionView().offset(x: 0, y: reader.size.height / 2 - 50)
35
36                Button(action: {
37                    fetch()
38                }, label: {
39                    CircleButtonShape(systemImage: "arrow.clockwise")
40                })
41                    .position(x: reader.size.width - 50, y: reader.size.height - 50)
42            }
43            .onAppear {
44                fetch()
45            }
46        }
47    }
48}
49
50struct HomeView_Previews: PreviewProvider {
51    static var previews: some View {
52        HomeView()
53    }
54}
55
56struct CircleButtonShape: View {
57    var systemImage: String
58    var color: Color = .pink
59    var body: some View {
60        ZStack {
61            Circle()
62                .fill(color)
63                .frame(width: 50, height: 50, alignment: .center)
64                .shadow(radius: 3)
65            Image(systemName: systemImage).foregroundColor(.white)
66        }
67    }
68}
69
70struct ActionView: View {
71    @State var liked = false
72
73    @ViewBuilder
74    var body: some View {
75        HStack(spacing: 20) {
76            Button(action: {
77            }, label: {
78                Image(systemName: liked ? "suit.heart.fill" : "suit.heart")
79                    .foregroundColor(liked ? .red : .primary)
80                    .font(.custom("icon", size: 28))
81            })
82            Button(action: {
83            }, label: {
84                Image(systemName: "square.and.arrow.up")
85                    .font(.custom("icon", size: 28))
86                    .foregroundColor(.primary)
87            })
88        }
89    }
90}

COPY

效果已经有了,但是没有加载完成时(受限于网络,弱网),会出现一片空白。如果未加载完成时,显示加载中.. 可能会比较好。

在未加载完成时,modelnil ,那么只需要判断是不是 nil 就行了。我本来想用 Group 包裹 if 判断语句实现。理论上是可行的,但是由于 Groupif 不支持使用 Stack 包裹。出现如下报错。

换一种方法。转而使用 @ViewBuilder,首先提取组件。在这个 struct 里新增一个 some View

swift

代码语言:javascript
复制
1 @ViewBuilder
2 var Preview: some View {
3        if let model = model {
4            VStack {
5                Text(model.hitokoto ?? "")
6                    .foregroundColor(.blue)
7                    .padding(.vertical)
8
9                HStack {
10                    Spacer()
11
12                    Text(model.creator ?? "")
13                }
14            }
15        } else {
16            Text("加载中")
17        }
18    }

COPY

然后在 body 的合适地方替换成。

swift

代码语言:javascript
复制
1ZStack {
2    Preview
3  
4  // ....
5}

COPY

响应式数据流

接下来我们实现保存 Hikotoko 到 喜欢。我们需要用到本地存储和响应式数据流。

本地存储可以使用 UserDefaults,响应式数据流使用 ObservableObject

新建一个 Swift 文件,命名为 Like.swift

swift

代码语言:javascript
复制
1import Foundation
2
3class Like: ObservableObject {
4    @Published var likes: [LikeModel] = []
5
6    public var codable: [LikeModel] {
7        likes
8    }
9
10    init() {
11       
12    }
13
14    func has(item: LikeModel) -> Int? {
15        return likes.firstIndex(where: { $0.id == item.id })
16    }
17
18    func add(item: LikeModel) -> Bool {
19        if has(item: item) != nil {
20            return false
21        } else {
22            likes.append(item)
23            return true
24        }
25    }
26
27    func remove(item: LikeModel) -> LikeModel? {
28        let id = item.id
29        if let index = likes.firstIndex(where: { $0.id == id }) {
30            let element = likes[index]
31            likes.remove(at: index)
32            return element
33        } else {
34            return nil
35        }
36    }
37    
38    func remove(uuid: UUID) -> LikeModel? {
39        let id = uuid
40        if let index = likes.firstIndex(where: { $0.id == id }) {
41            let element = likes[index]
42            likes.remove(at: index)
43            return element
44        } else {
45            return nil
46        }
47    }
48
49    func removeAll() {
50        likes.removeAll()
51    }
52}

COPY

使用 ObservableObject protocol 使得一个对象成为可被观察的,当被装饰 @Published 的属性改变时,会触发 UIView 更新。

在 MeetApp.swift 中挂载 LikeenvironmentObject。增加如下代码。

git

代码语言:javascript
复制
1@main
2struct MeetApp: App {
3    @State var activeTabIndex = 0
4
5+    let like = Like()
6
7    var body: some Scene {
8        WindowGroup {
9            TabView(selection: $activeTabIndex) {
10                ContentView().tabItem {
11                    Label("遇见", systemImage: activeTabIndex != 0 ? "circle" : "largecircle.fill.circle")
12                        .onTapGesture {
13                            activeTabIndex = 0
14                        }
15                }
16                .tag(0)
17
18                LikeView().tabItem {
19                    Label("喜欢", systemImage: activeTabIndex != 1 ? "heart.circle" : "heart.circle.fill")
20                        .onTapGesture {
21                            activeTabIndex = 1
22                        }
23                }
24                .tag(1)
25            }
26            .accentColor(.pink)
27+           .environmentObject(like)
28        }
29    }
30}

COPY

在 HomeView 中,ActionView 中的 Like Button,修改 action 为

swift

代码语言:javascript
复制
1if like.has(uuid: UUID(uuidString: model.uuid)) {
2                        if let uuid = UUID(uuidString: model.uuid) {
3                            like.remove(uuid: uuid)
4                        }
5
6                    } else {
7                        like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))
8                    }

COPY

在顶部增加

swift

代码语言:javascript
复制
1 @EnvironmentObject var like: Like

COPY

完整如下

swift

代码语言:javascript
复制
1struct ActionView: View {
2    @EnvironmentObject var like: Like
3
4    @ViewBuilder
5    var body: some View {
6        if let model = model {
7            HStack(spacing: 20) {
8                Button(action: {
9                    if like.has(uuid: UUID(uuidString: model.uuid)) {
10                        if let uuid = UUID(uuidString: model.uuid) {
11                            like.remove(uuid: uuid)
12                        }
13
14                    } else {
15                        like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))
16                    }
17
18                }, label: {
19                    Image(systemName: "suit.heart")
20                        .foregroundColor(.primary)
21                        .font(.custom("icon", size: 28))
22                })
23                Button(action: {
24
25                }, label: {
26                    Image(systemName: "square.and.arrow.up")
27                        .font(.custom("icon", size: 28))
28                        .foregroundColor(.primary)
29                })
30            }
31        }
32    }
33}

COPY

装饰了 @EnvironmentObject 的属性会自动获取上层 View 挂载的 environmentObject,不需要层层传递。类似 React 中的 Context

数据的存储

Like.swift 中新建一个 Class,代码如下。

swift

代码语言:javascript
复制
1class Store {
2    private(set) static var userDefaults = UserDefaults()
3
4    public static let storeKey = "like-list"
5
6    public static func refreshStore(_ like: Like) {
7
8        if let data = try? PropertyListEncoder().encode(like.codable) {
9            userDefaults.set(data, forKey: storeKey)
10        }
11    }
12}

COPY

我们使用 refreshStore 方法把 Like 中 likes 数据保存到本地数据中。因为 likes 不是普通的 Array,所以不能直接使用 Userdefaults.set() 的方法写入,否则会 runtime crash。首先使用 PropertyListEncoder 将数据序列化。在此之前,请注意 LikeModel 实现了 Codable Protocol。

同样在 Like init 的时候读取本地保存的数据。当然也需要先反序列化数据。

swift

代码语言:javascript
复制
1init() {
2        if let data = Store.userDefaults.data(forKey: Store.storeKey) {
3            let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)
4            likes = stored.map { $0 }
5        }
6    }

COPY

在修改 likes 后,同时写入到本地数据。可以使用 didSet 计算属性很容易完成。修改 likes 属性为。

swift

代码语言:javascript
复制
1@Published var likes: [LikeModel] = [] {
2    didSet {
3        Store.refreshStore(self)
4    }
5}

COPY

之后完整的 Like.swift 如下:

swift

代码语言:javascript
复制
1//
2//  Like.swift
3//  Meet
4//
5//  Created by Innei on 2020/12/27.
6//
7
8import Foundation
9
10class Like: ObservableObject {
11    @Published var likes: [LikeModel] = [] {
12        didSet {
13            Store.refreshStore(self)
14        }
15    }
16
17    public var codable: [LikeModel] {
18        likes
19    }
20
21    init() {
22        if let data = Store.userDefaults.data(forKey: Store.storeKey) {
23            let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)
24            likes = stored.map { $0 }
25        }
26    }
27
28    func has(item: LikeModel) -> Int? {
29        return likes.firstIndex(where: { $0.id == item.id })
30    }
31
32    func has(uuid: UUID?) -> Bool {
33        guard let uuid = uuid else { return false }
34        return likes.first { $0.id == uuid } != nil
35    }
36
37    func add(item: LikeModel) -> Bool {
38        if has(item: item) != nil {
39            return false
40        } else {
41            likes.append(item)
42//            Store.refreshStore()
43            return true
44        }
45    }
46
47    func remove(item: LikeModel) -> LikeModel? {
48        let id = item.id
49        if let index = likes.firstIndex(where: { $0.id == id }) {
50            let element = likes[index]
51            likes.remove(at: index)
52            return element
53        } else {
54            return nil
55        }
56    }
57
58    func remove(uuid: UUID) -> LikeModel? {
59        let id = uuid
60        if let index = likes.firstIndex(where: { $0.id == id }) {
61            let element = likes[index]
62            likes.remove(at: index)
63            return element
64        } else {
65            return nil
66        }
67    }
68
69    func removeAll() {
70        likes.removeAll()
71    }
72}
73
74class Store {
75    private(set) static var userDefaults = UserDefaults()
76
77    public static let storeKey = "like-list"
78
79    public static func refreshStore(_ like: Like) {
80
81        if let data = try? PropertyListEncoder().encode(like.codable) {
82            userDefaults.set(data, forKey: storeKey)
83        }
84    }
85}

COPY

下一篇文章,将构建 LikeView。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-12-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 数据的获取
  • 响应式数据流
  • 数据的存储
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档