- 位置选择器 通用化
This commit is contained in:
parent
13e688444c
commit
536e735f3b
|
|
@ -247,8 +247,6 @@
|
|||
30D74ABD2FEA67EA0050EB2C /* CreateScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74ABC2FEA67EA0050EB2C /* CreateScheduleVC.swift */; };
|
||||
30D74ABF2FEA67F30050EB2C /* CreateScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74ABE2FEA67F30050EB2C /* CreateScheduleView.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 */; };
|
||||
30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDA2FDFA9EE00E958FD /* MQTTService.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 */; };
|
||||
830C70B1B53657BB020721DA /* AppRestrictManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEEA6CEA0CEAAB42C22752B /* AppRestrictManager.swift */; };
|
||||
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 */; };
|
||||
A42DCB2A66E694B7CC50033B /* AppRestrictCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89519661B70A0ADEC2A76032 /* AppRestrictCell.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -828,6 +826,8 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -1356,6 +1356,7 @@
|
|||
305A76322FCA8C7000227D26 /* CircleMember.swift */,
|
||||
305A76332FCA8C7000227D26 /* MemberAnnotation.swift */,
|
||||
305A76342FCA8C7000227D26 /* MemberAnnotationView.swift */,
|
||||
A1B2C30531DC000100000001 /* LocationPicker */,
|
||||
30D87D032FE1336300E958FD /* Navigation */,
|
||||
);
|
||||
path = Map;
|
||||
|
|
@ -1832,7 +1833,6 @@
|
|||
30D74AB32FEA25B90050EB2C /* ViewedModel.swift */,
|
||||
30D74AB92FEA37AD0050EB2C /* ScheduleModel.swift */,
|
||||
30D74ABB2FEA67CE0050EB2C /* CreateSchedule */,
|
||||
30D74BF22FEB6F5B0050EB2C /* LocationPicker */,
|
||||
30BF300A2FED09A300D9CB52 /* ScheduleDetail */,
|
||||
30A87A492FEE1CB20095E7C6 /* ItineraryDetail */,
|
||||
30A87A4E2FEE4E390095E7C6 /* ScheduleHistory */,
|
||||
|
|
@ -1853,15 +1853,6 @@
|
|||
path = CreateSchedule;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30D74BF22FEB6F5B0050EB2C /* LocationPicker */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
30D74BF02FEB6F5B0050EB2C /* LocationPickerVC.swift */,
|
||||
30D74BF12FEB6F5B0050EB2C /* LocationPickerView.swift */,
|
||||
);
|
||||
path = LocationPicker;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30D87CD52FDF9F1900E958FD /* MQTT */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -2155,6 +2146,15 @@
|
|||
name = iOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A1B2C30531DC000100000001 /* LocationPicker */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A1B2C30131DC000100000001 /* LocationPickerViewController.swift */,
|
||||
A1B2C30231DC000100000001 /* LocationPickerView.swift */,
|
||||
);
|
||||
path = LocationPicker;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B07D45692FCECE07570D9B73 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -2441,8 +2441,8 @@
|
|||
305A76942FCA8C7000227D26 /* UploadImageCell.swift in Sources */,
|
||||
305A76952FCA8C7000227D26 /* CornerRadiusCell.swift in Sources */,
|
||||
305A76962FCA8C7000227D26 /* CornerRadiusFooterView.swift in Sources */,
|
||||
30D74BF32FEB6F5B0050EB2C /* LocationPickerVC.swift in Sources */,
|
||||
30D74BF42FEB6F5B0050EB2C /* LocationPickerView.swift in Sources */,
|
||||
A1B2C30331DC000100000001 /* LocationPickerViewController.swift in Sources */,
|
||||
A1B2C30431DC000100000001 /* LocationPickerView.swift in Sources */,
|
||||
30C4C0162FDB91B8009215C1 /* CheckPermissionVC.swift in Sources */,
|
||||
55C4C0242FDB91C8009215C2 /* CheckPermissionGuideVC.swift in Sources */,
|
||||
305A76972FCA8C7000227D26 /* CornerRadiusHeaderView.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ extension ApiManager {
|
|||
case GatewayStatusCode.failure.rawValue, GatewayStatusCode.noAuthority.rawValue: // 退出当前界面
|
||||
handlePopView(message, handle)
|
||||
case GatewayStatusCode.needVip.rawValue:
|
||||
handleNeedVip(message ?? "")
|
||||
handleNeedVip(message)
|
||||
default:
|
||||
// 接口没返回code的时候取success字段
|
||||
if success != nil {
|
||||
|
|
@ -172,8 +172,16 @@ extension ApiManager {
|
|||
GeYanSdk.deletePreResultCache()
|
||||
AppContextManager.shared.deleteAccount()
|
||||
GroupIMService.shared.logout()
|
||||
DLToast.showError(text: msg ?? "登录失效,请重新登录!") {
|
||||
AppDelegate.shared.showMainViewController()
|
||||
DLToast.showError(text: msg ?? "登录失效,请重新登录!") { [weak self] in
|
||||
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() {
|
||||
#if DEBUG
|
||||
setupApiEnv(0)
|
||||
setupApiEnv(1)
|
||||
openIM_API = "https://imapi.zuom8.cn" // http://38.207.176.65:10002
|
||||
openIM_WS = "ws://imws.zuom8.cn" // ws://38.207.176.65:10010
|
||||
#elseif AdHoc
|
||||
setupApiEnv(0)
|
||||
setupApiEnv(1)
|
||||
openIM_API = "https://imapi.zuom8.cn"
|
||||
openIM_WS = "ws://imws.zuom8.cn"
|
||||
#else
|
||||
|
|
|
|||
|
|
@ -513,12 +513,6 @@ final class GroupChatVC: BaseViewController {
|
|||
rootView.textField.delegate = self
|
||||
rootView.mentionTableView.delegate = 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() {
|
||||
|
|
@ -670,13 +664,7 @@ final class GroupChatVC: BaseViewController {
|
|||
|
||||
rootView.sendBtn.rx.tap
|
||||
.subscribe(onNext: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
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()
|
||||
self?.sendCurrentTextMessage()
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
|
|
@ -689,6 +677,15 @@ final class GroupChatVC: BaseViewController {
|
|||
.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: - 显示表情面板
|
||||
private func showEmojiPanel() {
|
||||
let show = self.rootView.emojiPanelView.isHidden
|
||||
|
|
@ -745,9 +742,12 @@ final class GroupChatVC: BaseViewController {
|
|||
}
|
||||
|
||||
private func showLocationPicker() {
|
||||
let picker = ChatLocationPickerVC()
|
||||
let picker = LocationPickerViewController()
|
||||
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)
|
||||
}
|
||||
|
|
@ -1123,15 +1123,35 @@ final class GroupChatVC: BaseViewController {
|
|||
}()
|
||||
}
|
||||
|
||||
// MARK: - UITextFieldDelegate / Mention table
|
||||
extension GroupChatVC: UITextFieldDelegate, UITableViewDataSource {
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
let text = textField.text ?? ""
|
||||
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return true }
|
||||
viewModel.input.sendMessage.onNext(text)
|
||||
textField.text = ""
|
||||
// MARK: - UITextViewDelegate / Mention table
|
||||
extension GroupChatVC: UITextViewDelegate, UITableViewDataSource {
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
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()
|
||||
}
|
||||
|
||||
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
rootView.refreshComposer()
|
||||
}
|
||||
|
||||
func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
|
||||
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: - 群聊位置详情
|
||||
final class ChatLocationDetailVC: BaseViewController {
|
||||
override var isNavigationBarHidden: Bool { true }
|
||||
|
|
@ -2032,7 +1377,7 @@ final class ChatLocationDetailVC: BaseViewController {
|
|||
target.title = "target"
|
||||
rootView.mapView.addAnnotation(target)
|
||||
if let currentCoordinate {
|
||||
let current = ChatCurrentLocationAnnotation(coordinate: currentCoordinate)
|
||||
let current = CurrentUserLocationAnnotation(coordinate: currentCoordinate)
|
||||
rootView.mapView.addAnnotation(current)
|
||||
rootView.mapView.showAnnotations([target, current], animated: true)
|
||||
}
|
||||
|
|
@ -2115,10 +1460,10 @@ extension ChatLocationDetailVC: AMapSearchDelegate, MAMapViewDelegate {
|
|||
}
|
||||
|
||||
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
|
||||
if annotation is ChatCurrentLocationAnnotation {
|
||||
if annotation is CurrentUserLocationAnnotation {
|
||||
let identifier = "ChatLocationDetailCurrent"
|
||||
guard let view = (mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? ChatCurrentLocationAnnotationView)
|
||||
?? ChatCurrentLocationAnnotationView(annotation: annotation, reuseIdentifier: identifier) else { return nil }
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
var disposeBag = DisposeBag()
|
||||
|
|
@ -147,6 +197,12 @@ class GroupChatView: UIView {
|
|||
var onLocationTap: (() -> Void)?
|
||||
private var emojiPlaybackGeneration = 0
|
||||
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) {
|
||||
quoteBar.isHidden = !visible
|
||||
|
|
@ -186,6 +242,7 @@ class GroupChatView: UIView {
|
|||
|
||||
func refreshComposer() {
|
||||
let hasText = !(textField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
textField.updatePlaceholderVisibility()
|
||||
if isVoiceInputMode {
|
||||
updateHoldSpeakAppearance()
|
||||
textField.isHidden = true
|
||||
|
|
@ -202,9 +259,36 @@ class GroupChatView: UIView {
|
|||
sendBtn.isHidden = !hasText
|
||||
voiceBtn.isHidden = hasText
|
||||
}
|
||||
updateComposerHeight()
|
||||
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() {
|
||||
var configuration = UIButton.Configuration.plain()
|
||||
configuration.title = "按住 说话"
|
||||
|
|
@ -434,8 +518,9 @@ class GroupChatView: UIView {
|
|||
|
||||
bottomBar.layoutChain
|
||||
.edgesHorzontal(10)
|
||||
.height(50)
|
||||
.bottom(kSafeBottomMargin)
|
||||
bottomBarHeightConstraint = bottomBar.heightAnchor.constraint(equalToConstant: minimumBottomBarHeight)
|
||||
bottomBarHeightConstraint?.isActive = true
|
||||
|
||||
disableIMView.layoutChain
|
||||
.edgesHorzontal(15)
|
||||
|
|
@ -467,7 +552,8 @@ class GroupChatView: UIView {
|
|||
.left(16)
|
||||
.rightToLeftOfView(emojiBtn, offset: -10)
|
||||
.centerY()
|
||||
.height(36)
|
||||
textFieldHeightConstraint = textField.heightAnchor.constraint(equalToConstant: minimumTextFieldHeight)
|
||||
textFieldHeightConstraint?.isActive = true
|
||||
|
||||
holdSpeakBtn.layoutChain
|
||||
.left(16)
|
||||
|
|
@ -750,14 +836,18 @@ class GroupChatView: UIView {
|
|||
return btn
|
||||
}()
|
||||
|
||||
lazy var textField: UITextField = {
|
||||
let tf = UITextField()
|
||||
lazy var textField: ChatComposerTextView = {
|
||||
let tf = ChatComposerTextView()
|
||||
tf.font = .systemFont(ofSize: 16)
|
||||
tf.textColor = UIColor(hexStr: "#293445")
|
||||
tf.backgroundColor = .clear
|
||||
tf.placeholder = "发消息或者按住说话"
|
||||
tf.returnKeyType = .send
|
||||
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
|
||||
}()
|
||||
|
||||
|
|
@ -879,6 +969,7 @@ class GroupChatView: UIView {
|
|||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
updateComposerHeight()
|
||||
if !mentionPickerView.isHidden {
|
||||
mentionPickerView.layer.shadowPath = UIBezierPath(
|
||||
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 RxDataSources
|
||||
import Kingfisher
|
||||
import SwiftyUserDefaults
|
||||
#if !targetEnvironment(simulator)
|
||||
import GeYanSdk
|
||||
import RxGesture
|
||||
|
|
@ -162,14 +163,24 @@ class AccountVC: BaseViewController {
|
|||
|
||||
// MARK: - API
|
||||
private func requestLogout() {
|
||||
DLToast.showLoading()
|
||||
UserService.logout().subscribe(onNext: { response in
|
||||
DLToast.dismiss()
|
||||
NotificationCenter.default.post(name: .invalidatePopupQueue, object: nil)
|
||||
GeYanSdk.deletePreResultCache()
|
||||
AppContextManager.shared.deleteAccount()
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class AvatarIconListVC: BaseViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
rootView.configure(headPic: iconIndex)
|
||||
rootView.saveBtn.isHidden = true
|
||||
|
||||
rootView.saveBtn.rx.tap.subscribe(onNext: { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
|
@ -35,6 +36,10 @@ class AvatarIconListVC: BaseViewController {
|
|||
rootView.onBadgeTap = { [weak self] in
|
||||
self?.showUploadPicker()
|
||||
}
|
||||
|
||||
rootView.onIconSelected = { [weak self] in
|
||||
self?.rootView.saveBtn.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
private func showUploadPicker() {
|
||||
|
|
@ -82,6 +87,7 @@ class AvatarIconListVC: BaseViewController {
|
|||
.subscribe(onNext: { [weak self] url in
|
||||
guard let self else { return }
|
||||
self.rootView.setPreviewImage(image)
|
||||
self.rootView.saveBtn.isHidden = true
|
||||
self.onSelectIcon?(url)
|
||||
}, onError: { error in
|
||||
DLToast.show(error, fallbackText: "更换失败")
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class AvatarIconListView: UIView {
|
|||
|
||||
let saveBtn = UIButton(type: .custom)
|
||||
var onBadgeTap: (() -> Void)?
|
||||
var onIconSelected: (() -> Void)?
|
||||
|
||||
private var iconImgList: [UIImage] = []
|
||||
private let collectionLayout = CollectionHFlowLayout()
|
||||
|
|
@ -97,6 +98,7 @@ class AvatarIconListView: UIView {
|
|||
saveBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||||
saveBtn.layer.cornerRadius = 28
|
||||
saveBtn.clipsToBounds = true
|
||||
saveBtn.isHidden = true
|
||||
saveBtn.layoutChain
|
||||
.edgesHorzontal(30)
|
||||
.bottom(kSafeBottomMargin + 13)
|
||||
|
|
@ -281,6 +283,7 @@ extension AvatarIconListView: UICollectionViewDelegate, UICollectionViewDataSour
|
|||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
selectedIndex = indexPath.item + 1
|
||||
onIconSelected?()
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
|
|
|
|||
|
|
@ -611,7 +611,6 @@ final class PigeonMessageVC: BaseViewController {
|
|||
)
|
||||
}
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
DLToast.dismiss()
|
||||
guard let self else { return }
|
||||
self.selectedTemplate = nil
|
||||
self.selectedVoiceTemplate = nil
|
||||
|
|
@ -620,8 +619,9 @@ final class PigeonMessageVC: BaseViewController {
|
|||
self.cleanupAudio(deleteFile: true)
|
||||
self.rootView.clearVoiceDraft()
|
||||
self.recordPigeonSendSuccess()
|
||||
self.refreshGroupVip()
|
||||
DLToast.show(text: "发送成功")
|
||||
DLToast.show(text: "发送成功") {
|
||||
self.refreshGroupVip()
|
||||
}
|
||||
}, onError: { error in
|
||||
DLToast.dismiss()
|
||||
DLToast.show(error)
|
||||
|
|
|
|||
|
|
@ -222,6 +222,7 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
|
|||
}
|
||||
|
||||
private func showTimePicker(for pointIndex: Int) {
|
||||
view.endEditing(true)
|
||||
let picker = BRDatePickerView(pickerMode: .HM)
|
||||
picker.title = "选择到达时间"
|
||||
if pointIndex < viewModel.pointsRelay.value.count,
|
||||
|
|
@ -247,14 +248,14 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
|
|||
self.viewModel.pointsRelay.accept(list)
|
||||
}
|
||||
}
|
||||
picker.show()
|
||||
showSchedulePicker(picker)
|
||||
}
|
||||
|
||||
private func showLocationPicker(for pointIndex: Int, item: SchedulePointItem) {
|
||||
let coord = item.latitude != 0 || item.longitude != 0
|
||||
? CLLocationCoordinate2D(latitude: item.latitude, longitude: item.longitude)
|
||||
: kCLLocationCoordinate2DInvalid
|
||||
let vc = LocationPickerVC()
|
||||
let vc = LocationPickerViewController()
|
||||
vc.modalPresentationStyle = .fullScreen
|
||||
if !item.locationName.isEmpty {
|
||||
vc.initialLocation = PickedLocation(
|
||||
|
|
@ -437,6 +438,7 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
|
|||
// MARK: - Date / Time Picker
|
||||
|
||||
private func showDatePicker() {
|
||||
view.endEditing(true)
|
||||
let picker = BRDatePickerView(pickerMode: .YMD)
|
||||
picker.title = "选择日期"
|
||||
picker.minDate = Date()
|
||||
|
|
@ -450,7 +452,7 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
|
|||
picker.resultBlock = { [weak self] date, _ in
|
||||
if let d = date { self?.viewModel.selectedDate.accept(d) }
|
||||
}
|
||||
picker.show()
|
||||
showSchedulePicker(picker)
|
||||
}
|
||||
|
||||
private func makeSchedulePickerStyle() -> BRPickerStyle {
|
||||
|
|
@ -477,11 +479,11 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
|
|||
style.cancelBorderStyle = .fill
|
||||
style.cancelBtnFrame = CGRect(x: width - 16 - 28, y: 14, width: 28, height: 28)
|
||||
|
||||
style.pickerColor = .white
|
||||
style.pickerColor = .clear
|
||||
style.pickerHeight = 230
|
||||
style.rowHeight = 44
|
||||
style.clearPickerNewStyle = true
|
||||
style.selectRowColor = UIColor(hexStr: "#E3F6FF")
|
||||
style.separatorColor = .clear
|
||||
style.selectRowTextColor = UIColor(hexStr: "#16B3FF")
|
||||
style.selectRowTextFont = .systemFont(ofSize: 18, weight: .medium)
|
||||
style.pickerTextColor = UIColor(hexStr: "#333333")
|
||||
|
|
@ -490,6 +492,34 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
|
|||
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 {
|
||||
let width = UIScreen.main.bounds.width
|
||||
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