- 位置选择器 通用化

This commit is contained in:
linshujie 2026-09-14 18:30:37 +08:00
parent 13e688444c
commit 536e735f3b
14 changed files with 1023 additions and 1289 deletions

View File

@ -247,8 +247,6 @@
30D74ABD2FEA67EA0050EB2C /* CreateScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74ABC2FEA67EA0050EB2C /* CreateScheduleVC.swift */; }; 30D74ABD2FEA67EA0050EB2C /* CreateScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74ABC2FEA67EA0050EB2C /* CreateScheduleVC.swift */; };
30D74ABF2FEA67F30050EB2C /* CreateScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74ABE2FEA67F30050EB2C /* CreateScheduleView.swift */; }; 30D74ABF2FEA67F30050EB2C /* CreateScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74ABE2FEA67F30050EB2C /* CreateScheduleView.swift */; };
30D74AC12FEA6EEF0050EB2C /* CreateSchedulePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AC02FEA6EEF0050EB2C /* CreateSchedulePopView.swift */; }; 30D74AC12FEA6EEF0050EB2C /* CreateSchedulePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74AC02FEA6EEF0050EB2C /* CreateSchedulePopView.swift */; };
30D74BF32FEB6F5B0050EB2C /* LocationPickerVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74BF02FEB6F5B0050EB2C /* LocationPickerVC.swift */; };
30D74BF42FEB6F5B0050EB2C /* LocationPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74BF12FEB6F5B0050EB2C /* LocationPickerView.swift */; };
30D74D1F2FEBB09B0050EB2C /* CreateScheduleVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */; }; 30D74D1F2FEBB09B0050EB2C /* CreateScheduleVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */; };
30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */; }; 30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */; };
30D87CDD2FDFF07500E958FD /* InteractionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDC2FDFF07500E958FD /* InteractionView.swift */; }; 30D87CDD2FDFF07500E958FD /* InteractionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDC2FDFF07500E958FD /* InteractionView.swift */; };
@ -381,6 +379,8 @@
80F749310651808BF00F95CF /* DeviceActivityMonitorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64156B4A0E806901AB223FA /* DeviceActivityMonitorExtension.swift */; }; 80F749310651808BF00F95CF /* DeviceActivityMonitorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64156B4A0E806901AB223FA /* DeviceActivityMonitorExtension.swift */; };
830C70B1B53657BB020721DA /* AppRestrictManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEEA6CEA0CEAAB42C22752B /* AppRestrictManager.swift */; }; 830C70B1B53657BB020721DA /* AppRestrictManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEEA6CEA0CEAAB42C22752B /* AppRestrictManager.swift */; };
89647BC98E0FF342511D42CF /* FamilyControls.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CD6B965A124276661A84C4C /* FamilyControls.framework */; }; 89647BC98E0FF342511D42CF /* FamilyControls.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CD6B965A124276661A84C4C /* FamilyControls.framework */; };
A1B2C30331DC000100000001 /* LocationPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C30131DC000100000001 /* LocationPickerViewController.swift */; };
A1B2C30431DC000100000001 /* LocationPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C30231DC000100000001 /* LocationPickerView.swift */; };
A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */; }; A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */; };
A42DCB2A66E694B7CC50033B /* AppRestrictCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89519661B70A0ADEC2A76032 /* AppRestrictCell.swift */; }; A42DCB2A66E694B7CC50033B /* AppRestrictCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89519661B70A0ADEC2A76032 /* AppRestrictCell.swift */; };
A55D8DE8A1F679EBF7B562D1 /* AppCatalogStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47922567FCD34DBE9186F8FD /* AppCatalogStore.swift */; }; A55D8DE8A1F679EBF7B562D1 /* AppCatalogStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47922567FCD34DBE9186F8FD /* AppCatalogStore.swift */; };
@ -690,8 +690,6 @@
30D74ABC2FEA67EA0050EB2C /* CreateScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateScheduleVC.swift; sourceTree = "<group>"; }; 30D74ABC2FEA67EA0050EB2C /* CreateScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateScheduleVC.swift; sourceTree = "<group>"; };
30D74ABE2FEA67F30050EB2C /* CreateScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateScheduleView.swift; sourceTree = "<group>"; }; 30D74ABE2FEA67F30050EB2C /* CreateScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateScheduleView.swift; sourceTree = "<group>"; };
30D74AC02FEA6EEF0050EB2C /* CreateSchedulePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSchedulePopView.swift; sourceTree = "<group>"; }; 30D74AC02FEA6EEF0050EB2C /* CreateSchedulePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSchedulePopView.swift; sourceTree = "<group>"; };
30D74BF02FEB6F5B0050EB2C /* LocationPickerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerVC.swift; sourceTree = "<group>"; };
30D74BF12FEB6F5B0050EB2C /* LocationPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerView.swift; sourceTree = "<group>"; };
30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateScheduleVM.swift; sourceTree = "<group>"; }; 30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateScheduleVM.swift; sourceTree = "<group>"; };
30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTService.swift; sourceTree = "<group>"; }; 30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTService.swift; sourceTree = "<group>"; };
30D87CDC2FDFF07500E958FD /* InteractionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionView.swift; sourceTree = "<group>"; }; 30D87CDC2FDFF07500E958FD /* InteractionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionView.swift; sourceTree = "<group>"; };
@ -828,6 +826,8 @@
88CC9D32980864EAD9F72004 /* DeviceActivity.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DeviceActivity.framework; path = System/Library/Frameworks/DeviceActivity.framework; sourceTree = SDKROOT; }; 88CC9D32980864EAD9F72004 /* DeviceActivity.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DeviceActivity.framework; path = System/Library/Frameworks/DeviceActivity.framework; sourceTree = SDKROOT; };
89519661B70A0ADEC2A76032 /* AppRestrictCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictCell.swift; path = AppRestrict/AppRestrictCell.swift; sourceTree = "<group>"; }; 89519661B70A0ADEC2A76032 /* AppRestrictCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictCell.swift; path = AppRestrict/AppRestrictCell.swift; sourceTree = "<group>"; };
98E91E51811235EAD0985421 /* DeviceActivityMonitorExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DeviceActivityMonitorExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 98E91E51811235EAD0985421 /* DeviceActivityMonitorExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DeviceActivityMonitorExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
A1B2C30131DC000100000001 /* LocationPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerViewController.swift; sourceTree = "<group>"; };
A1B2C30231DC000100000001 /* LocationPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerView.swift; sourceTree = "<group>"; };
A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; }; A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
AC46E80B848E4012D192E4CE /* LockDistractView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractView.swift; sourceTree = "<group>"; }; AC46E80B848E4012D192E4CE /* LockDistractView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractView.swift; sourceTree = "<group>"; };
BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ITunesSearchService.swift; path = AppRestrict/ITunesSearchService.swift; sourceTree = "<group>"; }; BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ITunesSearchService.swift; path = AppRestrict/ITunesSearchService.swift; sourceTree = "<group>"; };
@ -1356,6 +1356,7 @@
305A76322FCA8C7000227D26 /* CircleMember.swift */, 305A76322FCA8C7000227D26 /* CircleMember.swift */,
305A76332FCA8C7000227D26 /* MemberAnnotation.swift */, 305A76332FCA8C7000227D26 /* MemberAnnotation.swift */,
305A76342FCA8C7000227D26 /* MemberAnnotationView.swift */, 305A76342FCA8C7000227D26 /* MemberAnnotationView.swift */,
A1B2C30531DC000100000001 /* LocationPicker */,
30D87D032FE1336300E958FD /* Navigation */, 30D87D032FE1336300E958FD /* Navigation */,
); );
path = Map; path = Map;
@ -1832,7 +1833,6 @@
30D74AB32FEA25B90050EB2C /* ViewedModel.swift */, 30D74AB32FEA25B90050EB2C /* ViewedModel.swift */,
30D74AB92FEA37AD0050EB2C /* ScheduleModel.swift */, 30D74AB92FEA37AD0050EB2C /* ScheduleModel.swift */,
30D74ABB2FEA67CE0050EB2C /* CreateSchedule */, 30D74ABB2FEA67CE0050EB2C /* CreateSchedule */,
30D74BF22FEB6F5B0050EB2C /* LocationPicker */,
30BF300A2FED09A300D9CB52 /* ScheduleDetail */, 30BF300A2FED09A300D9CB52 /* ScheduleDetail */,
30A87A492FEE1CB20095E7C6 /* ItineraryDetail */, 30A87A492FEE1CB20095E7C6 /* ItineraryDetail */,
30A87A4E2FEE4E390095E7C6 /* ScheduleHistory */, 30A87A4E2FEE4E390095E7C6 /* ScheduleHistory */,
@ -1853,15 +1853,6 @@
path = CreateSchedule; path = CreateSchedule;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
30D74BF22FEB6F5B0050EB2C /* LocationPicker */ = {
isa = PBXGroup;
children = (
30D74BF02FEB6F5B0050EB2C /* LocationPickerVC.swift */,
30D74BF12FEB6F5B0050EB2C /* LocationPickerView.swift */,
);
path = LocationPicker;
sourceTree = "<group>";
};
30D87CD52FDF9F1900E958FD /* MQTT */ = { 30D87CD52FDF9F1900E958FD /* MQTT */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -2155,6 +2146,15 @@
name = iOS; name = iOS;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A1B2C30531DC000100000001 /* LocationPicker */ = {
isa = PBXGroup;
children = (
A1B2C30131DC000100000001 /* LocationPickerViewController.swift */,
A1B2C30231DC000100000001 /* LocationPickerView.swift */,
);
path = LocationPicker;
sourceTree = "<group>";
};
B07D45692FCECE07570D9B73 /* Pods */ = { B07D45692FCECE07570D9B73 /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -2441,8 +2441,8 @@
305A76942FCA8C7000227D26 /* UploadImageCell.swift in Sources */, 305A76942FCA8C7000227D26 /* UploadImageCell.swift in Sources */,
305A76952FCA8C7000227D26 /* CornerRadiusCell.swift in Sources */, 305A76952FCA8C7000227D26 /* CornerRadiusCell.swift in Sources */,
305A76962FCA8C7000227D26 /* CornerRadiusFooterView.swift in Sources */, 305A76962FCA8C7000227D26 /* CornerRadiusFooterView.swift in Sources */,
30D74BF32FEB6F5B0050EB2C /* LocationPickerVC.swift in Sources */, A1B2C30331DC000100000001 /* LocationPickerViewController.swift in Sources */,
30D74BF42FEB6F5B0050EB2C /* LocationPickerView.swift in Sources */, A1B2C30431DC000100000001 /* LocationPickerView.swift in Sources */,
30C4C0162FDB91B8009215C1 /* CheckPermissionVC.swift in Sources */, 30C4C0162FDB91B8009215C1 /* CheckPermissionVC.swift in Sources */,
55C4C0242FDB91C8009215C2 /* CheckPermissionGuideVC.swift in Sources */, 55C4C0242FDB91C8009215C2 /* CheckPermissionGuideVC.swift in Sources */,
305A76972FCA8C7000227D26 /* CornerRadiusHeaderView.swift in Sources */, 305A76972FCA8C7000227D26 /* CornerRadiusHeaderView.swift in Sources */,

View File

@ -115,7 +115,7 @@ extension ApiManager {
case GatewayStatusCode.failure.rawValue, GatewayStatusCode.noAuthority.rawValue: // 退 case GatewayStatusCode.failure.rawValue, GatewayStatusCode.noAuthority.rawValue: // 退
handlePopView(message, handle) handlePopView(message, handle)
case GatewayStatusCode.needVip.rawValue: case GatewayStatusCode.needVip.rawValue:
handleNeedVip(message ?? "") handleNeedVip(message)
default: default:
// codesuccess // codesuccess
if success != nil { if success != nil {
@ -172,8 +172,16 @@ extension ApiManager {
GeYanSdk.deletePreResultCache() GeYanSdk.deletePreResultCache()
AppContextManager.shared.deleteAccount() AppContextManager.shared.deleteAccount()
GroupIMService.shared.logout() GroupIMService.shared.logout()
DLToast.showError(text: msg ?? "登录失效,请重新登录!") { DLToast.showError(text: msg ?? "登录失效,请重新登录!") { [weak self] in
AppDelegate.shared.showMainViewController() guard let self else { return }
SystemService.userConfig().subscribe(onNext: { response in
guard let model = response.model else { return }
Defaults[\.loginToken] = model.token
AppContextManager.shared.systemConfig = model.config
DLToast.dismiss {
AppDelegate.shared.showMainViewController()
}
}, onError: { _ in }).disposed(by: self.disposeBag)
} }
} }
} }

View File

@ -50,11 +50,11 @@ extension DefaultsKeys {
func setupNetworkMode() { func setupNetworkMode() {
#if DEBUG #if DEBUG
setupApiEnv(0) setupApiEnv(1)
openIM_API = "https://imapi.zuom8.cn" // http://38.207.176.65:10002 openIM_API = "https://imapi.zuom8.cn" // http://38.207.176.65:10002
openIM_WS = "ws://imws.zuom8.cn" // ws://38.207.176.65:10010 openIM_WS = "ws://imws.zuom8.cn" // ws://38.207.176.65:10010
#elseif AdHoc #elseif AdHoc
setupApiEnv(0) setupApiEnv(1)
openIM_API = "https://imapi.zuom8.cn" openIM_API = "https://imapi.zuom8.cn"
openIM_WS = "ws://imws.zuom8.cn" openIM_WS = "ws://imws.zuom8.cn"
#else #else

View File

@ -513,12 +513,6 @@ final class GroupChatVC: BaseViewController {
rootView.textField.delegate = self rootView.textField.delegate = self
rootView.mentionTableView.delegate = self rootView.mentionTableView.delegate = self
rootView.mentionTableView.dataSource = self rootView.mentionTableView.dataSource = self
rootView.textField.rx.controlEvent(.editingChanged)
.subscribe(onNext: { [weak self] in
self?.rootView.refreshComposer()
self?.handleMentionTrigger()
})
.disposed(by: disposeBag)
} }
private func handleMentionTrigger() { private func handleMentionTrigger() {
@ -670,13 +664,7 @@ final class GroupChatVC: BaseViewController {
rootView.sendBtn.rx.tap rootView.sendBtn.rx.tap
.subscribe(onNext: { [weak self] in .subscribe(onNext: { [weak self] in
guard let self = self else { return } self?.sendCurrentTextMessage()
let text = self.rootView.textField.text ?? ""
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
self.viewModel.input.sendMessage.onNext(text)
self.rootView.textField.text = ""
self.rootView.refreshComposer()
self.hideMentionPicker()
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
@ -689,6 +677,15 @@ final class GroupChatVC: BaseViewController {
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
private func sendCurrentTextMessage() {
let text = rootView.textField.text ?? ""
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
viewModel.input.sendMessage.onNext(text)
rootView.textField.text = ""
rootView.refreshComposer()
hideMentionPicker()
}
// MARK: - // MARK: -
private func showEmojiPanel() { private func showEmojiPanel() {
let show = self.rootView.emojiPanelView.isHidden let show = self.rootView.emojiPanelView.isHidden
@ -745,9 +742,12 @@ final class GroupChatVC: BaseViewController {
} }
private func showLocationPicker() { private func showLocationPicker() {
let picker = ChatLocationPickerVC() let picker = LocationPickerViewController()
picker.onPickedLocation = { [weak self] location in picker.onPickedLocation = { [weak self] location in
self?.viewModel.sendLocation(location) self?.viewModel.sendLocation(ChatLocationPayload(name: location.name,
address: location.address,
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude))
} }
navigationController?.pushViewController(picker, animated: true) navigationController?.pushViewController(picker, animated: true)
} }
@ -1123,15 +1123,35 @@ final class GroupChatVC: BaseViewController {
}() }()
} }
// MARK: - UITextFieldDelegate / Mention table // MARK: - UITextViewDelegate / Mention table
extension GroupChatVC: UITextFieldDelegate, UITableViewDataSource { extension GroupChatVC: UITextViewDelegate, UITableViewDataSource {
func textFieldShouldReturn(_ textField: UITextField) -> Bool { func textViewDidChange(_ textView: UITextView) {
let text = textField.text ?? ""
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return true }
viewModel.input.sendMessage.onNext(text)
textField.text = ""
rootView.refreshComposer() rootView.refreshComposer()
handleMentionTrigger()
}
func textView(_ textView: UITextView,
shouldChangeTextIn range: NSRange,
replacementText text: String) -> Bool {
guard text == "\n", textView.markedTextRange == nil else { return true }
sendCurrentTextMessage()
return false
}
func textViewDidChangeSelection(_ textView: UITextView) {
guard textView.isFirstResponder else { return }
handleMentionTrigger()
}
func textViewDidEndEditing(_ textView: UITextView) {
hideMentionPicker() hideMentionPicker()
}
func textViewDidBeginEditing(_ textView: UITextView) {
rootView.refreshComposer()
}
func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
return true return true
} }
@ -1281,681 +1301,6 @@ extension GroupChatVC {
} }
} }
// MARK: -
private struct ChatLocationSuggestion {
let name: String
let address: String
let coordinate: CLLocationCoordinate2D
}
private final class ChatLocationSuggestionCell: UITableViewCell {
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(hexStr: "#1E2F4A")
label.lineBreakMode = .byTruncatingTail
return label
}()
private let distanceLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12, weight: .bold)
label.textColor = UIColor(hexStr: "#50BCEF")
label.textAlignment = .center
label.backgroundColor = UIColor(hexStr: "#E3F6FF")
label.layer.cornerRadius = 6
label.clipsToBounds = true
return label
}()
private let addressLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14)
label.textColor = UIColor(hexStr: "#A2A2A2")
label.lineBreakMode = .byTruncatingTail
return label
}()
private let checkmarkView: UIImageView = {
let view = UIImageView(image: UIImage(systemName: "checkmark"))
view.tintColor = UIColor(hexStr: "#16B3FF")
view.contentMode = .scaleAspectFit
return view
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .white
selectionStyle = .none
contentView.addSubview(nameLabel)
contentView.addSubview(distanceLabel)
contentView.addSubview(addressLabel)
contentView.addSubview(checkmarkView)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func configure(location: ChatLocationSuggestion, distanceText: String?, isSelected: Bool) {
nameLabel.text = location.name.isEmpty ? "已选位置" : location.name
addressLabel.text = location.address.isEmpty ? "暂无详细地址" : location.address
distanceLabel.text = distanceText
distanceLabel.isHidden = distanceText == nil
checkmarkView.isHidden = !isSelected
setNeedsLayout()
}
override func layoutSubviews() {
super.layoutSubviews()
let horizontalInset: CGFloat = 32
let checkWidth: CGFloat = 24
let rightInset: CGFloat = 20
let availableWidth = max(0, contentView.bounds.width - horizontalInset - checkWidth - rightInset)
let distanceText = distanceLabel.text ?? ""
let distanceWidth = distanceLabel.isHidden ? 0 : max(58, (distanceText as NSString).size(withAttributes: [.font: distanceLabel.font as Any]).width + 14)
let nameWidth = max(0, availableWidth - (distanceLabel.isHidden ? 0 : distanceWidth + 8))
nameLabel.frame = CGRect(x: horizontalInset, y: 11, width: nameWidth, height: 24)
distanceLabel.frame = CGRect(x: nameLabel.frame.maxX + 8, y: 13, width: distanceWidth, height: 20)
addressLabel.frame = CGRect(x: horizontalInset, y: 42, width: availableWidth, height: 20)
checkmarkView.frame = CGRect(x: contentView.bounds.width - rightInset - checkWidth, y: 26, width: checkWidth, height: checkWidth)
}
}
#if !targetEnvironment(simulator)
private final class ChatCurrentLocationAnnotation: NSObject, MAAnnotation {
var coordinate: CLLocationCoordinate2D
init(coordinate: CLLocationCoordinate2D) {
self.coordinate = coordinate
super.init()
}
}
private final class ChatCurrentLocationAnnotationView: MAAnnotationView {
private let avatarContainer: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 14
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 2)
view.layer.shadowRadius = 5
view.layer.shadowOpacity = 0.16
return view
}()
private let avatarImageView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
view.clipsToBounds = true
view.layer.cornerRadius = 10
return view
}()
private let statusOuterView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 8
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 1)
view.layer.shadowRadius = 2
view.layer.shadowOpacity = 0.16
return view
}()
private let statusInnerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#56DC42")
view.layer.cornerRadius = 5
return view
}()
override init!(annotation: MAAnnotation!, reuseIdentifier: String!) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
backgroundColor = .clear
canShowCallout = false
isUserInteractionEnabled = false
bounds = CGRect(x: 0, y: 0, width: 56, height: 72)
centerOffset = CGPoint(x: 0, y: -29)
addSubview(avatarContainer)
avatarContainer.addSubview(avatarImageView)
addSubview(statusOuterView)
statusOuterView.addSubview(statusInnerView)
avatarContainer.frame = CGRect(x: 0, y: 0, width: 56, height: 56)
avatarImageView.frame = avatarContainer.bounds.insetBy(dx: 4, dy: 4)
statusOuterView.frame = CGRect(x: 20, y: 57, width: 16, height: 16)
statusInnerView.frame = statusOuterView.bounds.insetBy(dx: 3, dy: 3)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func configure() {
avatarImageView.setHeadPic(AppContextManager.shared.head_pic)
}
}
#endif
final class ChatLocationPickerVC: BaseViewController {
override var isNavigationBarHidden: Bool { true }
private let collapsedResultPanelHeight: CGFloat = 290
var onPickedLocation: ((ChatLocationPayload) -> Void)?
private var rootView: ChatLocationPickerView!
private var currentCoordinate: CLLocationCoordinate2D?
private var selectedLocation: ChatLocationSuggestion?
private var suggestions: [ChatLocationSuggestion] = []
private var searchWorkItem: DispatchWorkItem?
private var isResultPanelExpanded = false
private var keyboardManagerWasEnabled = true
private var keyboardManagerResignOnTouchOutside = true
#if !targetEnvironment(simulator)
private let searchAPI = AMapSearchAPI()
private var currentLocationAnnotation: ChatCurrentLocationAnnotation?
#endif
override func loadView() {
rootView = ChatLocationPickerView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
rootView.resultTableView.dataSource = self
rootView.resultTableView.delegate = self
rootView.backBtn.addTarget(self, action: #selector(handleBack), for: .touchUpInside)
rootView.confirmBtn.addTarget(self, action: #selector(confirm), for: .touchUpInside)
rootView.recenterBtn.addTarget(self, action: #selector(recenterToCurrentLocation), for: .touchUpInside)
rootView.searchField.addTarget(self, action: #selector(searchChanged), for: .editingChanged)
rootView.searchField.addTarget(self, action: #selector(search), for: .editingDidEndOnExit)
setupKeyboard()
setupMap()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
keyboardManagerWasEnabled = IQKeyboardManager.shared.isEnabled
keyboardManagerResignOnTouchOutside = IQKeyboardManager.shared.resignOnTouchOutside
IQKeyboardManager.shared.isEnabled = false
IQKeyboardManager.shared.resignOnTouchOutside = false
view.transform = .identity
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
IQKeyboardManager.shared.isEnabled = keyboardManagerWasEnabled
IQKeyboardManager.shared.resignOnTouchOutside = keyboardManagerResignOnTouchOutside
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isMovingFromParent || isBeingDismissed {
#if !targetEnvironment(simulator)
rootView.cleanupMap()
#endif
}
}
@objc private func handleBack() {
navigationController?.popViewController(animated: true)
}
@objc private func confirm() {
guard let selectedLocation else {
DLToast.showError(text: "请选择位置")
return
}
onPickedLocation?(ChatLocationPayload(name: selectedLocation.name,
address: selectedLocation.address,
latitude: selectedLocation.coordinate.latitude,
longitude: selectedLocation.coordinate.longitude))
navigationController?.popViewController(animated: true)
}
@objc private func searchChanged() {
searchWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in self?.search() }
searchWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: workItem)
}
@objc private func search() {
let keyword = rootView.searchField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !keyword.isEmpty else {
loadNearbyPOIs(around: selectedLocation?.coordinate ?? currentCoordinate)
return
}
#if !targetEnvironment(simulator)
let request = AMapPOIKeywordsSearchRequest()
request.keywords = keyword
request.city = ""
request.offset = 20
if let coordinate = selectedLocation?.coordinate ?? currentCoordinate {
request.location = AMapGeoPoint.location(withLatitude: CGFloat(coordinate.latitude),
longitude: CGFloat(coordinate.longitude))
}
searchAPI?.aMapPOIKeywordsSearch(request)
#else
rootView.emptyLabel.isHidden = false
rootView.emptyLabel.text = "模拟器无法搜索地图地点"
#endif
}
private func setupKeyboard() {
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] notification in
guard let self,
self.isViewLoaded,
self.view.window != nil,
self.rootView.searchField.isFirstResponder else { return }
let duration = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
self.setResultPanelExpanded(true, duration: duration)
})
.disposed(by: disposeBag)
}
private func setResultPanelExpanded(_ expanded: Bool, duration: TimeInterval = 0.25) {
guard isResultPanelExpanded != expanded else { return }
isResultPanelExpanded = expanded
let height = expanded ? view.bounds.height * 2 / 3 : collapsedResultPanelHeight
rootView.setResultPanelHeight(height, duration: duration)
}
private func collapseResultPanel() {
setResultPanelExpanded(false)
rootView.searchField.resignFirstResponder()
}
@objc private func recenterToCurrentLocation() {
guard let currentCoordinate else {
DLToast.showError(text: "暂无当前位置")
return
}
#if !targetEnvironment(simulator)
rootView.mapView.setCenter(currentCoordinate, animated: true)
#endif
selectMapCenterLocation(currentCoordinate, placeholderName: "当前位置")
}
private func setupMap() {
if let latitude = Defaults[\.currentLatitude], let longitude = Defaults[\.currentLongitude] {
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
if CLLocationCoordinate2DIsValid(coordinate) {
currentCoordinate = coordinate
}
}
#if !targetEnvironment(simulator)
searchAPI?.delegate = self
rootView.mapView.delegate = self
rootView.mapView.showsUserLocation = false
let coordinate = currentCoordinate ?? CLLocationCoordinate2D(latitude: 39.9042, longitude: 116.4074)
rootView.mapView.setCenter(coordinate, animated: false)
rootView.mapView.setZoomLevel(15, animated: false)
if let currentCoordinate {
addCurrentLocationAnnotation(at: currentCoordinate)
selectMapCenterLocation(currentCoordinate, placeholderName: "当前位置")
}
#else
rootView.emptyLabel.isHidden = false
rootView.emptyLabel.text = "请在真机上选择地图位置"
#endif
}
private func loadNearbyPOIs(around coordinate: CLLocationCoordinate2D?) {
#if !targetEnvironment(simulator)
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else {
rootView.emptyLabel.isHidden = false
rootView.emptyLabel.text = "暂无当前位置,可直接在地图上选择"
return
}
let request = AMapPOIAroundSearchRequest()
request.location = AMapGeoPoint.location(withLatitude: CGFloat(coordinate.latitude),
longitude: CGFloat(coordinate.longitude))
request.radius = 1500
request.offset = 20
searchAPI?.aMapPOIAroundSearch(request)
#endif
}
private func updateSuggestions(_ values: [ChatLocationSuggestion]) {
suggestions = values
keepSelectedLocationInSuggestions()
rootView.resultTableView.reloadData()
rootView.emptyLabel.isHidden = !suggestions.isEmpty
}
private func select(_ location: ChatLocationSuggestion, recenterMap: Bool) {
selectedLocation = location
keepSelectedLocationInSuggestions()
rootView.selectedNameLabel.text = location.name.isEmpty ? "已选位置" : location.name
rootView.selectedAddressLabel.text = location.address.isEmpty ? "暂无详细地址" : location.address
rootView.selectedPanel.isHidden = false
rootView.confirmBtn.isEnabled = true
rootView.emptyLabel.isHidden = true
rootView.resultTableView.reloadData()
#if !targetEnvironment(simulator)
if recenterMap {
rootView.mapView.setCenter(location.coordinate, animated: true)
}
#endif
}
private func selectMapCenterLocation(_ coordinate: CLLocationCoordinate2D, placeholderName: String = "正在获取地点...") {
guard CLLocationCoordinate2DIsValid(coordinate) else { return }
let placeholder = ChatLocationSuggestion(name: placeholderName,
address: "正在获取地址...",
coordinate: coordinate)
select(placeholder, recenterMap: false)
#if !targetEnvironment(simulator)
requestReverseGeocode(for: coordinate)
loadNearbyPOIs(around: coordinate)
#endif
}
private func keepSelectedLocationInSuggestions() {
guard let selectedLocation else { return }
if let index = suggestions.firstIndex(where: { coordinatesMatch($0.coordinate, selectedLocation.coordinate) }) {
suggestions[index] = selectedLocation
} else {
suggestions.insert(selectedLocation, at: 0)
}
}
private func coordinatesMatch(_ lhs: CLLocationCoordinate2D, _ rhs: CLLocationCoordinate2D) -> Bool {
abs(lhs.latitude - rhs.latitude) < 0.000_001 && abs(lhs.longitude - rhs.longitude) < 0.000_001
}
private func distanceText(to coordinate: CLLocationCoordinate2D) -> String? {
guard let selectedLocation else { return nil }
let distance = CLLocation(latitude: selectedLocation.coordinate.latitude, longitude: selectedLocation.coordinate.longitude)
.distance(from: CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude))
if distance < 100 {
return "100m内"
}
if distance < 1000 {
return "\(Int(distance.rounded()))m"
}
return String(format: "%.1fkm", distance / 1000)
}
#if !targetEnvironment(simulator)
private func requestReverseGeocode(for coordinate: CLLocationCoordinate2D) {
let request = AMapReGeocodeSearchRequest()
request.location = AMapGeoPoint.location(withLatitude: CGFloat(coordinate.latitude), longitude: CGFloat(coordinate.longitude))
request.requireExtension = true
searchAPI?.aMapReGoecodeSearch(request)
}
private func addCurrentLocationAnnotation(at coordinate: CLLocationCoordinate2D) {
if let currentLocationAnnotation {
currentLocationAnnotation.coordinate = coordinate
return
}
let annotation = ChatCurrentLocationAnnotation(coordinate: coordinate)
currentLocationAnnotation = annotation
rootView.mapView.addAnnotation(annotation)
}
#endif
}
extension ChatLocationPickerVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { suggestions.count }
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
guard scrollView === rootView.resultTableView else { return }
rootView.searchField.resignFirstResponder()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "ChatLocationSuggestionCell", for: indexPath) as? ChatLocationSuggestionCell else {
return UITableViewCell()
}
let value = suggestions[indexPath.row]
let isSelected = selectedLocation.map { coordinatesMatch($0.coordinate, value.coordinate) } ?? false
cell.configure(location: value, distanceText: distanceText(to: value.coordinate), isSelected: isSelected)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
collapseResultPanel()
let location = suggestions[indexPath.row]
select(location, recenterMap: true)
}
}
#if !targetEnvironment(simulator)
extension ChatLocationPickerVC: AMapSearchDelegate, MAMapViewDelegate {
func onPOISearchDone(_ request: AMapPOISearchBaseRequest!, response: AMapPOISearchResponse!) {
if let aroundRequest = request as? AMapPOIAroundSearchRequest,
let point = aroundRequest.location {
let coordinate = CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude)
guard let selectedLocation, coordinatesMatch(selectedLocation.coordinate, coordinate) else { return }
} else if let keywordRequest = request as? AMapPOIKeywordsSearchRequest,
let point = keywordRequest.location {
let coordinate = CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude)
guard let selectedLocation, coordinatesMatch(selectedLocation.coordinate, coordinate) else { return }
}
let values = response.pois.compactMap { poi -> ChatLocationSuggestion? in
guard let point = poi.location else { return nil }
let coordinate = CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude)
guard CLLocationCoordinate2DIsValid(coordinate) else { return nil }
return ChatLocationSuggestion(name: poi.name ?? "",
address: poi.address ?? poi.district ?? "",
coordinate: coordinate)
}
updateSuggestions(values)
}
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
guard let point = request.location else { return }
let coordinate = CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude)
guard let selectedLocation, coordinatesMatch(selectedLocation.coordinate, coordinate) else { return }
let poi = response.regeocode?.pois?.first
let location = ChatLocationSuggestion(name: poi?.name ?? response.regeocode?.formattedAddress ?? "已选位置",
address: poi?.address ?? response.regeocode?.formattedAddress ?? "",
coordinate: coordinate)
select(location, recenterMap: false)
}
func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
if let reGeocodeRequest = request as? AMapReGeocodeSearchRequest,
let point = reGeocodeRequest.location {
let coordinate = CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude)
if let selectedLocation, coordinatesMatch(selectedLocation.coordinate, coordinate) {
select(ChatLocationSuggestion(name: "已选位置", address: "", coordinate: coordinate), recenterMap: false)
}
return
}
rootView.emptyLabel.isHidden = false
rootView.emptyLabel.text = "地点加载失败,请重试或在地图上选择"
}
func mapView(_ mapView: MAMapView!, regionDidChangeAnimated animated: Bool, wasUserAction: Bool) {
guard wasUserAction else { return }
collapseResultPanel()
let center = CGPoint(x: mapView.bounds.midX, y: mapView.bounds.midY)
let coordinate = mapView.convert(center, toCoordinateFrom: mapView)
selectMapCenterLocation(coordinate)
}
func mapView(_ mapView: MAMapView!, mapWillMoveByUser wasUserAction: Bool) {
guard wasUserAction else { return }
collapseResultPanel()
}
func mapView(_ mapView: MAMapView!, didSingleTappedAt coordinate: CLLocationCoordinate2D) {
collapseResultPanel()
}
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
guard annotation is ChatCurrentLocationAnnotation else { return nil }
let identifier = "ChatCurrentLocation"
guard let view = (mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? ChatCurrentLocationAnnotationView)
?? ChatCurrentLocationAnnotationView(annotation: annotation, reuseIdentifier: identifier) else { return nil }
view.annotation = annotation
view.zIndex = 10
view.configure()
return view
}
}
#endif
private final class ChatLocationPickerView: UIView {
private var resultPanelHeightConstraint: NSLayoutConstraint?
#if !targetEnvironment(simulator)
lazy var mapView: MAMapView! = {
let map = MAMapView()
map.showsCompass = false
map.showsScale = false
map.zoomLevel = 15
return map
}()
#endif
let backBtn: UIButton = {
let button = UIButton(type: .custom)
button.setBackgroundImage(UIImage(named: "Common/back"), for: .normal)
button.backgroundColor = .clear
button.extendEdgeInsets = UIEdgeInsets(top: 20, left: 18, bottom: 20, right: 20)
return button
}()
let recenterBtn: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "IM/location_recenter"), for: .normal)
button.backgroundColor = .white
button.layer.cornerRadius = 10
button.layer.shadowColor = UIColor.black.cgColor
button.layer.shadowOffset = CGSize(width: 0, height: 2)
button.layer.shadowRadius = 6
button.layer.shadowOpacity = 0.12
return button
}()
let centerPinView: UIImageView = {
let view = UIImageView(image: UIImage(named: "IM/location_pin"))
view.contentMode = .scaleAspectFit
view.isUserInteractionEnabled = false
return view
}()
let searchField: UITextField = {
let field = UITextField()
field.placeholder = "搜索地点、商圈或道路"
field.font = .systemFont(ofSize: 14)
field.clearButtonMode = .whileEditing
field.returnKeyType = .search
field.backgroundColor = UIColor(hexStr: "#F3F3F3")
field.cornerRadius = 11
field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 16, height: 36))
field.leftViewMode = .always
return field
}()
let resultTableView: UITableView = {
let table = UITableView(frame: .zero, style: .plain)
table.backgroundColor = .white
table.separatorColor = UIColor(hexStr: "#EEEEEE")
table.rowHeight = 74
table.register(ChatLocationSuggestionCell.self, forCellReuseIdentifier: "ChatLocationSuggestionCell")
table.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: kSafeBottomMargin, right: 0)
return table
}()
let emptyLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 13)
label.textColor = UIColor(hexStr: "#8A8A8A")
label.textAlignment = .center
label.numberOfLines = 0
label.isHidden = true
return label
}()
let selectedPanel: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 24
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.clipsToBounds = true
return view
}()
let selectedNameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .semibold)
label.textColor = UIColor(hexStr: "#282828")
return label
}()
let selectedAddressLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = UIColor(hexStr: "#8A8A8A")
label.lineBreakMode = .byTruncatingTail
return label
}()
let confirmBtn: UIButton = {
let button = UIButton(type: .custom)
button.setTitle("确认", for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
button.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
button.cornerRadius = 8
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#EFF7F8")
setupUI()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func setupUI() {
#if !targetEnvironment(simulator)
addSubview(mapView)
mapView.layoutChain.edges(excludingEdge: .bottom).bottom(250 + kSafeBottomMargin)
addSubview(centerPinView)
#endif
addSubview(backBtn)
addSubview(recenterBtn)
addSubview(confirmBtn)
addSubview(selectedPanel)
selectedPanel.addSubview(searchField)
selectedPanel.addSubview(resultTableView)
selectedPanel.addSubview(emptyLabel)
selectedPanel.addSubview(selectedNameLabel)
selectedPanel.addSubview(selectedAddressLabel)
backBtn.layoutChain.top(kStatusBarHeight + 12).left(18).width(32).height(32)
recenterBtn.layoutChain.left(13).bottomToTopOfView(selectedPanel, offset: -24).width(38).height(38)
confirmBtn.layoutChain.right(14).width(50).height(30).centerY(backBtn)
selectedPanel.layoutChain.edgesHorzontal().bottom().height(290)
resultPanelHeightConstraint = selectedPanel.layoutChain.constraint
searchField.layoutChain.top(13).edgesHorzontal(15).height(38)
resultTableView.layoutChain.topToBottomOfView(searchField, offset: 13).edgesHorzontal(0).bottom()
emptyLabel.layoutChain.centerX().centerY().edgesHorzontal(40)
selectedNameLabel.isHidden = true
selectedAddressLabel.isHidden = true
#if !targetEnvironment(simulator)
centerPinView.layoutChain.centerX(mapView).centerY(mapView, offset: -28).width(34).height(56)
#endif
}
func setResultPanelHeight(_ height: CGFloat, duration: TimeInterval) {
guard let resultPanelHeightConstraint,
abs(resultPanelHeightConstraint.constant - height) > 0.5 else { return }
layoutIfNeeded()
resultPanelHeightConstraint.constant = height
UIView.animate(withDuration: duration,
delay: 0,
options: [.beginFromCurrentState, .curveEaseInOut]) {
self.layoutIfNeeded()
}
}
#if !targetEnvironment(simulator)
func cleanupMap() {
mapView?.delegate = nil
mapView?.removeFromSuperview()
mapView = nil
}
#endif
}
// MARK: - // MARK: -
final class ChatLocationDetailVC: BaseViewController { final class ChatLocationDetailVC: BaseViewController {
override var isNavigationBarHidden: Bool { true } override var isNavigationBarHidden: Bool { true }
@ -2032,7 +1377,7 @@ final class ChatLocationDetailVC: BaseViewController {
target.title = "target" target.title = "target"
rootView.mapView.addAnnotation(target) rootView.mapView.addAnnotation(target)
if let currentCoordinate { if let currentCoordinate {
let current = ChatCurrentLocationAnnotation(coordinate: currentCoordinate) let current = CurrentUserLocationAnnotation(coordinate: currentCoordinate)
rootView.mapView.addAnnotation(current) rootView.mapView.addAnnotation(current)
rootView.mapView.showAnnotations([target, current], animated: true) rootView.mapView.showAnnotations([target, current], animated: true)
} }
@ -2115,10 +1460,10 @@ extension ChatLocationDetailVC: AMapSearchDelegate, MAMapViewDelegate {
} }
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! { func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
if annotation is ChatCurrentLocationAnnotation { if annotation is CurrentUserLocationAnnotation {
let identifier = "ChatLocationDetailCurrent" let identifier = "ChatLocationDetailCurrent"
guard let view = (mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? ChatCurrentLocationAnnotationView) guard let view = (mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? CurrentUserLocationAnnotationView)
?? ChatCurrentLocationAnnotationView(annotation: annotation, reuseIdentifier: identifier) else { return nil } ?? CurrentUserLocationAnnotationView(annotation: annotation, reuseIdentifier: identifier) else { return nil }
view.annotation = annotation view.annotation = annotation
view.zIndex = 10 view.zIndex = 10
view.configure() view.configure()

View File

@ -134,6 +134,56 @@ struct ChatMessage {
} }
} }
final class ChatComposerTextView: UITextView {
private let placeholderLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16)
label.textColor = UIColor.placeholderText
label.isUserInteractionEnabled = false
return label
}()
var placeholder: String? {
didSet {
placeholderLabel.text = placeholder
updatePlaceholderVisibility()
}
}
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
addSubview(placeholderLabel)
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
placeholderLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
placeholderLabel.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
placeholderLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8)
])
NotificationCenter.default.addObserver(
self,
selector: #selector(textDidChange),
name: UITextView.textDidChangeNotification,
object: self
)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func updatePlaceholderVisibility() {
placeholderLabel.isHidden = !text.isEmpty
}
@objc private func textDidChange() {
updatePlaceholderVisibility()
}
}
class GroupChatView: UIView { class GroupChatView: UIView {
var disposeBag = DisposeBag() var disposeBag = DisposeBag()
@ -147,6 +197,12 @@ class GroupChatView: UIView {
var onLocationTap: (() -> Void)? var onLocationTap: (() -> Void)?
private var emojiPlaybackGeneration = 0 private var emojiPlaybackGeneration = 0
private var quoteBarHeightConstraint: NSLayoutConstraint? private var quoteBarHeightConstraint: NSLayoutConstraint?
private var bottomBarHeightConstraint: NSLayoutConstraint?
private var textFieldHeightConstraint: NSLayoutConstraint?
private let minimumBottomBarHeight: CGFloat = 50
private let minimumTextFieldHeight: CGFloat = 36
private let maximumTextLines: CGFloat = 4
func setQuoteBarVisible(_ visible: Bool, summary: String? = nil) { func setQuoteBarVisible(_ visible: Bool, summary: String? = nil) {
quoteBar.isHidden = !visible quoteBar.isHidden = !visible
@ -186,6 +242,7 @@ class GroupChatView: UIView {
func refreshComposer() { func refreshComposer() {
let hasText = !(textField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty let hasText = !(textField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
textField.updatePlaceholderVisibility()
if isVoiceInputMode { if isVoiceInputMode {
updateHoldSpeakAppearance() updateHoldSpeakAppearance()
textField.isHidden = true textField.isHidden = true
@ -202,9 +259,36 @@ class GroupChatView: UIView {
sendBtn.isHidden = !hasText sendBtn.isHidden = !hasText
voiceBtn.isHidden = hasText voiceBtn.isHidden = hasText
} }
updateComposerHeight()
layoutIfNeeded() layoutIfNeeded()
} }
private func updateComposerHeight() {
let fittingWidth = textField.bounds.width
guard fittingWidth > 0 else { return }
let maximumTextHeight = ceil(
textField.font!.lineHeight * maximumTextLines
+ textField.textContainerInset.top
+ textField.textContainerInset.bottom
)
let fittingHeight = textField.sizeThatFits(
CGSize(width: fittingWidth, height: .greatestFiniteMagnitude)
).height
let textHeight = isVoiceInputMode
? minimumTextFieldHeight
: min(maximumTextHeight, max(minimumTextFieldHeight, ceil(fittingHeight)))
let bottomBarHeight = max(minimumBottomBarHeight, textHeight + 14)
textField.isScrollEnabled = !isVoiceInputMode && fittingHeight > maximumTextHeight
if abs((textFieldHeightConstraint?.constant ?? 0) - textHeight) > 0.5 {
textFieldHeightConstraint?.constant = textHeight
}
if abs((bottomBarHeightConstraint?.constant ?? 0) - bottomBarHeight) > 0.5 {
bottomBarHeightConstraint?.constant = bottomBarHeight
}
}
private func updateHoldSpeakAppearance() { private func updateHoldSpeakAppearance() {
var configuration = UIButton.Configuration.plain() var configuration = UIButton.Configuration.plain()
configuration.title = "按住 说话" configuration.title = "按住 说话"
@ -434,8 +518,9 @@ class GroupChatView: UIView {
bottomBar.layoutChain bottomBar.layoutChain
.edgesHorzontal(10) .edgesHorzontal(10)
.height(50)
.bottom(kSafeBottomMargin) .bottom(kSafeBottomMargin)
bottomBarHeightConstraint = bottomBar.heightAnchor.constraint(equalToConstant: minimumBottomBarHeight)
bottomBarHeightConstraint?.isActive = true
disableIMView.layoutChain disableIMView.layoutChain
.edgesHorzontal(15) .edgesHorzontal(15)
@ -467,7 +552,8 @@ class GroupChatView: UIView {
.left(16) .left(16)
.rightToLeftOfView(emojiBtn, offset: -10) .rightToLeftOfView(emojiBtn, offset: -10)
.centerY() .centerY()
.height(36) textFieldHeightConstraint = textField.heightAnchor.constraint(equalToConstant: minimumTextFieldHeight)
textFieldHeightConstraint?.isActive = true
holdSpeakBtn.layoutChain holdSpeakBtn.layoutChain
.left(16) .left(16)
@ -750,14 +836,18 @@ class GroupChatView: UIView {
return btn return btn
}() }()
lazy var textField: UITextField = { lazy var textField: ChatComposerTextView = {
let tf = UITextField() let tf = ChatComposerTextView()
tf.font = .systemFont(ofSize: 16) tf.font = .systemFont(ofSize: 16)
tf.textColor = UIColor(hexStr: "#293445") tf.textColor = UIColor(hexStr: "#293445")
tf.backgroundColor = .clear tf.backgroundColor = .clear
tf.placeholder = "发消息或者按住说话" tf.placeholder = "发消息或者按住说话"
tf.returnKeyType = .send tf.returnKeyType = .send
tf.tintColor = UIColor(hexStr: "#16B3FF") tf.tintColor = UIColor(hexStr: "#16B3FF")
tf.textContainerInset = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
tf.textContainer.lineFragmentPadding = 0
tf.isScrollEnabled = false
tf.showsVerticalScrollIndicator = false
return tf return tf
}() }()
@ -879,6 +969,7 @@ class GroupChatView: UIView {
override func layoutSubviews() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
updateComposerHeight()
if !mentionPickerView.isHidden { if !mentionPickerView.isHidden {
mentionPickerView.layer.shadowPath = UIBezierPath( mentionPickerView.layer.shadowPath = UIBezierPath(
roundedRect: mentionPickerView.bounds, roundedRect: mentionPickerView.bounds,

View File

@ -0,0 +1,156 @@
//
// LocationPickerView.swift
// QuickLocation
//
import UIKit
#if !targetEnvironment(simulator)
import AMapNaviKit
#endif
final class LocationPickerView: UIView {
static let cellIdentifier = "LocationSuggestionCell"
private var resultPanelHeightConstraint: NSLayoutConstraint?
#if !targetEnvironment(simulator)
lazy var mapView: MAMapView! = {
let map = MAMapView()
map.showsCompass = false
map.showsScale = false
map.zoomLevel = 15
return map
}()
#endif
let backBtn: UIButton = {
let button = UIButton(type: .custom)
button.setBackgroundImage(UIImage(named: "Common/back"), for: .normal)
button.backgroundColor = .clear
button.extendEdgeInsets = UIEdgeInsets(top: 20, left: 18, bottom: 20, right: 20)
return button
}()
let recenterBtn: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "IM/location_recenter"), for: .normal)
button.backgroundColor = .white
button.layer.cornerRadius = 10
button.layer.shadowColor = UIColor.black.cgColor
button.layer.shadowOffset = CGSize(width: 0, height: 2)
button.layer.shadowRadius = 6
button.layer.shadowOpacity = 0.12
return button
}()
let centerPinView: UIImageView = {
let view = UIImageView(image: UIImage(named: "IM/location_pin"))
view.contentMode = .scaleAspectFit
view.isUserInteractionEnabled = false
return view
}()
let searchField: UITextField = {
let field = UITextField()
field.placeholder = "搜索地点、商圈或道路"
field.font = .systemFont(ofSize: 14)
field.clearButtonMode = .whileEditing
field.returnKeyType = .search
field.backgroundColor = UIColor(hexStr: "#F3F3F3")
field.cornerRadius = 11
field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 16, height: 36))
field.leftViewMode = .always
return field
}()
let resultTableView: UITableView = {
let table = UITableView(frame: .zero, style: .plain)
table.backgroundColor = .white
table.separatorColor = UIColor(hexStr: "#EEEEEE")
table.rowHeight = 74
table.register(LocationSuggestionCell.self, forCellReuseIdentifier: cellIdentifier)
table.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: kSafeBottomMargin, right: 0)
return table
}()
let emptyLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 13)
label.textColor = UIColor(hexStr: "#8A8A8A")
label.textAlignment = .center
label.numberOfLines = 0
label.isHidden = true
return label
}()
let confirmBtn: UIButton = {
let button = UIButton(type: .custom)
button.setTitle("确认", for: .normal)
button.setTitleColor(.white, for: .normal)
button.setTitleColor(UIColor.white.withAlphaComponent(0.6), for: .disabled)
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
button.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
button.cornerRadius = 8
button.isEnabled = false
return button
}()
private let resultPanel: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 24
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.clipsToBounds = true
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#EFF7F8")
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
#if !targetEnvironment(simulator)
addSubview(mapView)
mapView.layoutChain.edges(excludingEdge: .bottom).bottom(250 + kSafeBottomMargin)
addSubview(centerPinView)
#endif
addSubview(backBtn)
addSubview(recenterBtn)
addSubview(confirmBtn)
addSubview(resultPanel)
resultPanel.addSubview(searchField)
resultPanel.addSubview(resultTableView)
resultPanel.addSubview(emptyLabel)
backBtn.layoutChain.top(kStatusBarHeight + 12).left(18).width(32).height(32)
recenterBtn.layoutChain.left(13).bottomToTopOfView(resultPanel, offset: -24).width(38).height(38)
confirmBtn.layoutChain.right(14).width(50).height(30).centerY(backBtn)
resultPanel.layoutChain.edgesHorzontal().bottom().height(290)
resultPanelHeightConstraint = resultPanel.layoutChain.constraint
searchField.layoutChain.top(13).edgesHorzontal(15).height(38)
resultTableView.layoutChain.topToBottomOfView(searchField, offset: 13).edgesHorzontal().bottom()
emptyLabel.layoutChain.centerX().centerY().edgesHorzontal(40)
#if !targetEnvironment(simulator)
centerPinView.layoutChain.centerX(mapView).centerY(mapView, offset: -28).width(34).height(56)
#endif
}
func setResultPanelHeight(_ height: CGFloat, duration: TimeInterval) {
guard let resultPanelHeightConstraint,
abs(resultPanelHeightConstraint.constant - height) > 0.5 else { return }
layoutIfNeeded()
resultPanelHeightConstraint.constant = height
UIView.animate(withDuration: duration,
delay: 0,
options: [.beginFromCurrentState, .curveEaseInOut]) {
self.layoutIfNeeded()
}
}
#if !targetEnvironment(simulator)
func cleanupMap() {
mapView?.delegate = nil
mapView?.removeFromSuperview()
mapView = nil
}
#endif
}

View File

@ -0,0 +1,636 @@
//
// LocationPickerViewController.swift
// QuickLocation
//
import UIKit
import CoreLocation
import IQKeyboardManagerSwift
import RxSwift
import RxCocoa
import SwiftyUserDefaults
#if !targetEnvironment(simulator)
import AMapNaviKit
import AMapSearchKit
#endif
struct PickedLocation {
let name: String
let address: String
let coordinate: CLLocationCoordinate2D
var province: String
var city: String
var district: String
var street: String
var country: String
var formatted_address: String
init(name: String,
address: String,
coordinate: CLLocationCoordinate2D,
province: String = "",
city: String = "",
district: String = "",
street: String = "",
country: String = "",
formatted_address: String = "") {
self.name = name
self.address = address
self.coordinate = coordinate
self.province = province
self.city = city
self.district = district
self.street = street
self.country = country
self.formatted_address = formatted_address
}
}
final class LocationSuggestionCell: UITableViewCell {
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(hexStr: "#1E2F4A")
label.lineBreakMode = .byTruncatingTail
return label
}()
private let distanceLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12, weight: .bold)
label.textColor = UIColor(hexStr: "#50BCEF")
label.textAlignment = .center
label.backgroundColor = UIColor(hexStr: "#E3F6FF")
label.layer.cornerRadius = 6
label.clipsToBounds = true
return label
}()
private let addressLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14)
label.textColor = UIColor(hexStr: "#A2A2A2")
label.lineBreakMode = .byTruncatingTail
return label
}()
private let checkmarkView: UIImageView = {
let view = UIImageView(image: UIImage(systemName: "checkmark"))
view.tintColor = UIColor(hexStr: "#16B3FF")
view.contentMode = .scaleAspectFit
return view
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .white
selectionStyle = .none
contentView.addSubview(nameLabel)
contentView.addSubview(distanceLabel)
contentView.addSubview(addressLabel)
contentView.addSubview(checkmarkView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(location: PickedLocation, distanceText: String?, isSelected: Bool) {
nameLabel.text = location.name.isEmpty ? "已选位置" : location.name
addressLabel.text = location.address.isEmpty ? "暂无详细地址" : location.address
distanceLabel.text = distanceText
distanceLabel.isHidden = distanceText == nil
checkmarkView.isHidden = !isSelected
setNeedsLayout()
}
override func layoutSubviews() {
super.layoutSubviews()
let horizontalInset: CGFloat = 32
let checkWidth: CGFloat = 24
let rightInset: CGFloat = 20
let availableWidth = max(0, contentView.bounds.width - horizontalInset - checkWidth - rightInset)
let distanceText = distanceLabel.text ?? ""
let distanceWidth = distanceLabel.isHidden
? 0
: max(58, (distanceText as NSString).size(withAttributes: [.font: distanceLabel.font as Any]).width + 14)
let nameWidth = max(0, availableWidth - (distanceLabel.isHidden ? 0 : distanceWidth + 8))
nameLabel.frame = CGRect(x: horizontalInset, y: 11, width: nameWidth, height: 24)
distanceLabel.frame = CGRect(x: nameLabel.frame.maxX + 8, y: 13, width: distanceWidth, height: 20)
addressLabel.frame = CGRect(x: horizontalInset, y: 42, width: availableWidth, height: 20)
checkmarkView.frame = CGRect(x: contentView.bounds.width - rightInset - checkWidth, y: 26, width: checkWidth, height: checkWidth)
}
}
#if !targetEnvironment(simulator)
final class CurrentUserLocationAnnotation: NSObject, MAAnnotation {
var coordinate: CLLocationCoordinate2D
init(coordinate: CLLocationCoordinate2D) {
self.coordinate = coordinate
super.init()
}
}
final class CurrentUserLocationAnnotationView: MAAnnotationView {
private let avatarContainer: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 14
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 2)
view.layer.shadowRadius = 5
view.layer.shadowOpacity = 0.16
return view
}()
private let avatarImageView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
view.clipsToBounds = true
view.layer.cornerRadius = 10
return view
}()
private let statusOuterView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 8
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 1)
view.layer.shadowRadius = 2
view.layer.shadowOpacity = 0.16
return view
}()
private let statusInnerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#56DC42")
view.layer.cornerRadius = 5
return view
}()
override init!(annotation: MAAnnotation!, reuseIdentifier: String!) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
backgroundColor = .clear
canShowCallout = false
isUserInteractionEnabled = false
bounds = CGRect(x: 0, y: 0, width: 56, height: 72)
centerOffset = CGPoint(x: 0, y: -29)
addSubview(avatarContainer)
avatarContainer.addSubview(avatarImageView)
addSubview(statusOuterView)
statusOuterView.addSubview(statusInnerView)
avatarContainer.frame = CGRect(x: 0, y: 0, width: 56, height: 56)
avatarImageView.frame = avatarContainer.bounds.insetBy(dx: 4, dy: 4)
statusOuterView.frame = CGRect(x: 20, y: 57, width: 16, height: 16)
statusInnerView.frame = statusOuterView.bounds.insetBy(dx: 3, dy: 3)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure() {
avatarImageView.setHeadPic(AppContextManager.shared.head_pic)
}
}
#endif
final class LocationPickerViewController: BaseViewController {
override var isNavigationBarHidden: Bool { true }
var initialLocation: PickedLocation?
var onPickedLocation: ((PickedLocation) -> Void)?
private let collapsedResultPanelHeight: CGFloat = 290
private var rootView: LocationPickerView!
private var currentCoordinate: CLLocationCoordinate2D?
private var selectedLocation: PickedLocation?
private var suggestions: [PickedLocation] = []
private var searchWorkItem: DispatchWorkItem?
private var isResultPanelExpanded = false
private var keyboardManagerWasEnabled = true
private var keyboardManagerResignOnTouchOutside = true
#if !targetEnvironment(simulator)
private let searchAPI = AMapSearchAPI()
private var currentLocationAnnotation: CurrentUserLocationAnnotation?
#endif
override func loadView() {
rootView = LocationPickerView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
rootView.resultTableView.dataSource = self
rootView.resultTableView.delegate = self
rootView.backBtn.addTarget(self, action: #selector(handleBack), for: .touchUpInside)
rootView.confirmBtn.addTarget(self, action: #selector(confirm), for: .touchUpInside)
rootView.recenterBtn.addTarget(self, action: #selector(recenterToCurrentLocation), for: .touchUpInside)
rootView.searchField.addTarget(self, action: #selector(searchChanged), for: .editingChanged)
rootView.searchField.addTarget(self, action: #selector(search), for: .editingDidEndOnExit)
setupKeyboard()
setupMap()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
keyboardManagerWasEnabled = IQKeyboardManager.shared.isEnabled
keyboardManagerResignOnTouchOutside = IQKeyboardManager.shared.resignOnTouchOutside
IQKeyboardManager.shared.isEnabled = false
IQKeyboardManager.shared.resignOnTouchOutside = false
view.transform = .identity
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
IQKeyboardManager.shared.isEnabled = keyboardManagerWasEnabled
IQKeyboardManager.shared.resignOnTouchOutside = keyboardManagerResignOnTouchOutside
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isMovingFromParent || isBeingDismissed {
#if !targetEnvironment(simulator)
rootView.cleanupMap()
#endif
}
}
@objc private func handleBack() {
close()
}
@objc private func confirm() {
guard let selectedLocation,
CLLocationCoordinate2DIsValid(selectedLocation.coordinate) else {
DLToast.showError(text: "请选择位置")
return
}
onPickedLocation?(selectedLocation)
close()
}
private func close() {
view.endEditing(true)
if presentingViewController != nil {
dismiss(animated: true)
} else {
navigationController?.popViewController(animated: true)
}
}
@objc private func searchChanged() {
searchWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in self?.search() }
searchWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: workItem)
}
@objc private func search() {
let keyword = rootView.searchField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !keyword.isEmpty else {
loadNearbyPOIs(around: selectedLocation?.coordinate ?? currentCoordinate)
return
}
#if !targetEnvironment(simulator)
let request = AMapPOIKeywordsSearchRequest()
request.keywords = keyword
request.city = ""
request.offset = 20
if let coordinate = selectedLocation?.coordinate ?? currentCoordinate {
request.location = AMapGeoPoint.location(withLatitude: CGFloat(coordinate.latitude),
longitude: CGFloat(coordinate.longitude))
}
searchAPI?.aMapPOIKeywordsSearch(request)
#else
rootView.emptyLabel.isHidden = false
rootView.emptyLabel.text = "模拟器无法搜索地图地点"
#endif
}
private func setupKeyboard() {
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] notification in
guard let self,
self.isViewLoaded,
self.view.window != nil,
self.rootView.searchField.isFirstResponder else { return }
let duration = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
self.setResultPanelExpanded(true, duration: duration)
})
.disposed(by: disposeBag)
}
private func setResultPanelExpanded(_ expanded: Bool, duration: TimeInterval = 0.25) {
guard isResultPanelExpanded != expanded else { return }
isResultPanelExpanded = expanded
let height = expanded ? view.bounds.height * 2 / 3 : collapsedResultPanelHeight
rootView.setResultPanelHeight(height, duration: duration)
}
private func collapseResultPanel() {
setResultPanelExpanded(false)
rootView.searchField.resignFirstResponder()
}
@objc private func recenterToCurrentLocation() {
guard let currentCoordinate else {
DLToast.showError(text: "暂无当前位置")
return
}
#if !targetEnvironment(simulator)
rootView.mapView.setCenter(currentCoordinate, animated: true)
#endif
selectMapCenterLocation(currentCoordinate, placeholderName: "当前位置")
}
private func setupMap() {
if let latitude = Defaults[\.currentLatitude], let longitude = Defaults[\.currentLongitude] {
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
if CLLocationCoordinate2DIsValid(coordinate) {
currentCoordinate = coordinate
}
}
let validInitialLocation = initialLocation.flatMap { location in
CLLocationCoordinate2DIsValid(location.coordinate) ? location : nil
}
#if !targetEnvironment(simulator)
searchAPI?.delegate = self
rootView.mapView.delegate = self
rootView.mapView.showsUserLocation = false
let coordinate = validInitialLocation?.coordinate
?? currentCoordinate
?? CLLocationCoordinate2D(latitude: 39.9042, longitude: 116.4074)
rootView.mapView.setCenter(coordinate, animated: false)
rootView.mapView.setZoomLevel(15, animated: false)
if let currentCoordinate {
addCurrentLocationAnnotation(at: currentCoordinate)
}
if let validInitialLocation {
select(validInitialLocation, recenterMap: false)
requestReverseGeocode(for: validInitialLocation.coordinate)
loadNearbyPOIs(around: validInitialLocation.coordinate)
} else if let currentCoordinate {
selectMapCenterLocation(currentCoordinate, placeholderName: "当前位置")
}
#else
if let validInitialLocation {
select(validInitialLocation, recenterMap: false)
}
rootView.emptyLabel.isHidden = false
rootView.emptyLabel.text = "请在真机上选择地图位置"
#endif
}
private func loadNearbyPOIs(around coordinate: CLLocationCoordinate2D?) {
#if !targetEnvironment(simulator)
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else {
rootView.emptyLabel.isHidden = false
rootView.emptyLabel.text = "暂无当前位置,可直接在地图上选择"
return
}
let request = AMapPOIAroundSearchRequest()
request.location = AMapGeoPoint.location(withLatitude: CGFloat(coordinate.latitude),
longitude: CGFloat(coordinate.longitude))
request.radius = 1500
request.offset = 20
searchAPI?.aMapPOIAroundSearch(request)
#endif
}
private func updateSuggestions(_ values: [PickedLocation]) {
suggestions = values
keepSelectedLocationInSuggestions()
rootView.resultTableView.reloadData()
rootView.emptyLabel.isHidden = !suggestions.isEmpty
}
private func select(_ location: PickedLocation, recenterMap: Bool) {
guard CLLocationCoordinate2DIsValid(location.coordinate) else { return }
selectedLocation = location
keepSelectedLocationInSuggestions()
rootView.confirmBtn.isEnabled = true
rootView.emptyLabel.isHidden = true
rootView.resultTableView.reloadData()
#if !targetEnvironment(simulator)
if recenterMap {
rootView.mapView.setCenter(location.coordinate, animated: true)
}
#endif
}
private func selectMapCenterLocation(_ coordinate: CLLocationCoordinate2D,
placeholderName: String = "正在获取地点...") {
guard CLLocationCoordinate2DIsValid(coordinate) else { return }
select(PickedLocation(name: placeholderName,
address: "正在获取地址...",
coordinate: coordinate),
recenterMap: false)
#if !targetEnvironment(simulator)
requestReverseGeocode(for: coordinate)
loadNearbyPOIs(around: coordinate)
#endif
}
private func keepSelectedLocationInSuggestions() {
guard let selectedLocation else { return }
if let index = suggestions.firstIndex(where: { coordinatesMatch($0.coordinate, selectedLocation.coordinate) }) {
suggestions[index] = selectedLocation
} else {
suggestions.insert(selectedLocation, at: 0)
}
}
private func coordinatesMatch(_ lhs: CLLocationCoordinate2D, _ rhs: CLLocationCoordinate2D) -> Bool {
abs(lhs.latitude - rhs.latitude) < 0.000_001 && abs(lhs.longitude - rhs.longitude) < 0.000_001
}
private func distanceText(to coordinate: CLLocationCoordinate2D) -> String? {
guard let selectedLocation else { return nil }
let distance = CLLocation(latitude: selectedLocation.coordinate.latitude,
longitude: selectedLocation.coordinate.longitude)
.distance(from: CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude))
if distance < 100 {
return "100m内"
}
if distance < 1000 {
return "\(Int(distance.rounded()))m"
}
return String(format: "%.1fkm", distance / 1000)
}
#if !targetEnvironment(simulator)
private func requestReverseGeocode(for coordinate: CLLocationCoordinate2D) {
let request = AMapReGeocodeSearchRequest()
request.location = AMapGeoPoint.location(withLatitude: CGFloat(coordinate.latitude),
longitude: CGFloat(coordinate.longitude))
request.requireExtension = true
searchAPI?.aMapReGoecodeSearch(request)
}
private func addCurrentLocationAnnotation(at coordinate: CLLocationCoordinate2D) {
if let currentLocationAnnotation {
currentLocationAnnotation.coordinate = coordinate
return
}
let annotation = CurrentUserLocationAnnotation(coordinate: coordinate)
currentLocationAnnotation = annotation
rootView.mapView.addAnnotation(annotation)
}
private func location(from poi: AMapPOI) -> PickedLocation? {
guard let point = poi.location else { return nil }
let coordinate = CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude)
guard CLLocationCoordinate2DIsValid(coordinate) else { return nil }
return PickedLocation(name: poi.name ?? "",
address: poi.address ?? poi.district ?? "",
coordinate: coordinate,
province: poi.province ?? "",
city: poi.city ?? "",
district: poi.district ?? "",
formatted_address: poi.address ?? "")
}
#endif
}
extension LocationPickerViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
suggestions.count
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
guard scrollView === rootView.resultTableView else { return }
rootView.searchField.resignFirstResponder()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: LocationPickerView.cellIdentifier,
for: indexPath) as? LocationSuggestionCell else {
return UITableViewCell()
}
let location = suggestions[indexPath.row]
let isSelected = selectedLocation.map { coordinatesMatch($0.coordinate, location.coordinate) } ?? false
cell.configure(location: location,
distanceText: distanceText(to: location.coordinate),
isSelected: isSelected)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
collapseResultPanel()
let location = suggestions[indexPath.row]
select(location, recenterMap: true)
rootView.searchField.text = location.name
#if !targetEnvironment(simulator)
requestReverseGeocode(for: location.coordinate)
#endif
}
}
#if !targetEnvironment(simulator)
extension LocationPickerViewController: AMapSearchDelegate, MAMapViewDelegate {
func onPOISearchDone(_ request: AMapPOISearchBaseRequest!, response: AMapPOISearchResponse!) {
if let aroundRequest = request as? AMapPOIAroundSearchRequest,
let point = aroundRequest.location {
let coordinate = CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude)
guard let selectedLocation, coordinatesMatch(selectedLocation.coordinate, coordinate) else { return }
} else if let keywordRequest = request as? AMapPOIKeywordsSearchRequest,
let point = keywordRequest.location {
let coordinate = CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude)
guard let selectedLocation, coordinatesMatch(selectedLocation.coordinate, coordinate) else { return }
}
updateSuggestions(response.pois.compactMap { self.location(from: $0) })
}
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
guard let point = request.location else { return }
let coordinate = CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude)
guard let selectedLocation, coordinatesMatch(selectedLocation.coordinate, coordinate) else { return }
let reGeocode = response.regeocode
let poi = reGeocode?.pois?.first
let component = reGeocode?.addressComponent
let formattedAddress = reGeocode?.formattedAddress ?? ""
let hasResolvedName = !selectedLocation.name.isEmpty
&& selectedLocation.name != "当前位置"
&& selectedLocation.name != "正在获取地点..."
let hasResolvedAddress = !selectedLocation.address.isEmpty
&& selectedLocation.address != "正在获取地址..."
let name = hasResolvedName
? selectedLocation.name
: (poi?.name ?? (formattedAddress.isEmpty ? "已选位置" : formattedAddress))
let address = hasResolvedAddress
? selectedLocation.address
: (poi?.address ?? formattedAddress)
select(PickedLocation(name: name,
address: address,
coordinate: coordinate,
province: component?.province ?? poi?.province ?? selectedLocation.province,
city: component?.city ?? poi?.city ?? selectedLocation.city,
district: component?.district ?? poi?.district ?? selectedLocation.district,
street: component?.streetNumber?.street ?? selectedLocation.street,
country: component?.country ?? selectedLocation.country,
formatted_address: formattedAddress.isEmpty
? (selectedLocation.formatted_address.isEmpty ? address : selectedLocation.formatted_address)
: formattedAddress),
recenterMap: false)
}
func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
if let reGeocodeRequest = request as? AMapReGeocodeSearchRequest,
let point = reGeocodeRequest.location {
let coordinate = CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude)
if let selectedLocation, coordinatesMatch(selectedLocation.coordinate, coordinate) {
let name = selectedLocation.name == "正在获取地点..." ? "已选位置" : selectedLocation.name
let address = selectedLocation.address == "正在获取地址..." ? "" : selectedLocation.address
select(PickedLocation(name: name,
address: address,
coordinate: coordinate,
province: selectedLocation.province,
city: selectedLocation.city,
district: selectedLocation.district,
street: selectedLocation.street,
country: selectedLocation.country,
formatted_address: selectedLocation.formatted_address),
recenterMap: false)
}
return
}
rootView.emptyLabel.isHidden = false
rootView.emptyLabel.text = "地点加载失败,请重试或在地图上选择"
}
func mapView(_ mapView: MAMapView!, regionDidChangeAnimated animated: Bool, wasUserAction: Bool) {
guard wasUserAction else { return }
collapseResultPanel()
let center = CGPoint(x: mapView.bounds.midX, y: mapView.bounds.midY)
selectMapCenterLocation(mapView.convert(center, toCoordinateFrom: mapView))
}
func mapView(_ mapView: MAMapView!, mapWillMoveByUser wasUserAction: Bool) {
guard wasUserAction else { return }
collapseResultPanel()
}
func mapView(_ mapView: MAMapView!, didSingleTappedAt coordinate: CLLocationCoordinate2D) {
collapseResultPanel()
}
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
guard annotation is CurrentUserLocationAnnotation else { return nil }
let identifier = "CurrentUserLocation"
guard let view = (mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? CurrentUserLocationAnnotationView)
?? CurrentUserLocationAnnotationView(annotation: annotation, reuseIdentifier: identifier) else { return nil }
view.annotation = annotation
view.zIndex = 10
view.configure()
return view
}
}
#endif

View File

@ -10,6 +10,7 @@ import RxSwift
import RxCocoa import RxCocoa
import RxDataSources import RxDataSources
import Kingfisher import Kingfisher
import SwiftyUserDefaults
#if !targetEnvironment(simulator) #if !targetEnvironment(simulator)
import GeYanSdk import GeYanSdk
import RxGesture import RxGesture
@ -162,14 +163,24 @@ class AccountVC: BaseViewController {
// MARK: - API // MARK: - API
private func requestLogout() { private func requestLogout() {
DLToast.showLoading()
UserService.logout().subscribe(onNext: { response in UserService.logout().subscribe(onNext: { response in
DLToast.dismiss()
NotificationCenter.default.post(name: .invalidatePopupQueue, object: nil) NotificationCenter.default.post(name: .invalidatePopupQueue, object: nil)
GeYanSdk.deletePreResultCache() GeYanSdk.deletePreResultCache()
AppContextManager.shared.deleteAccount() AppContextManager.shared.deleteAccount()
GroupIMService.shared.logout() GroupIMService.shared.logout()
AppDelegate.shared.showMainViewController() self.requestUserConfig()
}, onError: { _ in }).disposed(by: disposeBag)
}
private func requestUserConfig() {
DLToast.showLoading()
SystemService.userConfig().subscribe(onNext: { response in
guard let model = response.model else { return }
Defaults[\.loginToken] = model.token
AppContextManager.shared.systemConfig = model.config
DLToast.dismiss {
AppDelegate.shared.showMainViewController()
}
}, onError: { _ in }).disposed(by: disposeBag) }, onError: { _ in }).disposed(by: disposeBag)
} }

View File

@ -24,6 +24,7 @@ class AvatarIconListVC: BaseViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
rootView.configure(headPic: iconIndex) rootView.configure(headPic: iconIndex)
rootView.saveBtn.isHidden = true
rootView.saveBtn.rx.tap.subscribe(onNext: { [weak self] _ in rootView.saveBtn.rx.tap.subscribe(onNext: { [weak self] _ in
guard let self else { return } guard let self else { return }
@ -35,6 +36,10 @@ class AvatarIconListVC: BaseViewController {
rootView.onBadgeTap = { [weak self] in rootView.onBadgeTap = { [weak self] in
self?.showUploadPicker() self?.showUploadPicker()
} }
rootView.onIconSelected = { [weak self] in
self?.rootView.saveBtn.isHidden = false
}
} }
private func showUploadPicker() { private func showUploadPicker() {
@ -82,6 +87,7 @@ class AvatarIconListVC: BaseViewController {
.subscribe(onNext: { [weak self] url in .subscribe(onNext: { [weak self] url in
guard let self else { return } guard let self else { return }
self.rootView.setPreviewImage(image) self.rootView.setPreviewImage(image)
self.rootView.saveBtn.isHidden = true
self.onSelectIcon?(url) self.onSelectIcon?(url)
}, onError: { error in }, onError: { error in
DLToast.show(error, fallbackText: "更换失败") DLToast.show(error, fallbackText: "更换失败")

View File

@ -27,6 +27,7 @@ class AvatarIconListView: UIView {
let saveBtn = UIButton(type: .custom) let saveBtn = UIButton(type: .custom)
var onBadgeTap: (() -> Void)? var onBadgeTap: (() -> Void)?
var onIconSelected: (() -> Void)?
private var iconImgList: [UIImage] = [] private var iconImgList: [UIImage] = []
private let collectionLayout = CollectionHFlowLayout() private let collectionLayout = CollectionHFlowLayout()
@ -97,6 +98,7 @@ class AvatarIconListView: UIView {
saveBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) saveBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
saveBtn.layer.cornerRadius = 28 saveBtn.layer.cornerRadius = 28
saveBtn.clipsToBounds = true saveBtn.clipsToBounds = true
saveBtn.isHidden = true
saveBtn.layoutChain saveBtn.layoutChain
.edgesHorzontal(30) .edgesHorzontal(30)
.bottom(kSafeBottomMargin + 13) .bottom(kSafeBottomMargin + 13)
@ -281,6 +283,7 @@ extension AvatarIconListView: UICollectionViewDelegate, UICollectionViewDataSour
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedIndex = indexPath.item + 1 selectedIndex = indexPath.item + 1
onIconSelected?()
} }
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {

View File

@ -611,7 +611,6 @@ final class PigeonMessageVC: BaseViewController {
) )
} }
.subscribe(onNext: { [weak self] _ in .subscribe(onNext: { [weak self] _ in
DLToast.dismiss()
guard let self else { return } guard let self else { return }
self.selectedTemplate = nil self.selectedTemplate = nil
self.selectedVoiceTemplate = nil self.selectedVoiceTemplate = nil
@ -620,8 +619,9 @@ final class PigeonMessageVC: BaseViewController {
self.cleanupAudio(deleteFile: true) self.cleanupAudio(deleteFile: true)
self.rootView.clearVoiceDraft() self.rootView.clearVoiceDraft()
self.recordPigeonSendSuccess() self.recordPigeonSendSuccess()
self.refreshGroupVip() DLToast.show(text: "发送成功") {
DLToast.show(text: "发送成功") self.refreshGroupVip()
}
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.show(error) DLToast.show(error)

View File

@ -222,6 +222,7 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
} }
private func showTimePicker(for pointIndex: Int) { private func showTimePicker(for pointIndex: Int) {
view.endEditing(true)
let picker = BRDatePickerView(pickerMode: .HM) let picker = BRDatePickerView(pickerMode: .HM)
picker.title = "选择到达时间" picker.title = "选择到达时间"
if pointIndex < viewModel.pointsRelay.value.count, if pointIndex < viewModel.pointsRelay.value.count,
@ -247,14 +248,14 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
self.viewModel.pointsRelay.accept(list) self.viewModel.pointsRelay.accept(list)
} }
} }
picker.show() showSchedulePicker(picker)
} }
private func showLocationPicker(for pointIndex: Int, item: SchedulePointItem) { private func showLocationPicker(for pointIndex: Int, item: SchedulePointItem) {
let coord = item.latitude != 0 || item.longitude != 0 let coord = item.latitude != 0 || item.longitude != 0
? CLLocationCoordinate2D(latitude: item.latitude, longitude: item.longitude) ? CLLocationCoordinate2D(latitude: item.latitude, longitude: item.longitude)
: kCLLocationCoordinate2DInvalid : kCLLocationCoordinate2DInvalid
let vc = LocationPickerVC() let vc = LocationPickerViewController()
vc.modalPresentationStyle = .fullScreen vc.modalPresentationStyle = .fullScreen
if !item.locationName.isEmpty { if !item.locationName.isEmpty {
vc.initialLocation = PickedLocation( vc.initialLocation = PickedLocation(
@ -437,6 +438,7 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
// MARK: - Date / Time Picker // MARK: - Date / Time Picker
private func showDatePicker() { private func showDatePicker() {
view.endEditing(true)
let picker = BRDatePickerView(pickerMode: .YMD) let picker = BRDatePickerView(pickerMode: .YMD)
picker.title = "选择日期" picker.title = "选择日期"
picker.minDate = Date() picker.minDate = Date()
@ -450,7 +452,7 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
picker.resultBlock = { [weak self] date, _ in picker.resultBlock = { [weak self] date, _ in
if let d = date { self?.viewModel.selectedDate.accept(d) } if let d = date { self?.viewModel.selectedDate.accept(d) }
} }
picker.show() showSchedulePicker(picker)
} }
private func makeSchedulePickerStyle() -> BRPickerStyle { private func makeSchedulePickerStyle() -> BRPickerStyle {
@ -477,11 +479,11 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
style.cancelBorderStyle = .fill style.cancelBorderStyle = .fill
style.cancelBtnFrame = CGRect(x: width - 16 - 28, y: 14, width: 28, height: 28) style.cancelBtnFrame = CGRect(x: width - 16 - 28, y: 14, width: 28, height: 28)
style.pickerColor = .white style.pickerColor = .clear
style.pickerHeight = 230 style.pickerHeight = 230
style.rowHeight = 44 style.rowHeight = 44
style.clearPickerNewStyle = true style.clearPickerNewStyle = true
style.selectRowColor = UIColor(hexStr: "#E3F6FF") style.separatorColor = .clear
style.selectRowTextColor = UIColor(hexStr: "#16B3FF") style.selectRowTextColor = UIColor(hexStr: "#16B3FF")
style.selectRowTextFont = .systemFont(ofSize: 18, weight: .medium) style.selectRowTextFont = .systemFont(ofSize: 18, weight: .medium)
style.pickerTextColor = UIColor(hexStr: "#333333") style.pickerTextColor = UIColor(hexStr: "#333333")
@ -490,6 +492,34 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
return style return style
} }
private func showSchedulePicker(_ picker: BRDatePickerView) {
picker.show()
guard let alertView = picker.alertView,
let pickerView = firstSubview(of: UIPickerView.self, in: alertView) else { return }
let selectedBackgroundView = UIView()
selectedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
selectedBackgroundView.backgroundColor = UIColor(hexStr: "#E3F6FF")
selectedBackgroundView.layer.cornerRadius = 12
selectedBackgroundView.isUserInteractionEnabled = false
alertView.insertSubview(selectedBackgroundView, belowSubview: pickerView)
NSLayoutConstraint.activate([
selectedBackgroundView.leadingAnchor.constraint(equalTo: alertView.leadingAnchor, constant: 30),
selectedBackgroundView.trailingAnchor.constraint(equalTo: alertView.trailingAnchor, constant: -30),
selectedBackgroundView.centerYAnchor.constraint(equalTo: pickerView.centerYAnchor),
selectedBackgroundView.heightAnchor.constraint(equalToConstant: picker.pickerStyle?.rowHeight ?? 44)
])
}
private func firstSubview<View: UIView>(of type: View.Type, in view: UIView) -> View? {
if let match = view as? View { return match }
for subview in view.subviews {
if let match = firstSubview(of: type, in: subview) { return match }
}
return nil
}
private func makeSchedulePickerFooter(onDone: @escaping () -> Void) -> UIView { private func makeSchedulePickerFooter(onDone: @escaping () -> Void) -> UIView {
let width = UIScreen.main.bounds.width let width = UIScreen.main.bounds.width
let footer = UIView(frame: CGRect(x: 0, y: 0, width: width, height: 78)) let footer = UIView(frame: CGRect(x: 0, y: 0, width: width, height: 78))

View File

@ -1,360 +0,0 @@
//
// LocationPickerVC.swift
// QuickLocation
//
// Created by on 2026/6/24.
//
import UIKit
import RxSwift
import RxCocoa
import CoreLocation
import SwiftyUserDefaults
#if !targetEnvironment(simulator)
import AMapNaviKit
import AMapSearchKit
#endif
struct PickedLocation {
let name: String
let address: String
let coordinate: CLLocationCoordinate2D
var province: String = ""
var city: String = ""
var district: String = ""
var street: String = ""
var country: String = ""
var formatted_address: String = ""
}
class LocationPickerVC: BaseViewController {
override var isNavigationBarHidden: Bool { true }
var onPickedLocation: ((PickedLocation) -> Void)?
var initialLocation: PickedLocation?
fileprivate var rootView: LocationPickerView!
override func loadView() {
rootView = LocationPickerView(frame: UIScreen.main.bounds)
view = rootView
}
private var isShowPoi: Bool = false
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// pop/dismiss
if isMovingFromParent || isBeingDismissed {
#if !targetEnvironment(simulator)
rootView.cleanupMap()
#endif
}
}
override func viewDidLoad() {
super.viewDidLoad()
setupMap()
setupKeyboard()
setupSearch()
//
if let loc = initialLocation {
selectedLocation = loc
rootView.bottomView.isHidden = false
rootView.poiNameLab.text = loc.name
rootView.poiAddressLab.text = loc.address
if CLLocationCoordinate2DIsValid(loc.coordinate) {
rootView.mapView.setCenter(loc.coordinate, animated: false)
rootView.mapView.setZoomLevel(19, animated: false)
addLocationAnnotation(coordinate: loc.coordinate)
}
}
}
// MARK: - Keyboard
private var keyboardHeight: CGFloat = 0
private func setupKeyboard() {
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
.subscribe(onNext: { [weak self] noti in
guard let self = self,
let frame = noti.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
else { return }
self.keyboardHeight = frame.height
})
.disposed(by: disposeBag)
}
// MARK: - Search
#if !targetEnvironment(simulator)
private let searchAPI = AMapSearchAPI()
private var searchPoiRequest = AMapPOIKeywordsSearchRequest()
#endif
private var selectedLocation: PickedLocation?
private var searchKeyword: String = ""
private var poiItems: [AMapPOI] = []
private var maxTableHeight: CGFloat = 300
private var currentLocation: CLLocationCoordinate2D?
private func setupSearch() {
#if !targetEnvironment(simulator)
searchAPI?.delegate = self
searchPoiRequest.city = ""
searchPoiRequest.offset = 10
if let loc = currentLocation {
searchPoiRequest.location = AMapGeoPoint.location(withLatitude: CGFloat(loc.latitude), longitude: CGFloat(loc.longitude))
}
#endif
rootView.resultTableView.delegate = self
rootView.resultTableView.dataSource = self
//
rootView.searchField.rx.text.orEmpty
.debounce(.milliseconds(400), scheduler: MainScheduler.instance)
// .filter { $0.count >= 2 }
.subscribe(onNext: { [weak self] keyword in
self?.poiSearch(keyword: keyword)
})
.disposed(by: disposeBag)
rootView.searchField.rx.controlEvent(.editingDidBegin)
.subscribe(onNext: {
self.isShowPoi = false
})
.disposed(by: disposeBag)
rootView.searchField.rx.controlEvent(.editingDidEndOnExit)
.subscribe(onNext: { [weak self] _ in
guard let self = self, !self.poiItems.isEmpty else { return }
self.selectPOI(at: 0)
self.rootView.searchField.resignFirstResponder()
self.rootView.resultTableView.isHidden = true
})
.disposed(by: disposeBag)
//
rootView.searchBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
guard let self = self, !self.poiItems.isEmpty else { return }
self.selectPOI(at: 0)
self.rootView.searchField.resignFirstResponder()
self.rootView.resultTableView.isHidden = true
})
.disposed(by: disposeBag)
rootView.backBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.dismiss(animated: true)
})
.disposed(by: disposeBag)
rootView.confirmBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
guard let self = self, let loc = self.selectedLocation else { return }
self.onPickedLocation?(loc)
self.dismiss(animated: true)
})
.disposed(by: disposeBag)
}
private func poiSearch(keyword: String) {
searchKeyword = keyword
#if !targetEnvironment(simulator)
searchPoiRequest.keywords = keyword
searchAPI?.aMapPOIKeywordsSearch(searchPoiRequest)
#endif
}
private func selectPOI(at index: Int) {
guard index < poiItems.count else { return }
selectLocation(poi: poiItems[index])
}
private func selectLocation(poi: AMapPOI) {
let coord = CLLocationCoordinate2D(latitude: CGFloat(poi.location.latitude), longitude: CGFloat(poi.location.longitude))
guard CLLocationCoordinate2DIsValid(coord) else { return }
isShowPoi = true
selectedLocation = PickedLocation(
name: poi.name ?? "",
address: poi.address ?? "",
coordinate: coord,
province: poi.province ?? "",
city: poi.city ?? "",
district: poi.district ?? "",
street: "",
formatted_address: poi.address ?? ""
)
rootView.resultTableView.isHidden = true
rootView.bottomView.isHidden = false
rootView.poiNameLab.text = poi.name
rootView.poiAddressLab.text = poi.address
rootView.mapView.setCenter(coord, animated: true)
rootView.mapView.setZoomLevel(19, animated: true)
addLocationAnnotation(coordinate: coord)
// street
#if !targetEnvironment(simulator)
let regeo = AMapReGeocodeSearchRequest()
regeo.location = AMapGeoPoint.location(withLatitude: CGFloat(coord.latitude),
longitude: CGFloat(coord.longitude))
regeo.requireExtension = true
searchAPI?.aMapReGoecodeSearch(regeo)
#endif
}
private func updateTableHeight(itemCount: Int) {
let contentH = CGFloat(itemCount) * 50
let searchBarBottom = kStatusBarHeight + 8 + 44 + 10
let bottomOffset: CGFloat = keyboardHeight > 0 ? keyboardHeight : 200
let available = kScreenHeight - searchBarBottom - bottomOffset - 10
maxTableHeight = max(80, available)
let h = min(contentH, maxTableHeight)
rootView.resultTableView.layoutChain.height(h)
}
// MARK: - Map
private func setupMap() {
#if !targetEnvironment(simulator)
rootView.mapView.delegate = self
rootView.mapView.showsUserLocation = false
if let lat = Defaults[\.currentLatitude], let lon = Defaults[\.currentLongitude] {
let coord = CLLocationCoordinate2D(latitude: lat, longitude: lon)
if CLLocationCoordinate2DIsValid(coord) {
currentLocation = coord
rootView.mapView.setCenter(coord, animated: false)
rootView.mapView.setZoomLevel(18, animated: false)
}
}
#endif
}
private func addLocationAnnotation(coordinate: CLLocationCoordinate2D) {
#if !targetEnvironment(simulator)
for ann in rootView.mapView.annotations?.compactMap({ $0 as? MAPointAnnotation }) ?? [] {
rootView.mapView.removeAnnotation(ann)
}
let ann = MAPointAnnotation()
ann.coordinate = coordinate
rootView.mapView.addAnnotation(ann)
#endif
}
}
#if !targetEnvironment(simulator)
// MARK: - AMapSearchDelegate
extension LocationPickerVC: AMapSearchDelegate {
func onPOISearchDone(_ request: AMapPOISearchBaseRequest!, response: AMapPOISearchResponse!) {
// POI ID
if request is AMapPOIIDSearchRequest, let poi = response.pois?.first {
selectLocation(poi: poi)
return
}
//
guard response.pois.count > 0, isShowPoi == false else { return }
poiItems = response.pois
rootView.resultTableView.isHidden = false
rootView.resultTableView.reloadData()
updateTableHeight(itemCount: poiItems.count)
}
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
guard let regeo = response.regeocode else { return }
let address = regeo.formattedAddress ?? ""
// POI /
let poiName = regeo.pois?.first?.name
?? regeo.pois?.first?.address
?? regeo.pois?.first?.district
?? ""
selectedLocation = PickedLocation(
name: poiName,
address: regeo.pois?.first?.address ?? "",
coordinate: selectedLocation?.coordinate ?? kCLLocationCoordinate2DInvalid,
province: regeo.pois?.first?.province ?? "",
city: regeo.pois?.first?.city ?? "",
district: regeo.pois?.first?.district ?? "",
street: regeo.addressComponent?.streetNumber?.street ?? "",
formatted_address: regeo.pois?.first?.address ?? ""
)
rootView.bottomView.isHidden = false
rootView.poiNameLab.text = poiName
rootView.poiAddressLab.text = address
}
func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
print("AMapSearch error: \(error.localizedDescription)")
}
}
// MARK: - MAMapViewDelegate
extension LocationPickerVC: MAMapViewDelegate {
func mapView(_ mapView: MAMapView!, didSingleTappedAt coordinate: CLLocationCoordinate2D) {
isShowPoi = true
selectedLocation = PickedLocation(
name: "",
address: "",
coordinate: coordinate
)
addLocationAnnotation(coordinate: coordinate)
rootView.mapView.setCenter(coordinate, animated: true)
//
#if !targetEnvironment(simulator)
let regeo = AMapReGeocodeSearchRequest()
regeo.location = AMapGeoPoint.location(withLatitude: CGFloat(coordinate.latitude),
longitude: CGFloat(coordinate.longitude))
regeo.requireExtension = true
searchAPI?.aMapReGoecodeSearch(regeo)
#endif
}
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
if annotation is MAUserLocation { return nil }
guard annotation is MAPointAnnotation else { return nil }
let identifier = "LocationPin"
var view = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if view == nil {
view = MAAnnotationView(annotation: annotation, reuseIdentifier: identifier)
} else {
view?.annotation = annotation
}
view?.image = UIImage(named: "IM/location_pin")
view?.centerOffset = CGPoint(x: 0, y: -30)
return view
}
}
// MARK: - UITableViewDataSource / Delegate
extension LocationPickerVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
poiItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "cell")
let poi = poiItems[indexPath.row]
let name = poi.name ?? ""
if !searchKeyword.isEmpty, let range = name.lowercased().range(of: searchKeyword.lowercased()) {
let attr = NSMutableAttributedString(string: name)
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: NSRange(range, in: name))
cell.textLabel?.attributedText = attr
} else {
cell.textLabel?.text = name
}
cell.textLabel?.font = .systemFont(ofSize: 14)
cell.detailTextLabel?.text = poi.address
cell.detailTextLabel?.font = .systemFont(ofSize: 12)
cell.detailTextLabel?.textColor = .gray
let arrow = UIImageView(image: UIImage(named: "Schedule/arrow"))
arrow.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
cell.accessoryView = arrow
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
rootView.searchField.resignFirstResponder()
selectPOI(at: indexPath.row)
rootView.searchField.text = poiItems[indexPath.row].name
}
}
#endif

View File

@ -1,192 +0,0 @@
//
// LocationPickerView.swift
// QuickLocation
//
// Created by on 2026/6/24.
//
import UIKit
import RxSwift
import RxCocoa
#if !targetEnvironment(simulator)
import AMapNaviKit
#endif
class LocationPickerView: UIView {
var disposeBag = DisposeBag()
private func setupUI() {
#if !targetEnvironment(simulator)
addSubview(mapView)
#endif
addSubview(topBgView)
topBgView.addSubview(backBtn)
topBgView.addSubview(searchField)
topBgView.addSubview(searchBtn)
addSubview(resultTableView)
addSubview(bottomView)
bottomView.addSubview(poiNameLab)
bottomView.addSubview(poiAddressLab)
bottomView.addSubview(confirmBtn)
mapView.layoutChain
.edges()
topBgView.layoutChain
.top(kStatusBarHeight + 8)
.edgesHorzontal(15)
.height(44)
backBtn.layoutChain
.left().centerY()
.width(36).height(36)
searchField.layoutChain
.leftToRightOfView(backBtn, offset: 8)
.centerY()
.right(60)
.height(36)
searchBtn.layoutChain
.right(2).centerY()
.width(80).height(36)
resultTableView.layoutChain
.topToBottomOfView(topBgView, offset: 10)
.edgesHorzontal(15)
bottomView.layoutChain
.edgesHorzontal()
.bottom()
.height(160 + kSafeBottomMargin)
poiNameLab.layoutChain
.top(16).left(16).right(16)
poiAddressLab.layoutChain
.topToBottomOfView(poiNameLab, offset: 8)
.left(16).right(16)
confirmBtn.layoutChain
.edgesHorzontal(30)
.height(44)
.bottom(kSafeBottomMargin + 16)
}
// MARK: - Views
#if !targetEnvironment(simulator)
lazy var mapView: MAMapView! = {
let mv = MAMapView()
mv.zoomLevel = 18
mv.showsUserLocation = false
mv.showsCompass = false
// DispatchQueue.main.async { mv.logoCenter = CGPoint(x: mv.bounds.width - 55, y: kNaviHeight) }
return mv
}()
#endif
lazy var topBgView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.cornerRadius = 22
v.layer.shadowColor = UIColor.black.withAlphaComponent(0.1).cgColor
v.layer.shadowOffset = .zero
v.layer.shadowRadius = 6
v.layer.shadowOpacity = 1
return v
}()
lazy var backBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
return btn
}()
lazy var searchField: UITextField = {
let tf = UITextField()
tf.placeholder = "搜索地点、公园、商圈、道路"
tf.font = .systemFont(ofSize: 14)
tf.clearButtonMode = .whileEditing
tf.returnKeyType = .search
return tf
}()
lazy var searchBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("搜索", for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 18
return btn
}()
lazy var resultTableView: UITableView = {
let tv = UITableView(frame: .zero, style: .plain)
tv.backgroundColor = .white
tv.isHidden = true
tv.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tv.estimatedRowHeight = 60
tv.cornerRadius = 10
return tv
}()
lazy var bottomView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.isHidden = true
v.layer.cornerRadius = 16
v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
v.layer.shadowColor = UIColor.black.withAlphaComponent(0.1).cgColor
v.layer.shadowOffset = CGSize(width: 0, height: -2)
v.layer.shadowRadius = 10
v.layer.shadowOpacity = 1
return v
}()
lazy var poiNameLab: UILabel = {
let l = UILabel()
l.font = .systemFont(ofSize: 16, weight: .semibold)
l.textColor = UIColor(hexStr: "#333333")
return l
}()
lazy var poiAddressLab: UILabel = {
let l = UILabel()
l.font = .systemFont(ofSize: 13)
l.textColor = UIColor(hexStr: "#999999")
l.numberOfLines = 2
return l
}()
lazy var confirmBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("确定位置", for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 22
return btn
}()
#if !targetEnvironment(simulator)
func cleanupMap() {
mapView?.delegate = nil
mapView?.removeFromSuperview()
mapView = nil
}
#endif
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}