Bark/Controller/MessageListViewModel.swift
2025-01-08 12:02:51 +08:00

437 lines
16 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// MessageListViewModel.swift
// Bark
//
// Created by huangfeng on 2020/11/21.
// Copyright © 2020 Fin. All rights reserved.
//
import Foundation
import RealmSwift
import RxCocoa
import RxDataSources
import RxSwift
enum MessageListType: Int, Codable {
//
case list
//
case group
}
enum MessageSourceType {
///
case all
///
case group(String?)
}
class MessageListViewModel: ViewModel, ViewModelType {
struct Input {
///
var refresh: Driver<Void>
///
var loadMore: Driver<Void>
///
var itemDelete: Driver<MessageListCellItem>
///
var itemDeleteInGroup: Driver<MessageItemModel>
///
var delete: Driver<MessageDeleteTimeRange>
///
var groupToggleTap: Driver<Void>
///
var searchText: Observable<String?>
/// 10
var reload: Driver<Void>
}
struct Output {
///
var messages: Driver<[MessageSection]>
///
var refreshAction: Driver<MJRefreshAction>
///
var type: Driver<MessageListType>
///
var title: Driver<String>
///
var groupToggleButtonHidden: Driver<Bool>
///
var errorAlert: Driver<String>
}
private static let typeKey = "me.fin.messageListType"
///
private var type: MessageListType = {
if let t: MessageListType = Settings[MessageListViewModel.typeKey] {
return t
}
return .list
}() {
didSet {
Settings[MessageListViewModel.typeKey] = type
}
}
///
private var sourceType: MessageSourceType = .all
///
private var page = 0
///
private let pageCount = 20
///
private var groups: Results<Message>?
///
private var results: Results<Message>?
private var errorAlert: PublishRelay<String> = PublishRelay()
convenience init(sourceType: MessageSourceType) {
self.init()
self.sourceType = sourceType
}
///
private func getResults(filterGroups: [String?], searchText: String?) -> Results<Message>? {
do {
let realm = try Realm()
var results = realm.objects(Message.self)
.sorted(byKeyPath: "createDate", ascending: false)
if filterGroups.count > 0 {
results = results.filter("group in %@", filterGroups)
}
if let text = searchText, text.count > 0 {
results = results.filter("title CONTAINS[c] %@ OR subtitle CONTAINS[c] %@ OR body CONTAINS[c] %@", text, text, text)
}
return results
} catch {
// 1,
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.errorAlert.accept(error.localizedDescription)
}
}
return nil
}
///
private var searchText: String = ""
///
private func getGroups() -> Results<Message>? {
if let realm = try? Realm() {
return realm.objects(Message.self)
.sorted(byKeyPath: "createDate", ascending: false)
.distinct(by: ["group"])
}
return nil
}
/// message
private func getListNextPage(page: Int, pageCount: Int) -> [MessageListCellItem] {
guard let result = results else {
return []
}
let startIndex = page * pageCount
let endIndex = min(startIndex + pageCount, result.count)
guard endIndex > startIndex else {
return []
}
var messages: [MessageListCellItem] = []
for i in startIndex..<endIndex {
// messages.append(result[i].freeze())
// freeze freeze copy
// copy message 访退
messages.append(.message(model: MessageItemModel(message: result[i])))
}
return messages
}
/// group
private func getGroupNextPage(page: Int, pageCount: Int) -> [MessageListCellItem] {
guard let groups, let results else {
return []
}
let startIndex = page * pageCount
let endIndex = min(startIndex + pageCount, groups.count)
guard endIndex > startIndex else {
return []
}
var items: [MessageListCellItem] = []
for i in startIndex..<endIndex {
let group = groups[i].group
let messageResult = getMessages(in: results, group: group)
var messages: [MessageItemModel] = []
for i in 0..<min(messageResult.count, 5) {
messages.append(MessageItemModel(message: messageResult[i]))
}
if messages.count == 1 {
//
items.append(.message(model: messages[0]))
} else if messages.count > 0 {
//
items.append(.messageGroup(name: group ?? NSLocalizedString("default"), totalCount: messageResult.count, messages: messages))
}
}
return items
}
/// 使 groupName messages
func getMessages(in results: Results<Message>, group: String?) -> Results<Message> {
if let group {
return results.filter("group == %@", group)
} else {
return results.filter("group == nil")
}
}
private func getPage(page: Int, pageCount: Int) -> [MessageListCellItem] {
if case .group = self.sourceType {
//
return getListNextPage(page: page, pageCount: pageCount)
}
if type == .list || !searchText.isEmpty {
//
return getListNextPage(page: page, pageCount: pageCount)
}
return getGroupNextPage(page: page, pageCount: pageCount)
}
private func getNextPage() -> [MessageListCellItem] {
defer {
page += 1
}
return getPage(page: self.page, pageCount: self.pageCount)
}
func transform(input: Input) -> Output {
//
let titleRelay = BehaviorRelay<String>(value: NSLocalizedString("historyMessage"))
//
let messagesRelay = BehaviorRelay<[MessageSection]>(value: [])
//
let refreshAction = BehaviorRelay<MJRefreshAction>(value: .none)
//
let filterGroups: BehaviorRelay<[String?]> = { [weak self] in
guard let self = self else {
return BehaviorRelay<[String?]>(value: [])
}
if case .group(let name) = self.sourceType {
return BehaviorRelay<[String?]>(value: [name])
}
return BehaviorRelay<[String?]>(value: [])
}()
//
filterGroups
.subscribe(onNext: { filterGroups in
if filterGroups.count <= 0 {
titleRelay.accept(NSLocalizedString("historyMessage"))
} else {
titleRelay.accept(filterGroups.map { $0 ?? NSLocalizedString("default") }.joined(separator: " , "))
}
}).disposed(by: rx.disposeBag)
//
Observable
.combineLatest(filterGroups, input.searchText)
.subscribe(onNext: { [weak self] groups, searchText in
self?.searchText = searchText ?? ""
self?.results = self?.getResults(filterGroups: groups, searchText: searchText)
if case .all = self?.sourceType {
//
self?.groups = self?.getGroups()
}
}).disposed(by: rx.disposeBag)
//
let messageTypeChanged = input.groupToggleTap.compactMap { () -> MessageListType? in
self.type = self.type == .group ? .list : .group
return self.type
}
//
Observable
.merge(
input.refresh.asObservable().map { () },
input.searchText.asObservable().map { _ in () },
messageTypeChanged.asObservable().map { _ in () }
)
.subscribe(onNext: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.page = 0
let messages = strongSelf.getNextPage()
messagesRelay.accept(
[MessageSection(header: "model", messages: messages)]
)
refreshAction.accept(.endRefresh)
}).disposed(by: rx.disposeBag)
//
// delay 1+N loadMore Reentrancy anomaly
// APP退 UITableView is trying to layout cells with a global row ...
input.loadMore.asObservable()
.delay(.milliseconds(10), scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] in
guard let strongSelf = self else { return }
let items = strongSelf.getNextPage()
refreshAction.accept(.endLoadmore)
if var section = messagesRelay.value.first {
section.messages.append(contentsOf: items)
messagesRelay.accept([section])
} else {
messagesRelay.accept([MessageSection(header: "model", messages: items)])
}
}).disposed(by: rx.disposeBag)
// 10
input.reload.drive(onNext: { [weak self] _ in
guard let self, self.page > 0, self.page <= 10 else { return }
//
let messages = self.getPage(page: 0, pageCount: self.page * self.pageCount)
messagesRelay.accept(
[MessageSection(header: "model", messages: messages)]
)
}).disposed(by: rx.disposeBag)
// message
input.itemDelete.drive(onNext: { [weak self] item in
guard let self else { return }
guard var section = messagesRelay.value.first else {
return
}
//
switch item {
case .message(let model):
//
if let realm = try? Realm(),
let message = realm.objects(Message.self).filter("id == %@", model.id).first
{
try? realm.write {
realm.delete(message)
}
}
// cell item
section.messages.removeAll { cellItem in
if case .message(let m) = cellItem {
return m.id == model.id
}
return false
}
case .messageGroup(let groupName, _, let messages):
//
if let realm = try? Realm(), let first = messages.first {
let messageResult: Results<Message>?
if let group = first.group {
messageResult = self.results?.filter("group == %@", group)
} else {
messageResult = self.results?.filter("group == nil")
}
if let messageResult {
try? realm.write {
realm.delete(messageResult)
}
}
}
// cell item
section.messages.removeAll { cellItem in
if case .messageGroup(let name, _, _) = cellItem {
return name == groupName
}
return false
}
}
//
messagesRelay.accept([section])
}).disposed(by: rx.disposeBag)
//
input.itemDeleteInGroup.drive(onNext: { [weak self] model in
guard let self, let results else { return }
guard var section = messagesRelay.value.first else {
return
}
// message
if let realm = try? Realm(),
let message = realm.objects(Message.self).filter("id == %@", model.id).first
{
try? realm.write {
realm.delete(message)
}
}
if let index = section.messages.firstIndex(where: { item in
if case .messageGroup(_, _, let messages) = item {
return messages.contains { item in
return item.id == model.id
}
}
return false
}) {
// cellItem
if case .messageGroup(let name, _, var messages) = section.messages[index] {
let messagesResult = self.getMessages(in: results, group: messages.first?.group)
messages = messagesResult.prefix(5).map { MessageItemModel(message: $0) }
if messages.count == 0 {
section.messages.remove(at: index)
} else {
section.messages[index] = .messageGroup(name: name, totalCount: messagesResult.count, messages: messages)
}
}
}
messagesRelay.accept([section])
}).disposed(by: rx.disposeBag)
//
input.delete.drive(onNext: { [weak self] range in
guard let self else { return }
if let realm = try? Realm() {
guard let messages = self.getResults(filterGroups: filterGroups.value, searchText: nil)?.filter("createDate >= %@ and createDate <= %@ ", range.startDate, range.endDate) else {
return
}
try? realm.write {
realm.delete(messages)
}
}
self.page = 0
messagesRelay.accept([MessageSection(header: "model", messages: self.getNextPage())])
}).disposed(by: rx.disposeBag)
//
let groupToggleButtonHidden = {
if case .group = self.sourceType {
return true
}
return false
}()
return Output(
messages: messagesRelay.asDriver(onErrorJustReturn: []),
refreshAction: refreshAction.asDriver(),
type: Driver.merge(messageTypeChanged.asDriver(), Driver.just(self.type)),
title: titleRelay.asDriver(),
groupToggleButtonHidden: Driver.just(groupToggleButtonHidden),
errorAlert: errorAlert.asDriver(onErrorDriveWith: .empty())
)
}
}