jsdw_ios/QuickLocation/Section/Group/GroupViewController.swift

405 lines
15 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.

//
// GroupViewController.swift
// QuickLocation
//
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
import SDCycleScrollView
import ObjectMapper
final class GroupViewController: BaseViewController {
override var isNavigationBarHidden: Bool { true }
fileprivate var rootView: GroupView!
private let viewModel = GroupViewModel()
///
private var itineraryGroupModel: GroupModel?
private var itinerarySchedules: [ScheduleModel] = []
private var itineraryGroupKey: String = ""
private var groupRefreshWorkItem: DispatchWorkItem?
private var groupInfoRequestID = UUID()
private var hasAppeared = false
override func loadView() {
rootView = GroupView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
reactiveAction()
observeTableViews()
bindItineraryPage()
requestRecommandGroup()
requestGroupInfo(refreshIM: true)
guard let config = AppContextManager.shared.systemConfig else { return }
rootView.cycleScrollView.imageURLStringsGroup = config.groupBannerList
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if hasAppeared {
scheduleGroupInfoRefresh()
} else {
hasAppeared = true
}
}
// MARK: - OpenIM
private var hasSetupIMListeners = false
private var hasRequestedIMData = false
private var isRequestingIMData = false
/// IM /
private func loadIMData() {
guard !AppContextManager.shared.isGuest, !isRequestingIMData else { return }
isRequestingIMData = true
DLToast.showLoading()
GroupIMService.shared.ensureLogin { [weak self] success in
DispatchQueue.main.async {
guard let self else { return }
self.isRequestingIMData = false
guard success else {
DLToast.dismiss()
DLToast.showError(text: "IM登录失败")
return
}
self.hasRequestedIMData = true
self.setupIMListenersIfNeeded()
self.refreshIMData()
}
}
}
/// IM /
private func setupIMListenersIfNeeded() {
guard !hasSetupIMListeners else { return }
hasSetupIMListeners = true
GroupIMService.shared.setConversationListener { [weak self] conversations in
DispatchQueue.main.async {
self?.viewModel.updateConversations(conversations)
}
}
GroupIMService.shared.setGroupListener { [weak self] in
GroupIMService.shared.getConversationList { conversations in
DispatchQueue.main.async {
self?.viewModel.loadConversations(conversations)
}
}
}
}
private func refreshIMData() {
GroupIMService.shared.getConversationList { [weak self] conversations in
DispatchQueue.main.async {
DLToast.dismiss()
self?.viewModel.loadConversations(conversations)
}
}
}
// MARK: - Bindings
private func bindViewModel() {
viewModel.output.hotGroups
.observe(on: MainScheduler.asyncInstance)
.bind(to: rootView.hotGroupsCollectionView.rx.items(dataSource: hotGroupDataSource))
.disposed(by: disposeBag)
viewModel.output.createdSections
.observe(on: MainScheduler.asyncInstance)
.bind(to: rootView.createdTableView.rx.items(dataSource: createdDataSource))
.disposed(by: disposeBag)
viewModel.output.joinedSections
.observe(on: MainScheduler.asyncInstance)
.bind(to: rootView.joinedTableView.rx.items(dataSource: joinedDataSource))
.disposed(by: disposeBag)
rootView.hotGroupsCollectionView.rx.modelSelected(GroupInfoModel.self)
.subscribe(viewModel.hotGroupCellAction.inputs)
.disposed(by: disposeBag)
}
private func reactiveAction() {
rootView.createGroupBtn.rx.tapGesture
.subscribe(onNext: { _ in
AppRouter.push(Route.createGroup)
})
.disposed(by: disposeBag)
rootView.joinGroupBtn.rx.tapGesture
.subscribe(onNext: { _ in
AppRouter.push(Route.joinGroup)
})
.disposed(by: disposeBag)
rootView.messageBtn.rx.tap
.subscribe(onNext: {
AppRouter.push(Route.mailbox)
})
.disposed(by: disposeBag)
// Tab(0=joined) / (1=created)
rootView.joinedTabLabel.rx.tapGesture
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(0) })
.disposed(by: disposeBag)
rootView.createdTabLabel.rx.tapGesture
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(1) })
.disposed(by: disposeBag)
// /
rootView.circleTabLabel.rx.tapGesture
.subscribe(onNext: { [weak self] _ in
self?.rootView.selectTopTab(at: 0)
})
.disposed(by: disposeBag)
rootView.itineraryTabLabel.rx.tapGesture
.subscribe(onNext: { [weak self] _ in
guard let self = self else { return }
self.rootView.selectTopTab(at: 1)
self.refreshItineraryPage()
})
.disposed(by: disposeBag)
Observable.merge(
rootView.createdTableView.rx.modelSelected(GroupCellData.self).asObservable(),
rootView.joinedTableView.rx.modelSelected(GroupCellData.self).asObservable()
)
.subscribe(onNext: { data in
let groupId = data.group.group_key
guard !groupId.isEmpty else { return }
AppRouter.push(Route.groupChat, userInfo: ["groupId": groupId])
})
.disposed(by: disposeBag)
// MQTT join/leave/dismiss
NotificationCenter.default.rx.notification(.RefreshIMGroupListNotification)
.subscribe(onNext: { [weak self] _ in
self?.scheduleGroupInfoRefresh()
})
.disposed(by: disposeBag)
NotificationCenter.default.rx.notification(.RefreshUserConfigNotification)
.subscribe(onNext: { [weak self] _ in
self?.requestRecommandGroup()
self?.scheduleGroupInfoRefresh()
guard let config = AppContextManager.shared.systemConfig else { return }
self?.rootView.cycleScrollView.imageURLStringsGroup = config.groupBannerList
})
.disposed(by: disposeBag)
NotificationCenter.default.rx.notification(.RefreshGroupInfoNotification)
.subscribe(onNext: { [weak self] _ in
self?.scheduleGroupInfoRefresh()
})
.disposed(by: disposeBag)
}
private func bindItineraryPage() {
let page = rootView.itineraryPage
page.onSwitchGroup = { [weak self] in
self?.showSwitchGroupPop()
}
page.onSelectSchedule = { model in
AppRouter.push(Route.scheduleDetail, userInfo: ["scheduleJson": model.toJSON()])
}
page.onDeleteSchedule = { [weak self] model in
self?.deleteSchedule(model)
}
}
private func switchToSegment(_ index: Int) {
rootView.selectSegment(at: index)
let offset = CGPoint(x: CGFloat(index) * rootView.segmentScrollView.bounds.width, y: 0)
rootView.segmentScrollView.setContentOffset(offset, animated: false)
}
// MARK: -
private func refreshItineraryPage() {
if let itineraryGroupModel {
applyItineraryGroupModel(itineraryGroupModel)
return
}
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
guard let self = self, let model = response.model else { return }
self.applyItineraryGroupModel(model)
}).disposed(by: disposeBag)
}
private func applyItineraryGroupModel(_ model: GroupModel) {
itineraryGroupModel = model
let current = model.groups.first(where: { $0.group_key == model.default_group_key })
rootView.itineraryPage.updateGroupName(current?.name ?? "")
applyItineraryGroupKey(current?.group_key ?? "")
}
private func applyItineraryGroupKey(_ groupKey: String) {
if itineraryGroupKey != groupKey {
itineraryGroupKey = groupKey
rootView.itineraryPage.updateMembers([])
}
requestItinerarySchedules(groupKey: groupKey)
requestItineraryMembers(groupKey: groupKey)
}
private func requestItinerarySchedules(groupKey: String) {
guard !groupKey.isEmpty else {
itinerarySchedules = []
rootView.itineraryPage.reloadSchedules([])
return
}
ItineraryService.groupScheduleList(groupKey: groupKey)
.subscribe(onNext: { [weak self] response in
guard let self = self, self.itineraryGroupKey == groupKey else { return }
self.itinerarySchedules = response.list
self.rootView.itineraryPage.reloadSchedules(response.list)
})
.disposed(by: disposeBag)
}
private func requestItineraryMembers(groupKey: String) {
guard !groupKey.isEmpty else {
rootView.itineraryPage.updateMembers([])
return
}
GroupService.groupUsers(groupKey: groupKey)
.subscribe(onNext: { [weak self] response in
guard let self,
self.itineraryGroupKey == groupKey,
response.isValid(for: groupKey) else { return }
self.rootView.itineraryPage.updateMembers(response.list)
})
.disposed(by: disposeBag)
}
private func showSwitchGroupPop() {
if let groupModel = itineraryGroupModel {
presentItineraryGroupPicker(groupModel)
return
}
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
guard let self, let model = response.model else { return }
self.itineraryGroupModel = model
let current = model.groups.first(where: { $0.group_key == model.default_group_key })
self.rootView.itineraryPage.updateGroupName(current?.name ?? "")
self.presentItineraryGroupPicker(model)
}).disposed(by: disposeBag)
}
private func presentItineraryGroupPicker(_ groupModel: GroupModel) {
GroupListPopView.show(groupModel: groupModel) { [weak self] groupKey in
guard let self = self, let key = groupKey else { return }
GroupService.operate(opType: "setdefault", requestData: ["group_key": key])
.subscribe()
.disposed(by: self.disposeBag)
}
}
private func deleteSchedule(_ model: ScheduleModel) {
guard !model.id.isEmpty else { return }
DLToast.showLoading()
ItineraryService.delete(id: model.id)
.subscribe(onNext: { [weak self] _ in
DLToast.dismiss()
guard let self = self else { return }
self.itinerarySchedules.removeAll { $0.id == model.id }
self.rootView.itineraryPage.reloadSchedules(self.itinerarySchedules)
}, onError: { _ in
DLToast.dismiss()
})
.disposed(by: disposeBag)
}
// MARK: - tableView
private func observeTableViews() {
rootView.createdTableView.rx.didScroll
.observe(on: MainScheduler.asyncInstance)
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
self.rootView.handleTableViewScroll(self.rootView.createdTableView)
})
.disposed(by: disposeBag)
rootView.joinedTableView.rx.didScroll
.observe(on: MainScheduler.asyncInstance)
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
self.rootView.handleTableViewScroll(self.rootView.joinedTableView)
})
.disposed(by: disposeBag)
}
// MARK: - dataSource
private lazy var hotGroupDataSource: RxCollectionViewSectionedReloadDataSource<HotGroupListSectionModel> = {
RxCollectionViewSectionedReloadDataSource<HotGroupListSectionModel> { datasource, collectionView, indexPath, model in
let cell: HotGroupCell = collectionView.dequeueReusableCell(for: indexPath)
cell.configure(model)
return cell
}
}()
private lazy var createdDataSource: RxTableViewSectionedReloadDataSource<CircleListSectionModel> = {
RxTableViewSectionedReloadDataSource<CircleListSectionModel> { _, tableView, indexPath, model in
let cell: CircleGroupCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model)
return cell
}
}()
private lazy var joinedDataSource: RxTableViewSectionedReloadDataSource<CircleListSectionModel> = {
RxTableViewSectionedReloadDataSource<CircleListSectionModel> { _, tableView, indexPath, model in
let cell: CircleGroupCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model)
return cell
}
}()
// MARK: - API
private func requestRecommandGroup() {
GroupService.recommand(count: 5).subscribe(onNext: { response in
self.viewModel.loadHotGroupData(response.list)
}).disposed(by: disposeBag)
}
private func scheduleGroupInfoRefresh() {
groupRefreshWorkItem?.cancel()
let work = DispatchWorkItem { [weak self] in
self?.requestGroupInfo()
}
groupRefreshWorkItem = work
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: work)
}
private func requestGroupInfo(refreshIM: Bool = false) {
let requestID = UUID()
groupInfoRequestID = requestID
GroupService.groupInfo()
.subscribe(onNext: { [weak self] response in
guard let self,
self.groupInfoRequestID == requestID,
let model = response.model else { return }
self.viewModel.applyBusinessGroups(model.groups)
self.applyItineraryGroupModel(model)
if refreshIM || !self.hasRequestedIMData {
self.loadIMData()
}
})
.disposed(by: disposeBag)
}
}