jsdw_ios/QuickLocation/Section/Group/GroupItineraryView.swift

497 lines
17 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.

//
// GroupItineraryView.swift
// QuickLocation
//
import UIKit
import RxSwift
import RxCocoa
import SwiftDate
/// Tab
final class GroupItineraryView: UIView {
var disposeBag = DisposeBag()
var onSwitchGroup: (() -> Void)?
var onDeleteSchedule: ((ScheduleModel) -> Void)?
var onSelectSchedule: ((ScheduleModel) -> Void)?
func updateGroupName(_ name: String) {
groupNameLab.text = name.isEmpty ? " " : name
}
func reloadMembers(_ members: [GroupMemberModel], selectedId: String, groupKey: String = "") {
memberList = Self.sortedMemberList(members, groupKey: groupKey)
// selectedId
if memberList.contains(where: { $0.user_id == selectedMemberId }) {
// keep selectedMemberId
} else if memberList.contains(where: { $0.user_id == selectedId }) {
selectedMemberId = selectedId
} else if let me = memberList.first(where: { $0.user_id == AppContextManager.shared.userId }) {
selectedMemberId = me.user_id
} else {
selectedMemberId = memberList.first?.user_id ?? ""
}
memberCV.reloadData()
scrollToSelectedMember(animated: false)
}
func reloadSchedules(_ list: [ScheduleModel]) {
scheduleSections = Self.groupByDay(list)
tableView.reloadData()
emptyLab.isHidden = !list.isEmpty
}
private var memberList: [GroupMemberModel] = []
private var selectedMemberId: String = ""
private var scheduleSections: [(title: String, items: [ScheduleModel])] = []
/// HomeViewModel / GroupMemberView2 -> -> 线
private static func sortedMemberList(_ list: [GroupMemberModel], groupKey: String) -> [GroupMemberModel] {
list.enumerated()
.sorted { lhs, rhs in
let lhsPriority = memberSortPriority(lhs.element, groupKey: groupKey)
let rhsPriority = memberSortPriority(rhs.element, groupKey: groupKey)
if lhsPriority != rhsPriority { return lhsPriority < rhsPriority }
return lhs.offset < rhs.offset
}
.map(\.element)
}
private static func memberSortPriority(_ member: GroupMemberModel, groupKey: String) -> Int {
if !groupKey.isEmpty, groupKey.contains(member.user_id) { return 0 }
if member.user_id == AppContextManager.shared.userId { return 1 }
if member.is_online { return 2 }
return 3
}
private func scrollToSelectedMember(animated: Bool) {
guard !selectedMemberId.isEmpty,
let idx = memberList.firstIndex(where: { $0.user_id == selectedMemberId }) else { return }
let indexPath = IndexPath(item: idx, section: 0)
DispatchQueue.main.async { [weak self] in
guard let self = self,
self.memberCV.numberOfSections > 0,
self.memberCV.numberOfItems(inSection: 0) > idx else { return }
self.memberCV.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: animated)
}
}
private func setupUI() {
addSubview(headerRow)
headerRow.addSubview(groupIcon)
headerRow.addSubview(groupNameLab)
headerRow.addSubview(changeGroupBtn)
let memberView = UIView()
memberView.backgroundColor = .white
memberView.cornerRadius = 20
memberView.addSubview(memberCV)
addSubview(memberView)
memberView.layoutChain
.topToBottomOfView(headerRow, offset: 12)
.edgesHorzontal(15)
.height(100)
addSubview(tableView)
addSubview(emptyLab)
headerRow.layoutChain
.top(8)
.edgesHorzontal(16)
.height(28)
groupIcon.layoutChain
.left()
.centerY()
.width(22)
.height(22)
changeGroupBtn.layoutChain
.right()
.centerY()
.height(28)
groupNameLab.layoutChain
.leftToRightOfView(groupIcon, offset: 8)
.centerY()
.rightToLeftOfView(changeGroupBtn, offset: -8, relation: .lessThanOrEqual)
memberCV.layoutChain
.edges(all: 5, excludingEdge: .bottom)
.bottom()
tableView.layoutChain
.topToBottomOfView(memberCV, offset: 8)
.edgesHorzontal()
.bottom()
emptyLab.layoutChain
.centerX()
.centerY(tableView)
}
private func setupRx() {
changeGroupBtn.rx.tap
.subscribe(onNext: { [weak self] in self?.onSwitchGroup?() })
.disposed(by: disposeBag)
}
private static func groupByDay(_ list: [ScheduleModel]) -> [(title: String, items: [ScheduleModel])] {
let sorted = list.sorted { $0.timestamp > $1.timestamp }
var map: [String: [ScheduleModel]] = [:]
var order: [String] = []
let fmt = DateFormatter()
fmt.locale = Locale(identifier: "zh_CN")
let calendar = Calendar.current
for item in sorted {
let date = Date(timeIntervalSince1970: TimeInterval(item.timestamp) / 1000)
fmt.dateFormat = "M月d日"
let day = fmt.string(from: date)
let title: String
if calendar.isDateInToday(date) {
title = "今天 \(day)"
} else if calendar.isDateInYesterday(date) {
title = "昨天 \(day)"
} else {
fmt.dateFormat = "EEEE M月d日"
title = fmt.string(from: date)
}
if map[title] == nil {
order.append(title)
map[title] = []
}
map[title]?.append(item)
}
return order.map { ($0, map[$0] ?? []) }
}
lazy var headerRow: UIView = {
let v = UIView()
v.backgroundColor = .clear
return v
}()
lazy var groupIcon: UIImageView = {
let iv = UIImageView(image: UIImage(named: "Home/group_name_icon"))
iv.contentMode = .scaleAspectFit
return iv
}()
lazy var groupNameLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .bold)
label.textColor = UIColor(hexStr: "#353B4F")
label.text = " "
return label
}()
lazy var changeGroupBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("切换圈子 ", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#353B4F"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
btn.setImage(UIImage(named: "Common/arrow_right"), for: .normal)
btn.semanticContentAttribute = .forceRightToLeft
return btn
}()
lazy var memberCV: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 61, height: 95)
layout.minimumLineSpacing = 15
layout.sectionInset = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
layout.scrollDirection = .horizontal
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .clear
cv.showsHorizontalScrollIndicator = false
cv.register(GroupMemberListCell.self)
cv.dataSource = self
cv.delegate = self
return cv
}()
lazy var tableView: UITableView = {
let tv = UITableView(frame: .zero, style: .grouped)
tv.backgroundColor = .clear
tv.separatorStyle = .none
tv.showsVerticalScrollIndicator = false
tv.register(GroupItineraryCell.self)
tv.dataSource = self
tv.delegate = self
tv.estimatedRowHeight = 100
tv.rowHeight = UITableView.automaticDimension
tv.sectionHeaderHeight = 36
tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0)
if #available(iOS 15.0, *) {
tv.sectionHeaderTopPadding = 0
}
return tv
}()
lazy var emptyLab: UILabel = {
let label = UILabel()
label.text = "暂无行程"
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hexStr: "#999999")
label.isHidden = true
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#FAFAFA")
setupUI()
setupRx()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - UICollectionView
extension GroupItineraryView: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
memberList.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: GroupMemberListCell = collectionView.dequeueReusableCell(for: indexPath)
let model = memberList[indexPath.item]
cell.configure(
model: model,
isCurrentUser: model.user_id == AppContextManager.shared.userId,
isSelected: model.user_id == selectedMemberId
)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedMemberId = memberList[indexPath.item].user_id
memberCV.reloadData()
scrollToSelectedMember(animated: true)
}
}
// MARK: - UITableView
extension GroupItineraryView: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
scheduleSections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
scheduleSections[section].items.count
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = UIView()
header.backgroundColor = .clear
let label = UILabel()
label.text = scheduleSections[section].title
label.font = .systemFont(ofSize: 13, weight: .semibold)
label.textColor = UIColor(hexStr: "#353B4F")
header.addSubview(label)
label.layoutChain.left(16).centerY()
return header
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
.leastNormalMagnitude
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
UIView()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: GroupItineraryCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(scheduleSections[indexPath.section].items[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
onSelectSchedule?(scheduleSections[indexPath.section].items[indexPath.row])
}
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let model = scheduleSections[indexPath.section].items[indexPath.row]
let delete = UIContextualAction(style: .destructive, title: nil) { [weak self] _, _, done in
self?.onDeleteSchedule?(model)
done(true)
}
delete.image = UIImage(systemName: "trash")
delete.backgroundColor = UIColor(hexStr: "#FF5A5F")
return UISwipeActionsConfiguration(actions: [delete])
}
}
// MARK: - GroupItineraryCell
final class GroupItineraryCell: UITableViewCell {
func configure(_ model: ScheduleModel) {
avatarImg.image = model.userIcon
nameLab.text = model.nick_name
let count = max(model.points.count, 0)
placeCountLab.text = "\(count)"
let date = Date(timeIntervalSince1970: TimeInterval(model.timestamp) / 1000)
let ongoing = Calendar.current.isDateInToday(date) || date > Date()
statusLab.text = ongoing ? "进行中" : "已完成"
statusLab.textColor = ongoing ? UIColor(hexStr: "#16B3FF") : UIColor(hexStr: "#999999")
statusLab.backgroundColor = ongoing
? UIColor(hexStr: "#E3F6FF")
: UIColor(hexStr: "#F0F0F0")
let tag = model.groups.first?.group_name ?? ""
tagLab.text = tag.isEmpty ? nil : " \(tag) "
tagLab.isHidden = tag.isEmpty
setNeedsLayout()
}
override init(style: CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.backgroundColor = .clear
contentView.addSubview(cardView)
cardView.addSubview(avatarImg)
cardView.addSubview(nameLab)
cardView.addSubview(statusLab)
cardView.addSubview(summaryLab)
cardView.addSubview(placeCountLab)
cardView.addSubview(summaryTailLab)
cardView.addSubview(tagLab)
cardView.layoutChain
.edgesVertical(6)
.edgesHorzontal(16)
avatarImg.layoutChain
.top(14)
.left(14)
.width(36)
.height(36)
nameLab.layoutChain
.leftToRightOfView(avatarImg, offset: 10)
.centerY(avatarImg)
statusLab.layoutChain
.right(14)
.centerY(avatarImg)
.height(22)
summaryLab.layoutChain
.topToBottomOfView(avatarImg, offset: 12)
.left(14)
placeCountLab.layoutChain
.leftToRightOfView(summaryLab, offset: 4)
.centerY(summaryLab)
.width(22)
.height(22)
summaryTailLab.layoutChain
.leftToRightOfView(placeCountLab, offset: 4)
.centerY(summaryLab)
tagLab.layoutChain
.topToBottomOfView(summaryLab, offset: 12)
.right(14)
.bottom(14)
.height(20)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private lazy var cardView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.cornerRadius = 16
return v
}()
private lazy var avatarImg: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.cornerRadius = 18
iv.clipsToBounds = true
return iv
}()
private lazy var nameLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14, weight: .semibold)
label.textColor = UIColor(hexStr: "#353B4F")
return label
}()
private lazy var statusLab: PaddingLabel = {
let label = PaddingLabel()
label.font = .systemFont(ofSize: 11, weight: .medium)
label.textAlignment = .center
label.cornerRadius = 6
label.clipsToBounds = true
label.insets = UIEdgeInsets(top: 2, left: 8, bottom: 2, right: 8)
return label
}()
private lazy var summaryLab: UILabel = {
let label = UILabel()
label.text = "今天停留了"
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hexStr: "#353B4F")
return label
}()
private lazy var placeCountLab: UILabel = {
let label = UILabel()
label.font = FontManager.youSheBiaoTiHei(12)
label.textColor = .white
label.textAlignment = .center
label.backgroundColor = UIColor(hexStr: "#353B4F")
label.cornerRadius = 11
label.clipsToBounds = true
return label
}()
private lazy var summaryTailLab: UILabel = {
let label = UILabel()
label.text = "个地方"
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hexStr: "#353B4F")
return label
}()
private lazy var tagLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 10, weight: .medium)
label.textColor = UIColor(hexStr: "#FF8A3D")
label.backgroundColor = UIColor(hexStr: "#FFE8D6")
label.cornerRadius = 4
label.clipsToBounds = true
label.textAlignment = .center
return label
}()
}
private final class PaddingLabel: UILabel {
var insets = UIEdgeInsets.zero
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: insets))
}
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(width: size.width + insets.left + insets.right,
height: size.height + insets.top + insets.bottom)
}
}