jsdw_ios/QuickLocation/Manager/Account/RelationStore.swift

245 lines
7.3 KiB
Swift

//
// RelationStore.swift
// QuickLocation
//
import UIKit
import Kingfisher
import ObjectMapper
import RxSwift
import SwiftyUserDefaults
private extension Notification.Name {
static let relationStoreDidUpdate = Notification.Name("relationStoreDidUpdate")
}
struct RelationListResponse: BaseModelProtocol {
var code: String?
var message: String?
var list: [RelationModel] = []
init?(map: Map) {}
mutating func mapping(map: Map) {
code <- (map["code"], kIntTransformStr)
message <- map["message"]
if message == nil {
message <- map["msg"]
}
list <- map["data"]
}
}
struct RelationModel: Mappable, Equatable {
var name: String = ""
var type: String = ""
var limit: Int = -1
var icon: String = ""
init?(map: Map) {}
mutating func mapping(map: Map) {
name <- map["name"]
type <- (map["const"], kRelationTypeTransform)
if type.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
type <- (map["type"], kRelationTypeTransform)
}
if type.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
type <- (map["idx"], kRelationTypeTransform)
}
if type.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
type <- (map["relation_idx"], kRelationTypeTransform)
}
if type.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
type <- (map["id"], kRelationTypeTransform)
}
limit <- (map["limit"], kStrTransformInt)
icon <- map["icon"]
}
var idx: String { type.trimmingCharacters(in: .whitespacesAndNewlines) }
var showsHeart: Bool { idx == "1" }
}
private let kRelationTypeTransform = TransformOf<String, Any>(fromJSON: { value in
transformStr(value)
}, toJSON: { value in
value
})
private enum RelationStoreError: LocalizedError {
case business(String)
case empty
var errorDescription: String? {
switch self {
case let .business(message):
return message
case .empty:
return "关系列表为空"
}
}
}
final class RelationStore {
static let shared = RelationStore()
private(set) var list: [RelationModel] = []
private let disposeBag = DisposeBag()
private init() {
loadDisk()
}
func preload(completion: ((Result<[RelationModel], Error>) -> Void)? = nil) {
UserService.relations()
.subscribe(onNext: { [weak self] response in
guard response.code == "0" else {
completion?(.failure(RelationStoreError.business(response.message ?? "获取关系列表失败")))
return
}
guard !response.list.isEmpty else {
completion?(.failure(RelationStoreError.empty))
return
}
self?.replace(response.list)
completion?(.success(response.list))
}, onError: { error in
completion?(.failure(error))
})
.disposed(by: disposeBag)
}
func item(for idx: String) -> RelationModel? {
let key = idx.trimmingCharacters(in: .whitespacesAndNewlines)
guard !key.isEmpty else { return nil }
return list.first { $0.idx == key }
}
func name(for idx: String) -> String? {
let name = item(for: idx)?.name.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return name.isEmpty ? nil : name
}
func showsHeart(for idx: String) -> Bool {
item(for: idx)?.showsHeart == true
}
func iconURL(for idx: String) -> URL? {
guard let icon = item(for: idx)?.icon else { return nil }
return Self.validIconURL(from: icon)
}
static func validIconURL(from value: String) -> URL? {
let trimmedValue = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard let url = URL(string: trimmedValue),
let scheme = url.scheme?.lowercased(),
scheme == "http" || scheme == "https",
url.host != nil else { return nil }
return url
}
private func replace(_ list: [RelationModel]) {
let normalizedList = Self.normalized(list)
self.list = normalizedList
let json = normalizedList.map { $0.toJSON() }
Defaults[\.userRelations] = try? JSONSerialization.data(withJSONObject: json)
NotificationCenter.default.post(name: .relationStoreDidUpdate, object: nil)
}
private func loadDisk() {
guard let data = Defaults[\.userRelations],
let json = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { return }
list = Self.normalized(json.compactMap { RelationModel(JSON: $0) })
}
private static func normalized(_ list: [RelationModel]) -> [RelationModel] {
list.enumerated().map { index, model in
guard model.idx.isEmpty else { return model }
var normalizedModel = model
// Legacy relation responses omit an identifier and define it by list order.
normalizedModel.type = String(index + 1)
return normalizedModel
}
}
}
final class RelationIconImageView: UIImageView {
private var requestVersion = 0
private var visibilityChanged: ((Bool) -> Void)?
private var relationIdx = ""
private var relationObserver: NSObjectProtocol?
convenience init() {
self.init(frame: .zero)
}
override init(frame: CGRect) {
super.init(frame: frame)
contentMode = .scaleAspectFit
isHidden = true
relationObserver = NotificationCenter.default.addObserver(
forName: .relationStoreDidUpdate,
object: nil,
queue: .main
) { [weak self] _ in
self?.reloadImage()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
if let relationObserver {
NotificationCenter.default.removeObserver(relationObserver)
}
}
override var intrinsicContentSize: CGSize {
CGSize(width: 12, height: 12)
}
func configure(
relationIdx: String,
visibilityChanged: ((Bool) -> Void)? = nil
) {
self.relationIdx = relationIdx.trimmingCharacters(in: .whitespacesAndNewlines)
self.visibilityChanged = visibilityChanged
reloadImage()
}
private func reloadImage() {
kf.cancelDownloadTask()
image = nil
isHidden = true
requestVersion += 1
visibilityChanged?(false)
guard let url = RelationStore.shared.iconURL(for: relationIdx) else { return }
let version = requestVersion
kf.setImage(with: url) { [weak self] result in
guard let self, self.requestVersion == version else { return }
switch result {
case .success:
self.isHidden = false
self.visibilityChanged?(true)
case .failure:
self.image = nil
self.isHidden = true
self.visibilityChanged?(false)
}
}
}
func clear() {
kf.cancelDownloadTask()
requestVersion += 1
relationIdx = ""
visibilityChanged = nil
image = nil
isHidden = true
}
}