- 位置选择器 通用化
This commit is contained in:
parent
13e688444c
commit
536e735f3b
|
|
@ -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 */,
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
// 接口没返回code的时候取success字段
|
// 接口没返回code的时候取success字段
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
@ -688,6 +676,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() {
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: "更换失败")
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue