From 0103281cd447602c8ccb06687b46d8c9258dfec8 Mon Sep 17 00:00:00 2001 From: Ching Date: Sun, 19 Mar 2023 21:15:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(InstanceInfoView,=20Instance=20Model,=20In?= =?UTF-8?q?stanceViewModel):=20=E9=80=9A=E8=BF=87=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=E5=9C=B0=E5=9D=80=E8=8E=B7=E5=8F=96=E5=AE=9E=E4=BE=8B=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=B9=B6=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 通过实例地址获取实例信息并展示 Signed-off-by: Ching --- DUDUJI.xcodeproj/project.pbxproj | 16 ++- .../xcschemes/xcschememanagement.plist | 2 +- DUDUJI/Managers/InstanceAppManager.swift | 15 +-- DUDUJI/MastodonService.swift | 36 +++--- DUDUJI/Models/Instance.swift | 115 ++++++++++++++++++ DUDUJI/Models/InstanceApp.swift | 2 +- DUDUJI/ViewModels/InstanceAppViewModel.swift | 4 +- DUDUJI/ViewModels/InstanceViewModel.swift | 32 +++++ DUDUJI/Views/AddAccountView.swift | 32 ++++- DUDUJI/Views/InstanceInfoView.swift | 29 +++++ 10 files changed, 240 insertions(+), 43 deletions(-) create mode 100644 DUDUJI/Models/Instance.swift create mode 100644 DUDUJI/ViewModels/InstanceViewModel.swift create mode 100644 DUDUJI/Views/InstanceInfoView.swift diff --git a/DUDUJI.xcodeproj/project.pbxproj b/DUDUJI.xcodeproj/project.pbxproj index 575bae7..3a85947 100644 --- a/DUDUJI.xcodeproj/project.pbxproj +++ b/DUDUJI.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 24BB349029C5F28100A52D20 /* InstanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24BB348F29C5F28100A52D20 /* InstanceViewModel.swift */; }; 24D4D3B529AB83E80064E566 /* DUDUJIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D4D3B429AB83E80064E566 /* DUDUJIApp.swift */; }; 24D4D3B729AB83E80064E566 /* DUDUJIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D4D3B629AB83E80064E566 /* DUDUJIView.swift */; }; 24D4D3B929AB83E90064E566 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 24D4D3B829AB83E90064E566 /* Assets.xcassets */; }; @@ -21,9 +22,12 @@ 24D4D3D629AFA1970064E566 /* InstanceAppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D4D3D529AFA1970064E566 /* InstanceAppManager.swift */; }; 24D4D3DA29B2EB7B0064E566 /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 24D4D3D929B2EB7B0064E566 /* Moya */; }; 24D4D3DC29B2EC160064E566 /* MastodonService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D4D3DB29B2EC160064E566 /* MastodonService.swift */; }; + 24D4D3DE29B6341A0064E566 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D4D3DD29B6341A0064E566 /* Instance.swift */; }; + 24D4D3E029B640B20064E566 /* InstanceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D4D3DF29B640B20064E566 /* InstanceInfoView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 24BB348F29C5F28100A52D20 /* InstanceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceViewModel.swift; sourceTree = ""; }; 24D4D3B129AB83E80064E566 /* DUDUJI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DUDUJI.app; sourceTree = BUILT_PRODUCTS_DIR; }; 24D4D3B429AB83E80064E566 /* DUDUJIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DUDUJIApp.swift; sourceTree = ""; }; 24D4D3B629AB83E80064E566 /* DUDUJIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DUDUJIView.swift; sourceTree = ""; }; @@ -38,6 +42,8 @@ 24D4D3D329AF97A40064E566 /* InstanceAppViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceAppViewModel.swift; sourceTree = ""; }; 24D4D3D529AFA1970064E566 /* InstanceAppManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InstanceAppManager.swift; path = DUDUJI/Managers/InstanceAppManager.swift; sourceTree = SOURCE_ROOT; }; 24D4D3DB29B2EC160064E566 /* MastodonService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MastodonService.swift; path = DUDUJI/MastodonService.swift; sourceTree = SOURCE_ROOT; }; + 24D4D3DD29B6341A0064E566 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = ""; }; + 24D4D3DF29B640B20064E566 /* InstanceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceInfoView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -73,9 +79,9 @@ isa = PBXGroup; children = ( 24D4D3DB29B2EC160064E566 /* MastodonService.swift */, - 24D4D3D729B10FB90064E566 /* Managers */, - 24D4D3D229AF8E9D0064E566 /* ViewModels */, 24D4D3D129ABC0280064E566 /* Views */, + 24D4D3D229AF8E9D0064E566 /* ViewModels */, + 24D4D3D729B10FB90064E566 /* Managers */, 24D4D3C529ABB5830064E566 /* Models */, 24D4D3C829ABB7590064E566 /* Const.swift */, 24D4D3B429AB83E80064E566 /* DUDUJIApp.swift */, @@ -101,6 +107,7 @@ 24D4D3C329ABAE5A0064E566 /* AccountInfo.swift */, 24D4D3C629ABB6500064E566 /* OauthToken.swift */, 24D4D3CD29ABBC030064E566 /* InstanceApp.swift */, + 24D4D3DD29B6341A0064E566 /* Instance.swift */, ); path = Models; sourceTree = ""; @@ -109,6 +116,7 @@ isa = PBXGroup; children = ( 24D4D3CF29ABC01E0064E566 /* AddAccountView.swift */, + 24D4D3DF29B640B20064E566 /* InstanceInfoView.swift */, ); path = Views; sourceTree = ""; @@ -117,6 +125,7 @@ isa = PBXGroup; children = ( 24D4D3D329AF97A40064E566 /* InstanceAppViewModel.swift */, + 24BB348F29C5F28100A52D20 /* InstanceViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -207,6 +216,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 24BB349029C5F28100A52D20 /* InstanceViewModel.swift in Sources */, + 24D4D3DE29B6341A0064E566 /* Instance.swift in Sources */, 24D4D3C429ABAE5A0064E566 /* AccountInfo.swift in Sources */, 24D4D3C729ABB6500064E566 /* OauthToken.swift in Sources */, 24D4D3CE29ABBC030064E566 /* InstanceApp.swift in Sources */, @@ -215,6 +226,7 @@ 24D4D3B529AB83E80064E566 /* DUDUJIApp.swift in Sources */, 24D4D3C929ABB7590064E566 /* Const.swift in Sources */, 24D4D3D029ABC01E0064E566 /* AddAccountView.swift in Sources */, + 24D4D3E029B640B20064E566 /* InstanceInfoView.swift in Sources */, 24D4D3D629AFA1970064E566 /* InstanceAppManager.swift in Sources */, 24D4D3D429AF97A40064E566 /* InstanceAppViewModel.swift in Sources */, ); diff --git a/DUDUJI.xcodeproj/xcuserdata/ching.xcuserdatad/xcschemes/xcschememanagement.plist b/DUDUJI.xcodeproj/xcuserdata/ching.xcuserdatad/xcschemes/xcschememanagement.plist index dec2069..9289e74 100644 --- a/DUDUJI.xcodeproj/xcuserdata/ching.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/DUDUJI.xcodeproj/xcuserdata/ching.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ DUDUJI.xcscheme_^#shared#^_ orderHint - 1 + 0 ReactiveSwift (Playground) 1.xcscheme diff --git a/DUDUJI/Managers/InstanceAppManager.swift b/DUDUJI/Managers/InstanceAppManager.swift index 2849382..0c5e59a 100644 --- a/DUDUJI/Managers/InstanceAppManager.swift +++ b/DUDUJI/Managers/InstanceAppManager.swift @@ -12,23 +12,16 @@ class InstanceAppManager { return ("123", "321") } - func getInstanceInfo(at serverAddress: String) { - let client = MastodonClient(baseURL: serverAddress) - client.getInstance() -// print("服务器信息:\()") - } - - func createInstanceApp(at serverAddress: String) { + func createInstanceApp(at serverAddress: String) -> InstanceApp { print("服务器地址是\(serverAddress)") - // save things to InstanceApp model - // let instanceApp = InstanceApp(id: xxx....) let instanceApp = InstanceApp( - name: DUDUConst.name, + // name: DUDUConst.name, website: nil, redirectUri: serverAddress, clientId: "abc", clientSecret: "cba") // save the instanceApp to database - print("客户端名称:\(instanceApp.name)") + print("客户端名称:\(instanceApp.clientId)") + return instanceApp } } diff --git a/DUDUJI/MastodonService.swift b/DUDUJI/MastodonService.swift index cd64acc..87ce795 100644 --- a/DUDUJI/MastodonService.swift +++ b/DUDUJI/MastodonService.swift @@ -23,7 +23,7 @@ extension MastodonSevice: TargetType { var path: String { switch self { case .instance: - return "/api/v2/instance" + return "/api/v1/instance" } } @@ -65,30 +65,22 @@ class MastodonClient { }) } - func getInstance() { -// var instanceData = [Any]() - service.request(.instance) { result in - switch result { - case let .success(response): - do { -// let data = try response.map(Data.self) - let json = try response.mapJSON() -// let stringData = String(data: data, encoding: .utf8) -// print(stringData) -// print("服务器信息:\(String(describing: stringData))") - if let dict = json as? [String: Any], - let title = dict["title"] as? String - { - print(title) + func getInstance() async throws -> Instance { + return try await withCheckedThrowingContinuation { continuation in + service.request(.instance) { result in + switch result { + case .success(let response): + do { + let instance = try JSONDecoder().decode(Instance.self, from: response.data) + print("instance: \(instance.title)") + continuation.resume(returning: instance) + } catch { + continuation.resume(throwing: error) } - - } catch { - print("Error parsing JSON, response: \(response)") + case .failure(let error): + continuation.resume(throwing: error) } - case let .failure(error): - print(error.localizedDescription) } } -// return instanceData } } diff --git a/DUDUJI/Models/Instance.swift b/DUDUJI/Models/Instance.swift new file mode 100644 index 0000000..a0d53a2 --- /dev/null +++ b/DUDUJI/Models/Instance.swift @@ -0,0 +1,115 @@ +// +// Instance.swift +// DUDUJI +// +// Created by ching on 2023/3/6. +// + +import Foundation + +struct Instance: Decodable { + public let title: String + public let url: URL + public let userCount: Int? + public let statusCount: Int? + public let thumbnail: URL? + public let shortDescription: String? + public let description: String? + + enum CodingKeys: String, CodingKey { + case title + case url = "uri" + case stats + case thumbnail + case shortDescription = "short_description" + case description + } + + enum StatsCodingKeys: String, CodingKey { + case userCount = "user_count" + case statusCount = "status_count" + } + + private struct Container: Decodable { + let title: String + let url: URL + let thumbnail: URL? + let shortDescription: String? + let description: String? + let stats: Stats? + + private enum CodingKeys: String, CodingKey { + case title + case url = "uri" + case thumbnail + case shortDescription = "short_description" + case description + case stats + } + + struct Stats: Decodable { + let userCount: Int? + let statusCount: Int? + + private enum CodingKeys: String, CodingKey { + case userCount = "user_count" + case statusCount = "status_count" + } + } + } + + init(from url: URL) throws { + let data = try Data(contentsOf: url) + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + let container = try decoder.decode(Container.self, from: data) + title = container.title + self.url = container.url + thumbnail = container.thumbnail + shortDescription = container.shortDescription + description = container.description + userCount = container.stats?.userCount + statusCount = container.stats?.statusCount + } + + init(from decoder: Decoder) throws { + print("into decoder") + let container = try decoder.container(keyedBy: CodingKeys.self) + + title = try container.decode(String.self, forKey: .title) + url = try container.decode(URL.self, forKey: .url) + thumbnail = try container.decodeIfPresent(URL.self, forKey: .thumbnail) + shortDescription = try container.decodeIfPresent(String.self, forKey: .shortDescription) + description = try container.decodeIfPresent(String.self, forKey: .description) + + if let statsContainer = try? container.nestedContainer(keyedBy: StatsCodingKeys.self, forKey: .stats) { + userCount = try statsContainer.decodeIfPresent(Int.self, forKey: .userCount) + statusCount = try statsContainer.decodeIfPresent(Int.self, forKey: .statusCount) + } else { + userCount = nil + statusCount = nil + } + print("title: \(title)") + } + + init(title: String, url: URL, userCount: Int? = nil, statusCount: Int? = nil, thumbnail: URL? = nil, shortDescription: String? = nil, description: String? = nil) { + self.title = title + self.url = url + self.userCount = userCount + self.statusCount = statusCount + self.thumbnail = thumbnail + self.shortDescription = shortDescription + self.description = description + } + + static func defaultInstance() -> Instance { + Instance(title: "Example", url: URL(string: "http://example.com")!, userCount: 100, statusCount: 200, thumbnail: nil, shortDescription: "This is an example.", description: "This is a longer description.") + } + + static func instanceFromURL(url: String) async throws -> Instance { + let client = MastodonClient(baseURL: url) + let newInstance = try await client.getInstance() + return newInstance + } +} diff --git a/DUDUJI/Models/InstanceApp.swift b/DUDUJI/Models/InstanceApp.swift index a65ad01..51c1121 100644 --- a/DUDUJI/Models/InstanceApp.swift +++ b/DUDUJI/Models/InstanceApp.swift @@ -8,7 +8,7 @@ import Foundation struct InstanceApp { - public let name: String +// public let name: String public let website: URL? public let redirectUri: String public let clientId: String diff --git a/DUDUJI/ViewModels/InstanceAppViewModel.swift b/DUDUJI/ViewModels/InstanceAppViewModel.swift index 5593f17..4304776 100644 --- a/DUDUJI/ViewModels/InstanceAppViewModel.swift +++ b/DUDUJI/ViewModels/InstanceAppViewModel.swift @@ -9,14 +9,14 @@ import SwiftUI class InstanceAppViewModel: ObservableObject { private let instanceAppManager = InstanceAppManager() + @Published private var instance: Instance? // MARK: intents /// 提交实例地址 /// 在该实例中创建对应的 App /// - Parameter url: 实例地址 - func submitServerAddress(with url: String) { - instanceAppManager.getInstanceInfo(at: url) + func submitServerAddress(with url: String) -> InstanceApp { instanceAppManager.createInstanceApp(at: url) } } diff --git a/DUDUJI/ViewModels/InstanceViewModel.swift b/DUDUJI/ViewModels/InstanceViewModel.swift new file mode 100644 index 0000000..5f43b79 --- /dev/null +++ b/DUDUJI/ViewModels/InstanceViewModel.swift @@ -0,0 +1,32 @@ +// +// InstanceViewModel.swift +// DUDUJI +// +// Created by ching on 2023/3/18. +// + +import Foundation + +class InstanceViewModel: ObservableObject { + @Published var instance: Instance = .defaultInstance() + @Published var isLoading: Bool = false + + func getInstance(from url: String) async { + DispatchQueue.main.async { + self.isLoading = true + } + do { + let newInstance = try await Instance.instanceFromURL(url: url) + DispatchQueue.main.async { + self.instance = newInstance + self.isLoading = false + } + } catch { + print("!!!Error: \(error)") + DispatchQueue.main.async { + self.instance = Instance.defaultInstance() + self.isLoading = false + } + } + } +} diff --git a/DUDUJI/Views/AddAccountView.swift b/DUDUJI/Views/AddAccountView.swift index b2aac38..886c5da 100644 --- a/DUDUJI/Views/AddAccountView.swift +++ b/DUDUJI/Views/AddAccountView.swift @@ -10,11 +10,18 @@ import SwiftUI struct AddAccountView: View { @State var serverAddress: String = "" @StateObject var instanceAccountViewModel = InstanceAppViewModel() + @ObservedObject var instanceViewModel = InstanceViewModel() + @State var showInstanceInfo = false var body: some View { - Form { - serverAddressSection - submitButtonSection + VStack { + Form { + serverAddressSection + submitButtonSection + if showInstanceInfo { + InstanceInfoSection + } + } } } @@ -29,7 +36,24 @@ struct AddAccountView: View { var submitButtonSection: some View { Section { Button("提交") { - instanceAccountViewModel.submitServerAddress(with: serverAddress) + Task { + await instanceViewModel.getInstance(from: serverAddress) + showInstanceInfo = true + } + } + } + } + + var InstanceInfoSection: some View { + Section { + if instanceViewModel.isLoading { + HStack { + Spacer() + ProgressView() // 显示加载指示器 + Spacer() + } + } else { + InstanceInfoView(instance: instanceViewModel.instance) } } } diff --git a/DUDUJI/Views/InstanceInfoView.swift b/DUDUJI/Views/InstanceInfoView.swift new file mode 100644 index 0000000..854f83f --- /dev/null +++ b/DUDUJI/Views/InstanceInfoView.swift @@ -0,0 +1,29 @@ +// +// InstanceInfoView.swift +// DUDUJI +// +// Created by ching on 2023/3/6. +// + +import SwiftUI + +struct InstanceInfoView: View { +// @ObservedObject private var instanceViewModel = InstanceViewModel() + var instance: Instance + var body: some View { + VStack(alignment: .leading) { + Text("实例名称: \(instance.title)") + Text("实例人数: \(instance.userCount != nil ? "\(instance.userCount!)" : "N/A")") + Text("嘟嘟数量: \(instance.statusCount != nil ? "\(instance.statusCount!)" : "N/A")") + Text("缩略图: \(instance.thumbnail?.absoluteString ?? "N/A")") + Text("简介: \(instance.shortDescription ?? "N/A")") + Text("介绍: \(instance.description ?? "N/A")") + } + } +} + +struct InstanceInfoView_Previews: PreviewProvider { + static var previews: some View { + InstanceInfoView(instance: .defaultInstance()) + } +}