jsdw_ios/QuickLocation/Section/Mine/PrivacySettings/PrivacySettingsViewControll...

664 lines
22 KiB
Swift

//
// PrivacySettingsViewController.swift
// QuickLocation
//
import UIKit
import RxSwift
import Kingfisher
enum PrivacySettingItem: CaseIterable {
case phoneReport
case groupLock
case pigeonMessage
var title: String {
switch self {
case .phoneReport:
return "手机报告"
case .groupLock:
return "一键锁机"
case .pigeonMessage:
return "飞鸽传书"
}
}
var imageName: String {
switch self {
case .phoneReport:
return "PrivacySettings/phone_report"
case .groupLock:
return "PrivacySettings/group_lock"
case .pigeonMessage:
return "PrivacySettings/pigeon_message"
}
}
var apiKey: String {
switch self {
case .phoneReport:
return "phone_report"
case .groupLock:
return "app_lock"
case .pigeonMessage:
return "pigeon_mail"
}
}
}
final class PrivacySettingsViewController: BaseViewController {
private var rootView: PrivacySettingsView!
private var groups: [GroupInfoModel] = []
private var isSubmitting = false
override func loadView() {
rootView = PrivacySettingsView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
bind()
requestGroups()
}
private func bind() {
rootView.onGroupChanged = { [weak self] in
guard let self else { return }
self.loadPrivilege(for: self.rootView.selectedGroupKey)
}
rootView.onToggle = { [weak self] item, isOn in
self?.changePrivilege(item: item, isOn: isOn)
}
}
private func requestGroups() {
rootView.setSettingsEnabled(false)
GroupService.groupInfo()
.subscribe(onNext: { [weak self] response in
guard let self else { return }
guard let model = response.model else {
self.groups = []
self.rootView.configure(groups: [], selectedGroupKey: "")
DLToast.showError(text: response.message ?? "获取圈子信息失败")
return
}
self.groups = model.groups
self.rootView.configure(
groups: model.groups,
selectedGroupKey: model.default_group_key
)
self.loadPrivilege(for: self.rootView.selectedGroupKey)
}, onError: { [weak self] error in
self?.groups = []
self?.rootView.configure(groups: [], selectedGroupKey: "")
DLToast.showError(text: error.localizedDescription)
})
.disposed(by: disposeBag)
}
private func applySelectedPrivilege() {
let privilege = groups.first(where: { $0.group_key == rootView.selectedGroupKey })?.privilege
?? GroupPrivilege()
rootView.apply(privilege)
}
private func loadPrivilege(for groupKey: String) {
guard !groupKey.isEmpty else {
applySelectedPrivilege()
return
}
rootView.setSettingsEnabled(false)
GroupService.groupUsers(groupKey: groupKey)
.subscribe(onNext: { [weak self] response in
guard let self else { return }
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
guard response.isValid(for: groupKey),
self.rootView.selectedGroupKey == groupKey else {
self.applySelectedPrivilege()
return
}
let mine = response.list.first { $0.user_id == AppContextManager.shared.userId }
if let index = self.groups.firstIndex(where: { $0.group_key == groupKey }) {
self.groups[index].privilege = mine?.extra.privilege ?? GroupPrivilege()
}
self.applySelectedPrivilege()
}, onError: { [weak self] error in
self?.rootView.setSettingsEnabled(!(self?.groups.isEmpty ?? true))
self?.applySelectedPrivilege()
DLToast.showError(text: error.localizedDescription)
})
.disposed(by: disposeBag)
}
private func changePrivilege(item: PrivacySettingItem, isOn: Bool) {
let groupKey = rootView.selectedGroupKey
guard !groupKey.isEmpty,
let index = groups.firstIndex(where: { $0.group_key == groupKey }) else {
applySelectedPrivilege()
return
}
guard !isSubmitting else {
applySelectedPrivilege()
return
}
if item == .groupLock, !isOn {
interceptTurnOffAppLock { [weak self] blocked in
guard let self else { return }
if blocked {
self.applySelectedPrivilege()
return
}
self.submitPrivilege(item: item, isOn: isOn, groupKey: groupKey, index: index)
}
return
}
submitPrivilege(item: item, isOn: isOn, groupKey: groupKey, index: index)
}
private func interceptTurnOffAppLock(completion: @escaping (Bool) -> Void) {
if let record = lockRecordForPopup(from: PhoneLockSession.currentLocks) {
LockedAppPopView.show(record)
completion(true)
return
}
let userId = AppContextManager.shared.userId.trimmed
let revision = AppUnlockCoordinator.shared.currentLockStateRevision()
isSubmitting = true
rootView.setSettingsEnabled(false)
UserService.phoneLocked(os: "ios")
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] response in
guard let self else { return }
self.isSubmitting = false
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
let locks = AppUnlockCoordinator.shared.locksForRestoration(
response.model?.locks ?? [],
startedAt: revision,
userId: userId
) ?? []
if !locks.isEmpty {
PhoneLockSession.currentLocks = locks
}
if let record = self.lockRecordForPopup(from: locks) {
LockedAppPopView.show(record)
completion(true)
} else {
completion(false)
}
}, onError: { [weak self] _ in
guard let self else { return }
self.isSubmitting = false
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
completion(false)
})
.disposed(by: disposeBag)
}
private func lockRecordForPopup(from locks: [PhoneLockRecord]) -> PhoneLockRecord? {
let groupKey = rootView.selectedGroupKey
return locks.first { $0.groupKey == groupKey } ?? locks.first
}
private func submitPrivilege(item: PrivacySettingItem, isOn: Bool, groupKey: String, index: Int) {
let previous = groups[index].privilege
var updated = previous
switch item {
case .phoneReport:
updated.phone_report = isOn
case .groupLock:
updated.app_lock = isOn
case .pigeonMessage:
updated.pigeon_mail = isOn
}
groups[index].privilege = updated
rootView.apply(updated)
isSubmitting = true
rootView.setSettingsEnabled(false)
GroupService.operate(
opType: "changeprivilege",
requestData: [
"group_key": groupKey,
"privilege_code": item.apiKey,
"enable": isOn
]
)
.subscribe(onNext: { [weak self] response in
guard let self else { return }
self.isSubmitting = false
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
guard response.code == "0" else {
self.revertPrivilege(groupKey: groupKey, previous: previous)
DLToast.showError(text: response.message ?? "设置失败")
return
}
}, onError: { [weak self] error in
guard let self else { return }
self.isSubmitting = false
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
self.revertPrivilege(groupKey: groupKey, previous: previous)
DLToast.showError(text: error.localizedDescription)
})
.disposed(by: disposeBag)
}
private func revertPrivilege(groupKey: String, previous: GroupPrivilege) {
if let index = groups.firstIndex(where: { $0.group_key == groupKey }) {
groups[index].privilege = previous
}
applySelectedPrivilege()
}
}
final class PrivacySettingsView: UIView {
var onGroupChanged: (() -> Void)?
var onToggle: ((PrivacySettingItem, Bool) -> Void)?
var selectedGroupKey: String { groupSelectorView.selectedGroupKey }
private let navBackgroundView = UIImageView(image: UIImage(named: "Common/navBar_bg_2"))
private let navView = BaseNavigationView(title: "隐私设置")
private let scrollView = UIScrollView()
private let contentView = UIView()
private let groupSelectorView = PrivacyGroupSelectorView()
private let phoneReportRow = PrivacySettingRow(item: .phoneReport)
private let groupLockRow = PrivacySettingRow(item: .groupLock)
private let pigeonMessageRow = PrivacySettingRow(item: .pigeonMessage)
private let footerLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(groups: [GroupInfoModel], selectedGroupKey: String) {
groupSelectorView.configure(groups: groups, selectedGroupKey: selectedGroupKey)
setSettingsEnabled(!groups.isEmpty)
}
func apply(_ privilege: GroupPrivilege) {
phoneReportRow.setOn(privilege.phone_report)
groupLockRow.setOn(privilege.app_lock)
pigeonMessageRow.setOn(privilege.pigeon_mail)
}
func setSettingsEnabled(_ enabled: Bool) {
[phoneReportRow, groupLockRow, pigeonMessageRow].forEach {
$0.setSwitchEnabled(enabled)
}
}
private func setupUI() {
backgroundColor = UIColor(hexStr: "#FAFAFA")
navBackgroundView.contentMode = .scaleAspectFill
navBackgroundView.clipsToBounds = true
addSubview(navBackgroundView)
addSubview(navView)
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
addSubview(scrollView)
scrollView.addSubview(contentView)
footerLabel.text = "ⓘ 仅对当前选中圈子生效哟~"
footerLabel.textColor = UIColor(hexStr: "#536177")
footerLabel.font = .systemFont(ofSize: 13, weight: .regular)
footerLabel.textAlignment = .center
addSubview(footerLabel)
contentView.addSubview(groupSelectorView)
let shareHeader = makeSectionHeader(title: "圈子中可以共享的数据")
contentView.addSubview(shareHeader)
contentView.addSubview(phoneReportRow)
let ownerHeader = makeSectionHeader(title: "圈主可以对你使用的功能")
contentView.addSubview(ownerHeader)
contentView.addSubview(groupLockRow)
contentView.addSubview(pigeonMessageRow)
navBackgroundView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
navView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
footerLabel.layoutChain
.left(20)
.right(20)
.bottom(kSafeBottomMargin + 12)
.height(24)
scrollView.layoutChain
.topToBottomOfView(navView)
.left()
.right()
.bottomToTopOfView(footerLabel, offset: -8)
contentView.layoutChain
.edges()
.widthToView(scrollView)
groupSelectorView.layoutChain
.top(12)
.left(15)
.right(15)
.height(100)
shareHeader.layoutChain
.topToBottomOfView(groupSelectorView, offset: 20)
.left(17)
.right(15)
.height(24)
phoneReportRow.layoutChain
.topToBottomOfView(shareHeader, offset: 12)
.left(15)
.right(15)
.height(64)
ownerHeader.layoutChain
.topToBottomOfView(phoneReportRow, offset: 18)
.left(17)
.right(15)
.height(24)
groupLockRow.layoutChain
.topToBottomOfView(ownerHeader, offset: 12)
.left(15)
.right(15)
.height(64)
pigeonMessageRow.layoutChain
.topToBottomOfView(groupLockRow, offset: 12)
.left(15)
.right(15)
.height(64)
.bottom(20)
groupSelectorView.onSelectionChanged = { [weak self] in
self?.onGroupChanged?()
}
phoneReportRow.onToggle = { [weak self] isOn in
self?.onToggle?(.phoneReport, isOn)
}
groupLockRow.onToggle = { [weak self] isOn in
self?.onToggle?(.groupLock, isOn)
}
pigeonMessageRow.onToggle = { [weak self] isOn in
self?.onToggle?(.pigeonMessage, isOn)
}
}
private func makeSectionHeader(title: String) -> UIView {
let view = UIView()
let sparkleLabel = UILabel()
sparkleLabel.text = ""
sparkleLabel.textColor = UIColor(hexStr: "#08ADEF")
sparkleLabel.font = .systemFont(ofSize: 18, weight: .bold)
sparkleLabel.textAlignment = .center
view.addSubview(sparkleLabel)
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.textColor = UIColor(hexStr: "#28364B")
titleLabel.font = .systemFont(ofSize: 16, weight: .regular)
view.addSubview(titleLabel)
sparkleLabel.layoutChain
.left()
.centerY()
.width(18)
.height(24)
titleLabel.layoutChain
.leftToRightOfView(sparkleLabel, offset: 3)
.right()
.centerY()
.height(24)
return view
}
}
private final class PrivacyGroupSelectorView: UIView,
UICollectionViewDataSource,
UICollectionViewDelegateFlowLayout {
var onSelectionChanged: (() -> Void)?
private(set) var selectedGroupKey = ""
private let layout = UICollectionViewFlowLayout()
private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
private let emptyLabel = UILabel()
private var groups: [GroupInfoModel] = []
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
layer.cornerRadius = 20
clipsToBounds = true
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 4
layout.minimumInteritemSpacing = 4
layout.sectionInset = UIEdgeInsets(top: 4, left: 6, bottom: 4, right: 6)
collectionView.backgroundColor = .clear
collectionView.showsHorizontalScrollIndicator = false
collectionView.alwaysBounceHorizontal = true
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(PrivacyGroupCell.self)
addSubview(collectionView)
collectionView.layoutChain.edges()
emptyLabel.text = "暂无圈子"
emptyLabel.textColor = UIColor(hexStr: "#9BA3AF")
emptyLabel.font = .systemFont(ofSize: 15, weight: .regular)
emptyLabel.textAlignment = .center
emptyLabel.isHidden = true
addSubview(emptyLabel)
emptyLabel.layoutChain.edges()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(groups: [GroupInfoModel], selectedGroupKey: String) {
self.groups = groups
if groups.contains(where: { $0.group_key == selectedGroupKey }) {
self.selectedGroupKey = selectedGroupKey
} else {
self.selectedGroupKey = groups.first?.group_key ?? ""
}
emptyLabel.isHidden = !groups.isEmpty
collectionView.isHidden = groups.isEmpty
collectionView.reloadData()
guard let index = groups.firstIndex(where: { $0.group_key == self.selectedGroupKey }) else {
return
}
collectionView.layoutIfNeeded()
collectionView.scrollToItem(
at: IndexPath(item: index, section: 0),
at: .centeredHorizontally,
animated: false
)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
groups.count
}
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: PrivacyGroupCell = collectionView.dequeueReusableCell(for: indexPath)
let group = groups[indexPath.item]
cell.configure(group: group, selected: group.group_key == selectedGroupKey)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let groupKey = groups[indexPath.item].group_key
guard selectedGroupKey != groupKey else { return }
selectedGroupKey = groupKey
collectionView.reloadData()
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
onSelectionChanged?()
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
CGSize(width: 76, height: 92)
}
}
private final class PrivacyGroupCell: UICollectionViewCell {
private let selectionView = UIView()
private let selectionPointer = UIView()
private let iconView = UIImageView()
private let nameLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
selectionView.backgroundColor = UIColor(hexStr: "#C9F4FF")
selectionView.layer.cornerRadius = 13
selectionView.isHidden = true
contentView.addSubview(selectionView)
selectionPointer.backgroundColor = UIColor(hexStr: "#C9F4FF")
selectionPointer.transform = CGAffineTransform(rotationAngle: .pi / 4)
selectionPointer.isHidden = true
contentView.addSubview(selectionPointer)
iconView.contentMode = .scaleAspectFill
iconView.clipsToBounds = true
iconView.layer.cornerRadius = 25
contentView.addSubview(iconView)
nameLabel.textColor = UIColor(hexStr: "#26364B")
nameLabel.font = .systemFont(ofSize: 14, weight: .regular)
nameLabel.textAlignment = .center
nameLabel.lineBreakMode = .byTruncatingTail
contentView.addSubview(nameLabel)
selectionView.layoutChain
.top(1)
.left(2)
.right(2)
.bottom(4)
selectionPointer.layoutChain
.centerX()
.bottom(1)
.width(12)
.height(12)
iconView.layoutChain
.top(8)
.centerX()
.width(50)
.height(50)
nameLabel.layoutChain
.topToBottomOfView(iconView, offset: 5)
.left(5)
.right(5)
.height(20)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
iconView.kf.cancelDownloadTask()
iconView.image = nil
}
func configure(group: GroupInfoModel, selected: Bool) {
nameLabel.text = group.name
iconView.setGroupIcon(group)
selectionView.isHidden = !selected
selectionPointer.isHidden = !selected
}
}
private final class PrivacySettingRow: UIView {
var onToggle: ((Bool) -> Void)?
private let settingSwitch = UISwitch()
init(item: PrivacySettingItem) {
super.init(frame: .zero)
backgroundColor = .white
layer.cornerRadius = 20
let imageView = UIImageView(image: UIImage(named: item.imageName))
imageView.contentMode = .scaleAspectFit
addSubview(imageView)
let titleLabel = UILabel()
titleLabel.text = item.title
titleLabel.textColor = UIColor(hexStr: "#28364B")
titleLabel.font = .systemFont(ofSize: 17, weight: .regular)
addSubview(titleLabel)
settingSwitch.onTintColor = UIColor(hexStr: "#08ADEF")
settingSwitch.setOn(false, animated: false)
settingSwitch.addTarget(self, action: #selector(switchChanged), for: .valueChanged)
addSubview(settingSwitch)
imageView.layoutChain
.left(8)
.centerY()
.width(96)
.height(50)
settingSwitch.layoutChain
.right(13)
.centerY()
titleLabel.layoutChain
.leftToRightOfView(imageView, offset: 8)
.rightToLeftOfView(settingSwitch, offset: -8)
.centerY()
.height(26)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setSwitchEnabled(_ enabled: Bool) {
settingSwitch.isEnabled = enabled
}
func setOn(_ on: Bool) {
settingSwitch.setOn(on, animated: false)
}
@objc private func switchChanged() {
onToggle?(settingSwitch.isOn)
}
}