// // 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(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 } }