diff --git a/apps/core/package.json b/apps/core/package.json index a935d098..bc542081 100644 --- a/apps/core/package.json +++ b/apps/core/package.json @@ -1,7 +1,7 @@ { "name": "@mx-space/core", "version": "5.7.9", - "author": "Innei ", + "author": "Innei ", "private": true, "license": "AGPLv3", "dashboard": { @@ -167,4 +167,4 @@ "mongodb-memory-server": "^9.1.5", "redis-memory-server": "^0.10.0" } -} +} \ No newline at end of file diff --git a/apps/core/src/common/guards/spider.guard.ts b/apps/core/src/common/guards/spider.guard.ts index 30e66d16..a7ae0b2a 100644 --- a/apps/core/src/common/guards/spider.guard.ts +++ b/apps/core/src/common/guards/spider.guard.ts @@ -1,7 +1,7 @@ /** * @module common/guard/spider.guard * @description 禁止爬虫的守卫 - * @author Innei + * @author Innei */ import { ForbiddenException, Injectable } from '@nestjs/common' diff --git a/apps/core/src/common/interceptors/cache.interceptor.ts b/apps/core/src/common/interceptors/cache.interceptor.ts index b0eb68b4..55b35112 100644 --- a/apps/core/src/common/interceptors/cache.interceptor.ts +++ b/apps/core/src/common/interceptors/cache.interceptor.ts @@ -3,7 +3,7 @@ * @file 缓存拦截器 * @module interceptor/cache * @author Surmon - * @author Innei + * @author Innei */ import { of, tap } from 'rxjs' diff --git a/apps/core/src/modules/comment/comment.email.default.ts b/apps/core/src/modules/comment/comment.email.default.ts index dec2de24..200f7409 100644 --- a/apps/core/src/modules/comment/comment.email.default.ts +++ b/apps/core/src/modules/comment/comment.email.default.ts @@ -46,7 +46,7 @@ export const baseRenderProps = Object.freeze({ mail: defaultCommentModelForRenderProps.mail, text: defaultCommentModelForRenderProps.text, ip: defaultCommentModelForRenderProps.ip, - link: 'https://innei.ren/note/122#comments-37ccbeec9c15bb0ddc51ca7d' as string, + link: 'https://innei.in/note/122#comments-37ccbeec9c15bb0ddc51ca7d' as string, time: dayjs().format('YYYY/MM/DD'), title: defaultPostModelForRenderProps.title, diff --git a/apps/core/test/src/modules/link/link.controller.e2e-spec.ts b/apps/core/test/src/modules/link/link.controller.e2e-spec.ts index 930d442f..2c72fe51 100644 --- a/apps/core/test/src/modules/link/link.controller.e2e-spec.ts +++ b/apps/core/test/src/modules/link/link.controller.e2e-spec.ts @@ -3,7 +3,6 @@ import { gatewayProviders } from 'test/mock/modules/gateway.mock' import { userProvider } from 'test/mock/modules/user.mock' import { emailProvider } from 'test/mock/processors/email.mock' import { eventEmitterProvider } from 'test/mock/processors/event.mock' -import type { ReturnModelType } from '@typegoose/typegoose' import { ExtendedValidationPipe } from '~/common/pipes/validation.pipe' import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant' @@ -16,6 +15,7 @@ import { import { LinkModel, LinkState } from '~/modules/link/link.model' import { LinkService } from '~/modules/link/link.service' import { HttpService } from '~/processors/helper/helper.http.service' +import type { ReturnModelType } from '@typegoose/typegoose' describe('Test LinkController(E2E)', () => { const proxy = createE2EApp({ @@ -41,9 +41,9 @@ describe('Test LinkController(E2E)', () => { const linkModel = modelMap.get(LinkModel) ;(linkModel.model as ReturnModelType).create({ - url: 'https://innei.ren', + url: 'https://innei.in', name: 'innei', - avatar: 'https://innei.ren/avatar.png', + avatar: 'https://innei.in/avatar.png', description: 'innei', state: LinkState.Outdate, }) @@ -56,10 +56,10 @@ describe('Test LinkController(E2E)', () => { method: 'post', url: '/links/audit', payload: { - url: 'https://innei.ren', + url: 'https://innei.in', name: 'innnnn', author: 'innei', - avatar: 'https://innei.ren/avatar.png', + avatar: 'https://innei.in/avatar.png', description: 'innei', }, }) @@ -72,10 +72,10 @@ describe('Test LinkController(E2E)', () => { method: 'post', url: '/links/audit', payload: { - url: 'https://innei.ren', + url: 'https://innei.in', name: 'innnnn', author: 'innei', - avatar: 'https://innei.ren/avatar.png', + avatar: 'https://innei.in/avatar.png', description: 'innei', }, }) diff --git a/packages/api-client/__tests__/controllers/aggregate.test.ts b/packages/api-client/__tests__/controllers/aggregate.test.ts index 066e5fd7..f974f653 100644 --- a/packages/api-client/__tests__/controllers/aggregate.test.ts +++ b/packages/api-client/__tests__/controllers/aggregate.test.ts @@ -17,7 +17,7 @@ describe('test aggregate client', () => { id: '5ea4fe632507ba128f4c938c', introduce: '这是我的小世界呀', mail: 'i@innei.ren', - url: 'https://innei.ren', + url: 'https://innei.in', name: 'Innei', social_ids: { bili_id: 26578164, @@ -126,7 +126,7 @@ describe('test aggregate client', () => { url: { ws_url: 'https://api.innei.ren', server_url: 'https://api.innei.ren/v2', - web_url: 'https://innei.ren', + web_url: 'https://innei.in', }, }, ) diff --git a/packages/api-client/__tests__/controllers/link.test.ts b/packages/api-client/__tests__/controllers/link.test.ts index 6b33ad57..b5059c14 100644 --- a/packages/api-client/__tests__/controllers/link.test.ts +++ b/packages/api-client/__tests__/controllers/link.test.ts @@ -56,7 +56,7 @@ describe('test link client, /links', () => { const mocked = mockResponse('/links/5eaabe10cd5bca719652179d', { id: '5eaabe10cd5bca719652179d', name: '静かな森', - url: 'https://innei.ren', + url: 'https://innei.in', avatar: 'https://cdn.innei.ren/avatar.png', created: '2020-04-30T12:01:20.738Z', type: 0, diff --git a/packages/api-client/__tests__/controllers/user.test.ts b/packages/api-client/__tests__/controllers/user.test.ts index bd8d8fff..2df13ddd 100644 --- a/packages/api-client/__tests__/controllers/user.test.ts +++ b/packages/api-client/__tests__/controllers/user.test.ts @@ -12,7 +12,7 @@ describe('test user client', () => { id: '5ea4fe632507ba128f4c938c', introduce: '这是我的小世界呀', mail: 'i@innei.ren', - url: 'https://innei.ren', + url: 'https://innei.in', name: 'Innei', social_ids: { bili_id: 26578164, diff --git a/packages/api-client/__tests__/mock/algolia.json b/packages/api-client/__tests__/mock/algolia.json index 59ac1eb6..6b17e673 100644 --- a/packages/api-client/__tests__/mock/algolia.json +++ b/packages/api-client/__tests__/mock/algolia.json @@ -174,17 +174,19 @@ "raw": { "hits": [ { - "text": "接上文:[从零开始的 Swift UI (二)](https://innei.ren/posts/programming/swift-ui-meet_2)\n\n上篇文章介绍了如何使用 UserDefaults 和 ObserveableObject 来进行数据管理。\n\n这篇文章来完成 LikeView 的布局和功能实现。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609134619435.png)\n\n## Layout\n\n在 LikeView 中编写如下代码。\n\n```swift\nstruct LikeView: View {\n @EnvironmentObject var like: Like\n\n var likes: [LikeModel] {\n like.likes\n }\n\n var body: some View {\n ZStack {\n GeometryReader { _ in\n List {\n ForEach(likes) { like in\n Button(action: {}, label: { Text(like.text) })\n }\n }\n }\n }\n }\n}\n\n```\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609136864498.png)\n\n再修改 HomeView 中的 Like Button 代码。\n\n```swift\nButton(action: {\n like.add(hikotoko: model)\n}, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n})\n```\n\nLike.swift 中新增一个方法。\n\n```swift\nfunc add(hikotoko: HitokotoModel) -> Bool {\n let date = ISO8601DateFormatter().date(from: hikotoko.createdAt) ?? Date()\n\n return add(item: LikeModel(id: UUID(uuidString: hikotoko.uuid) ?? UUID(), text: hikotoko.hitokoto,\n createdAt: date, from: hikotoko.from, author: hikotoko.creator))\n}\n```\n\n上面 Like Button 时候被选中,可以根据 Like 中有没有存储判断。\n\n将 ActionView 修改为如下代码:\n\n```swift\nstruct ActionView: View {\n @Binding var model: HitokotoModel?\n @EnvironmentObject var like: Like\n\n var liked: Bool {\n guard let model = model else {\n return false\n }\n return like.has(uuid: UUID(uuidString: model.uuid))\n }\n\n @ViewBuilder\n var body: some View {\n if let model = model {\n HStack(spacing: 20) {\n Button(action: {\n like.add(hikotoko: model)\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n }\n}\n\n```\n\n`liked` 计算属性根据 model 中的 uuid 推断状态。因为使用了 `@Binding` 所以上层 View 还需要传一个 Binding 给他。可以理解为 React 中的 Props。注意的是 只有加了 `@Binding` 的参数传递才是引用传递,也就是上层数据更新后下层也会被更新。\n\n在 HomeView 中修改为 `ActionView(model: $model).offset(x: 0, y: reader.size.height / 2 - 50)`\n\n被 `@State` 装饰的属性,取他的 Binding 只需要在前面加一个 `$`\n\n这样点击 Like Button 后 ❤就会变红啦。\n\n## Navigation\n\n为了实现能在各个 View 之间导航。使用 NavigationView 就可以做到啦。\n\n修改 HomeView,在外层加上 NavigationView。\n\n修改 LikeView,在外层加上 NavigationView。\n\n```swift\nvar body: some View {\n NavigationView {\n ZStack {\n GeometryReader { _ in\n List {\n ForEach(likes) { like in\n Button(action: {}, label: { Text(like.text) })\n }\n }\n }\n }.navigationBarTitle(\"喜欢\")\n }\n}\n```\n\n注意在设定 `.navigationBarTitle` 必须加在 NavigationView 的子 View 上才会生效。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609137171258.png)\n\n接下来,调整一下 List 的 style,让 Item 撑满整个宽度。只需要使用内置的 `.listStyle(PlainListStyle())` 即可。\n\n其余知识点将通过小 Demo 描述。\n\n- Share\n- Sheet Modal\n\n完整 App:\n\n(完)", + "text": "接上文:[从零开始的 Swift UI (二)](https://innei.in/posts/programming/swift-ui-meet_2)\n\n上篇文章介绍了如何使用 UserDefaults 和 ObserveableObject 来进行数据管理。\n\n这篇文章来完成 LikeView 的布局和功能实现。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609134619435.png)\n\n## Layout\n\n在 LikeView 中编写如下代码。\n\n```swift\nstruct LikeView: View {\n @EnvironmentObject var like: Like\n\n var likes: [LikeModel] {\n like.likes\n }\n\n var body: some View {\n ZStack {\n GeometryReader { _ in\n List {\n ForEach(likes) { like in\n Button(action: {}, label: { Text(like.text) })\n }\n }\n }\n }\n }\n}\n\n```\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609136864498.png)\n\n再修改 HomeView 中的 Like Button 代码。\n\n```swift\nButton(action: {\n like.add(hikotoko: model)\n}, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n})\n```\n\nLike.swift 中新增一个方法。\n\n```swift\nfunc add(hikotoko: HitokotoModel) -> Bool {\n let date = ISO8601DateFormatter().date(from: hikotoko.createdAt) ?? Date()\n\n return add(item: LikeModel(id: UUID(uuidString: hikotoko.uuid) ?? UUID(), text: hikotoko.hitokoto,\n createdAt: date, from: hikotoko.from, author: hikotoko.creator))\n}\n```\n\n上面 Like Button 时候被选中,可以根据 Like 中有没有存储判断。\n\n将 ActionView 修改为如下代码:\n\n```swift\nstruct ActionView: View {\n @Binding var model: HitokotoModel?\n @EnvironmentObject var like: Like\n\n var liked: Bool {\n guard let model = model else {\n return false\n }\n return like.has(uuid: UUID(uuidString: model.uuid))\n }\n\n @ViewBuilder\n var body: some View {\n if let model = model {\n HStack(spacing: 20) {\n Button(action: {\n like.add(hikotoko: model)\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n }\n}\n\n```\n\n`liked` 计算属性根据 model 中的 uuid 推断状态。因为使用了 `@Binding` 所以上层 View 还需要传一个 Binding 给他。可以理解为 React 中的 Props。注意的是 只有加了 `@Binding` 的参数传递才是引用传递,也就是上层数据更新后下层也会被更新。\n\n在 HomeView 中修改为 `ActionView(model: $model).offset(x: 0, y: reader.size.height / 2 - 50)`\n\n被 `@State` 装饰的属性,取他的 Binding 只需要在前面加一个 `$`\n\n这样点击 Like Button 后 ❤就会变红啦。\n\n## Navigation\n\n为了实现能在各个 View 之间导航。使用 NavigationView 就可以做到啦。\n\n修改 HomeView,在外层加上 NavigationView。\n\n修改 LikeView,在外层加上 NavigationView。\n\n```swift\nvar body: some View {\n NavigationView {\n ZStack {\n GeometryReader { _ in\n List {\n ForEach(likes) { like in\n Button(action: {}, label: { Text(like.text) })\n }\n }\n }\n }.navigationBarTitle(\"喜欢\")\n }\n}\n```\n\n注意在设定 `.navigationBarTitle` 必须加在 NavigationView 的子 View 上才会生效。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609137171258.png)\n\n接下来,调整一下 List 的 style,让 Item 撑满整个宽度。只需要使用内置的 `.listStyle(PlainListStyle())` 即可。\n\n其余知识点将通过小 Demo 描述。\n\n- Share\n- Sheet Modal\n\n完整 App:\n\n(完)", "title": "从零开始的 Swift UI (三)", "id": "5fe97d1d5b11408f99ada0fd", "type": "post", "object_id": "5fe97d1d5b11408f99ada0fd", "highlight_result": { "text": { - "value": "接上文:[从零开始的 Swift UI (二)](https://innei.ren/posts/programming/swift-ui-meet_2)\n\n上篇文章介绍了如何使用 UserDefaults 和 ObserveableObject 来进行数据管理。\n\n这篇文章来完成 LikeView 的布局和功能实现。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609134619435.png)\n\n## Layout\n\n在 LikeView 中编写如下代码。\n\n```swift\nstruct LikeView: View {\n @EnvironmentObject var like: Like\n\n var likes: [LikeModel] {\n like.likes\n }\n\n var body: some View {\n ZStack {\n GeometryReader { _ in\n List {\n ForEach(likes) { like in\n Button(action: {}, label: { Text(like.text) })\n }\n }\n }\n }\n }\n}\n\n```\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609136864498.png)\n\n再修改 HomeView 中的 Like Button 代码。\n\n```swift\nButton(action: {\n like.add(hikotoko: model)\n}, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n})\n```\n\nLike.swift 中新增一个方法。\n\n```swift\nfunc add(hikotoko: HitokotoModel) -> Bool {\n let date = ISO8601DateFormatter().date(from: hikotoko.createdAt) ?? Date()\n\n return add(item: LikeModel(id: UUID(uuidString: hikotoko.uuid) ?? UUID(), text: hikotoko.hitokoto,\n createdAt: date, from: hikotoko.from, author: hikotoko.creator))\n}\n```\n\n上面 Like Button 时候被选中,可以根据 Like 中有没有存储判断。\n\n将 ActionView 修改为如下代码:\n\n```swift\nstruct ActionView: View {\n @Binding var model: HitokotoModel?\n @EnvironmentObject var like: Like\n\n var liked: Bool {\n guard let model = model else {\n return false\n }\n return like.has(uuid: UUID(uuidString: model.uuid))\n }\n\n @ViewBuilder\n var body: some View {\n if let model = model {\n HStack(spacing: 20) {\n Button(action: {\n like.add(hikotoko: model)\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n }\n}\n\n```\n\n`liked` 计算属性根据 model 中的 uuid 推断状态。因为使用了 `@Binding` 所以上层 View 还需要传一个 Binding 给他。可以理解为 React 中的 Props。注意的是 只有加了 `@Binding` 的参数传递才是引用传递,也就是上层数据更新后下层也会被更新。\n\n在 HomeView 中修改为 `ActionView(model: $model).offset(x: 0, y: reader.size.height / 2 - 50)`\n\n被 `@State` 装饰的属性,取他的 Binding 只需要在前面加一个 `$`\n\n这样点击 Like Button 后 ❤就会变红啦。\n\n## Navigation\n\n为了实现能在各个 View 之间导航。使用 NavigationView 就可以做到啦。\n\n修改 HomeView,在外层加上 NavigationView。\n\n修改 LikeView,在外层加上 NavigationView。\n\n```swift\nvar body: some View {\n NavigationView {\n ZStack {\n GeometryReader { _ in\n List {\n ForEach(likes) { like in\n Button(action: {}, label: { Text(like.text) })\n }\n }\n }\n }.navigationBarTitle(\"喜欢\")\n }\n}\n```\n\n注意在设定 `.navigationBarTitle` 必须加在 NavigationView 的子 View 上才会生效。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609137171258.png)\n\n接下来,调整一下 List 的 style,让 Item 撑满整个宽度。只需要使用内置的 `.listStyle(PlainListStyle())` 即可。\n\n其余知识点将通过小 Demo 描述。\n\n- Share\n- Sheet Modal\n\n完整 App:\n\n(完)", + "value": "接上文:[从零开始的 Swift UI (二)](https://innei.in/posts/programming/swift-ui-meet_2)\n\n上篇文章介绍了如何使用 UserDefaults 和 ObserveableObject 来进行数据管理。\n\n这篇文章来完成 LikeView 的布局和功能实现。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609134619435.png)\n\n## Layout\n\n在 LikeView 中编写如下代码。\n\n```swift\nstruct LikeView: View {\n @EnvironmentObject var like: Like\n\n var likes: [LikeModel] {\n like.likes\n }\n\n var body: some View {\n ZStack {\n GeometryReader { _ in\n List {\n ForEach(likes) { like in\n Button(action: {}, label: { Text(like.text) })\n }\n }\n }\n }\n }\n}\n\n```\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609136864498.png)\n\n再修改 HomeView 中的 Like Button 代码。\n\n```swift\nButton(action: {\n like.add(hikotoko: model)\n}, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n})\n```\n\nLike.swift 中新增一个方法。\n\n```swift\nfunc add(hikotoko: HitokotoModel) -> Bool {\n let date = ISO8601DateFormatter().date(from: hikotoko.createdAt) ?? Date()\n\n return add(item: LikeModel(id: UUID(uuidString: hikotoko.uuid) ?? UUID(), text: hikotoko.hitokoto,\n createdAt: date, from: hikotoko.from, author: hikotoko.creator))\n}\n```\n\n上面 Like Button 时候被选中,可以根据 Like 中有没有存储判断。\n\n将 ActionView 修改为如下代码:\n\n```swift\nstruct ActionView: View {\n @Binding var model: HitokotoModel?\n @EnvironmentObject var like: Like\n\n var liked: Bool {\n guard let model = model else {\n return false\n }\n return like.has(uuid: UUID(uuidString: model.uuid))\n }\n\n @ViewBuilder\n var body: some View {\n if let model = model {\n HStack(spacing: 20) {\n Button(action: {\n like.add(hikotoko: model)\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n }\n}\n\n```\n\n`liked` 计算属性根据 model 中的 uuid 推断状态。因为使用了 `@Binding` 所以上层 View 还需要传一个 Binding 给他。可以理解为 React 中的 Props。注意的是 只有加了 `@Binding` 的参数传递才是引用传递,也就是上层数据更新后下层也会被更新。\n\n在 HomeView 中修改为 `ActionView(model: $model).offset(x: 0, y: reader.size.height / 2 - 50)`\n\n被 `@State` 装饰的属性,取他的 Binding 只需要在前面加一个 `$`\n\n这样点击 Like Button 后 ❤就会变红啦。\n\n## Navigation\n\n为了实现能在各个 View 之间导航。使用 NavigationView 就可以做到啦。\n\n修改 HomeView,在外层加上 NavigationView。\n\n修改 LikeView,在外层加上 NavigationView。\n\n```swift\nvar body: some View {\n NavigationView {\n ZStack {\n GeometryReader { _ in\n List {\n ForEach(likes) { like in\n Button(action: {}, label: { Text(like.text) })\n }\n }\n }\n }.navigationBarTitle(\"喜欢\")\n }\n}\n```\n\n注意在设定 `.navigationBarTitle` 必须加在 NavigationView 的子 View 上才会生效。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609137171258.png)\n\n接下来,调整一下 List 的 style,让 Item 撑满整个宽度。只需要使用内置的 `.listStyle(PlainListStyle())` 即可。\n\n其余知识点将通过小 Demo 描述。\n\n- Share\n- Sheet Modal\n\n完整 App:\n\n(完)", "match_level": "full", "fully_highlighted": false, - "matched_words": ["1"] + "matched_words": [ + "1" + ] }, "title": { "value": "从零开始的 Swift UI (三)", @@ -199,17 +201,19 @@ } }, { - "text": "接上文:[从零开始的 Swift UI (一)](https://innei.ren/posts/programming/swift-ui-meet_1)\n\n在上一篇文章中,我们完成了 HomeView 的基本布局。接下来我们来编写一下数据层(Model ViewModel)。\n\n大概包括两个方面:数据的获取(JSON URLSession) 和 UI ViewModel 的数据同步。\n\n## 数据的获取\n\n首先我们使用的 Api 是 [Hikotoko](http://v1.hitokoto.cn/)。随机获取一条 Hikotoko 的 JSON 如下。\n\n```json\n{\n\"id\": 5716,\n\"uuid\": \"71396790-6d06-49dd-bc72-2568311cdd7b\",\n\"hitokoto\": \"粗缯大布裹生涯,腹有诗书气自华。\",\n\"type\": \"i\",\n\"from\": \"和董传留别\",\n\"from_who\": \"苏轼\",\n\"creator\": \"a632079\",\n\"creator_uid\": 1044,\n\"reviewer\": 4756,\n\"commit_from\": \"web\",\n\"created_at\": \"1586333487\",\n\"length\": 16\n}\n```\n\n使用工具 JSON2Swift 将 JSON Model 转化为 Swift Struct。工具推荐使用: \n\n右侧选项根据需要修改。仅参考。\n\n![1609121675559](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609121675559.png)\n\n使用此工具的好处是,他把 URLSession 也自动构建好了。并给出了实例。\n\n新建一个 Swift 文件,命名为 `Model.swift` 将生成的代码复制到新文件。\n\n再新建一个 Swift 文件,命名为 `ViewModel.swift`,写入以下代码。\n\n```swift\nimport Foundation\n\nclass HitokotoViewModel {\n static func fetch(completion: @escaping (HitokotoModel) -> Void) {\n let task = URLSession.shared.hitokotoModelTask(with: URL(string: \"https://v1.hitokoto.cn/\")!) { hitokotoModel, _, _ in\n if let hitokotoModel = hitokotoModel {\n DispatchQueue.main.async {\n completion(hitokotoModel)\n }\n }\n }\n\n task.resume()\n }\n}\n```\n\n在 HomeView 中调用此方法。修改 HomeView 的代码为\n\n```swift\n//\n// HomeView.swift\n// Meet\n//\n// Created by Innei on 2020/12/28.\n//\n\nimport SwiftUI\n\nstruct HomeView: View {\n @State var model: HitokotoModel? = nil\n\n func fetch() {\n HitokotoViewModel.fetch {\n self.model = $0\n }\n }\n\n var body: some View {\n GeometryReader { reader in\n ZStack {\n VStack {\n Text(model?.hitokoto ?? \"\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(model?.creator ?? \"\")\n }\n }.padding()\n\n ActionView().offset(x: 0, y: reader.size.height / 2 - 50)\n\n Button(action: {\n fetch()\n }, label: {\n CircleButtonShape(systemImage: \"arrow.clockwise\")\n })\n .position(x: reader.size.width - 50, y: reader.size.height - 50)\n }\n .onAppear {\n fetch()\n }\n }\n }\n}\n\nstruct HomeView_Previews: PreviewProvider {\n static var previews: some View {\n HomeView()\n }\n}\n\nstruct CircleButtonShape: View {\n var systemImage: String\n var color: Color = .pink\n var body: some View {\n ZStack {\n Circle()\n .fill(color)\n .frame(width: 50, height: 50, alignment: .center)\n .shadow(radius: 3)\n Image(systemName: systemImage).foregroundColor(.white)\n }\n }\n}\n\nstruct ActionView: View {\n @State var liked = false\n\n @ViewBuilder\n var body: some View {\n HStack(spacing: 20) {\n Button(action: {\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n}\n\n```\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609122653326.png)\n\n效果已经有了,但是没有加载完成时(受限于网络,弱网),会出现一片空白。如果未加载完成时,显示加载中.. 可能会比较好。\n\n在未加载完成时,`model` 为 `nil` ,那么只需要判断是不是 `nil` 就行了。我本来想用 `Group` 包裹 `if` 判断语句实现。理论上是可行的,但是由于 `Group ` 中 `if` 不支持使用 `Stack` 包裹。出现如下报错。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609123006056.png)\n\n换一种方法。转而使用 `@ViewBuilder`,首先提取组件。在这个 struct 里新增一个 `some View`。\n\n```swift\n @ViewBuilder\n var Preview: some View {\n if let model = model {\n VStack {\n Text(model.hitokoto ?? \"\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(model.creator ?? \"\")\n }\n }\n } else {\n Text(\"加载中\")\n }\n }\n```\n\n然后在 `body` 的合适地方替换成。\n\n```swift\nZStack {\n Preview\n \n // ....\n}\n```\n\n## 响应式数据流\n\n接下来我们实现保存 Hikotoko 到 喜欢。我们需要用到本地存储和响应式数据流。\n\n本地存储可以使用 `UserDefaults`,响应式数据流使用 `ObservableObject`。\n\n新建一个 Swift 文件,命名为 `Like.swift`\n\n```swift\nimport Foundation\n\nclass Like: ObservableObject {\n @Published var likes: [LikeModel] = []\n\n public var codable: [LikeModel] {\n likes\n }\n\n init() {\n \n }\n\n func has(item: LikeModel) -> Int? {\n return likes.firstIndex(where: { $0.id == item.id })\n }\n\n func add(item: LikeModel) -> Bool {\n if has(item: item) != nil {\n return false\n } else {\n likes.append(item)\n return true\n }\n }\n\n func remove(item: LikeModel) -> LikeModel? {\n let id = item.id\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n \n func remove(uuid: UUID) -> LikeModel? {\n let id = uuid\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func removeAll() {\n likes.removeAll()\n }\n}\n\n```\n\n使用 `ObservableObject` protocol 使得一个对象成为可被观察的,当被装饰 `@Published` 的属性改变时,会触发 UIView 更新。\n\n在 MeetApp.swift 中挂载 `Like` 为 `environmentObject`。增加如下代码。\n\n```git\n@main\nstruct MeetApp: App {\n @State var activeTabIndex = 0\n\n+ let like = Like()\n\n var body: some Scene {\n WindowGroup {\n TabView(selection: $activeTabIndex) {\n ContentView().tabItem {\n Label(\"遇见\", systemImage: activeTabIndex != 0 ? \"circle\" : \"largecircle.fill.circle\")\n .onTapGesture {\n activeTabIndex = 0\n }\n }\n .tag(0)\n\n LikeView().tabItem {\n Label(\"喜欢\", systemImage: activeTabIndex != 1 ? \"heart.circle\" : \"heart.circle.fill\")\n .onTapGesture {\n activeTabIndex = 1\n }\n }\n .tag(1)\n }\n .accentColor(.pink)\n+ .environmentObject(like)\n }\n }\n}\n\n```\n\n在 HomeView 中,ActionView 中的 Like Button,修改 action 为\n\n```swift\nif like.has(uuid: UUID(uuidString: model.uuid)) {\n if let uuid = UUID(uuidString: model.uuid) {\n like.remove(uuid: uuid)\n }\n\n } else {\n like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))\n }\n```\n\n在顶部增加\n\n```swift\n @EnvironmentObject var like: Like\n```\n\n完整如下\n\n```swift\nstruct ActionView: View {\n @EnvironmentObject var like: Like\n\n @ViewBuilder\n var body: some View {\n if let model = model {\n HStack(spacing: 20) {\n Button(action: {\n if like.has(uuid: UUID(uuidString: model.uuid)) {\n if let uuid = UUID(uuidString: model.uuid) {\n like.remove(uuid: uuid)\n }\n\n } else {\n like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))\n }\n\n }, label: {\n Image(systemName: \"suit.heart\")\n .foregroundColor(.primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n }\n}\n```\n\n装饰了 `@EnvironmentObject` 的属性会自动获取上层 View 挂载的 `environmentObject`,不需要层层传递。类似 React 中的 `Context`。\n\n## 数据的存储\n\n在 `Like.swift` 中新建一个 Class,代码如下。\n\n```swift\nclass Store {\n private(set) static var userDefaults = UserDefaults()\n\n public static let storeKey = \"like-list\"\n\n public static func refreshStore(_ like: Like) {\n\n if let data = try? PropertyListEncoder().encode(like.codable) {\n userDefaults.set(data, forKey: storeKey)\n }\n }\n}\n\n```\n\n我们使用 `refreshStore` 方法把 Like 中 `likes` 数据保存到本地数据中。因为 `likes` 不是普通的 Array,所以不能直接使用 `Userdefaults.set()` 的方法写入,否则会 runtime crash。首先使用 `PropertyListEncoder` 将数据序列化。在此之前,请注意 `LikeModel` 实现了 `Codable` Protocol。\n\n同样在 Like init 的时候读取本地保存的数据。当然也需要先反序列化数据。\n\n```swift\ninit() {\n if let data = Store.userDefaults.data(forKey: Store.storeKey) {\n let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)\n likes = stored.map { $0 }\n }\n }\n```\n\n在修改 likes 后,同时写入到本地数据。可以使用 `didSet` 计算属性很容易完成。修改 likes 属性为。\n\n```swift\n@Published var likes: [LikeModel] = [] {\n didSet {\n Store.refreshStore(self)\n }\n}\n\n```\n\n之后完整的 `Like.swift` 如下:\n\n```swift\n//\n// Like.swift\n// Meet\n//\n// Created by Innei on 2020/12/27.\n//\n\nimport Foundation\n\nclass Like: ObservableObject {\n @Published var likes: [LikeModel] = [] {\n didSet {\n Store.refreshStore(self)\n }\n }\n\n public var codable: [LikeModel] {\n likes\n }\n\n init() {\n if let data = Store.userDefaults.data(forKey: Store.storeKey) {\n let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)\n likes = stored.map { $0 }\n }\n }\n\n func has(item: LikeModel) -> Int? {\n return likes.firstIndex(where: { $0.id == item.id })\n }\n\n func has(uuid: UUID?) -> Bool {\n guard let uuid = uuid else { return false }\n return likes.first { $0.id == uuid } != nil\n }\n\n func add(item: LikeModel) -> Bool {\n if has(item: item) != nil {\n return false\n } else {\n likes.append(item)\n// Store.refreshStore()\n return true\n }\n }\n\n func remove(item: LikeModel) -> LikeModel? {\n let id = item.id\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func remove(uuid: UUID) -> LikeModel? {\n let id = uuid\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func removeAll() {\n likes.removeAll()\n }\n}\n\nclass Store {\n private(set) static var userDefaults = UserDefaults()\n\n public static let storeKey = \"like-list\"\n\n public static func refreshStore(_ like: Like) {\n\n if let data = try? PropertyListEncoder().encode(like.codable) {\n userDefaults.set(data, forKey: storeKey)\n }\n }\n}\n\n```\n\n下一篇文章,将构建 LikeView。\n\n(未待完续)", + "text": "接上文:[从零开始的 Swift UI (一)](https://innei.in/posts/programming/swift-ui-meet_1)\n\n在上一篇文章中,我们完成了 HomeView 的基本布局。接下来我们来编写一下数据层(Model ViewModel)。\n\n大概包括两个方面:数据的获取(JSON URLSession) 和 UI ViewModel 的数据同步。\n\n## 数据的获取\n\n首先我们使用的 Api 是 [Hikotoko](http://v1.hitokoto.cn/)。随机获取一条 Hikotoko 的 JSON 如下。\n\n```json\n{\n\"id\": 5716,\n\"uuid\": \"71396790-6d06-49dd-bc72-2568311cdd7b\",\n\"hitokoto\": \"粗缯大布裹生涯,腹有诗书气自华。\",\n\"type\": \"i\",\n\"from\": \"和董传留别\",\n\"from_who\": \"苏轼\",\n\"creator\": \"a632079\",\n\"creator_uid\": 1044,\n\"reviewer\": 4756,\n\"commit_from\": \"web\",\n\"created_at\": \"1586333487\",\n\"length\": 16\n}\n```\n\n使用工具 JSON2Swift 将 JSON Model 转化为 Swift Struct。工具推荐使用: \n\n右侧选项根据需要修改。仅参考。\n\n![1609121675559](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609121675559.png)\n\n使用此工具的好处是,他把 URLSession 也自动构建好了。并给出了实例。\n\n新建一个 Swift 文件,命名为 `Model.swift` 将生成的代码复制到新文件。\n\n再新建一个 Swift 文件,命名为 `ViewModel.swift`,写入以下代码。\n\n```swift\nimport Foundation\n\nclass HitokotoViewModel {\n static func fetch(completion: @escaping (HitokotoModel) -> Void) {\n let task = URLSession.shared.hitokotoModelTask(with: URL(string: \"https://v1.hitokoto.cn/\")!) { hitokotoModel, _, _ in\n if let hitokotoModel = hitokotoModel {\n DispatchQueue.main.async {\n completion(hitokotoModel)\n }\n }\n }\n\n task.resume()\n }\n}\n```\n\n在 HomeView 中调用此方法。修改 HomeView 的代码为\n\n```swift\n//\n// HomeView.swift\n// Meet\n//\n// Created by Innei on 2020/12/28.\n//\n\nimport SwiftUI\n\nstruct HomeView: View {\n @State var model: HitokotoModel? = nil\n\n func fetch() {\n HitokotoViewModel.fetch {\n self.model = $0\n }\n }\n\n var body: some View {\n GeometryReader { reader in\n ZStack {\n VStack {\n Text(model?.hitokoto ?? \"\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(model?.creator ?? \"\")\n }\n }.padding()\n\n ActionView().offset(x: 0, y: reader.size.height / 2 - 50)\n\n Button(action: {\n fetch()\n }, label: {\n CircleButtonShape(systemImage: \"arrow.clockwise\")\n })\n .position(x: reader.size.width - 50, y: reader.size.height - 50)\n }\n .onAppear {\n fetch()\n }\n }\n }\n}\n\nstruct HomeView_Previews: PreviewProvider {\n static var previews: some View {\n HomeView()\n }\n}\n\nstruct CircleButtonShape: View {\n var systemImage: String\n var color: Color = .pink\n var body: some View {\n ZStack {\n Circle()\n .fill(color)\n .frame(width: 50, height: 50, alignment: .center)\n .shadow(radius: 3)\n Image(systemName: systemImage).foregroundColor(.white)\n }\n }\n}\n\nstruct ActionView: View {\n @State var liked = false\n\n @ViewBuilder\n var body: some View {\n HStack(spacing: 20) {\n Button(action: {\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n}\n\n```\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609122653326.png)\n\n效果已经有了,但是没有加载完成时(受限于网络,弱网),会出现一片空白。如果未加载完成时,显示加载中.. 可能会比较好。\n\n在未加载完成时,`model` 为 `nil` ,那么只需要判断是不是 `nil` 就行了。我本来想用 `Group` 包裹 `if` 判断语句实现。理论上是可行的,但是由于 `Group ` 中 `if` 不支持使用 `Stack` 包裹。出现如下报错。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609123006056.png)\n\n换一种方法。转而使用 `@ViewBuilder`,首先提取组件。在这个 struct 里新增一个 `some View`。\n\n```swift\n @ViewBuilder\n var Preview: some View {\n if let model = model {\n VStack {\n Text(model.hitokoto ?? \"\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(model.creator ?? \"\")\n }\n }\n } else {\n Text(\"加载中\")\n }\n }\n```\n\n然后在 `body` 的合适地方替换成。\n\n```swift\nZStack {\n Preview\n \n // ....\n}\n```\n\n## 响应式数据流\n\n接下来我们实现保存 Hikotoko 到 喜欢。我们需要用到本地存储和响应式数据流。\n\n本地存储可以使用 `UserDefaults`,响应式数据流使用 `ObservableObject`。\n\n新建一个 Swift 文件,命名为 `Like.swift`\n\n```swift\nimport Foundation\n\nclass Like: ObservableObject {\n @Published var likes: [LikeModel] = []\n\n public var codable: [LikeModel] {\n likes\n }\n\n init() {\n \n }\n\n func has(item: LikeModel) -> Int? {\n return likes.firstIndex(where: { $0.id == item.id })\n }\n\n func add(item: LikeModel) -> Bool {\n if has(item: item) != nil {\n return false\n } else {\n likes.append(item)\n return true\n }\n }\n\n func remove(item: LikeModel) -> LikeModel? {\n let id = item.id\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n \n func remove(uuid: UUID) -> LikeModel? {\n let id = uuid\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func removeAll() {\n likes.removeAll()\n }\n}\n\n```\n\n使用 `ObservableObject` protocol 使得一个对象成为可被观察的,当被装饰 `@Published` 的属性改变时,会触发 UIView 更新。\n\n在 MeetApp.swift 中挂载 `Like` 为 `environmentObject`。增加如下代码。\n\n```git\n@main\nstruct MeetApp: App {\n @State var activeTabIndex = 0\n\n+ let like = Like()\n\n var body: some Scene {\n WindowGroup {\n TabView(selection: $activeTabIndex) {\n ContentView().tabItem {\n Label(\"遇见\", systemImage: activeTabIndex != 0 ? \"circle\" : \"largecircle.fill.circle\")\n .onTapGesture {\n activeTabIndex = 0\n }\n }\n .tag(0)\n\n LikeView().tabItem {\n Label(\"喜欢\", systemImage: activeTabIndex != 1 ? \"heart.circle\" : \"heart.circle.fill\")\n .onTapGesture {\n activeTabIndex = 1\n }\n }\n .tag(1)\n }\n .accentColor(.pink)\n+ .environmentObject(like)\n }\n }\n}\n\n```\n\n在 HomeView 中,ActionView 中的 Like Button,修改 action 为\n\n```swift\nif like.has(uuid: UUID(uuidString: model.uuid)) {\n if let uuid = UUID(uuidString: model.uuid) {\n like.remove(uuid: uuid)\n }\n\n } else {\n like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))\n }\n```\n\n在顶部增加\n\n```swift\n @EnvironmentObject var like: Like\n```\n\n完整如下\n\n```swift\nstruct ActionView: View {\n @EnvironmentObject var like: Like\n\n @ViewBuilder\n var body: some View {\n if let model = model {\n HStack(spacing: 20) {\n Button(action: {\n if like.has(uuid: UUID(uuidString: model.uuid)) {\n if let uuid = UUID(uuidString: model.uuid) {\n like.remove(uuid: uuid)\n }\n\n } else {\n like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))\n }\n\n }, label: {\n Image(systemName: \"suit.heart\")\n .foregroundColor(.primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n }\n}\n```\n\n装饰了 `@EnvironmentObject` 的属性会自动获取上层 View 挂载的 `environmentObject`,不需要层层传递。类似 React 中的 `Context`。\n\n## 数据的存储\n\n在 `Like.swift` 中新建一个 Class,代码如下。\n\n```swift\nclass Store {\n private(set) static var userDefaults = UserDefaults()\n\n public static let storeKey = \"like-list\"\n\n public static func refreshStore(_ like: Like) {\n\n if let data = try? PropertyListEncoder().encode(like.codable) {\n userDefaults.set(data, forKey: storeKey)\n }\n }\n}\n\n```\n\n我们使用 `refreshStore` 方法把 Like 中 `likes` 数据保存到本地数据中。因为 `likes` 不是普通的 Array,所以不能直接使用 `Userdefaults.set()` 的方法写入,否则会 runtime crash。首先使用 `PropertyListEncoder` 将数据序列化。在此之前,请注意 `LikeModel` 实现了 `Codable` Protocol。\n\n同样在 Like init 的时候读取本地保存的数据。当然也需要先反序列化数据。\n\n```swift\ninit() {\n if let data = Store.userDefaults.data(forKey: Store.storeKey) {\n let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)\n likes = stored.map { $0 }\n }\n }\n```\n\n在修改 likes 后,同时写入到本地数据。可以使用 `didSet` 计算属性很容易完成。修改 likes 属性为。\n\n```swift\n@Published var likes: [LikeModel] = [] {\n didSet {\n Store.refreshStore(self)\n }\n}\n\n```\n\n之后完整的 `Like.swift` 如下:\n\n```swift\n//\n// Like.swift\n// Meet\n//\n// Created by Innei on 2020/12/27.\n//\n\nimport Foundation\n\nclass Like: ObservableObject {\n @Published var likes: [LikeModel] = [] {\n didSet {\n Store.refreshStore(self)\n }\n }\n\n public var codable: [LikeModel] {\n likes\n }\n\n init() {\n if let data = Store.userDefaults.data(forKey: Store.storeKey) {\n let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)\n likes = stored.map { $0 }\n }\n }\n\n func has(item: LikeModel) -> Int? {\n return likes.firstIndex(where: { $0.id == item.id })\n }\n\n func has(uuid: UUID?) -> Bool {\n guard let uuid = uuid else { return false }\n return likes.first { $0.id == uuid } != nil\n }\n\n func add(item: LikeModel) -> Bool {\n if has(item: item) != nil {\n return false\n } else {\n likes.append(item)\n// Store.refreshStore()\n return true\n }\n }\n\n func remove(item: LikeModel) -> LikeModel? {\n let id = item.id\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func remove(uuid: UUID) -> LikeModel? {\n let id = uuid\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func removeAll() {\n likes.removeAll()\n }\n}\n\nclass Store {\n private(set) static var userDefaults = UserDefaults()\n\n public static let storeKey = \"like-list\"\n\n public static func refreshStore(_ like: Like) {\n\n if let data = try? PropertyListEncoder().encode(like.codable) {\n userDefaults.set(data, forKey: storeKey)\n }\n }\n}\n\n```\n\n下一篇文章,将构建 LikeView。\n\n(未待完续)", "title": "从零开始的 Swift UI (二)", "id": "5fe951565b11408f99ad9edd", "type": "post", "object_id": "5fe951565b11408f99ad9edd", "highlight_result": { "text": { - "value": "接上文:[从零开始的 Swift UI (一)](https://innei.ren/posts/programming/swift-ui-meet_1)\n\n在上一篇文章中,我们完成了 HomeView 的基本布局。接下来我们来编写一下数据层(Model ViewModel)。\n\n大概包括两个方面:数据的获取(JSON URLSession) 和 UI ViewModel 的数据同步。\n\n## 数据的获取\n\n首先我们使用的 Api 是 [Hikotoko](http://v1.hitokoto.cn/)。随机获取一条 Hikotoko 的 JSON 如下。\n\n```json\n{\n\"id\": 5716,\n\"uuid\": \"71396790-6d06-49dd-bc72-2568311cdd7b\",\n\"hitokoto\": \"粗缯大布裹生涯,腹有诗书气自华。\",\n\"type\": \"i\",\n\"from\": \"和董传留别\",\n\"from_who\": \"苏轼\",\n\"creator\": \"a632079\",\n\"creator_uid\": 1044,\n\"reviewer\": 4756,\n\"commit_from\": \"web\",\n\"created_at\": \"1586333487\",\n\"length\": 16\n}\n```\n\n使用工具 JSON2Swift 将 JSON Model 转化为 Swift Struct。工具推荐使用: \n\n右侧选项根据需要修改。仅参考。\n\n![1609121675559](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609121675559.png)\n\n使用此工具的好处是,他把 URLSession 也自动构建好了。并给出了实例。\n\n新建一个 Swift 文件,命名为 `Model.swift` 将生成的代码复制到新文件。\n\n再新建一个 Swift 文件,命名为 `ViewModel.swift`,写入以下代码。\n\n```swift\nimport Foundation\n\nclass HitokotoViewModel {\n static func fetch(completion: @escaping (HitokotoModel) -> Void) {\n let task = URLSession.shared.hitokotoModelTask(with: URL(string: \"https://v1.hitokoto.cn/\")!) { hitokotoModel, _, _ in\n if let hitokotoModel = hitokotoModel {\n DispatchQueue.main.async {\n completion(hitokotoModel)\n }\n }\n }\n\n task.resume()\n }\n}\n```\n\n在 HomeView 中调用此方法。修改 HomeView 的代码为\n\n```swift\n//\n// HomeView.swift\n// Meet\n//\n// Created by Innei on 2020/12/28.\n//\n\nimport SwiftUI\n\nstruct HomeView: View {\n @State var model: HitokotoModel? = nil\n\n func fetch() {\n HitokotoViewModel.fetch {\n self.model = $0\n }\n }\n\n var body: some View {\n GeometryReader { reader in\n ZStack {\n VStack {\n Text(model?.hitokoto ?? \"\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(model?.creator ?? \"\")\n }\n }.padding()\n\n ActionView().offset(x: 0, y: reader.size.height / 2 - 50)\n\n Button(action: {\n fetch()\n }, label: {\n CircleButtonShape(systemImage: \"arrow.clockwise\")\n })\n .position(x: reader.size.width - 50, y: reader.size.height - 50)\n }\n .onAppear {\n fetch()\n }\n }\n }\n}\n\nstruct HomeView_Previews: PreviewProvider {\n static var previews: some View {\n HomeView()\n }\n}\n\nstruct CircleButtonShape: View {\n var systemImage: String\n var color: Color = .pink\n var body: some View {\n ZStack {\n Circle()\n .fill(color)\n .frame(width: 50, height: 50, alignment: .center)\n .shadow(radius: 3)\n Image(systemName: systemImage).foregroundColor(.white)\n }\n }\n}\n\nstruct ActionView: View {\n @State var liked = false\n\n @ViewBuilder\n var body: some View {\n HStack(spacing: 20) {\n Button(action: {\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n}\n\n```\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609122653326.png)\n\n效果已经有了,但是没有加载完成时(受限于网络,弱网),会出现一片空白。如果未加载完成时,显示加载中.. 可能会比较好。\n\n在未加载完成时,`model` 为 `nil` ,那么只需要判断是不是 `nil` 就行了。我本来想用 `Group` 包裹 `if` 判断语句实现。理论上是可行的,但是由于 `Group ` 中 `if` 不支持使用 `Stack` 包裹。出现如下报错。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609123006056.png)\n\n换一种方法。转而使用 `@ViewBuilder`,首先提取组件。在这个 struct 里新增一个 `some View`。\n\n```swift\n @ViewBuilder\n var Preview: some View {\n if let model = model {\n VStack {\n Text(model.hitokoto ?? \"\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(model.creator ?? \"\")\n }\n }\n } else {\n Text(\"加载中\")\n }\n }\n```\n\n然后在 `body` 的合适地方替换成。\n\n```swift\nZStack {\n Preview\n \n // ....\n}\n```\n\n## 响应式数据流\n\n接下来我们实现保存 Hikotoko 到 喜欢。我们需要用到本地存储和响应式数据流。\n\n本地存储可以使用 `UserDefaults`,响应式数据流使用 `ObservableObject`。\n\n新建一个 Swift 文件,命名为 `Like.swift`\n\n```swift\nimport Foundation\n\nclass Like: ObservableObject {\n @Published var likes: [LikeModel] = []\n\n public var codable: [LikeModel] {\n likes\n }\n\n init() {\n \n }\n\n func has(item: LikeModel) -> Int? {\n return likes.firstIndex(where: { $0.id == item.id })\n }\n\n func add(item: LikeModel) -> Bool {\n if has(item: item) != nil {\n return false\n } else {\n likes.append(item)\n return true\n }\n }\n\n func remove(item: LikeModel) -> LikeModel? {\n let id = item.id\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n \n func remove(uuid: UUID) -> LikeModel? {\n let id = uuid\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func removeAll() {\n likes.removeAll()\n }\n}\n\n```\n\n使用 `ObservableObject` protocol 使得一个对象成为可被观察的,当被装饰 `@Published` 的属性改变时,会触发 UIView 更新。\n\n在 MeetApp.swift 中挂载 `Like` 为 `environmentObject`。增加如下代码。\n\n```git\n@main\nstruct MeetApp: App {\n @State var activeTabIndex = 0\n\n+ let like = Like()\n\n var body: some Scene {\n WindowGroup {\n TabView(selection: $activeTabIndex) {\n ContentView().tabItem {\n Label(\"遇见\", systemImage: activeTabIndex != 0 ? \"circle\" : \"largecircle.fill.circle\")\n .onTapGesture {\n activeTabIndex = 0\n }\n }\n .tag(0)\n\n LikeView().tabItem {\n Label(\"喜欢\", systemImage: activeTabIndex != 1 ? \"heart.circle\" : \"heart.circle.fill\")\n .onTapGesture {\n activeTabIndex = 1\n }\n }\n .tag(1)\n }\n .accentColor(.pink)\n+ .environmentObject(like)\n }\n }\n}\n\n```\n\n在 HomeView 中,ActionView 中的 Like Button,修改 action 为\n\n```swift\nif like.has(uuid: UUID(uuidString: model.uuid)) {\n if let uuid = UUID(uuidString: model.uuid) {\n like.remove(uuid: uuid)\n }\n\n } else {\n like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))\n }\n```\n\n在顶部增加\n\n```swift\n @EnvironmentObject var like: Like\n```\n\n完整如下\n\n```swift\nstruct ActionView: View {\n @EnvironmentObject var like: Like\n\n @ViewBuilder\n var body: some View {\n if let model = model {\n HStack(spacing: 20) {\n Button(action: {\n if like.has(uuid: UUID(uuidString: model.uuid)) {\n if let uuid = UUID(uuidString: model.uuid) {\n like.remove(uuid: uuid)\n }\n\n } else {\n like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))\n }\n\n }, label: {\n Image(systemName: \"suit.heart\")\n .foregroundColor(.primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n }\n}\n```\n\n装饰了 `@EnvironmentObject` 的属性会自动获取上层 View 挂载的 `environmentObject`,不需要层层传递。类似 React 中的 `Context`。\n\n## 数据的存储\n\n在 `Like.swift` 中新建一个 Class,代码如下。\n\n```swift\nclass Store {\n private(set) static var userDefaults = UserDefaults()\n\n public static let storeKey = \"like-list\"\n\n public static func refreshStore(_ like: Like) {\n\n if let data = try? PropertyListEncoder().encode(like.codable) {\n userDefaults.set(data, forKey: storeKey)\n }\n }\n}\n\n```\n\n我们使用 `refreshStore` 方法把 Like 中 `likes` 数据保存到本地数据中。因为 `likes` 不是普通的 Array,所以不能直接使用 `Userdefaults.set()` 的方法写入,否则会 runtime crash。首先使用 `PropertyListEncoder` 将数据序列化。在此之前,请注意 `LikeModel` 实现了 `Codable` Protocol。\n\n同样在 Like init 的时候读取本地保存的数据。当然也需要先反序列化数据。\n\n```swift\ninit() {\n if let data = Store.userDefaults.data(forKey: Store.storeKey) {\n let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)\n likes = stored.map { $0 }\n }\n }\n```\n\n在修改 likes 后,同时写入到本地数据。可以使用 `didSet` 计算属性很容易完成。修改 likes 属性为。\n\n```swift\n@Published var likes: [LikeModel] = [] {\n didSet {\n Store.refreshStore(self)\n }\n}\n\n```\n\n之后完整的 `Like.swift` 如下:\n\n```swift\n//\n// Like.swift\n// Meet\n//\n// Created by Innei on 2020/12/27.\n//\n\nimport Foundation\n\nclass Like: ObservableObject {\n @Published var likes: [LikeModel] = [] {\n didSet {\n Store.refreshStore(self)\n }\n }\n\n public var codable: [LikeModel] {\n likes\n }\n\n init() {\n if let data = Store.userDefaults.data(forKey: Store.storeKey) {\n let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)\n likes = stored.map { $0 }\n }\n }\n\n func has(item: LikeModel) -> Int? {\n return likes.firstIndex(where: { $0.id == item.id })\n }\n\n func has(uuid: UUID?) -> Bool {\n guard let uuid = uuid else { return false }\n return likes.first { $0.id == uuid } != nil\n }\n\n func add(item: LikeModel) -> Bool {\n if has(item: item) != nil {\n return false\n } else {\n likes.append(item)\n// Store.refreshStore()\n return true\n }\n }\n\n func remove(item: LikeModel) -> LikeModel? {\n let id = item.id\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func remove(uuid: UUID) -> LikeModel? {\n let id = uuid\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func removeAll() {\n likes.removeAll()\n }\n}\n\nclass Store {\n private(set) static var userDefaults = UserDefaults()\n\n public static let storeKey = \"like-list\"\n\n public static func refreshStore(_ like: Like) {\n\n if let data = try? PropertyListEncoder().encode(like.codable) {\n userDefaults.set(data, forKey: storeKey)\n }\n }\n}\n\n```\n\n下一篇文章,将构建 LikeView。\n\n(未待完续)", + "value": "接上文:[从零开始的 Swift UI (一)](https://innei.in/posts/programming/swift-ui-meet_1)\n\n在上一篇文章中,我们完成了 HomeView 的基本布局。接下来我们来编写一下数据层(Model ViewModel)。\n\n大概包括两个方面:数据的获取(JSON URLSession) 和 UI ViewModel 的数据同步。\n\n## 数据的获取\n\n首先我们使用的 Api 是 [Hikotoko](http://v1.hitokoto.cn/)。随机获取一条 Hikotoko 的 JSON 如下。\n\n```json\n{\n\"id\": 5716,\n\"uuid\": \"71396790-6d06-49dd-bc72-2568311cdd7b\",\n\"hitokoto\": \"粗缯大布裹生涯,腹有诗书气自华。\",\n\"type\": \"i\",\n\"from\": \"和董传留别\",\n\"from_who\": \"苏轼\",\n\"creator\": \"a632079\",\n\"creator_uid\": 1044,\n\"reviewer\": 4756,\n\"commit_from\": \"web\",\n\"created_at\": \"1586333487\",\n\"length\": 16\n}\n```\n\n使用工具 JSON2Swift 将 JSON Model 转化为 Swift Struct。工具推荐使用: \n\n右侧选项根据需要修改。仅参考。\n\n![1609121675559](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609121675559.png)\n\n使用此工具的好处是,他把 URLSession 也自动构建好了。并给出了实例。\n\n新建一个 Swift 文件,命名为 `Model.swift` 将生成的代码复制到新文件。\n\n再新建一个 Swift 文件,命名为 `ViewModel.swift`,写入以下代码。\n\n```swift\nimport Foundation\n\nclass HitokotoViewModel {\n static func fetch(completion: @escaping (HitokotoModel) -> Void) {\n let task = URLSession.shared.hitokotoModelTask(with: URL(string: \"https://v1.hitokoto.cn/\")!) { hitokotoModel, _, _ in\n if let hitokotoModel = hitokotoModel {\n DispatchQueue.main.async {\n completion(hitokotoModel)\n }\n }\n }\n\n task.resume()\n }\n}\n```\n\n在 HomeView 中调用此方法。修改 HomeView 的代码为\n\n```swift\n//\n// HomeView.swift\n// Meet\n//\n// Created by Innei on 2020/12/28.\n//\n\nimport SwiftUI\n\nstruct HomeView: View {\n @State var model: HitokotoModel? = nil\n\n func fetch() {\n HitokotoViewModel.fetch {\n self.model = $0\n }\n }\n\n var body: some View {\n GeometryReader { reader in\n ZStack {\n VStack {\n Text(model?.hitokoto ?? \"\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(model?.creator ?? \"\")\n }\n }.padding()\n\n ActionView().offset(x: 0, y: reader.size.height / 2 - 50)\n\n Button(action: {\n fetch()\n }, label: {\n CircleButtonShape(systemImage: \"arrow.clockwise\")\n })\n .position(x: reader.size.width - 50, y: reader.size.height - 50)\n }\n .onAppear {\n fetch()\n }\n }\n }\n}\n\nstruct HomeView_Previews: PreviewProvider {\n static var previews: some View {\n HomeView()\n }\n}\n\nstruct CircleButtonShape: View {\n var systemImage: String\n var color: Color = .pink\n var body: some View {\n ZStack {\n Circle()\n .fill(color)\n .frame(width: 50, height: 50, alignment: .center)\n .shadow(radius: 3)\n Image(systemName: systemImage).foregroundColor(.white)\n }\n }\n}\n\nstruct ActionView: View {\n @State var liked = false\n\n @ViewBuilder\n var body: some View {\n HStack(spacing: 20) {\n Button(action: {\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n}\n\n```\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609122653326.png)\n\n效果已经有了,但是没有加载完成时(受限于网络,弱网),会出现一片空白。如果未加载完成时,显示加载中.. 可能会比较好。\n\n在未加载完成时,`model` 为 `nil` ,那么只需要判断是不是 `nil` 就行了。我本来想用 `Group` 包裹 `if` 判断语句实现。理论上是可行的,但是由于 `Group ` 中 `if` 不支持使用 `Stack` 包裹。出现如下报错。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609123006056.png)\n\n换一种方法。转而使用 `@ViewBuilder`,首先提取组件。在这个 struct 里新增一个 `some View`。\n\n```swift\n @ViewBuilder\n var Preview: some View {\n if let model = model {\n VStack {\n Text(model.hitokoto ?? \"\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(model.creator ?? \"\")\n }\n }\n } else {\n Text(\"加载中\")\n }\n }\n```\n\n然后在 `body` 的合适地方替换成。\n\n```swift\nZStack {\n Preview\n \n // ....\n}\n```\n\n## 响应式数据流\n\n接下来我们实现保存 Hikotoko 到 喜欢。我们需要用到本地存储和响应式数据流。\n\n本地存储可以使用 `UserDefaults`,响应式数据流使用 `ObservableObject`。\n\n新建一个 Swift 文件,命名为 `Like.swift`\n\n```swift\nimport Foundation\n\nclass Like: ObservableObject {\n @Published var likes: [LikeModel] = []\n\n public var codable: [LikeModel] {\n likes\n }\n\n init() {\n \n }\n\n func has(item: LikeModel) -> Int? {\n return likes.firstIndex(where: { $0.id == item.id })\n }\n\n func add(item: LikeModel) -> Bool {\n if has(item: item) != nil {\n return false\n } else {\n likes.append(item)\n return true\n }\n }\n\n func remove(item: LikeModel) -> LikeModel? {\n let id = item.id\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n \n func remove(uuid: UUID) -> LikeModel? {\n let id = uuid\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func removeAll() {\n likes.removeAll()\n }\n}\n\n```\n\n使用 `ObservableObject` protocol 使得一个对象成为可被观察的,当被装饰 `@Published` 的属性改变时,会触发 UIView 更新。\n\n在 MeetApp.swift 中挂载 `Like` 为 `environmentObject`。增加如下代码。\n\n```git\n@main\nstruct MeetApp: App {\n @State var activeTabIndex = 0\n\n+ let like = Like()\n\n var body: some Scene {\n WindowGroup {\n TabView(selection: $activeTabIndex) {\n ContentView().tabItem {\n Label(\"遇见\", systemImage: activeTabIndex != 0 ? \"circle\" : \"largecircle.fill.circle\")\n .onTapGesture {\n activeTabIndex = 0\n }\n }\n .tag(0)\n\n LikeView().tabItem {\n Label(\"喜欢\", systemImage: activeTabIndex != 1 ? \"heart.circle\" : \"heart.circle.fill\")\n .onTapGesture {\n activeTabIndex = 1\n }\n }\n .tag(1)\n }\n .accentColor(.pink)\n+ .environmentObject(like)\n }\n }\n}\n\n```\n\n在 HomeView 中,ActionView 中的 Like Button,修改 action 为\n\n```swift\nif like.has(uuid: UUID(uuidString: model.uuid)) {\n if let uuid = UUID(uuidString: model.uuid) {\n like.remove(uuid: uuid)\n }\n\n } else {\n like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))\n }\n```\n\n在顶部增加\n\n```swift\n @EnvironmentObject var like: Like\n```\n\n完整如下\n\n```swift\nstruct ActionView: View {\n @EnvironmentObject var like: Like\n\n @ViewBuilder\n var body: some View {\n if let model = model {\n HStack(spacing: 20) {\n Button(action: {\n if like.has(uuid: UUID(uuidString: model.uuid)) {\n if let uuid = UUID(uuidString: model.uuid) {\n like.remove(uuid: uuid)\n }\n\n } else {\n like.add(item: LikeModel(id: UUID(uuidString: model.uuid) ?? UUID(), text: model.hitokoto, createdAt: Date(), from: model.from, author: model.creator))\n }\n\n }, label: {\n Image(systemName: \"suit.heart\")\n .foregroundColor(.primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n }\n}\n```\n\n装饰了 `@EnvironmentObject` 的属性会自动获取上层 View 挂载的 `environmentObject`,不需要层层传递。类似 React 中的 `Context`。\n\n## 数据的存储\n\n在 `Like.swift` 中新建一个 Class,代码如下。\n\n```swift\nclass Store {\n private(set) static var userDefaults = UserDefaults()\n\n public static let storeKey = \"like-list\"\n\n public static func refreshStore(_ like: Like) {\n\n if let data = try? PropertyListEncoder().encode(like.codable) {\n userDefaults.set(data, forKey: storeKey)\n }\n }\n}\n\n```\n\n我们使用 `refreshStore` 方法把 Like 中 `likes` 数据保存到本地数据中。因为 `likes` 不是普通的 Array,所以不能直接使用 `Userdefaults.set()` 的方法写入,否则会 runtime crash。首先使用 `PropertyListEncoder` 将数据序列化。在此之前,请注意 `LikeModel` 实现了 `Codable` Protocol。\n\n同样在 Like init 的时候读取本地保存的数据。当然也需要先反序列化数据。\n\n```swift\ninit() {\n if let data = Store.userDefaults.data(forKey: Store.storeKey) {\n let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)\n likes = stored.map { $0 }\n }\n }\n```\n\n在修改 likes 后,同时写入到本地数据。可以使用 `didSet` 计算属性很容易完成。修改 likes 属性为。\n\n```swift\n@Published var likes: [LikeModel] = [] {\n didSet {\n Store.refreshStore(self)\n }\n}\n\n```\n\n之后完整的 `Like.swift` 如下:\n\n```swift\n//\n// Like.swift\n// Meet\n//\n// Created by Innei on 2020/12/27.\n//\n\nimport Foundation\n\nclass Like: ObservableObject {\n @Published var likes: [LikeModel] = [] {\n didSet {\n Store.refreshStore(self)\n }\n }\n\n public var codable: [LikeModel] {\n likes\n }\n\n init() {\n if let data = Store.userDefaults.data(forKey: Store.storeKey) {\n let stored = try! PropertyListDecoder().decode([LikeModel].self, from: data)\n likes = stored.map { $0 }\n }\n }\n\n func has(item: LikeModel) -> Int? {\n return likes.firstIndex(where: { $0.id == item.id })\n }\n\n func has(uuid: UUID?) -> Bool {\n guard let uuid = uuid else { return false }\n return likes.first { $0.id == uuid } != nil\n }\n\n func add(item: LikeModel) -> Bool {\n if has(item: item) != nil {\n return false\n } else {\n likes.append(item)\n// Store.refreshStore()\n return true\n }\n }\n\n func remove(item: LikeModel) -> LikeModel? {\n let id = item.id\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func remove(uuid: UUID) -> LikeModel? {\n let id = uuid\n if let index = likes.firstIndex(where: { $0.id == id }) {\n let element = likes[index]\n likes.remove(at: index)\n return element\n } else {\n return nil\n }\n }\n\n func removeAll() {\n likes.removeAll()\n }\n}\n\nclass Store {\n private(set) static var userDefaults = UserDefaults()\n\n public static let storeKey = \"like-list\"\n\n public static func refreshStore(_ like: Like) {\n\n if let data = try? PropertyListEncoder().encode(like.codable) {\n userDefaults.set(data, forKey: storeKey)\n }\n }\n}\n\n```\n\n下一篇文章,将构建 LikeView。\n\n(未待完续)", "match_level": "full", "fully_highlighted": false, - "matched_words": ["1"] + "matched_words": [ + "1" + ] }, "title": { "value": "从零开始的 Swift UI (二)", @@ -234,7 +238,9 @@ "value": "注: 本文编写时,使用 Xcode 12.3、Swift 5.3.2 来构建 App\n\n入门 Swift UI 已经有一段时间了,但是却一直没有写过什么练手项目,虽然之前跟着 Hackingwithswift 上找着写过几个 Demo。突然打算自己独立写一个练手项目,因为是练手项目,所以布局和功能上也很简单,App 的类型大概和 TODO 类似。\n\n![](https://cdn.jsdelivr.net/gh/Innei/img-bed@master/uPic/WCXU9K.png)\n\n## 准备\n\n打开 Xcode 新建一个项目在此不再展开。在左侧文件树中打开 `ContentView.swift`,这是 View 的入口文件。你可以看到如下代码。\n\n```swift\nimport SwiftUI\n\nstruct ContentView: View {\n var body: some View {\n Text(\"Hello, world!\")\n .padding()\n }\n}\n\nstruct ContentView_Previews: PreviewProvider {\n static var previews: some View {\n ContentView()\n }\n}\n```\n\n在 Swift UI 2.0 中,UI 主入口文件从复杂的 `AppDelegate.swift` 和 `SceneDelegate.swift` 转变为仅仅只有几行的 `xxApp.swift`,得益于 Swift 5.3 加入的 `@main` 关键字\n\n```swift\nimport SwiftUI\n\n@main\nstruct MeetApp: App {\n var body: some Scene {\n WindowGroup {\n ContentView()\n }\n }\n}\n```\n\n## 布局\n\n### HomeView\n\n首先新建一个 View,`Command + N` 选择 SwiftUI View,命名为 `HomeView.swift`。将 HomeView 修改为如下代码。\n\n```swift\nstruct HomeView: View {\n var body: some View {\n VStack {\n Text(\"我不去想,是否能够成功 ,既然选择了远方 ,便只顾风雨兼程。\")\n .foregroundColor(.blue)\n .padding(.vertical)\n \n Text(\"hasty\")\n }.padding()\n }\n}\n```\n\n![1609116435368](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609116435368.png)\n\n接下来,绘制圆形 Button。在 Swift UI 中绘制图形十分简单,Swift UI 中内置了 `Circle` 组件,只要使用 ZStack 和 Circle 结合,很容易编写这个组件。\n\n```swift\nstruct CircleButtonShape: View {\n var systemImage: String\n var color: Color = .pink\n var body: some View {\n ZStack {\n Circle()\n .fill(color)\n .frame(width: 50, height: 50, alignment: .center)\n .shadow(radius: 3)\n Image(systemName: systemImage).foregroundColor(.white)\n }\n }\n}\n```\n\n这个组件绘制了整个图形,其中 Image 接收一个 SFSymbol 字符串。SF Symbols 可以在[这里下载](https://developer.apple.com/sf-symbols/)。绘制完了图形接下来需要在 View 中使用这个图形,并定位到对应的地点。\n\n在 Swift UI 中,可以使用 ZStack 结合 `.postion` 定位到指定地点。为了获取到整个视窗的长宽,还需要 `GeometryReader` 去读取子 View 的长宽。在根 View 包裹可以获取到设备的长宽。\n\n```swift\n GeometryReader { reader in\n ZStack {\n VStack {\n Text(\"我不去想,是否能够成功 ,既然选择了远方 ,便只顾风雨兼程。\")\n .foregroundColor(.blue)\n .padding(.vertical)\n\n HStack {\n Spacer()\n\n Text(\"hasty\")\n }\n }.padding()\n\n Button(action: {\n // TODO:\n }, label: {\n CircleButtonShape(systemImage: \"arrow.clockwise\")\n })\n .position(x: reader.size.width - 50, y: reader.size.height - 50)\n\n }\n }\n```\n\n\n\n![1609117208574](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609117208574.png)\n\n接下来绘制底部的 ActionView。包含两个 Icon。\n\n```swift\nstruct ActionView: View {\n @State var liked = false\n\n var body: some View {\n HStack(spacing: 20) {\n Button(action: {\n }, label: {\n Image(systemName: liked ? \"suit.heart.fill\" : \"suit.heart\")\n .foregroundColor(liked ? .red : .primary)\n .font(.custom(\"icon\", size: 28))\n })\n Button(action: {\n }, label: {\n Image(systemName: \"square.and.arrow.up\")\n .font(.custom(\"icon\", size: 28))\n .foregroundColor(.primary)\n })\n }\n }\n}\n```\n\n在 HomeView 中 ZStack 末尾添加。\n\n```swift\nActionView().offset(x: 0, y: reader.size.height / 2 - 50)\n```\n\n可以看到如图。\n\n![1609117544917](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609117544917.png)\n\n这里使用了 `.offset` 而不是 `.position` 去定位,是因为使用 position 去定位会丢失 `width: 100%` ,可以理解为 CSS 中 `block` 使用 `absolute` 之后变成了 `inline-block`, 而使用 `.offset` 只是` relative` 中的定位。\n\n### TabView\n\n接下来,绘制底部 Tabbar。在 Swift UI 中使用默认的 Tabbar 极为简单。只需要使用 `TabView` 即可。\n\n在 `xxApp.swift` (为你的 project_nameApp.swift,比如我的 Project 为 Meet,则为 `MeetApp.swift`) 中增加 `TabView`\n\n```swift\nstruct MeetApp: App {\n var body: some Scene {\n WindowGroup {\n TabView {\n ContentView().tabItem { Label(\"遇见\", systemImage: \"circle\") }\n }\n }\n }\n}\n```\n\nTabView 中每个 View 都会在底部 tab 中存在一个 Item,使用 `.tabItem` 定义这个 item 的文字和 image。有且只有一个 text 和 image。我们再新建一个 SwiftUI View 文件,命名为 `LikeView.swift` 。在 `MeetApp.swift` 中增加一个 View。\n\n```swift\n TabView(selection: $activeTabIndex) {\n ContentView().tabItem {\n Label(\"遇见\", systemImage: \"largecircle.fill.circle\")\n }\n\n LikeView().tabItem {\n Label(\"喜欢\", systemImage: \"heart.circle.fill\")\n }\n }\n .accentColor(.pink) // 修改默认主题色\n```\n\n然后我们给 tabItem 增加 tag,让 Swift UI 知道当前选定的 tab 是哪个。如果被选中,修改为 Solid 的 Icon。当然我们可以使用 `@State` 和 `.onTapGesture` 实现。\n\n```swift\n@main\nstruct MeetApp: App {\n @State var activeTabIndex = 0\n \n var body: some Scene {\n return WindowGroup {\n TabView(selection: $activeTabIndex) {\n HomeView().tabItem {\n Label(\"遇见\", systemImage: activeTabIndex != 0 ? \"circle\" : \"largecircle.fill.circle\")\n .onTapGesture {\n activeTabIndex = 0\n }\n }\n .tag(0)\n\n LikeView().tabItem {\n Label(\"喜欢\", systemImage: activeTabIndex != 1 ? \"heart.circle\" : \"heart.circle.fill\")\n .onTapGesture {\n activeTabIndex = 1\n }\n }\n .tag(1)\n }\n .accentColor(.pink)\n }\n }\n}\n```\n\n注意:`.tag` 是不可或缺的。否则无效。\n\n大功告成!\n\n![1609118401778](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1609118401778.png)\n\n下一篇文章,将构建数据层。\n\n(未待完续)", "match_level": "full", "fully_highlighted": false, - "matched_words": ["1"] + "matched_words": [ + "1" + ] }, "title": { "value": "从零开始的 Swift UI (一)", @@ -259,7 +265,9 @@ "value": "记录 9月 至 12月 此网站的更新内容。\n\n# 前端\n\n- 增加了日记音乐自动播放\n- 利用 Socket 实时更新文章的最新内容\n- 利用 Socket 实时更新当前文章的评论\n- 其他优化\n\n# 后端\n\n- 增加了 GraphQL 的支持\n- 其他 Bug 修复", "match_level": "full", "fully_highlighted": false, - "matched_words": ["1"] + "matched_words": [ + "1" + ] }, "title": { "value": "年终更新小记", @@ -284,7 +292,9 @@ "value": "Vue 3 终于在 2020.9.18 发布了第一个正式版「One Piece」,到现在已经一周了。终于有时间来体验一把正式版的 Vue 3 是什么样子了。\n\n## 准备\n\n### 初始化项目\n\n这次,我不再使用 vite 来建立项目,而使用 vue-cli。\n\n```sh\nvue create vue3-blog\ncd vue3-blog\nvue add typescript\nyarn add vue-router@next\nyarn add vuex@next\nyarn serve\n```\n\n注意在选择 vue 版本的时候选择 vue3-preview\n\n```sh\n? Please pick a preset: Default (Vue 3 Preview) ([Vue 3] babel, eslint)\n```\n\n首先打开 App.vue,清理一下默认的模板,如下\n\n```vue\n\n\n\n\n\n\n```\n\n注:除了这个文件使用 `.vue` 后缀之外,其他一律文件采用 `tsx` 编写。\n\n### 引入路由\n\n在 `src` 目录新建一个 `router.ts`,写入如下代码\n\n```ts\n/*\n * @Author: Innei\n * @Date: 2020-09-25 15:16:26\n * @LastEditTime: 2020-09-25 15:31:18\n * @LastEditors: Innei\n * @FilePath: /vue3-blog/src/router.ts\n * @Mark: Coding with Love\n */\nimport { defineAsyncComponent } from 'vue'\nimport { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'\n\nconst routes: RouteRecordRaw[] = [\n {\n name: 'root',\n path: '/',\n component: () => import('./App.vue'),\n children: [\n {\n path: '/',\n component: defineAsyncComponent(() =>\n import('./views/home').then(mo => mo.HomeView),\n ),\n name: 'home',\n },\n ],\n },\n]\n\nexport const router = createRouter({\n history: createWebHashHistory(),\n routes,\n})\n\nrouter.beforeEach((before, to, next) => {\n // todo\n next()\n})\n\nexport default router\n\n```\n\n写法略微和 vue2-router 有点不同。\n\n接下来来写一个视图(view)。新建一个目录`views`,新建`home/index.tsx`。\n\n写如下代码。\n\n```tsx\nimport { defineComponent, ref } from 'vue'\n\nexport const HomeView = defineComponent({\n setup() {\n const names = ref([{ name: 'foo' }, { name: 'bar' }])\n\n return () => (\n
\n

HomeView

\n\n
    \n {names.value.map(item => {\n return
  • {item.name}
  • \n })}\n
\n
\n )\n },\n})\n\n```\n\n执行`yarn serve`之后,应该会显示如下。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1601026508347.png)\n\n## 数据\n\n如果使用 vue 3 composition api 的写法,所有的数据操作都发生在 setup 函数。写法类似于 react hooks。\n\n接下来我以调用 api 获取文章标题,渲染一个列表为例,填一填遇到的坑。\n\n代码如下\n\n```tsx\nimport { useApi } from '@/hooks/useApi'\nimport { PostResModel } from '@/models/post'\nimport { defineComponent, ref } from 'vue'\n\nexport const HomeView = defineComponent({\n setup() {\n const api = useApi()\n const posts = ref([])\n\n api('Post')\n .gets(1, 10)\n .then(res => {\n const data = res.data\n // posts.push(...data)\n posts.value = data\n })\n\n return () => (\n
\n

HomeView

\n\n
    \n {posts.value.map(post => {\n return
  • {post.title}
  • \n })}\n
\n
\n )\n },\n})\n\n```\n\napi 的部分暂时忽略,返回为的 response 为一个 `data` 的数组。包括了 `title` 的字段。像上面的写法是可以达到预期效果的。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1601027103296.png)\n\n但是有几个达不到预期的写法,在这里也提一下。\n\n首先是数据的更改的时候。\n\n如果用了 `reactive` 包裹了 data,如:\n\n```ts\n// const posts = ref([])\nlet posts = reactive([])\n```\n\n那么,想要在获取数据之后改变 `posts` 中的值,貌似只能用 `posts.push()` 的方式,以下方式会失去响应式。\n\n```ts\nposts = res.data // 不能达到预期\nposts = reactive(res.data) // 不能达到预期\nposts.push(...res.data) // 可以\n```\n\n但是如果用 `ref`。那就可以这样写了。\n\n```ts\nposts.value = res.data\n// or\nposts.value.push(...res.data)\n```\n\n注意,ref 需要通过 `.value` 获取被 proxy 的值。\n\n个人认为,一般的对象可以用 `reactive` wrap,而 array 以及原始类型可以用 `ref` wrap。`reactive` 的好处是不用多写一个 `.value`。\n\n\n\n\n\n**未待完续**", "match_level": "full", "fully_highlighted": false, - "matched_words": ["1"] + "matched_words": [ + "1" + ] }, "title": { "value": "Vue 3 正式发布,再度踩坑", @@ -309,7 +319,9 @@ "value": "为什么要使用位操作,因为位操作是直接操作二进制数,是所有语言中执行效率最高的运算。\n\n以下代码以 JavaScript 为例,部分代码在所有支持位操作的语言通用。\n\n注:JavaScript 中数值以 IEEE 754 双精度浮点数表示。\n\n**快速取整**:\n\n```js\nparseInt(2.2) === ~~2.2 // true\nparseInt('1.3') === ~~'1.3' // true\n1<<30 === ~~1<<30 // true\n\nMath.floor(1.2) == 1.2 | 0\nMath.floor(1.2) == 1.2 ^ 0\n\nMath.floor(-1.2) == -1.2 | 0\nMath.floor(-1.2) == -1.2 ^ 0\n\n// HINT\n\nparseInt('4294967296') === ~~4294967296 //false, 越界\nparseInt('4294967296') === ~~'4294967296' //false, 越界\n```\n\n**快速累加**\n\n```js\n-~undefined === 1 // true\n-~0 === 1 // true\n-~1 === 2 // true\n-~-1 === 0 // true, -~-1 为 -0\n\n-~2<<30 === 2<<29+1 //false\n```\n\n**是否奇数**\n\n```js\n1 & 1 === 1 // 1 为奇数\n2 & 1 === 0 // 0 为偶数\n3 & 1 === 1 // etc.\n```\n\n**权限**\n\n```js\n// 比如我有 2 3 4 号权限\npermission = 1 << 2 | 1 << 3 | 1 << 4\n// 现在判断我有没有 3 号权限\nhasPerssion3 = !!(permission & 1 << 3) // res is 8, true\nhasPerssion5 = !!(permission & 1 << 5) // res is 0, false\nhasPerssion0 = !!(permission & 1 << 0) // res is 0, false\n\n```\n\n注意最大边界为 `1 << 30`, 更大需要用 `BigInt`\n\n**获取数组中只出现一次的数字**\n\n1. 交换律:a ^ b ^ c <=> a ^ c ^ b\n1. 任何数于0异或为任何数 0 ^ n => n\n1. 相同的数异或为0: n ^ n => 0\n\n\n```js\nlet res = 0\narr.forEach(i => res ^= i)\n// return res\n```\n\n**0-1互转**\n\n```js\n0^1 == 1\n1^1 == 0\n```\n\n**两数中点**\n\n```js\nleft + ((right - left) >> 1) === (left + right) / 2 // int\n```\n\n\n\n**持续更新,有更好的用法欢迎评论区指出**", "match_level": "full", "fully_highlighted": false, - "matched_words": ["1"] + "matched_words": [ + "1" + ] }, "title": { "value": "每天一个劝退小技巧之位操作", @@ -334,7 +346,9 @@ "value": "在很多时候都需要处理文字的溢出,尤其是对单行网格处理时,需要避免文字过长导致容器撑坏的情况。一般会固定文字最大宽度和 `overflow: hidden; text-overflow: ellipsis` 让溢出的文字显示成 `...`。但是现在可以用 CSS 的 mark 属性,让溢出的文字边缘羽化。\n\n如图 Chrome 的 tab。\n\n![¡边缘羽化](https://gitee.com/xun7788/my-imagination/raw/master/uPic/u6iu7c.png)\n\n首先看看 `mark-image` 的兼容性。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/AiXPO6.png)\n\n基本上都支持,需要注意的是我使用的 Chrome 85,还在试验性阶段,需要加上前缀 `-webkit-`\n\n`mask-image` 和 `background-image` 的值一样,和蒙版一样,黑色的显示,透明的不显示。我们可以很简单的用 `linear-gradient` 完成边缘羽化效果。\n\n我们来模仿一个 Chrome Tab 的样式。首先建立一个骨架。\n\n```html\n
\n
\n 一个标题很长的标签 一个标题很长的标签 一个标题很长的标签\n 一个标题很长的标签\n
\n
×
\n
\n```\n\n确定好外层容器的宽高后,可以对 `span` 的父元素设置 `mask`。\n\n```css\n.tab-wrap .tab {\n width: 100%;\n overflow: hidden;\n -webkit-mask-image: linear-gradient(\n to right,\n rgba(0, 0, 0, 1) calc(100% - 2em),\n transparent\n );\n mask-image: linear-gradient(\n to right,\n rgba(0, 0, 0, 1) calc(100% - 2em),\n transparent\n );\n}\n.tab .tab-text {\n white-space: nowrap;\n}\n```\n\n最后再加亿点点小细节,大功告成啦。\n\n![](https://gitee.com/xun7788/my-imagination/raw/master/uPic/tLQusO.png)\n\n当然啦,如果遇到不支持的浏览器就显示直接截断的效果,很不好看,我们还想要让他显示 `...`,那么可以用 `@supports` 查询,是否支持这个属性,如果支持才使用,不支持就使用 `text-overflow: ellipsis;`。\n\n修改一下,`span` 的父级样式。\n\n```css\n.tab-wrap .tab {\n width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n@supports (-webkit-mask-image: inherit) or (mask-image: inherit) {\n .tab-wrap .tab {\n text-overflow: clip;\n -webkit-mask-image: linear-gradient(\n to right,\n rgba(0, 0, 0, 1) calc(100% - 2em),\n transparent\n );\n mask-image: linear-gradient(\n to right,\n rgba(0, 0, 0, 1) calc(100% - 2em),\n transparent\n );\n }\n}\n```\n\n![¡如果不支持 mask](https://gitee.com/xun7788/my-imagination/raw/master/uPic/lTbVx0.png)\n\n完整的代码请戳: [Gist](https://gist.github.com/Innei/d8dcaebe9ac919c4a1d0462b2f0ef6b8)", "match_level": "full", "fully_highlighted": false, - "matched_words": ["1"] + "matched_words": [ + "1" + ] }, "title": { "value": "文字溢出边缘羽化 CSS Mask 实现", @@ -359,7 +373,9 @@ "value": "好的代码,往往不是又臭又长,而是小而精悍。用更少的代码,实现相同的功能。不管是自己还是同事日后阅读,都不会感到困惑。\n\n在这里,记录前端开发中,优化代码的几种方式。\n\n**噩梦地狱嵌套**\n\n大概大家都看过怎么一张图\n\n![RkwiBh](https://gitee.com/xun7788/my-imagination/raw/master/uPic/RkwiBh.jpg)\n\n在遇到这个写法的时候, 首先要想一想这样到底有没有用,有没有办法优化。像图中的根本就没有必要去嵌套。\n\n再来看个例子。\n\n```ts\nconst payload: | undefined = {\n page: '1',\n size: 10\n} // payload from server via request\nif (payload) {\n if (typeof payload.page === 'number' && typeof payload.size === 'number') {\n // do anything..\n }\n}\n// do anything..\n```\n\n这是很简单的数据验证,一般用于后端防止 noSQL 注入。\n\n简单的写法如下\n\n```ts\nif (!payload) {\n // do anything..\n // throw a bad request error\n}\nif (typeof payload.page !== 'number' || typeof payload.size !== 'number') {\n // do anything..\n // throw a 422 error\n}\n// do anything\n```\n\n这样就有效避免了嵌套。\n\n**糟糕的 if 判断**\n\n不知道这种写法,你们见的多不多。\n\n```ts\nif (a === 1 || b === 1 || c === 1) {\n // do anything...\n}\n```\n\n可以转换成\n\n```ts\nif ([a, b, c].includes(1)) {\n}\n```\n\n**表格选择法**\n\n你还是使用大量 `if`, 或者 `switch`,作为分支选择值吗,那就显得很没水平了,快来试试表格选择。\n\n```ts\n// switch\nlet week;\n\nswitch (week) {\n case 0:\n week = '周日';\n break;\n case 1:\n week = '周一';\n // ...\n default:\n break;\n}\n\n// convert to \nlet week = 0\nweek = ['日', '一', '二', '三', '四', '五', '六'][week]\n\n```\n\n更复杂一点? 当然可以,而且更加简洁。这里以判断文件类型为例。\n\n```ts\n const checkTypes = (ext: string) => {\n const i18n = {\n VIDEO: '视频',\n TXT: '文本',\n MUSIC: '音乐',\n DOC: '文档',\n EXL: '表格',\n PPT: '幻灯片',\n PDF: '图书',\n CODE: '代码',\n IMG: '图片',\n FILE: '文件',\n };\n return i18n[\n Object.entries(FileTypes).find(([key, val]) => {\n if (val.includes(ext)) {\n return key;\n }\n return false;\n })?.[0] || 'FILE'\n ];\n};\n\nconst FileTypes = Object.freeze({\n VIDEO: ['.mp4', '.avi', '.mov', '.mpg'],\n TXT: ['.txt'],\n MUSIC: ['.mp3'],\n DOC: ['.doc', '.docx'],\n EXL: ['.xls'],\n PPT: ['.ppt', '.pptx'],\n PDF: ['.pdf'],\n CODE: [\n '.js',\n '.c',\n '.cpp',\n '.py',\n '.html',\n '.css',\n '.scss',\n '.xml',\n '.swift',\n '.ts',\n '.java',\n '.go',\n '.asp',\n '.aspx',\n '.class',\n '.clw',\n '.cs',\n '.dsp',\n '.dsw',\n '.frm',\n '.frx',\n '.h',\n '.hpp',\n '.jar',\n '.lib',\n '.ocx',\n '.pyc',\n '.vbp',\n '.vbs',\n '.xsl',\n ],\n IMG: ['.png', '.jpg', '.jpeg', '.gif', '.svg'],\n});\n\nexport default FileTypes;\n\n```\n\n以上是我临时想到的,之后继续补充。", "match_level": "full", "fully_highlighted": false, - "matched_words": ["1"] + "matched_words": [ + "1" + ] }, "title": { "value": "编写更加简洁易阅读的代码", @@ -384,7 +400,9 @@ "value": "前几天,我在推特上看到这样一张图。\n\n![ezgif-6-2b27134bbba1](https://gitee.com/xun7788/my-imagination/raw/master/uPic/ezgif-6-2b27134bbba1.gif)\n\n原来地址栏还能这么玩,瞬间就觉得自己弱爆了。然后我决定去实现一下这个效果,然后做成一个库。\n\n花了一个晚上,终于做好了。这是最后的成果。\n\n![2020-08-1519.20.53](https://gitee.com/xun7788/my-imagination/raw/master/uPic/2020-08-15%2019.20.53.gif)\n\n这个库使用非常的简单。\n\n你只需要,\n\n```sh\nyarn add animate-uri\n```\n\n然后\n\n```js\nimport { animateUriFactory, bindAllLink } from 'animate-uri'\n\nanimateUriFactory({ duration: 60, shouldPushState: false }).start(\n '/hello-world',\n '/',\n)\n```\n\n这样就是一个简单的过渡效果了。\n\n玩玩可没有意思,在项目中使用才有意思。\n\n接下来我们在 Next.js 项目中加入一个好玩的东西。\n\n在 nextjs 中的自定义 `_app.tsx` 中加入如下,监听路由变化。\n\n\n```tsx\nimport { animateUriFactory } from 'animate-uri/publish/index.esm'\nconst animateInstance = animateUriFactory()\n\n// componentDidMount(): void {\nRouter.events.on('routeChangeStart', (url) => {\n animateInstance?.start(url)\n})\n\nRouter.events.on('routeChangeComplete', () => {\n animateInstance?.stop()\n})\n\n// }\n```\n\n大功告成。\n\n随便偷偷说一下仓库地址:[animate-uri](https://github.com/Innei/animate-uri)", "match_level": "full", "fully_highlighted": false, - "matched_words": ["1"] + "matched_words": [ + "1" + ] }, "title": { "value": "不同寻常的地址栏过渡", @@ -414,7 +432,9 @@ "value": "说起 Electron,大家能定不会感觉到陌生,庞大的体积,内置浏览器,Hello World 都有 200+M... 我个人是很反感跨段应用的,虽然对于开发来说,节省了很多时间,但是站在用户的角度来讲,体验就不是那么称心如意了。但是最近一些业务需要用到 Electron,折腾过程中也踩了不少坑,总结一下。\n\n ## 开发环境的搭建\n\n平时我们在开发前端应用时,一般都是使用 Webpack 去打包,在开发环境中,也是由 Webpack dev server 来实现 HMR。在 Electron 中也是可以使用 Webpack 的。\n\n我们使用 `electron-wepack` 包,简单搭建一下环境。\n\n```shell\nyarn add source-map-support\nyarn add -D electron electron-webpack electron-builder webpack \n```\n\n然后我们参考这个项目结构建立目录:\n\n```\nproject/\n├─ resources/\n│ ├─ icon \t\t\t\t\t\t// 程序图标\n├─ src/\n│ ├─ main/ \t\t\t\t\t// 主进程\n│ │ └─ index.ts\n│ ├─ renderer/ \t\t\t// 渲染层(启动界面)\n│ └─ index.js\n└─ static/ \t\t\t\t\t\t // 静态资源\n```\n\n`src` 目录下的分别为存放 Electron 主进程逻辑(main) 和 渲染层(renderer)。入口文件必须为 `index` 或 `main`\n\n### TypeScript 支持 (可选)\n\n```shell\nyarn add electron-webpack-ts typescript -D\n```\n\n安装完以上依赖,`electron-webpack` 会识别支持 TypeScript。\n\n### 渲染层\n\n在 `src/renderer/index.ts` 中,你可以操作 DOM 树。`electron-wepack`默认会提供一个空白的 HTML 文档,只有一个 `#app` 节点供你使用,你无法通过一般操作自定义一个入口 `index.html`, 但是你也可以用其他手段达到这个目标,在此不多赘述 (参看 issue)。\n\n```ts\n// src/renderer/index.ts\nconst $app = document.getElementById('app')!\n\n$app.textContent = 'Hello World'\n\n```\n\n### 主进程\n\n在 `src/main/index.ts` 中, 简单建立一个 app\n\n```ts\nimport { app, BrowserWindow } from 'electron'\nimport { createWindow } from './common/window'\n\nlet mainWindow: BrowserWindow\napp.on('ready', () => {\n mainWindow = createWindow()\n app.show()\n})\n\napp.on('window-all-closed', () => {\n if (process.platform !== 'darwin') {\n app.quit()\n }\n})\n\napp.on('activate', function () {\n if (mainWindow === null) {\n createWindow()\n }\n})\n```\n\n其次是 `window.ts`,建立一个 window\n\n```ts\nimport { BrowserWindow } from 'electron'\nimport path from 'path'\nimport { isDev } from '../utils'\nimport { format } from 'url'\nexport function createWindow() {\n const mainWindow = new BrowserWindow({\n height: 620,\n width: 400,\n webPreferences: { nodeIntegration: true }, // 一定要加!!!\n })\n if (isDev) {\n mainWindow.loadURL(\n `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`, // 开发环境\n )\n } else {\n mainWindow.loadURL(\n format({\n pathname: path.join(__dirname, 'index.html'),\n protocol: 'file',\n slashes: true,\n }),\n )\n }\n return mainWindow\n}\n\n```\n\n### 脚本\n\n在 `package.json` 中添加。\n\n```json\n{\n \"scripts\": {\n \"prebuild\": \"rm -rf dist\",\n \"build\": \"cross-env NODE_ENV=production electron-webpack\",\n \"start\": \"electron-webpack dev\",\n \"package\": \"yarn build && electron-builder build --publish never\"\n }\n}\n```\n\n执行 `yarn start` 。发现正确显示了 Hello World。\n\n![oihA1t](https://cdn.jsdelivr.net/gh/Innei/img-bed@master/uPic/oihA1t.png)\n\n使用 `yarn package` 来生成 dmg 也是没有问题的。一般教程到此就结束了,但是我们的需求并不是这么简单,我们还需要配置其他,比如 app version,app icon,app sign key... 而这些配置也有很多坑。\n\n## 配置\n\n### 图标\n\n应用图标需要不同大小的几张 png 以及 icns 等格式的图片,手动操作比较麻烦,我们可以用一张 png 去生成,使用 ` electron-icon-builder` 工具就能轻松转换到我们想要的结果。\n\n```\nnpx electron-icon-builder -i ./path-your-icon-file.png -o output\n```\n\n```\n.\n├── icon.icns\n├── icon.ico\n├── icon.png\n└── icons\n ├── 1024x1024.png\n ├── 128x128.png\n ├── 16x16.png\n ├── 24x24.png\n ├── 256x256.png\n ├── 32x32.png\n ├── 48x48.png\n ├── 512x512.png\n └── 64x64.png\n```\n\n把生成的文件放入 `resources` 文件夹内,如不存在则新建。\n\n![jq8DsT](https://cdn.jsdelivr.net/gh/Innei/img-bed@master/uPic/jq8DsT.png)\n\n在 `package.json` 中加入 `build` 字段,用于配置 `electron-builder`。\n\n```json\n{\n \"build\": {\n \"appId\": \"com.innei.electron-template\",\n \"productName\": \"template\",\n \"extraMetadata\": {\n \"main\": \"main.js\" // **必须** \n },\n \"copyright\": \"Copyright © 2019-2020 ${author}\",\n \"mac\": {\n \"category\": \"public.app-category.utilities\"\n },\n \"files\": [\n \"package.json\",\n \"resources/**/*\", \t\t\t\t\t\t// **必须** \n \"static\", \n {\n \"from\": \"dist/main\" \t // **必须** \n },\t\n {\n \"from\": \"dist/renderer\" // **必须** \n }\n ],\n \"extends\": null,\n \"dmg\": {\n \"contents\": [\n {\n \"x\": 130,\n \"y\": 220\n },\n {\n \"x\": 410,\n \"y\": 220,\n \"type\": \"link\",\n \"path\": \"/Applications\"\n }\n ]\n },\n \"win\": {\n \"icon\": \"resources/icon.ico\",\n \"target\": [\n \"nsis\",\n \"msi\"\n ]\n },\n \"nsis\": {\n \"oneClick\": false,\n \"allowToChangeInstallationDirectory\": true,\n \"installerIcon\": \"resources/icon.ico\"\n },\n \"linux\": {\n \"target\": [\n \"deb\",\n \"rpm\",\n \"AppImage\"\n ],\n \"category\": \"Development\"\n },\n \"directories\": {\n \"buildResources\": \"resources\",\n \"output\": \"release\"\n },\n \"extraResources\": [\n {\n \"from\": \"resources/\", // **必须** \n \"to\": \"resources/\" // **必须** \n },\n {\n \"from\": \"static\",\n \"to\": \"static\"\n }\n ]\n }\n}\n```\n\n这里是个大坑,因为我们自定义了配置,覆盖了原来 `electron-webpack` 的配置,所以有几个地方是必须要这么写的,否则就会在打包之后无法显示 renderer 或者 找不到入口文件。这是我自己摸索出来的,比较 hack 的方法。因为我实在找不到答案。\n\n如果你需要使用 `__static ` 常量的话,\n\n```\n{ // 也是必须的\n \"from\": \"static\",\n \"to\": \"static\"\n}\n```\n\n最后,附上 GitHub 地址:\n\n", "match_level": "full", "fully_highlighted": false, - "matched_words": ["1"] + "matched_words": [ + "1" + ] }, "id": { "value": "5f0dc4dbddf2006d12774b6a", @@ -443,4 +463,4 @@ "size": 10, "total_page": 12 } -} +} \ No newline at end of file