- 轨迹回放优化

- 登录页第三方字体显示问题修复
- 版本更新提示
This commit is contained in:
linshujie 2026-07-10 15:52:01 +08:00
parent d88176a8bc
commit 15e4c3df17
20 changed files with 232 additions and 73 deletions

View File

@ -2166,7 +2166,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.zuomeng.jisuloca;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -2218,7 +2218,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.zuomeng.jisuloca;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -19,6 +19,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UIFont.loadFontName("douyu.otf")
UIFont.loadFontName("zihunbiantaoti-r.ttf")
setupLocation()
ApiManager.shared.setup()
AppRouter.shared.setup()

View File

@ -34,7 +34,7 @@ public extension UIFont {
///
/// - Parameter name:
private static func loadFontName(_ fileName: String) -> String? {
public static func loadFontName(_ fileName: String) -> String? {
//
guard let fontURL = Bundle.main.url(forResource: fileName, withExtension: nil),
let data = try? Data(contentsOf: fontURL),

View File

@ -40,6 +40,8 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSContactsUsageDescription</key>
<string>需要访问您的通讯录以导入联系人号码</string>
<key>UIAppFonts</key>
<array>
<string>douyu.otf</string>

View File

@ -70,6 +70,8 @@ struct SystemConfigModel: Mappable {
var timeConfig: PopupWindowTimeModel?
///
var shareConfig: WxShareConfigModel?
///
var versionUpgradeModel: VersionUpgradeModel?
///
var chatWarning: String = ""
@ -100,6 +102,7 @@ struct SystemConfigModel: Mappable {
shareConfig <- map["client.weixin.share", nested: false]
guideDisplay <- map["client.guide_display", nested: false]
globalDisplay <- map["client.global_display", nested: false]
versionUpgradeModel <- map["client.version.upgrade", nested: false]
}
}
@ -173,3 +176,32 @@ struct WxShareConfigModel: Mappable {
image <- map["image"]
}
}
struct VersionUpgradeModel: Mappable {
///
var url: String = ""
///
var title: String = ""
///
var description: String = ""
///
var version: String = ""
///
var force: Bool = false
init?(map: Map) {
}
mutating func mapping(map: Map) {
url <- map["url"]
title <- map["title"]
description <- map["description"]
force <- map["force"]
version <- map["version"]
}
}

View File

@ -111,7 +111,7 @@ extension ApiManager {
case GatewayStatusCode.userLoginExpair.rawValue: // token
handleTokenExpired(msg: message)
case GatewayStatusCode.failure.rawValue, GatewayStatusCode.noAuthority.rawValue: // 退
handlePopView(message)
handlePopView(message, handle)
default:
// codesuccess
if success != nil {
@ -171,8 +171,9 @@ extension ApiManager {
}
// MARK: - (退)
private func handlePopView(_ msg: String?) {
private func handlePopView(_ msg: String?, _ handle: Bool) {
MainAsync {
guard handle else { return }
DLToast.showInfo(text: msg ?? "Error...") {
guard let viewController = UIViewController.currentViewController() else { return }
viewController.navigationController?.popViewController(animated: true)

View File

@ -23,14 +23,8 @@ final class AppCacheManager {
let fullPath = path + "/" + subPath
var isDir: ObjCBool = false
if manager.fileExists(atPath: fullPath, isDirectory: &isDir) {
if isDir.boolValue {
totalSize += folderSize(at: fullPath)
} else {
//
if let attrs = try? manager.attributesOfItem(atPath: fullPath) {
totalSize += attrs[.size] as? Int64 ?? 0
}
}
if isDir.boolValue { continue }
totalSize += (try? manager.attributesOfItem(atPath: fullPath))?[.size] as? Int64 ?? 0
}
}
return totalSize

View File

@ -13,6 +13,7 @@ import UserNotifications
import AdSupport
import AppTrackingTransparency
import URLNavigator
import Contacts
// MARK: - AuthorizeType
@ -112,6 +113,8 @@ public protocol AuthorizeProtocol {
return AuthorizeMicrophone()
case .tracking:
return AuthorizeTracking()
case .contacts:
return AuthorizeContacts()
default:
return nil
}
@ -402,4 +405,26 @@ class AuthorizeTracking: NSObject, AuthorizeProtocol {
}
}
// MARK: - AuthorizeContacts
private class AuthorizeContacts: NSObject, AuthorizeProtocol {
func authorizeStatus() -> AuthorizeStatus {
switch CNContactStore.authorizationStatus(for: .contacts) {
case .restricted:
return .restricted
case .denied:
return .denied
case .authorized:
return .authorized
default:
return .notDetermined
}
}
func authorize(_ completion: ((AuthorizeStatus) -> Void)?) {
CNContactStore().requestAccess(for: .contacts) { _, _ in
DispatchQueue.main.async { completion?(self.authorizeStatus()) }
}
}
}

View File

@ -20,7 +20,6 @@ class ItineraryTraceVC: BaseViewController {
private let memberModel: GroupMemberModel
private var routeOverlays: [MAPolyline] = []
private let routeSearch = AMapSearchAPI()
private var drivingSegmentIndexes: [ObjectIdentifier: Int] = [:]
private var playbackSegments: [[CLLocationCoordinate2D]?] = []
private var playbackPath: [(coordinate: CLLocationCoordinate2D, distance: Double)] = []
private var playbackTotalDistance: Double = 0
@ -41,10 +40,6 @@ class ItineraryTraceVC: BaseViewController {
private var playbackTargetCoordinate: CLLocationCoordinate2D?
private let minPointDistance: CLLocationDistance = 10
private let directSegmentDistance: CLLocationDistance = 80
private let maxDrivingSegmentDistance: CLLocationDistance = 1500
private let maxDrivingSegmentTimeGap: Int64 = 5 * 60 * 1000
private let maxDrivingSegmentRequests = 40
private let eventCalloutTag = 91301
override func loadView() {
@ -146,32 +141,38 @@ class ItineraryTraceVC: BaseViewController {
let points = cleanedTracePoints(model.trajectory_path)
guard points.count >= 2 else { print("[trace] insufficient points: \(points.count)"); return }
playbackSegments = Array(repeating: nil, count: points.count - 1)
playbackSegments = []
var drivingRequestCount = 0
for i in 0..<(points.count - 1) {
let from = points[i]
let to = points[i + 1]
let fallback = [from.coordinate, to.coordinate]
let distance = from.coordinate.distance(to: to.coordinate)
let timeGap = abs(to.timestamp - from.timestamp)
if distance < directSegmentDistance {
addPolyline(fallback)
playbackSegments[i] = fallback
} else if distance <= maxDrivingSegmentDistance,
timeGap <= maxDrivingSegmentTimeGap,
drivingRequestCount < maxDrivingSegmentRequests {
requestDrivingSegment(from: from.coordinate, to: to.coordinate, segmentIndex: i)
drivingRequestCount += 1
var sampled: [(timestamp: Int64, coordinate: CLLocationCoordinate2D)] = []
for pt in points {
if let last = sampled.last {
if pt.timestamp - last.timestamp >= 5_000 {
sampled.append(pt)
}
} else {
// /
continue
sampled.append(pt)
}
}
let coords = sampled.map { $0.coordinate }
addPolyline(coords)
playbackSegments = [coords]
buildPlaybackPath()
updatePlaybackAnnotation(progress: playbackProgress)
print("[trace] cleaned=\(points.count) drivingRequests=\(drivingRequestCount)")
// let middle = sampled.dropFirst().dropLast().prefix(16)
// let waypoints = middle.map { $0.coordinate }
//
// let request = AMapDrivingRouteSearchRequest()
// request.origin = AMapGeoPoint.location(withLatitude: CGFloat(points.first!.coordinate.latitude), longitude: CGFloat(points.first!.coordinate.longitude))
// request.destination = AMapGeoPoint.location(withLatitude: CGFloat(points.last!.coordinate.latitude), longitude: CGFloat(points.last!.coordinate.longitude))
// request.strategy = 0
// if !waypoints.isEmpty {
// request.waypoints = waypoints.map { AMapGeoPoint.location(withLatitude: CGFloat($0.latitude), longitude: CGFloat($0.longitude)) }
// }
// routeSearch?.aMapDrivingRouteSearch(request)
print("[trace] cleaned=\(points.count) sampled=\(sampled.count)")
}
private func buildPlaybackPath() {
@ -198,7 +199,6 @@ class ItineraryTraceVC: BaseViewController {
rootView.mapView.remove(overlay)
}
routeOverlays.removeAll()
drivingSegmentIndexes.removeAll()
}
private func cleanedTracePoints(_ points: [TrackPoint]) -> [(timestamp: Int64, coordinate: CLLocationCoordinate2D)] {
@ -222,15 +222,6 @@ class ItineraryTraceVC: BaseViewController {
return result
}
private func requestDrivingSegment(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D, segmentIndex: Int) {
let request = AMapDrivingRouteSearchRequest()
request.origin = AMapGeoPoint.location(withLatitude: CGFloat(from.latitude), longitude: CGFloat(from.longitude))
request.destination = AMapGeoPoint.location(withLatitude: CGFloat(to.latitude), longitude: CGFloat(to.longitude))
request.strategy = 0
drivingSegmentIndexes[ObjectIdentifier(request)] = segmentIndex
routeSearch?.aMapDrivingRouteSearch(request)
}
private func setupPlaybackControls() {
rootView.playBtn.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside)
rootView.progressSlider.addTarget(self, action: #selector(progressSliderTouchDown), for: .touchDown)
@ -567,29 +558,18 @@ extension ItineraryTraceVC: MAMapViewDelegate {
// MARK: - AMapSearchDelegate
extension ItineraryTraceVC: AMapSearchDelegate {
func onRouteSearchDone(_ request: AMapRouteSearchBaseRequest!, response: AMapRouteSearchResponse!) {
let requestId = ObjectIdentifier(request)
let segmentIndex = drivingSegmentIndexes.removeValue(forKey: requestId)
guard let path = response.route?.paths?.first as? AMapPath else {
return
}
guard let path = response.route?.paths?.first as? AMapPath else { return }
let coords = coordinates(from: path)
if coords.count > 1 {
addPolyline(coords)
if let segmentIndex, playbackSegments.indices.contains(segmentIndex) {
playbackSegments[segmentIndex] = coords
buildPlaybackPath()
updatePlaybackAnnotation(progress: playbackProgress)
}
}
guard coords.count > 1 else { return }
addPolyline(coords)
playbackSegments = [coords]
buildPlaybackPath()
updatePlaybackAnnotation(progress: playbackProgress)
}
func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
if let request = request as? AMapRouteSearchBaseRequest {
let requestId = ObjectIdentifier(request)
drivingSegmentIndexes.removeValue(forKey: requestId)
}
print("Trace route search error: \(error.localizedDescription)")
}
}

View File

@ -188,6 +188,7 @@ class GroupView: UIView {
lazy var scanBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Group/scan"), for: .normal)
btn.isHidden = true
return btn
}()

View File

@ -50,6 +50,14 @@ class SearchLocationResultVC: BaseViewController {
GroupChooseView.show(groupModel: model) { _ in }
}
}).disposed(by: disposeBag)
rootView.inviteBtn.rx.tap.subscribe(onNext: { _ in
guard let model = self.groupModel else {
self.requestGroupInfo(showPicker: true)
return
}
GroupChooseView.show(groupModel: model) { _ in }
}).disposed(by: disposeBag)
}
private func handleCode() {

View File

@ -28,6 +28,7 @@ class SearchLocationVC: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
fd_interactivePopDisabled = true
searchBtn.rx.tap.subscribe(onNext: { _ in
guard let phone = self.rootView.phoneInputTF.text, phone.isPhoneNumber else {

View File

@ -10,6 +10,9 @@ import RxSwift
import RxCocoa
import AVFoundation
import Lottie
import Contacts
import ContactsUI
import URLNavigator
class SearchLocationView: UIView {
@ -365,8 +368,22 @@ class SearchLocationView: UIView {
contactsBtn.titleLabel?.font = .systemFont(ofSize: 12, weight: .regular)
contactsBtn.setImage(UIImage(named: "SearchLocation/contacts"), for: .normal)
contactsBtn.extendEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 15)
contactsBtn.rx.tap.subscribe(onNext: { _ in
//TODO: -
contactsBtn.rx.tap.subscribe(onNext: { [weak self] in
guard let self = self else { return }
self.phoneInputTF.resignFirstResponder()
let status = CNContactStore.authorizationStatus(for: .contacts)
switch status {
case .notDetermined:
CNContactStore().requestAccess(for: .contacts) { granted, _ in
DispatchQueue.main.async { if granted { self.presentContactPicker() } }
}
case .authorized:
self.presentContactPicker()
case .denied, .restricted:
DLToast.show(text: "请在设置中开启通讯录权限")
default:
break
}
}).disposed(by: disposeBag)
inputView.addSubview(contactsBtn)
contactsBtn.layoutChain
@ -540,4 +557,20 @@ class SearchLocationView: UIView {
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func presentContactPicker() {
let picker = CNContactPickerViewController()
picker.delegate = self
picker.displayedPropertyKeys = [CNContactPhoneNumbersKey]
UIViewController.topMost?.present(picker, animated: true)
}
}
extension SearchLocationView: CNContactPickerDelegate {
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
if let phone = contact.phoneNumbers.first?.value.stringValue {
phoneInputTF.text = phone.filter { $0.isNumber }
phoneInputTF.sendActions(for: .editingChanged)
}
}
}

View File

@ -59,8 +59,14 @@ class LaunchViewController: BaseViewController {
guard let model = response.model else { return }
MQTTService.shared.connect()
Defaults[\.loginToken] = model.token
AppContextManager.shared.systemConfig = model.config
self.getUserIMToken()
if let upgrade = model.config?.versionUpgradeModel {
self.showVersionUpgrade(upgrade, model: model)
return
}
self.continueAfterConfig(model: model)
}, onError: { [weak self] (error) in
DLAlert.show(title: error.localizedDescription,
defaultTitle: "重试") { [weak self] in
@ -70,6 +76,31 @@ class LaunchViewController: BaseViewController {
}).disposed(by: disposeBag)
}
private func continueAfterConfig(model: UserConfigModel) {
model.temp ? self.navigateAfterDelay() : self.getUserIMToken()
}
private func showVersionUpgrade(_ upgrade: VersionUpgradeModel, model: UserConfigModel) {
self.showConfirmPop(
showCloseBtn: !upgrade.force,
title: upgrade.title + upgrade.version,
message: upgrade.description,
confirmText: "立即更新",
confirmBlock: {
if let url = URL(string: upgrade.url) {
UIApplication.shared.open(url)
}
if !upgrade.force {
self.continueAfterConfig(model: model)
}
},
cancelText: upgrade.force ? nil : "稍后再说",
cancelBlock: upgrade.force ? nil : { [weak self] in
self?.continueAfterConfig(model: model)
}
)
}
/// IM Token
func getUserIMToken() {
UserService.imToken().subscribe(onNext: { response in
@ -77,6 +108,8 @@ class LaunchViewController: BaseViewController {
AppContextManager.shared.imToken = token
GroupIMService.shared.login { _ in }
self.navigateAfterDelay()
}, onError: { [weak self] (error) in
self?.navigateAfterDelay()
}).disposed(by: disposeBag)
}

View File

@ -243,7 +243,7 @@ class LoginView: UIView {
lazy var welcomeLabel: UILabel = {
let label = UILabel()
label.text = "欢迎登录"
label.font = UIFont(name: "DOUYU Font", size: 24)
label.font = UIFont(name: "DOUYU Font", size: 24) ?? .systemFont(ofSize: 24, weight: .medium)
label.textColor = UIColor(hexStr: "#030303")
label.textAlignment = .center
return label

View File

@ -167,7 +167,7 @@ class OneTapLoginView: UIView {
lazy var welcomeLabel: UILabel = {
let label = UILabel()
label.text = "欢迎登录"
label.font = UIFont(name: "DOUYU Font", size: 24)
label.font = UIFont(name: "DOUYU Font", size: 24) ?? .systemFont(ofSize: 24, weight: .medium)
label.textColor = UIColor(hexStr: "#030303")
label.textAlignment = .center
return label

View File

@ -9,8 +9,10 @@ import UIKit
import RxSwift
import RxCocoa
import RxDataSources
import Kingfisher
#if !targetEnvironment(simulator)
import GeYanSdk
import RxGesture
#endif
class AccountVC: BaseViewController {
@ -55,6 +57,12 @@ class AccountVC: BaseViewController {
self.deleteAccount()
}
}).disposed(by: disposeBag)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleClearCacheTap))
rootView.clearView.addGestureRecognizer(tap)
let versionTap = UITapGestureRecognizer(target: self, action: #selector(handleVersionViewTap))
rootView.versionView.addGestureRecognizer(versionTap)
}
// MARK: - API
@ -76,4 +84,42 @@ class AccountVC: BaseViewController {
DLToast.show(text: "申请成功")
}, onError: { _ in }).disposed(by: disposeBag)
}
@objc private func handleClearCacheTap() {
clearCache()
}
@objc private func handleVersionViewTap() {
guard let upgrade = AppContextManager.shared.systemConfig?.versionUpgradeModel else {
DLToast.show(text: "已是最新版本")
return
}
self.showConfirmPop(
showCloseBtn: !upgrade.force,
title: upgrade.title,
message: upgrade.description,
confirmText: "立即更新",
confirmBlock: {
if let url = URL(string: upgrade.url) {
UIApplication.shared.open(url)
}
},
cancelText: upgrade.force ? nil : "稍后再说",
cancelBlock: nil
)
}
private func clearCache() {
DLToast.showLoading()
KingfisherManager.shared.cache.clearMemoryCache()
KingfisherManager.shared.cache.clearDiskCache {
AppCacheManager.clearCache {
DispatchQueue.main.async {
DLToast.dismiss()
DLToast.show(text: "缓存已清除")
self.rootView.cacheLab.text = "0 B"
}
}
}
}
}

View File

@ -238,7 +238,7 @@ class AccountView: UIView {
let titleLab = UILabel()
titleLab.text = "当前版本"
titleLab.font = .systemFont(ofSize: 14, weight: .medium)
titleLab.textColor = UIColor(hexStr: "#1A1A1A")
titleLab.textColor = UIColor(hexStr: "#1A1A1A ")
view.addSubview(titleLab)
titleLab.layoutChain
.left(15)

View File

@ -156,6 +156,7 @@ class ScheduleViewedListCell: UITableViewCell {
.top(18)
.height(50)
.widthToHeight(1)
.bottom(18, relation: .greaterThanOrEqual)
nameLab.layoutChain
.topToView(iconView)

View File

@ -36,7 +36,7 @@ struct UserService {
static func imToken() -> Observable<ResponseModel> {
let api = UserAPI.imToken.multiTarget
return APIProvider.request(token: api)
return APIProvider.request(token: api, handle: false)
.map(ResponseModel.self)
.asObservable()
}