feat(InstanceInfoView, Instance Model, InstanceViewModel): 通过实例地址获取实例信息并展示

通过实例地址获取实例信息并展示

Signed-off-by: Ching <loooching@gmail.com>
This commit is contained in:
Ching 2023-03-19 21:15:00 +08:00
parent 872c69df6b
commit 0103281cd4
10 changed files with 240 additions and 43 deletions

View File

@ -7,6 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; 24D4D3B529AB83E80064E566 /* DUDUJIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D4D3B429AB83E80064E566 /* DUDUJIApp.swift */; };
24D4D3B729AB83E80064E566 /* DUDUJIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D4D3B629AB83E80064E566 /* DUDUJIView.swift */; }; 24D4D3B729AB83E80064E566 /* DUDUJIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D4D3B629AB83E80064E566 /* DUDUJIView.swift */; };
24D4D3B929AB83E90064E566 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 24D4D3B829AB83E90064E566 /* Assets.xcassets */; }; 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 */; }; 24D4D3D629AFA1970064E566 /* InstanceAppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D4D3D529AFA1970064E566 /* InstanceAppManager.swift */; };
24D4D3DA29B2EB7B0064E566 /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 24D4D3D929B2EB7B0064E566 /* Moya */; }; 24D4D3DA29B2EB7B0064E566 /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 24D4D3D929B2EB7B0064E566 /* Moya */; };
24D4D3DC29B2EC160064E566 /* MastodonService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D4D3DB29B2EC160064E566 /* MastodonService.swift */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
24BB348F29C5F28100A52D20 /* InstanceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceViewModel.swift; sourceTree = "<group>"; };
24D4D3B129AB83E80064E566 /* DUDUJI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DUDUJI.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 24D4D3B429AB83E80064E566 /* DUDUJIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DUDUJIApp.swift; sourceTree = "<group>"; };
24D4D3B629AB83E80064E566 /* DUDUJIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DUDUJIView.swift; sourceTree = "<group>"; }; 24D4D3B629AB83E80064E566 /* DUDUJIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DUDUJIView.swift; sourceTree = "<group>"; };
@ -38,6 +42,8 @@
24D4D3D329AF97A40064E566 /* InstanceAppViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceAppViewModel.swift; sourceTree = "<group>"; }; 24D4D3D329AF97A40064E566 /* InstanceAppViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceAppViewModel.swift; sourceTree = "<group>"; };
24D4D3D529AFA1970064E566 /* InstanceAppManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InstanceAppManager.swift; path = DUDUJI/Managers/InstanceAppManager.swift; sourceTree = SOURCE_ROOT; }; 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; }; 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 = "<group>"; };
24D4D3DF29B640B20064E566 /* InstanceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceInfoView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -73,9 +79,9 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
24D4D3DB29B2EC160064E566 /* MastodonService.swift */, 24D4D3DB29B2EC160064E566 /* MastodonService.swift */,
24D4D3D729B10FB90064E566 /* Managers */,
24D4D3D229AF8E9D0064E566 /* ViewModels */,
24D4D3D129ABC0280064E566 /* Views */, 24D4D3D129ABC0280064E566 /* Views */,
24D4D3D229AF8E9D0064E566 /* ViewModels */,
24D4D3D729B10FB90064E566 /* Managers */,
24D4D3C529ABB5830064E566 /* Models */, 24D4D3C529ABB5830064E566 /* Models */,
24D4D3C829ABB7590064E566 /* Const.swift */, 24D4D3C829ABB7590064E566 /* Const.swift */,
24D4D3B429AB83E80064E566 /* DUDUJIApp.swift */, 24D4D3B429AB83E80064E566 /* DUDUJIApp.swift */,
@ -101,6 +107,7 @@
24D4D3C329ABAE5A0064E566 /* AccountInfo.swift */, 24D4D3C329ABAE5A0064E566 /* AccountInfo.swift */,
24D4D3C629ABB6500064E566 /* OauthToken.swift */, 24D4D3C629ABB6500064E566 /* OauthToken.swift */,
24D4D3CD29ABBC030064E566 /* InstanceApp.swift */, 24D4D3CD29ABBC030064E566 /* InstanceApp.swift */,
24D4D3DD29B6341A0064E566 /* Instance.swift */,
); );
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
@ -109,6 +116,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
24D4D3CF29ABC01E0064E566 /* AddAccountView.swift */, 24D4D3CF29ABC01E0064E566 /* AddAccountView.swift */,
24D4D3DF29B640B20064E566 /* InstanceInfoView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@ -117,6 +125,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
24D4D3D329AF97A40064E566 /* InstanceAppViewModel.swift */, 24D4D3D329AF97A40064E566 /* InstanceAppViewModel.swift */,
24BB348F29C5F28100A52D20 /* InstanceViewModel.swift */,
); );
path = ViewModels; path = ViewModels;
sourceTree = "<group>"; sourceTree = "<group>";
@ -207,6 +216,8 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
24BB349029C5F28100A52D20 /* InstanceViewModel.swift in Sources */,
24D4D3DE29B6341A0064E566 /* Instance.swift in Sources */,
24D4D3C429ABAE5A0064E566 /* AccountInfo.swift in Sources */, 24D4D3C429ABAE5A0064E566 /* AccountInfo.swift in Sources */,
24D4D3C729ABB6500064E566 /* OauthToken.swift in Sources */, 24D4D3C729ABB6500064E566 /* OauthToken.swift in Sources */,
24D4D3CE29ABBC030064E566 /* InstanceApp.swift in Sources */, 24D4D3CE29ABBC030064E566 /* InstanceApp.swift in Sources */,
@ -215,6 +226,7 @@
24D4D3B529AB83E80064E566 /* DUDUJIApp.swift in Sources */, 24D4D3B529AB83E80064E566 /* DUDUJIApp.swift in Sources */,
24D4D3C929ABB7590064E566 /* Const.swift in Sources */, 24D4D3C929ABB7590064E566 /* Const.swift in Sources */,
24D4D3D029ABC01E0064E566 /* AddAccountView.swift in Sources */, 24D4D3D029ABC01E0064E566 /* AddAccountView.swift in Sources */,
24D4D3E029B640B20064E566 /* InstanceInfoView.swift in Sources */,
24D4D3D629AFA1970064E566 /* InstanceAppManager.swift in Sources */, 24D4D3D629AFA1970064E566 /* InstanceAppManager.swift in Sources */,
24D4D3D429AF97A40064E566 /* InstanceAppViewModel.swift in Sources */, 24D4D3D429AF97A40064E566 /* InstanceAppViewModel.swift in Sources */,
); );

View File

@ -7,7 +7,7 @@
<key>DUDUJI.xcscheme_^#shared#^_</key> <key>DUDUJI.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>1</integer> <integer>0</integer>
</dict> </dict>
<key>ReactiveSwift (Playground) 1.xcscheme</key> <key>ReactiveSwift (Playground) 1.xcscheme</key>
<dict> <dict>

View File

@ -12,23 +12,16 @@ class InstanceAppManager {
return ("123", "321") return ("123", "321")
} }
func getInstanceInfo(at serverAddress: String) { func createInstanceApp(at serverAddress: String) -> InstanceApp {
let client = MastodonClient(baseURL: serverAddress)
client.getInstance()
// print("\()")
}
func createInstanceApp(at serverAddress: String) {
print("服务器地址是\(serverAddress)") print("服务器地址是\(serverAddress)")
// save things to InstanceApp model
// let instanceApp = InstanceApp(id: xxx....)
let instanceApp = InstanceApp( let instanceApp = InstanceApp(
name: DUDUConst.name, // name: DUDUConst.name,
website: nil, website: nil,
redirectUri: serverAddress, redirectUri: serverAddress,
clientId: "abc", clientId: "abc",
clientSecret: "cba") clientSecret: "cba")
// save the instanceApp to database // save the instanceApp to database
print("客户端名称:\(instanceApp.name)") print("客户端名称:\(instanceApp.clientId)")
return instanceApp
} }
} }

View File

@ -23,7 +23,7 @@ extension MastodonSevice: TargetType {
var path: String { var path: String {
switch self { switch self {
case .instance: case .instance:
return "/api/v2/instance" return "/api/v1/instance"
} }
} }
@ -65,30 +65,22 @@ class MastodonClient {
}) })
} }
func getInstance() { func getInstance() async throws -> Instance {
// var instanceData = [Any]() return try await withCheckedThrowingContinuation { continuation in
service.request(.instance) { result in service.request(.instance) { result in
switch result { switch result {
case let .success(response): case .success(let response):
do { do {
// let data = try response.map(Data.self) let instance = try JSONDecoder().decode(Instance.self, from: response.data)
let json = try response.mapJSON() print("instance: \(instance.title)")
// let stringData = String(data: data, encoding: .utf8) continuation.resume(returning: instance)
// print(stringData)
// print("\(String(describing: stringData))")
if let dict = json as? [String: Any],
let title = dict["title"] as? String
{
print(title)
}
} catch { } catch {
print("Error parsing JSON, response: \(response)") continuation.resume(throwing: error)
}
case .failure(let error):
continuation.resume(throwing: error)
} }
case let .failure(error):
print(error.localizedDescription)
} }
} }
// return instanceData
} }
} }

View File

@ -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
}
}

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
struct InstanceApp { struct InstanceApp {
public let name: String // public let name: String
public let website: URL? public let website: URL?
public let redirectUri: String public let redirectUri: String
public let clientId: String public let clientId: String

View File

@ -9,14 +9,14 @@ import SwiftUI
class InstanceAppViewModel: ObservableObject { class InstanceAppViewModel: ObservableObject {
private let instanceAppManager = InstanceAppManager() private let instanceAppManager = InstanceAppManager()
@Published private var instance: Instance?
// MARK: intents // MARK: intents
/// ///
/// App /// App
/// - Parameter url: /// - Parameter url:
func submitServerAddress(with url: String) { func submitServerAddress(with url: String) -> InstanceApp {
instanceAppManager.getInstanceInfo(at: url)
instanceAppManager.createInstanceApp(at: url) instanceAppManager.createInstanceApp(at: url)
} }
} }

View File

@ -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
}
}
}
}

View File

@ -10,11 +10,18 @@ import SwiftUI
struct AddAccountView: View { struct AddAccountView: View {
@State var serverAddress: String = "" @State var serverAddress: String = ""
@StateObject var instanceAccountViewModel = InstanceAppViewModel() @StateObject var instanceAccountViewModel = InstanceAppViewModel()
@ObservedObject var instanceViewModel = InstanceViewModel()
@State var showInstanceInfo = false
var body: some View { var body: some View {
VStack {
Form { Form {
serverAddressSection serverAddressSection
submitButtonSection submitButtonSection
if showInstanceInfo {
InstanceInfoSection
}
}
} }
} }
@ -29,7 +36,24 @@ struct AddAccountView: View {
var submitButtonSection: some View { var submitButtonSection: some View {
Section { Section {
Button("提交") { 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)
} }
} }
} }

View File

@ -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())
}
}