集成高德 SDK 并替换所有定位相关的 CoreLocation 与 MapKit
This commit is contained in:
20
Podfile
Normal file
20
Podfile
Normal file
@ -0,0 +1,20 @@
|
||||
platform :ios, '16.0'
|
||||
|
||||
target 'suixinkan' do
|
||||
use_frameworks!
|
||||
pod 'AMapLocation-NO-IDFA'
|
||||
pod 'AMap3DMap-NO-IDFA'
|
||||
pod 'AMapSearch-NO-IDFA'
|
||||
end
|
||||
|
||||
target 'suixinkanTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
30
Podfile.lock
Normal file
30
Podfile.lock
Normal file
@ -0,0 +1,30 @@
|
||||
PODS:
|
||||
- AMap3DMap-NO-IDFA (11.2.000):
|
||||
- AMapFoundation-NO-IDFA (>= 1.9.0)
|
||||
- AMapFoundation-NO-IDFA (1.9.0)
|
||||
- AMapLocation-NO-IDFA (2.12.0):
|
||||
- AMapFoundation-NO-IDFA (>= 1.9.0)
|
||||
- AMapSearch-NO-IDFA (9.8.0):
|
||||
- AMapFoundation-NO-IDFA (>= 1.9.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- AMap3DMap-NO-IDFA
|
||||
- AMapLocation-NO-IDFA
|
||||
- AMapSearch-NO-IDFA
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- AMap3DMap-NO-IDFA
|
||||
- AMapFoundation-NO-IDFA
|
||||
- AMapLocation-NO-IDFA
|
||||
- AMapSearch-NO-IDFA
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
AMap3DMap-NO-IDFA: f53ee0cb33db83c1a08856b5995e53a0c2096158
|
||||
AMapFoundation-NO-IDFA: a2e3c895398d7ee757278e1a0a8f9359da4b146e
|
||||
AMapLocation-NO-IDFA: 7cd8fc837ea41edfbf4d937cd20572e277b77d18
|
||||
AMapSearch-NO-IDFA: c0afd2a69a076d4228becda4401dbe4a279a03ef
|
||||
|
||||
PODFILE CHECKSUM: bf22a001ae02e51eb203f1eae5556a33b831ada7
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
@ -7,10 +7,12 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
82444F963D925740D4BACF57 /* libPods-suixinkanTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C3624DF243B82E619461C0F /* libPods-suixinkanTests.a */; };
|
||||
93FF17E42FFB84950033C304 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 93FF17E22FFB84950033C304 /* SnapKit */; };
|
||||
93FF17E52FFB84950033C304 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 93FF17E32FFB84950033C304 /* Kingfisher */; };
|
||||
A1B2C3D12FFB84950033C305 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 93FF17E22FFB84950033C304 /* SnapKit */; };
|
||||
A1B2C3D22FFB84950033C305 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 93FF17E32FFB84950033C304 /* Kingfisher */; };
|
||||
AE05F6CBAB71C321373D686E /* Pods_suixinkan.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C69C08A144C2D5D4A448B7D2 /* Pods_suixinkan.framework */; };
|
||||
B2C3D4E12FFB84950033C306 /* AlibabaCloudOSS in Frameworks */ = {isa = PBXBuildFile; productRef = B2C3D4E32FFB84950033C306 /* AlibabaCloudOSS */; };
|
||||
C3D4E5F12FFB84950033C307 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = C3D4E5F32FFB84950033C307 /* Lottie */; };
|
||||
/* End PBXBuildFile section */
|
||||
@ -26,8 +28,14 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1060D5E389506965D83EA1CC /* Pods-suixinkan.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-suixinkan.release.xcconfig"; path = "Target Support Files/Pods-suixinkan/Pods-suixinkan.release.xcconfig"; sourceTree = "<group>"; };
|
||||
2C3624DF243B82E619461C0F /* libPods-suixinkanTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-suixinkanTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5BBB56C2281879245220A9F3 /* Pods-suixinkan.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-suixinkan.debug.xcconfig"; path = "Target Support Files/Pods-suixinkan/Pods-suixinkan.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
6CB3E1DA4E97AA4961B25733 /* Pods-suixinkanTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-suixinkanTests.release.xcconfig"; path = "Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
83445E7B4CCCF42C6FD481D3 /* Pods-suixinkanTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-suixinkanTests.debug.xcconfig"; path = "Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
93FF17C52FFB84950033C304 /* suixinkan.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = suixinkan.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A1B2C3D02FFB84950033C305 /* suixinkanTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = suixinkanTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C69C08A144C2D5D4A448B7D2 /* Pods_suixinkan.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_suixinkan.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
@ -51,6 +59,8 @@
|
||||
};
|
||||
A1B2C3D42FFB84950033C305 /* suixinkanTests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = suixinkanTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@ -65,6 +75,7 @@
|
||||
93FF17E52FFB84950033C304 /* Kingfisher in Frameworks */,
|
||||
B2C3D4E12FFB84950033C306 /* AlibabaCloudOSS in Frameworks */,
|
||||
C3D4E5F12FFB84950033C307 /* Lottie in Frameworks */,
|
||||
AE05F6CBAB71C321373D686E /* Pods_suixinkan.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -74,18 +85,42 @@
|
||||
files = (
|
||||
A1B2C3D12FFB84950033C305 /* SnapKit in Frameworks */,
|
||||
A1B2C3D22FFB84950033C305 /* Kingfisher in Frameworks */,
|
||||
82444F963D925740D4BACF57 /* libPods-suixinkanTests.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
27017AD20F47EB5E23B21608 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C69C08A144C2D5D4A448B7D2 /* Pods_suixinkan.framework */,
|
||||
2C3624DF243B82E619461C0F /* libPods-suixinkanTests.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
365F718ADCDEA1FB75E2A740 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5BBB56C2281879245220A9F3 /* Pods-suixinkan.debug.xcconfig */,
|
||||
1060D5E389506965D83EA1CC /* Pods-suixinkan.release.xcconfig */,
|
||||
83445E7B4CCCF42C6FD481D3 /* Pods-suixinkanTests.debug.xcconfig */,
|
||||
6CB3E1DA4E97AA4961B25733 /* Pods-suixinkanTests.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
93FF17BC2FFB84950033C304 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
93FF17C72FFB84950033C304 /* suixinkan */,
|
||||
A1B2C3D42FFB84950033C305 /* suixinkanTests */,
|
||||
93FF17C62FFB84950033C304 /* Products */,
|
||||
365F718ADCDEA1FB75E2A740 /* Pods */,
|
||||
27017AD20F47EB5E23B21608 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@ -105,9 +140,11 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 93FF17D82FFB84950033C304 /* Build configuration list for PBXNativeTarget "suixinkan" */;
|
||||
buildPhases = (
|
||||
2327F484D72C90FA032D7F91 /* [CP] Check Pods Manifest.lock */,
|
||||
93FF17C12FFB84950033C304 /* Sources */,
|
||||
93FF17C22FFB84950033C304 /* Frameworks */,
|
||||
93FF17C32FFB84950033C304 /* Resources */,
|
||||
D83AC0A37C1CDC54D21AB6C9 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@ -131,6 +168,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = A1B2C3DB2FFB84950033C305 /* Build configuration list for PBXNativeTarget "suixinkanTests" */;
|
||||
buildPhases = (
|
||||
0C58C4D3EC0596A54F75FECF /* [CP] Check Pods Manifest.lock */,
|
||||
A1B2C3D52FFB84950033C305 /* Sources */,
|
||||
A1B2C3D62FFB84950033C305 /* Frameworks */,
|
||||
A1B2C3D72FFB84950033C305 /* Resources */,
|
||||
@ -214,6 +252,70 @@
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
0C58C4D3EC0596A54F75FECF /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-suixinkanTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
2327F484D72C90FA032D7F91 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-suixinkan-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
D83AC0A37C1CDC54D21AB6C9 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
93FF17C12FFB84950033C304 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
@ -242,12 +344,14 @@
|
||||
/* Begin XCBuildConfiguration section */
|
||||
93FF17D92FFB84950033C304 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5BBB56C2281879245220A9F3 /* Pods-suixinkan.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = suixinkan/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
@ -270,6 +374,7 @@
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "suixinkan/suixinkan-Bridging-Header.h";
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
@ -278,12 +383,14 @@
|
||||
};
|
||||
93FF17DA2FFB84950033C304 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1060D5E389506965D83EA1CC /* Pods-suixinkan.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = suixinkan/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
@ -306,6 +413,7 @@
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "suixinkan/suixinkan-Bridging-Header.h";
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
@ -435,11 +543,13 @@
|
||||
};
|
||||
A1B2C3DA2FFB84950033C305 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 6CB3E1DA4E97AA4961B25733 /* Pods-suixinkanTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@ -458,11 +568,13 @@
|
||||
};
|
||||
A1B2C3DC2FFB84950033C305 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 83445E7B4CCCF42C6FD481D3 /* Pods-suixinkanTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16;
|
||||
MARKETING_VERSION = 1.0;
|
||||
|
||||
10
suixinkan.xcworkspace/contents.xcworkspacedata
generated
Normal file
10
suixinkan.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:suixinkan.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
69
suixinkan.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
69
suixinkan.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"originHash" : "cb8f20fce1c6d5182e1505245f7fa3cd864954e3b45b7397e3bc7cd403003882",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "alibabacloud-oss-swift-sdk-v2",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/aliyun/alibabacloud-oss-swift-sdk-v2.git",
|
||||
"state" : {
|
||||
"revision" : "e208a6c3c55847f90944a5bb4453c2e2ce00f1b1",
|
||||
"version" : "0.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "kingfisher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/onevcat/Kingfisher.git",
|
||||
"state" : {
|
||||
"revision" : "ac632bd26a1c00f139ff62fd01806f21cf67325e",
|
||||
"version" : "8.10.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "lottie-spm",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/airbnb/lottie-spm.git",
|
||||
"state" : {
|
||||
"revision" : "0aff16288f8dc1e3dab35cbf5cf45979673fd3df",
|
||||
"version" : "4.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "snapkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SnapKit/SnapKit.git",
|
||||
"state" : {
|
||||
"revision" : "2842e6e84e82eb9a8dac0100ca90d9444b0307f4",
|
||||
"version" : "5.7.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-asn1",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-asn1.git",
|
||||
"state" : {
|
||||
"revision" : "a9a5efd40eaf558a2bcd48d64b1d1646be686008",
|
||||
"version" : "1.7.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-crypto",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-crypto.git",
|
||||
"state" : {
|
||||
"revision" : "95ba0316a9b733e92bb6b071255ff46263bbe7dc",
|
||||
"version" : "3.15.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "xmlcoder",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/CoreOffice/XMLCoder.git",
|
||||
"state" : {
|
||||
"revision" : "42f62383dbcd074440cb1f6a750b9c02df9e7325",
|
||||
"version" : "0.18.2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
@ -13,7 +13,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
if AppStore.shared.privacyAgreementAccepted, !AppStore.shared.token.isEmpty {
|
||||
AMapBootstrap.configureIfNeeded()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
12
suixinkan/Config/AMapConfig.swift
Normal file
12
suixinkan/Config/AMapConfig.swift
Normal file
@ -0,0 +1,12 @@
|
||||
//
|
||||
// AMapConfig.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 高德地图 SDK 配置。
|
||||
enum AMapConfig {
|
||||
/// 高德开放平台 iOS Key,需与 Bundle ID `com.yuanzhixiang.suixinkan` 绑定。
|
||||
nonisolated static let apiKey = "ea25441168468f39e276687dc94b8e7c"
|
||||
}
|
||||
22
suixinkan/Config/AppAgreementURLs.swift
Normal file
22
suixinkan/Config/AppAgreementURLs.swift
Normal file
@ -0,0 +1,22 @@
|
||||
//
|
||||
// AppAgreementURLs.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// App 协议与隐私政策链接,对齐 Android `AgreementRoutes`。
|
||||
enum AppAgreementURLs {
|
||||
/// 隐私政策 H5 地址。
|
||||
nonisolated static var privacyPolicy: URL {
|
||||
APIEnvironment.current.baseURL.appending(path: "h5/app/privacy-policy")
|
||||
}
|
||||
|
||||
/// 用户协议 H5 地址。
|
||||
nonisolated static var userAgreement: URL {
|
||||
APIEnvironment.current.baseURL.appending(path: "h5/app/user-agreement")
|
||||
}
|
||||
|
||||
/// 高德地图开放平台隐私权政策。
|
||||
nonisolated static let amapPrivacyPolicy = URL(string: "https://lbs.amap.com/pages/privacy/")!
|
||||
}
|
||||
@ -23,6 +23,10 @@ enum AuthSessionHelper {
|
||||
AppStore.shared.privacyAgreementAccepted = privacyAgreementAccepted
|
||||
applyAccountSession(from: selectedAccount, in: response)
|
||||
|
||||
if privacyAgreementAccepted {
|
||||
AMapBootstrap.configureIfNeeded()
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: NotificationName.userDidLogin, object: nil)
|
||||
}
|
||||
|
||||
|
||||
@ -1,98 +0,0 @@
|
||||
//
|
||||
// HomeLocationProvider.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
|
||||
/// 首页位置采集结果。
|
||||
struct HomeLocationSnapshot: Sendable {
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
let address: String
|
||||
}
|
||||
|
||||
/// 首页位置采集,基于 CoreLocation 并提供可选逆地理。
|
||||
final class HomeLocationProvider: NSObject, CLLocationManagerDelegate {
|
||||
|
||||
private let manager = CLLocationManager()
|
||||
private var coordinateContinuation: CheckedContinuation<CLLocationCoordinate2D, Error>?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
manager.delegate = self
|
||||
manager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
}
|
||||
|
||||
/// 请求当前位置与地址。
|
||||
func requestSnapshot() async throws -> HomeLocationSnapshot {
|
||||
let status = manager.authorizationStatus
|
||||
if status == .notDetermined {
|
||||
manager.requestWhenInUseAuthorization()
|
||||
try await Task.sleep(nanoseconds: 300_000_000)
|
||||
}
|
||||
|
||||
let resolvedStatus = manager.authorizationStatus
|
||||
guard resolvedStatus == .authorizedWhenInUse || resolvedStatus == .authorizedAlways else {
|
||||
throw HomeLocationProviderError.permissionDenied
|
||||
}
|
||||
|
||||
let coordinate = try await requestCoordinate()
|
||||
let address = await reverseGeocode(latitude: coordinate.latitude, longitude: coordinate.longitude)
|
||||
return HomeLocationSnapshot(
|
||||
latitude: coordinate.latitude,
|
||||
longitude: coordinate.longitude,
|
||||
address: address
|
||||
)
|
||||
}
|
||||
|
||||
/// 逆地理编码,供地图标记使用。
|
||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||
let location = CLLocation(latitude: latitude, longitude: longitude)
|
||||
let geocoder = CLGeocoder()
|
||||
do {
|
||||
let placemarks = try await geocoder.reverseGeocodeLocation(location)
|
||||
return placemarks.first?.formattedAddress ?? ""
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
private func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.coordinateContinuation = continuation
|
||||
manager.requestLocation()
|
||||
}
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.last else { return }
|
||||
coordinateContinuation?.resume(returning: location.coordinate)
|
||||
coordinateContinuation = nil
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
coordinateContinuation?.resume(throwing: error)
|
||||
coordinateContinuation = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 首页定位错误。
|
||||
enum HomeLocationProviderError: LocalizedError {
|
||||
case permissionDenied
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .permissionDenied: "需要定位权限才能使用此功能"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CLPlacemark {
|
||||
var formattedAddress: String {
|
||||
[subLocality, locality, administrativeArea]
|
||||
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.joined(separator: "")
|
||||
}
|
||||
}
|
||||
@ -30,7 +30,7 @@ final class HomeViewModel {
|
||||
let locationReportService: LocationReportService
|
||||
private let appStore: AppStore
|
||||
private let commonMenuStore: HomeCommonMenuStore
|
||||
private let locationProvider: HomeLocationProvider
|
||||
private let locationProvider: any LocationProviding
|
||||
|
||||
private var timeoutDialogTriggered = false
|
||||
private var dismissedLocationTimeoutDialog = false
|
||||
@ -39,7 +39,7 @@ final class HomeViewModel {
|
||||
appStore: AppStore = .shared,
|
||||
locationStateStore: HomeLocationStateStore = HomeLocationStateStore(),
|
||||
commonMenuStore: HomeCommonMenuStore = HomeCommonMenuStore(),
|
||||
locationProvider: HomeLocationProvider = HomeLocationProvider()
|
||||
locationProvider: any LocationProviding = LocationProvider.shared
|
||||
) {
|
||||
self.appStore = appStore
|
||||
self.locationStateStore = locationStateStore
|
||||
|
||||
@ -27,11 +27,11 @@ final class ScenicSelectionViewModel {
|
||||
var onNavigateApplyStatus: ((String) -> Void)?
|
||||
|
||||
private let appStore: AppStore
|
||||
private let locationProvider: HomeLocationProvider
|
||||
private let locationProvider: any LocationProviding
|
||||
private var currentLat: Double?
|
||||
private var currentLng: Double?
|
||||
|
||||
init(appStore: AppStore = .shared, locationProvider: HomeLocationProvider = HomeLocationProvider()) {
|
||||
init(appStore: AppStore = .shared, locationProvider: any LocationProviding = LocationProvider.shared) {
|
||||
self.appStore = appStore
|
||||
self.locationProvider = locationProvider
|
||||
}
|
||||
@ -63,7 +63,7 @@ final class ScenicSelectionViewModel {
|
||||
let address = snapshot.address.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
currentLocationText = address.isEmpty ? "最近的景区" : address
|
||||
loadFromLocal()
|
||||
} catch HomeLocationProviderError.permissionDenied {
|
||||
} catch LocationProviderError.permissionDenied {
|
||||
onShowMessage?("未授予定位权限,无法获取附近景区")
|
||||
} catch {
|
||||
onShowMessage?("定位失败: \(error.localizedDescription)")
|
||||
|
||||
49
suixinkan/Features/Location/AMapBootstrap.swift
Normal file
49
suixinkan/Features/Location/AMapBootstrap.swift
Normal file
@ -0,0 +1,49 @@
|
||||
//
|
||||
// AMapBootstrap.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 高德 SDK 隐私合规与 Key 初始化,必须在实例化任何 SDK 类之前完成。
|
||||
enum AMapBootstrap {
|
||||
|
||||
private(set) static var isConfigured = false
|
||||
private static let lock = NSLock()
|
||||
|
||||
/// 在用户已同意 App 隐私协议后配置高德 SDK,幂等。
|
||||
@discardableResult
|
||||
static func configureIfNeeded(appStore: AppStore = .shared) -> Bool {
|
||||
guard appStore.privacyAgreementAccepted else { return false }
|
||||
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
guard !isConfigured else { return true }
|
||||
|
||||
AMapLocationManager.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
||||
AMapLocationManager.updatePrivacyAgree(.didAgree)
|
||||
MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
||||
MAMapView.updatePrivacyAgree(.didAgree)
|
||||
AMapSearchAPI.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
||||
AMapSearchAPI.updatePrivacyAgree(.didAgree)
|
||||
|
||||
AMapServices.shared().apiKey = AMapConfig.apiKey
|
||||
AMapServices.shared().enableHTTPS = true
|
||||
isConfigured = true
|
||||
return true
|
||||
}
|
||||
|
||||
/// 要求高德 SDK 已完成合规初始化,否则抛出错误。
|
||||
static func requireConfigured(appStore: AppStore = .shared) throws {
|
||||
guard configureIfNeeded(appStore: appStore) else {
|
||||
throw LocationProviderError.privacyNotAccepted
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试专用:重置初始化状态。
|
||||
static func resetForTesting() {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
isConfigured = false
|
||||
}
|
||||
}
|
||||
64
suixinkan/Features/Location/LocationGeocoder.swift
Normal file
64
suixinkan/Features/Location/LocationGeocoder.swift
Normal file
@ -0,0 +1,64 @@
|
||||
//
|
||||
// LocationGeocoder.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 高德逆地理编码封装,对齐 Android `GeocodeSearch`。
|
||||
final class LocationGeocoder: NSObject, AMapSearchDelegate {
|
||||
|
||||
static let shared = LocationGeocoder()
|
||||
|
||||
private var searchAPI: AMapSearchAPI?
|
||||
private var continuation: CheckedContinuation<String, Never>?
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
/// 将坐标解析为可读地址。
|
||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||
do {
|
||||
try AMapBootstrap.requireConfigured()
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
self.continuation?.resume(returning: "")
|
||||
self.continuation = continuation
|
||||
|
||||
guard let api = makeSearchAPI() else {
|
||||
continuation.resume(returning: "")
|
||||
return
|
||||
}
|
||||
let request = AMapReGeocodeSearchRequest()
|
||||
request.location = AMapGeoPoint.location(withLatitude: CGFloat(latitude), longitude: CGFloat(longitude))
|
||||
request.requireExtension = true
|
||||
request.radius = 200
|
||||
api.aMapReGoecodeSearch(request)
|
||||
}
|
||||
}
|
||||
|
||||
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
|
||||
let address = response?.regeocode?.formattedAddress ?? ""
|
||||
continuation?.resume(returning: address)
|
||||
continuation = nil
|
||||
}
|
||||
|
||||
func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
|
||||
continuation?.resume(returning: "")
|
||||
continuation = nil
|
||||
}
|
||||
|
||||
private func makeSearchAPI() -> AMapSearchAPI? {
|
||||
if let searchAPI {
|
||||
return searchAPI
|
||||
}
|
||||
guard let api = AMapSearchAPI() else { return nil }
|
||||
api.delegate = self
|
||||
searchAPI = api
|
||||
return api
|
||||
}
|
||||
}
|
||||
100
suixinkan/Features/Location/LocationProvider.swift
Normal file
100
suixinkan/Features/Location/LocationProvider.swift
Normal file
@ -0,0 +1,100 @@
|
||||
//
|
||||
// LocationProvider.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
/// 统一定位服务,封装高德 `AMapLocationManager`,对齐 Android `LocationProvider`。
|
||||
final class LocationProvider: NSObject, LocationProviding {
|
||||
|
||||
static let shared = LocationProvider()
|
||||
|
||||
private let permissionManager = CLLocationManager()
|
||||
private let geocoder: LocationGeocoder
|
||||
|
||||
init(geocoder: LocationGeocoder = .shared) {
|
||||
self.geocoder = geocoder
|
||||
super.init()
|
||||
permissionManager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
}
|
||||
|
||||
func requestSnapshot() async throws -> HomeLocationSnapshot {
|
||||
try AMapBootstrap.requireConfigured()
|
||||
try await ensureLocationPermission()
|
||||
|
||||
let manager = makeLocationManager()
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
let started = manager.requestLocation(withReGeocode: true) { location, regeocode, error in
|
||||
if let error {
|
||||
continuation.resume(throwing: error)
|
||||
return
|
||||
}
|
||||
guard let location else {
|
||||
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||||
return
|
||||
}
|
||||
let address = regeocode?.formattedAddress ?? ""
|
||||
continuation.resume(returning: HomeLocationSnapshot(
|
||||
latitude: location.coordinate.latitude,
|
||||
longitude: location.coordinate.longitude,
|
||||
address: address
|
||||
))
|
||||
}
|
||||
if !started {
|
||||
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
||||
try AMapBootstrap.requireConfigured()
|
||||
try await ensureLocationPermission()
|
||||
|
||||
let manager = makeLocationManager()
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
let started = manager.requestLocation(withReGeocode: false) { location, _, error in
|
||||
if let error {
|
||||
continuation.resume(throwing: error)
|
||||
return
|
||||
}
|
||||
guard let location else {
|
||||
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||||
return
|
||||
}
|
||||
continuation.resume(returning: location.coordinate)
|
||||
}
|
||||
if !started {
|
||||
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||
await geocoder.reverseGeocode(latitude: latitude, longitude: longitude)
|
||||
}
|
||||
|
||||
/// 请求系统定位权限,首次弹窗后等待用户响应。
|
||||
func ensureLocationPermission() async throws {
|
||||
let initialStatus = permissionManager.authorizationStatus
|
||||
if initialStatus == .notDetermined {
|
||||
permissionManager.requestWhenInUseAuthorization()
|
||||
try await Task.sleep(nanoseconds: 500_000_000)
|
||||
}
|
||||
|
||||
let status = permissionManager.authorizationStatus
|
||||
guard status == .authorizedWhenInUse || status == .authorizedAlways else {
|
||||
throw LocationProviderError.permissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
private func makeLocationManager() -> AMapLocationManager {
|
||||
let manager = AMapLocationManager()
|
||||
manager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
manager.locationTimeout = 30
|
||||
manager.reGeocodeTimeout = 10
|
||||
manager.locatingWithReGeocode = true
|
||||
return manager
|
||||
}
|
||||
}
|
||||
54
suixinkan/Features/Location/LocationProviding.swift
Normal file
54
suixinkan/Features/Location/LocationProviding.swift
Normal file
@ -0,0 +1,54 @@
|
||||
//
|
||||
// LocationProviding.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
/// 位置采集结果。
|
||||
struct HomeLocationSnapshot: Sendable, Equatable {
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
let address: String
|
||||
}
|
||||
|
||||
/// 定位服务协议,便于 ViewModel 注入与单元测试 mock。
|
||||
protocol LocationProviding: Sendable {
|
||||
/// 请求当前坐标与地址。
|
||||
func requestSnapshot() async throws -> HomeLocationSnapshot
|
||||
/// 请求当前坐标。
|
||||
func requestCoordinate() async throws -> CLLocationCoordinate2D
|
||||
/// 逆地理编码。
|
||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String
|
||||
}
|
||||
|
||||
/// 定位错误。
|
||||
enum LocationProviderError: LocalizedError, Equatable {
|
||||
case privacyNotAccepted
|
||||
case permissionDenied
|
||||
case locationFailed
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .privacyNotAccepted:
|
||||
"请先阅读并同意隐私政策"
|
||||
case .permissionDenied:
|
||||
"需要定位权限才能使用此功能"
|
||||
case .locationFailed:
|
||||
"定位失败,请稍后重试"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 订单核销场景定位错误,保持原有提示文案。
|
||||
enum OrderLocationError: LocalizedError {
|
||||
case permissionDenied
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .permissionDenied:
|
||||
"缺少定位权限,功能无法进行"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -23,12 +23,12 @@ final class LocationReportService {
|
||||
|
||||
private let appStore: AppStore
|
||||
private let locationStateStore: HomeLocationStateStore
|
||||
private let locationProvider: HomeLocationProvider
|
||||
private let locationProvider: any LocationProviding
|
||||
|
||||
init(
|
||||
appStore: AppStore = .shared,
|
||||
locationStateStore: HomeLocationStateStore,
|
||||
locationProvider: HomeLocationProvider = HomeLocationProvider()
|
||||
locationProvider: any LocationProviding = LocationProvider.shared
|
||||
) {
|
||||
self.appStore = appStore
|
||||
self.locationStateStore = locationStateStore
|
||||
|
||||
@ -26,11 +26,11 @@ final class LocationReportViewModel {
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
|
||||
private let locationProvider: HomeLocationProvider
|
||||
private let locationProvider: any LocationProviding
|
||||
|
||||
init(
|
||||
locationStateStore: HomeLocationStateStore = HomeLocationStateStore(),
|
||||
locationProvider: HomeLocationProvider = HomeLocationProvider()
|
||||
locationProvider: any LocationProviding = LocationProvider.shared
|
||||
) {
|
||||
self.locationStateStore = locationStateStore
|
||||
self.locationProvider = locationProvider
|
||||
|
||||
@ -3,12 +3,11 @@
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import UIKit
|
||||
|
||||
/// 主 Tab 中间扫码按钮结果处理,对齐 Android `MainTabViewModel.signIn`。
|
||||
@MainActor
|
||||
final class MainTabScanHandler: NSObject, CLLocationManagerDelegate {
|
||||
final class MainTabScanHandler {
|
||||
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onShowSignIn: ((OrderSignInResponse) -> Void)?
|
||||
@ -18,15 +17,12 @@ final class MainTabScanHandler: NSObject, CLLocationManagerDelegate {
|
||||
var onNavigateBindAcquirer: ((Int) -> Void)?
|
||||
|
||||
private let api: OrderAPI
|
||||
private let locationManager = CLLocationManager()
|
||||
private var locationContinuation: ((Result<CLLocationCoordinate2D, Error>) -> Void)?
|
||||
private let locationProvider: any LocationProviding
|
||||
private(set) var pendingMultiVerify: MultiVerifyParseData?
|
||||
|
||||
init(api: OrderAPI) {
|
||||
init(api: OrderAPI, locationProvider: any LocationProviding = LocationProvider.shared) {
|
||||
self.api = api
|
||||
super.init()
|
||||
locationManager.delegate = self
|
||||
locationManager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
self.locationProvider = locationProvider
|
||||
}
|
||||
|
||||
func handleScanResult(_ scanResult: String) async {
|
||||
@ -141,14 +137,8 @@ final class MainTabScanHandler: NSObject, CLLocationManagerDelegate {
|
||||
}
|
||||
|
||||
private func fetchSpotsForMultiVerify(pending: MultiVerifyParseData) async {
|
||||
let status = locationManager.authorizationStatus
|
||||
guard status == .authorizedWhenInUse || status == .authorizedAlways else {
|
||||
locationManager.requestWhenInUseAuthorization()
|
||||
onShowMessage?("缺少定位权限,功能无法进行")
|
||||
return
|
||||
}
|
||||
do {
|
||||
let coordinate = try await requestLocation()
|
||||
let coordinate = try await locationProvider.requestCoordinate()
|
||||
let response = try await api.multiTravelScenicSpotList(
|
||||
orderNumber: pending.orderNumber,
|
||||
lat: coordinate.latitude,
|
||||
@ -160,35 +150,12 @@ final class MainTabScanHandler: NSObject, CLLocationManagerDelegate {
|
||||
} else {
|
||||
onShowMultiVerifySheet?(spots, spots.first)
|
||||
}
|
||||
} catch LocationProviderError.permissionDenied {
|
||||
onShowMessage?(OrderLocationError.permissionDenied.errorDescription ?? "缺少定位权限,功能无法进行")
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
onShowMessage?((error as? LocalizedError)?.errorDescription ?? error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func requestLocation() async throws -> CLLocationCoordinate2D {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
locationContinuation = { result in
|
||||
switch result {
|
||||
case .success(let coordinate):
|
||||
continuation.resume(returning: coordinate)
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
locationManager.requestLocation()
|
||||
}
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.last else { return }
|
||||
locationContinuation?(.success(location.coordinate))
|
||||
locationContinuation = nil
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
locationContinuation?(.failure(error))
|
||||
locationContinuation = nil
|
||||
}
|
||||
}
|
||||
|
||||
private extension MainTabScanHandler {
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
//
|
||||
// OrderLocationProvider.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
|
||||
/// 一次性定位,用于多点位核销拉取打卡点。
|
||||
final class OrderLocationProvider: NSObject, CLLocationManagerDelegate {
|
||||
private let manager = CLLocationManager()
|
||||
private var continuation: ((Result<CLLocationCoordinate2D, Error>) -> Void)?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
manager.delegate = self
|
||||
manager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
}
|
||||
|
||||
func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
||||
let status = manager.authorizationStatus
|
||||
if status == .notDetermined {
|
||||
manager.requestWhenInUseAuthorization()
|
||||
}
|
||||
guard status == .authorizedWhenInUse || status == .authorizedAlways || manager.authorizationStatus == .authorizedWhenInUse else {
|
||||
throw OrderLocationError.permissionDenied
|
||||
}
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
self.continuation = { result in
|
||||
continuation.resume(with: result)
|
||||
}
|
||||
manager.requestLocation()
|
||||
}
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.last else { return }
|
||||
continuation?(.success(location.coordinate))
|
||||
continuation = nil
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
continuation?(.failure(error))
|
||||
continuation = nil
|
||||
}
|
||||
}
|
||||
|
||||
enum OrderLocationError: LocalizedError {
|
||||
case permissionDenied
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .permissionDenied: "缺少定位权限,功能无法进行"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要访问相机以扫描订单核销二维码</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>需要定位以获取多点位核销打卡点列表</string>
|
||||
<string>需要获取您的位置信息,用于位置上报、景区距离排序及多点位核销打卡</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>需要访问相册以选择头像、身份证和银行卡照片</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
|
||||
@ -110,17 +110,13 @@ final class LocationReportViewController: BaseViewController {
|
||||
}
|
||||
|
||||
private func requestLocationIfNeeded() {
|
||||
let status = CLLocationManager.authorizationStatus()
|
||||
switch status {
|
||||
case .authorizedAlways, .authorizedWhenInUse:
|
||||
viewModel.startLocation()
|
||||
case .notDetermined:
|
||||
CLLocationManager().requestWhenInUseAuthorization()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
self?.viewModel.startLocation()
|
||||
Task {
|
||||
do {
|
||||
try await LocationProvider.shared.ensureLocationPermission()
|
||||
viewModel.startLocation()
|
||||
} catch {
|
||||
showToast((error as? LocalizedError)?.errorDescription ?? error.localizedDescription)
|
||||
}
|
||||
default:
|
||||
showToast("需要定位权限才能使用此功能")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import MapKit
|
||||
import CoreLocation
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
@ -287,21 +287,22 @@ final class LocationReportBottomActionView: UIControl {
|
||||
}
|
||||
}
|
||||
|
||||
/// 地图容器,封装 MKMapView。
|
||||
final class LocationReportMapView: UIView, MKMapViewDelegate {
|
||||
/// 地图容器,封装高德 `MAMapView`。
|
||||
final class LocationReportMapView: UIView, MAMapViewDelegate {
|
||||
|
||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||
|
||||
let mapView = MKMapView()
|
||||
private var markerAnnotation: MKPointAnnotation?
|
||||
let mapView: MAMapView
|
||||
private var markerAnnotation: MAPointAnnotation?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
_ = AMapBootstrap.configureIfNeeded()
|
||||
mapView = MAMapView(frame: .zero)
|
||||
super.init(frame: frame)
|
||||
mapView.delegate = self
|
||||
mapView.showsUserLocation = true
|
||||
mapView.showsCompass = false
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
|
||||
mapView.addGestureRecognizer(tap)
|
||||
mapView.zoomLevel = 15
|
||||
addSubview(mapView)
|
||||
mapView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
@ -314,27 +315,22 @@ final class LocationReportMapView: UIView, MKMapViewDelegate {
|
||||
}
|
||||
|
||||
func centerOnUserLocation() {
|
||||
guard let coordinate = mapView.userLocation.location?.coordinate else { return }
|
||||
let coordinate = mapView.userLocation.location?.coordinate
|
||||
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
|
||||
setCenter(coordinate)
|
||||
}
|
||||
|
||||
func setCenter(_ coordinate: CLLocationCoordinate2D, animated: Bool = true) {
|
||||
let region = MKCoordinateRegion(center: coordinate, latitudinalMeters: 800, longitudinalMeters: 800)
|
||||
mapView.setRegion(region, animated: animated)
|
||||
mapView.setCenter(coordinate, animated: animated)
|
||||
mapView.setZoomLevel(15, animated: animated)
|
||||
}
|
||||
|
||||
func zoomIn() {
|
||||
var region = mapView.region
|
||||
region.span.latitudeDelta *= 0.5
|
||||
region.span.longitudeDelta *= 0.5
|
||||
mapView.setRegion(region, animated: true)
|
||||
mapView.setZoomLevel(mapView.zoomLevel + 1, animated: true)
|
||||
}
|
||||
|
||||
func zoomOut() {
|
||||
var region = mapView.region
|
||||
region.span.latitudeDelta = min(region.span.latitudeDelta * 2, 180)
|
||||
region.span.longitudeDelta = min(region.span.longitudeDelta * 2, 180)
|
||||
mapView.setRegion(region, animated: true)
|
||||
mapView.setZoomLevel(mapView.zoomLevel - 1, animated: true)
|
||||
}
|
||||
|
||||
func updateMarker(latitude: Double?, longitude: Double?) {
|
||||
@ -343,15 +339,13 @@ final class LocationReportMapView: UIView, MKMapViewDelegate {
|
||||
markerAnnotation = nil
|
||||
}
|
||||
guard let latitude, let longitude else { return }
|
||||
let annotation = MKPointAnnotation()
|
||||
let annotation = MAPointAnnotation()
|
||||
annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
||||
markerAnnotation = annotation
|
||||
mapView.addAnnotation(annotation)
|
||||
}
|
||||
|
||||
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
|
||||
let point = gesture.location(in: mapView)
|
||||
let coordinate = mapView.convert(point, toCoordinateFrom: mapView)
|
||||
func mapView(_ mapView: MAMapView!, didSingleTappedAt coordinate: CLLocationCoordinate2D) {
|
||||
onMapTap?(coordinate)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SafariServices
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
@ -212,10 +213,10 @@ final class LoginViewController: BaseViewController {
|
||||
self?.showToast("注册待接入")
|
||||
}
|
||||
viewModel.onUserAgreement = { [weak self] in
|
||||
self?.showToast("用户协议待接入")
|
||||
self?.openAgreementURL(AppAgreementURLs.userAgreement)
|
||||
}
|
||||
viewModel.onPrivacyPolicy = { [weak self] in
|
||||
self?.showToast("隐私政策待接入")
|
||||
self?.openAgreementURL(AppAgreementURLs.privacyPolicy)
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,6 +346,11 @@ final class LoginViewController: BaseViewController {
|
||||
view.endEditing(true)
|
||||
viewModel.onRegister?()
|
||||
}
|
||||
|
||||
private func openAgreementURL(_ url: URL) {
|
||||
let controller = SFSafariViewController(url: url)
|
||||
present(controller, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension LoginViewController: UIGestureRecognizerDelegate {
|
||||
|
||||
@ -23,7 +23,7 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
||||
private weak var orderSourcePicker: OrderSourcePickerViewController?
|
||||
private var pendingGiftOrderNumber: String?
|
||||
private var isFetchingMultiVerifySpots = false
|
||||
private let locationProvider = OrderLocationProvider()
|
||||
private let locationProvider: any LocationProviding = LocationProvider.shared
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "订单"
|
||||
|
||||
9
suixinkan/suixinkan-Bridging-Header.h
Normal file
9
suixinkan/suixinkan-Bridging-Header.h
Normal file
@ -0,0 +1,9 @@
|
||||
//
|
||||
// suixinkan-Bridging-Header.h
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
#import <AMapFoundationKit/AMapFoundationKit.h>
|
||||
#import <AMapLocationKit/AMapLocationKit.h>
|
||||
#import <MAMapKit/MAMapKit.h>
|
||||
#import <AMapSearchKit/AMapSearchKit.h>
|
||||
51
suixinkanTests/AMapBootstrapTests.swift
Normal file
51
suixinkanTests/AMapBootstrapTests.swift
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// AMapBootstrapTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 高德 SDK 初始化 gate 测试。
|
||||
final class AMapBootstrapTests: XCTestCase {
|
||||
|
||||
private var defaults: UserDefaults!
|
||||
private var appStore: AppStore!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
AMapBootstrap.resetForTesting()
|
||||
defaults = UserDefaults(suiteName: "AMapBootstrapTests")!
|
||||
defaults.removePersistentDomain(forName: "AMapBootstrapTests")
|
||||
appStore = AppStore(defaults: defaults)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
AMapBootstrap.resetForTesting()
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testConfigureReturnsFalseWhenPrivacyNotAccepted() {
|
||||
appStore.privacyAgreementAccepted = false
|
||||
|
||||
XCTAssertFalse(AMapBootstrap.configureIfNeeded(appStore: appStore))
|
||||
XCTAssertFalse(AMapBootstrap.isConfigured)
|
||||
}
|
||||
|
||||
func testConfigureIsIdempotentWhenPrivacyAccepted() {
|
||||
appStore.privacyAgreementAccepted = true
|
||||
|
||||
XCTAssertTrue(AMapBootstrap.configureIfNeeded(appStore: appStore))
|
||||
XCTAssertTrue(AMapBootstrap.isConfigured)
|
||||
XCTAssertTrue(AMapBootstrap.configureIfNeeded(appStore: appStore))
|
||||
XCTAssertTrue(AMapBootstrap.isConfigured)
|
||||
}
|
||||
|
||||
func testRequireConfiguredThrowsWhenPrivacyNotAccepted() {
|
||||
appStore.privacyAgreementAccepted = false
|
||||
|
||||
XCTAssertThrowsError(try AMapBootstrap.requireConfigured(appStore: appStore)) { error in
|
||||
XCTAssertEqual(error as? LocationProviderError, .privacyNotAccepted)
|
||||
}
|
||||
}
|
||||
}
|
||||
60
suixinkanTests/LocationProviderTests.swift
Normal file
60
suixinkanTests/LocationProviderTests.swift
Normal file
@ -0,0 +1,60 @@
|
||||
//
|
||||
// LocationProviderTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 定位协议与 ViewModel 注入测试。
|
||||
final class LocationProviderTests: XCTestCase {
|
||||
|
||||
private var defaults: UserDefaults!
|
||||
private var appStore: AppStore!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
defaults = UserDefaults(suiteName: "LocationProviderTests")!
|
||||
defaults.removePersistentDomain(forName: "LocationProviderTests")
|
||||
appStore = AppStore(defaults: defaults)
|
||||
}
|
||||
|
||||
func testScenicSelectionUsesInjectedProviderSnapshot() async {
|
||||
let mock = MockLocationProvider()
|
||||
mock.snapshot = HomeLocationSnapshot(latitude: 30.2, longitude: 120.1, address: "杭州")
|
||||
appStore.saveRoleScenicList([
|
||||
ScenicInfo(id: 1, name: "西湖", status: 1, location: ScenicLocationInfo(lng: 120.1, lat: 30.2, address: "杭州"), coverImg: nil),
|
||||
])
|
||||
|
||||
let viewModel = ScenicSelectionViewModel(appStore: appStore, locationProvider: mock)
|
||||
var message: String?
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
|
||||
await viewModel.startLocation()
|
||||
|
||||
XCTAssertEqual(viewModel.currentLocationText, "杭州")
|
||||
XCTAssertNil(message)
|
||||
}
|
||||
|
||||
func testLocationReportViewModelUsesInjectedReverseGeocode() async {
|
||||
let mock = MockLocationProvider()
|
||||
mock.reverseGeocodeResult = "标记地址"
|
||||
let viewModel = LocationReportViewModel(locationProvider: mock)
|
||||
|
||||
viewModel.toggleMarking()
|
||||
viewModel.setMarkerLocation(latitude: 30.1, longitude: 120.2)
|
||||
try? await Task.sleep(nanoseconds: 200_000_000)
|
||||
|
||||
XCTAssertEqual(viewModel.markerAddress, "标记地址")
|
||||
}
|
||||
|
||||
func testMockLocationProviderCoordinate() async throws {
|
||||
let mock = MockLocationProvider()
|
||||
mock.coordinate = CLLocationCoordinate2D(latitude: 31.0, longitude: 121.0)
|
||||
|
||||
let coordinate = try await mock.requestCoordinate()
|
||||
XCTAssertEqual(coordinate.latitude, 31.0, accuracy: 0.0001)
|
||||
XCTAssertEqual(coordinate.longitude, 121.0, accuracy: 0.0001)
|
||||
}
|
||||
}
|
||||
30
suixinkanTests/MockLocationProvider.swift
Normal file
30
suixinkanTests/MockLocationProvider.swift
Normal file
@ -0,0 +1,30 @@
|
||||
//
|
||||
// MockLocationProvider.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
@testable import suixinkan
|
||||
|
||||
/// 测试用定位替身。
|
||||
final class MockLocationProvider: LocationProviding, @unchecked Sendable {
|
||||
var snapshot = HomeLocationSnapshot(latitude: 39.9087, longitude: 116.3975, address: "北京市东城区")
|
||||
var coordinate = CLLocationCoordinate2D(latitude: 39.9087, longitude: 116.3975)
|
||||
var reverseGeocodeResult = "北京市东城区"
|
||||
var requestSnapshotError: Error?
|
||||
var requestCoordinateError: Error?
|
||||
|
||||
func requestSnapshot() async throws -> HomeLocationSnapshot {
|
||||
if let requestSnapshotError { throw requestSnapshotError }
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
||||
if let requestCoordinateError { throw requestCoordinateError }
|
||||
return coordinate
|
||||
}
|
||||
|
||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||
reverseGeocodeResult
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user