feat: 支持模拟器定位与地图 fallback
This commit is contained in:
16
Podfile
16
Podfile
@ -1,10 +1,17 @@
|
|||||||
platform :ios, '16.0'
|
platform :ios, '16.0'
|
||||||
|
|
||||||
|
project 'suixinkan.xcodeproj',
|
||||||
|
'Debug' => :debug,
|
||||||
|
'SimulatorDebug' => :debug,
|
||||||
|
'Release' => :release
|
||||||
|
|
||||||
target 'suixinkan' do
|
target 'suixinkan' do
|
||||||
use_frameworks!
|
use_frameworks!
|
||||||
pod 'AMapLocation-NO-IDFA'
|
# 高德 SDK 未提供 arm64-simulator slice,仅在真机配置中接入。
|
||||||
pod 'AMap3DMap-NO-IDFA'
|
pod 'AMapFoundation-NO-IDFA', :configurations => ['Debug', 'Release']
|
||||||
pod 'AMapSearch-NO-IDFA'
|
pod 'AMapLocation-NO-IDFA', :configurations => ['Debug', 'Release']
|
||||||
|
pod 'AMap3DMap-NO-IDFA', :configurations => ['Debug', 'Release']
|
||||||
|
pod 'AMapSearch-NO-IDFA', :configurations => ['Debug', 'Release']
|
||||||
pod 'UMCommon'
|
pod 'UMCommon'
|
||||||
pod 'UMDevice'
|
pod 'UMDevice'
|
||||||
pod 'UMAPM'
|
pod 'UMAPM'
|
||||||
@ -24,6 +31,9 @@ post_install do |installer|
|
|||||||
installer.pods_project.targets.each do |target|
|
installer.pods_project.targets.each do |target|
|
||||||
target.build_configurations.each do |config|
|
target.build_configurations.each do |config|
|
||||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
|
||||||
|
if target.name == 'Pods-suixinkanTests' && config.name == 'SimulatorDebug'
|
||||||
|
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = ''
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -58,6 +58,7 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- AMap3DMap-NO-IDFA
|
- AMap3DMap-NO-IDFA
|
||||||
|
- AMapFoundation-NO-IDFA
|
||||||
- AMapLocation-NO-IDFA
|
- AMapLocation-NO-IDFA
|
||||||
- AMapSearch-NO-IDFA
|
- AMapSearch-NO-IDFA
|
||||||
- IQKeyboardManagerSwift
|
- IQKeyboardManagerSwift
|
||||||
@ -109,6 +110,6 @@ SPEC CHECKSUMS:
|
|||||||
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
||||||
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
||||||
|
|
||||||
PODFILE CHECKSUM: e7971a58b5d46ffc03f36ef716b922b6a75a6412
|
PODFILE CHECKSUM: 5abf70ade4a9c4b66b64877fdea91f1d086bcaf5
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
3
Pods/Manifest.lock
generated
3
Pods/Manifest.lock
generated
@ -58,6 +58,7 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- AMap3DMap-NO-IDFA
|
- AMap3DMap-NO-IDFA
|
||||||
|
- AMapFoundation-NO-IDFA
|
||||||
- AMapLocation-NO-IDFA
|
- AMapLocation-NO-IDFA
|
||||||
- AMapSearch-NO-IDFA
|
- AMapSearch-NO-IDFA
|
||||||
- IQKeyboardManagerSwift
|
- IQKeyboardManagerSwift
|
||||||
@ -109,6 +110,6 @@ SPEC CHECKSUMS:
|
|||||||
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
||||||
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
||||||
|
|
||||||
PODFILE CHECKSUM: e7971a58b5d46ffc03f36ef716b922b6a75a6412
|
PODFILE CHECKSUM: 5abf70ade4a9c4b66b64877fdea91f1d086bcaf5
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
5067
Pods/Pods.xcodeproj/project.pbxproj
generated
5067
Pods/Pods.xcodeproj/project.pbxproj
generated
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,9 @@
|
|||||||
|
${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks.sh
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework
|
||||||
|
${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardCore.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManagerSwift.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardNotification.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardReturnManager.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardToolbar.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardToolbarManager.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQTextInputViewNotification.framework
|
||||||
|
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQTextView.framework
|
||||||
@ -195,6 +195,16 @@ if [[ "$CONFIGURATION" == "Release" ]]; then
|
|||||||
install_framework "${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework"
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework"
|
||||||
fi
|
fi
|
||||||
|
if [[ "$CONFIGURATION" == "SimulatorDebug" ]]; then
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework"
|
||||||
|
fi
|
||||||
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
|
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
|
||||||
wait
|
wait
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources.sh
|
||||||
18
Pods/Target Support Files/Pods-suixinkan/Pods-suixinkan.simulatordebug.xcconfig
generated
Normal file
18
Pods/Target Support Files/Pods-suixinkan/Pods-suixinkan.simulatordebug.xcconfig
generated
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||||
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||||
|
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||||
|
OTHER_LDFLAGS = $(inherited) -ObjC -l"JCore" -l"JPush" -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreTelephony" -framework "Foundation" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
|
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/JCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/JPush" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
|
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||||
|
PODS_ROOT = ${SRCROOT}/Pods
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
|
VALID_ARCHS = arm64 x86_64
|
||||||
15
Pods/Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests.simulatordebug.xcconfig
generated
Normal file
15
Pods/Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests.simulatordebug.xcconfig
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
ARCHS = $(ARCHS_STANDARD)
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||||
|
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||||
|
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||||
|
OTHER_LDFLAGS = $(inherited) -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreTelephony" -framework "Foundation" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||||
|
PODS_ROOT = ${SRCROOT}/Pods
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
|
VALID_ARCHS = arm64 x86_64
|
||||||
@ -36,6 +36,8 @@
|
|||||||
93FF17C52FFB84950033C304 /* suixinkan.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = suixinkan.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
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; };
|
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; };
|
C69C08A144C2D5D4A448B7D2 /* Pods_suixinkan.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_suixinkan.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
DC152A95F1705B826BE8A56A /* Pods-suixinkanTests.simulatordebug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-suixinkanTests.simulatordebug.xcconfig"; path = "Target Support Files/Pods-suixinkanTests/Pods-suixinkanTests.simulatordebug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
ECE6D74459339B5075CF14AD /* Pods-suixinkan.simulatordebug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-suixinkan.simulatordebug.xcconfig"; path = "Target Support Files/Pods-suixinkan/Pods-suixinkan.simulatordebug.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
@ -59,11 +61,15 @@
|
|||||||
};
|
};
|
||||||
9A01B8E22FFB84950033C402 /* Vendor */ = {
|
9A01B8E22FFB84950033C402 /* Vendor */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
);
|
||||||
path = Vendor;
|
path = Vendor;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
A1B2C3D42FFB84950033C305 /* suixinkanTests */ = {
|
A1B2C3D42FFB84950033C305 /* suixinkanTests */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
);
|
||||||
path = suixinkanTests;
|
path = suixinkanTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -111,6 +117,8 @@
|
|||||||
1060D5E389506965D83EA1CC /* Pods-suixinkan.release.xcconfig */,
|
1060D5E389506965D83EA1CC /* Pods-suixinkan.release.xcconfig */,
|
||||||
83445E7B4CCCF42C6FD481D3 /* Pods-suixinkanTests.debug.xcconfig */,
|
83445E7B4CCCF42C6FD481D3 /* Pods-suixinkanTests.debug.xcconfig */,
|
||||||
6CB3E1DA4E97AA4961B25733 /* Pods-suixinkanTests.release.xcconfig */,
|
6CB3E1DA4E97AA4961B25733 /* Pods-suixinkanTests.release.xcconfig */,
|
||||||
|
ECE6D74459339B5075CF14AD /* Pods-suixinkan.simulatordebug.xcconfig */,
|
||||||
|
DC152A95F1705B826BE8A56A /* Pods-suixinkanTests.simulatordebug.xcconfig */,
|
||||||
);
|
);
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -361,14 +369,10 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-frameworks.sh\"\n";
|
||||||
@ -382,14 +386,10 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-suixinkan/Pods-suixinkan-resources.sh\"\n";
|
||||||
@ -728,6 +728,150 @@
|
|||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
D1A2B3C42FFB84950033C401 /* SimulatorDebug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = ECE6D74459339B5075CF14AD /* Pods-suixinkan.simulatordebug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
APS_ENVIRONMENT = development;
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1010402;
|
||||||
|
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = suixinkan/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "随心瞰商家版";
|
||||||
|
INFOPLIST_KEY_NSCameraUsageDescription = "需要访问相机以拍摄任务素材、扫描订单核销二维码,并连接有线相机进行旅拍相册传输";
|
||||||
|
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "需要获取您的位置信息,用于位置上报、景区距离排序及多点位核销打卡";
|
||||||
|
INFOPLIST_KEY_NSMicrophoneUsageDescription = "需要访问麦克风以在拍摄素材视频时录制声音";
|
||||||
|
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "需要保存收款二维码、排队打卡点小程序码和云盘图片视频到相册";
|
||||||
|
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "需要访问相册以选择头像、身份证、银行卡照片和云盘上传素材";
|
||||||
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.1.4;
|
||||||
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"-ObjC",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.yuanzhixiang.suixinkan;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = iphonesimulator;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SUPPORTED_PLATFORMS = iphonesimulator;
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
name = SimulatorDebug;
|
||||||
|
};
|
||||||
|
D1A2B3C52FFB84950033C402 /* SimulatorDebug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphonesimulator;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = SimulatorDebug;
|
||||||
|
};
|
||||||
|
D1A2B3C62FFB84950033C403 /* SimulatorDebug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = DC152A95F1705B826BE8A56A /* Pods-suixinkanTests.simulatordebug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.yuanzhixiang.suixinkanTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = iphonesimulator;
|
||||||
|
SUPPORTED_PLATFORMS = iphonesimulator;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/suixinkan.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/suixinkan";
|
||||||
|
};
|
||||||
|
name = SimulatorDebug;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@ -735,6 +879,7 @@
|
|||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
93FF17DB2FFB84950033C304 /* Debug */,
|
93FF17DB2FFB84950033C304 /* Debug */,
|
||||||
|
D1A2B3C52FFB84950033C402 /* SimulatorDebug */,
|
||||||
93FF17DC2FFB84950033C304 /* Release */,
|
93FF17DC2FFB84950033C304 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
@ -744,6 +889,7 @@
|
|||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
93FF17D92FFB84950033C304 /* Debug */,
|
93FF17D92FFB84950033C304 /* Debug */,
|
||||||
|
D1A2B3C42FFB84950033C401 /* SimulatorDebug */,
|
||||||
93FF17DA2FFB84950033C304 /* Release */,
|
93FF17DA2FFB84950033C304 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
@ -753,6 +899,7 @@
|
|||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
A1B2C3DC2FFB84950033C305 /* Debug */,
|
A1B2C3DC2FFB84950033C305 /* Debug */,
|
||||||
|
D1A2B3C62FFB84950033C403 /* SimulatorDebug */,
|
||||||
A1B2C3DA2FFB84950033C305 /* Release */,
|
A1B2C3DA2FFB84950033C305 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
|
|||||||
@ -0,0 +1,100 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2660"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "NO"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "93FF17C42FFB84950033C304"
|
||||||
|
BuildableName = "suixinkan.app"
|
||||||
|
BlueprintName = "suixinkan"
|
||||||
|
ReferencedContainer = "container:suixinkan.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "SimulatorDebug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "93FF17C42FFB84950033C304"
|
||||||
|
BuildableName = "suixinkan.app"
|
||||||
|
BlueprintName = "suixinkan"
|
||||||
|
ReferencedContainer = "container:suixinkan.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "A1B2C3D32FFB84950033C305"
|
||||||
|
BuildableName = "suixinkanTests.xctest"
|
||||||
|
BlueprintName = "suixinkanTests"
|
||||||
|
ReferencedContainer = "container:suixinkan.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "SimulatorDebug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "93FF17C42FFB84950033C304"
|
||||||
|
BuildableName = "suixinkan.app"
|
||||||
|
BlueprintName = "suixinkan"
|
||||||
|
ReferencedContainer = "container:suixinkan.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "SimulatorDebug"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "93FF17C42FFB84950033C304"
|
||||||
|
BuildableName = "suixinkan.app"
|
||||||
|
BlueprintName = "suixinkan"
|
||||||
|
ReferencedContainer = "container:suixinkan.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "SimulatorDebug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@ -20,6 +20,7 @@ enum AMapBootstrap {
|
|||||||
defer { lock.unlock() }
|
defer { lock.unlock() }
|
||||||
guard !isConfigured else { return true }
|
guard !isConfigured else { return true }
|
||||||
|
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
AMapLocationManager.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
AMapLocationManager.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
||||||
AMapLocationManager.updatePrivacyAgree(.didAgree)
|
AMapLocationManager.updatePrivacyAgree(.didAgree)
|
||||||
MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
||||||
@ -29,6 +30,7 @@ enum AMapBootstrap {
|
|||||||
|
|
||||||
AMapServices.shared().apiKey = AMapConfig.apiKey
|
AMapServices.shared().apiKey = AMapConfig.apiKey
|
||||||
AMapServices.shared().enableHTTPS = true
|
AMapServices.shared().enableHTTPS = true
|
||||||
|
#endif
|
||||||
isConfigured = true
|
isConfigured = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,16 +3,28 @@
|
|||||||
// suixinkan
|
// suixinkan
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CoreLocation
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// 高德逆地理编码封装,对齐 Android `GeocodeSearch`。
|
/// 地址逆地理编码能力,供平台定位实现注入。
|
||||||
@MainActor
|
@MainActor
|
||||||
final class LocationGeocoder: NSObject, AMapSearchDelegate {
|
protocol LocationAddressGeocoding: AnyObject {
|
||||||
|
/// 将坐标解析为可读地址,失败时返回空字符串。
|
||||||
|
func reverseGeocode(latitude: Double, longitude: Double) async -> String
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 平台逆地理编码封装;真机使用高德,模拟器使用 Core Location。
|
||||||
|
@MainActor
|
||||||
|
final class LocationGeocoder: NSObject, LocationAddressGeocoding {
|
||||||
|
|
||||||
nonisolated static let shared = LocationGeocoder()
|
nonisolated static let shared = LocationGeocoder()
|
||||||
|
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
private let geocoder = CLGeocoder()
|
||||||
|
#else
|
||||||
private var searchAPI: AMapSearchAPI?
|
private var searchAPI: AMapSearchAPI?
|
||||||
private var continuation: CheckedContinuation<String, Never>?
|
private var continuation: CheckedContinuation<String, Never>?
|
||||||
|
#endif
|
||||||
|
|
||||||
nonisolated private override init() {
|
nonisolated private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
@ -20,6 +32,26 @@ final class LocationGeocoder: NSObject, AMapSearchDelegate {
|
|||||||
|
|
||||||
/// 将坐标解析为可读地址。
|
/// 将坐标解析为可读地址。
|
||||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
let location = CLLocation(latitude: latitude, longitude: longitude)
|
||||||
|
guard let placemark = try? await geocoder.reverseGeocodeLocation(location).first else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if let name = placemark.name?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
|
!name.isEmpty {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
placemark.administrativeArea,
|
||||||
|
placemark.locality,
|
||||||
|
placemark.subLocality,
|
||||||
|
placemark.thoroughfare,
|
||||||
|
placemark.subThoroughfare,
|
||||||
|
]
|
||||||
|
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
.joined()
|
||||||
|
#else
|
||||||
do {
|
do {
|
||||||
try AMapBootstrap.requireConfigured()
|
try AMapBootstrap.requireConfigured()
|
||||||
} catch {
|
} catch {
|
||||||
@ -40,8 +72,10 @@ final class LocationGeocoder: NSObject, AMapSearchDelegate {
|
|||||||
request.radius = 200
|
request.radius = 200
|
||||||
api.aMapReGoecodeSearch(request)
|
api.aMapReGoecodeSearch(request)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
|
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
|
||||||
let address = response?.regeocode?.formattedAddress ?? ""
|
let address = response?.regeocode?.formattedAddress ?? ""
|
||||||
continuation?.resume(returning: address)
|
continuation?.resume(returning: address)
|
||||||
@ -62,4 +96,9 @@ final class LocationGeocoder: NSObject, AMapSearchDelegate {
|
|||||||
searchAPI = api
|
searchAPI = api
|
||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
|
extension LocationGeocoder: AMapSearchDelegate {}
|
||||||
|
#endif
|
||||||
|
|||||||
@ -6,7 +6,165 @@
|
|||||||
import CoreLocation
|
import CoreLocation
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// 统一定位服务,封装高德 `AMapLocationManager`,对齐 Android `LocationProvider`。
|
/// Core Location 管理器最小接口,便于模拟器定位实现注入测试替身。
|
||||||
|
@MainActor
|
||||||
|
protocol CoreLocationManaging: AnyObject {
|
||||||
|
var authorizationStatus: CLAuthorizationStatus { get }
|
||||||
|
var desiredAccuracy: CLLocationAccuracy { get set }
|
||||||
|
var delegate: CLLocationManagerDelegate? { get set }
|
||||||
|
|
||||||
|
/// 请求使用期间定位权限。
|
||||||
|
func requestWhenInUseAuthorization()
|
||||||
|
/// 请求一次当前位置。
|
||||||
|
func requestLocation()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CLLocationManager: CoreLocationManaging {}
|
||||||
|
|
||||||
|
/// Core Location 定位实现,供模拟器运行并支持依赖注入测试。
|
||||||
|
@MainActor
|
||||||
|
final class CoreLocationProvider: NSObject, LocationProviding, CLLocationManagerDelegate {
|
||||||
|
|
||||||
|
nonisolated static let shared = CoreLocationProvider()
|
||||||
|
|
||||||
|
private var appStore: AppStore = .shared
|
||||||
|
private var manager: any CoreLocationManaging = CLLocationManager()
|
||||||
|
private var geocoder: any LocationAddressGeocoding = LocationGeocoder.shared
|
||||||
|
private var permissionContinuation: CheckedContinuation<Void, Error>?
|
||||||
|
private var locationContinuation: CheckedContinuation<CLLocationCoordinate2D, Error>?
|
||||||
|
|
||||||
|
nonisolated override init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用可控依赖创建 Core Location 定位服务。
|
||||||
|
init(
|
||||||
|
appStore: AppStore,
|
||||||
|
manager: any CoreLocationManaging,
|
||||||
|
geocoder: any LocationAddressGeocoding
|
||||||
|
) {
|
||||||
|
self.appStore = appStore
|
||||||
|
self.manager = manager
|
||||||
|
self.geocoder = geocoder
|
||||||
|
super.init()
|
||||||
|
manager.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 请求坐标及逆地理编码地址。
|
||||||
|
func requestSnapshot(
|
||||||
|
desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest
|
||||||
|
) async throws -> HomeLocationSnapshot {
|
||||||
|
let coordinate = try await requestCoordinate(desiredAccuracy: desiredAccuracy)
|
||||||
|
let address = await geocoder.reverseGeocode(
|
||||||
|
latitude: coordinate.latitude,
|
||||||
|
longitude: coordinate.longitude
|
||||||
|
)
|
||||||
|
return HomeLocationSnapshot(
|
||||||
|
latitude: coordinate.latitude,
|
||||||
|
longitude: coordinate.longitude,
|
||||||
|
address: address
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 请求一次当前位置坐标。
|
||||||
|
func requestCoordinate(
|
||||||
|
desiredAccuracy: CLLocationAccuracy
|
||||||
|
) async throws -> CLLocationCoordinate2D {
|
||||||
|
try requirePrivacyAgreement()
|
||||||
|
try await ensureLocationPermission()
|
||||||
|
guard locationContinuation == nil else {
|
||||||
|
throw LocationProviderError.locationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.delegate = self
|
||||||
|
manager.desiredAccuracy = desiredAccuracy
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
locationContinuation = continuation
|
||||||
|
manager.requestLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用系统服务将坐标解析为地址。
|
||||||
|
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||||
|
guard appStore.session.privacyAgreementAccepted else { return "" }
|
||||||
|
return await geocoder.reverseGeocode(latitude: latitude, longitude: longitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 请求系统定位权限,等待用户完成首次授权。
|
||||||
|
func ensureLocationPermission() async throws {
|
||||||
|
manager.delegate = self
|
||||||
|
switch manager.authorizationStatus {
|
||||||
|
case .authorizedAlways, .authorizedWhenInUse:
|
||||||
|
return
|
||||||
|
case .denied, .restricted:
|
||||||
|
throw LocationProviderError.permissionDenied
|
||||||
|
case .notDetermined:
|
||||||
|
guard permissionContinuation == nil else {
|
||||||
|
throw LocationProviderError.locationFailed
|
||||||
|
}
|
||||||
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
|
permissionContinuation = continuation
|
||||||
|
manager.requestWhenInUseAuthorization()
|
||||||
|
}
|
||||||
|
@unknown default:
|
||||||
|
throw LocationProviderError.permissionDenied
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||||
|
resolvePermission(status: self.manager.authorizationStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||||
|
guard let continuation = locationContinuation else { return }
|
||||||
|
locationContinuation = nil
|
||||||
|
guard let coordinate = locations.last?.coordinate,
|
||||||
|
CLLocationCoordinate2DIsValid(coordinate),
|
||||||
|
coordinate.latitude != 0 || coordinate.longitude != 0 else {
|
||||||
|
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continuation.resume(returning: coordinate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||||
|
guard let continuation = locationContinuation else { return }
|
||||||
|
locationContinuation = nil
|
||||||
|
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requirePrivacyAgreement() throws {
|
||||||
|
guard appStore.session.privacyAgreementAccepted else {
|
||||||
|
throw LocationProviderError.privacyNotAccepted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvePermission(status: CLAuthorizationStatus) {
|
||||||
|
guard let continuation = permissionContinuation else { return }
|
||||||
|
switch status {
|
||||||
|
case .authorizedAlways, .authorizedWhenInUse:
|
||||||
|
permissionContinuation = nil
|
||||||
|
continuation.resume()
|
||||||
|
case .denied, .restricted:
|
||||||
|
permissionContinuation = nil
|
||||||
|
continuation.resume(throwing: LocationProviderError.permissionDenied)
|
||||||
|
case .notDetermined:
|
||||||
|
break
|
||||||
|
@unknown default:
|
||||||
|
permissionContinuation = nil
|
||||||
|
continuation.resume(throwing: LocationProviderError.permissionDenied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
|
||||||
|
/// 模拟器默认定位服务,使用 Core Location 与系统逆地理编码。
|
||||||
|
typealias LocationProvider = CoreLocationProvider
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
/// 真机统一定位服务,封装高德 `AMapLocationManager`,对齐 Android `LocationProvider`。
|
||||||
@MainActor
|
@MainActor
|
||||||
final class LocationProvider: NSObject, LocationProviding {
|
final class LocationProvider: NSObject, LocationProviding {
|
||||||
|
|
||||||
@ -19,7 +177,7 @@ final class LocationProvider: NSObject, LocationProviding {
|
|||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
/// 请求坐标及高德逆地理编码地址。
|
||||||
func requestSnapshot(desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest) async throws -> HomeLocationSnapshot {
|
func requestSnapshot(desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest) async throws -> HomeLocationSnapshot {
|
||||||
try AMapBootstrap.requireConfigured()
|
try AMapBootstrap.requireConfigured()
|
||||||
try await ensureLocationPermission()
|
try await ensureLocationPermission()
|
||||||
@ -51,7 +209,7 @@ final class LocationProvider: NSObject, LocationProviding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
/// 请求一次当前位置坐标。
|
||||||
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D {
|
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D {
|
||||||
try AMapBootstrap.requireConfigured()
|
try AMapBootstrap.requireConfigured()
|
||||||
try await ensureLocationPermission()
|
try await ensureLocationPermission()
|
||||||
@ -78,7 +236,7 @@ final class LocationProvider: NSObject, LocationProviding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
/// 使用高德服务将坐标解析为地址。
|
||||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||||
await geocoder.reverseGeocode(latitude: latitude, longitude: longitude)
|
await geocoder.reverseGeocode(latitude: latitude, longitude: longitude)
|
||||||
}
|
}
|
||||||
@ -110,3 +268,5 @@ final class LocationProvider: NSObject, LocationProviding {
|
|||||||
return manager
|
return manager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|||||||
@ -16,20 +16,25 @@ struct HomeLocationSnapshot: Sendable, Equatable {
|
|||||||
/// 定位服务协议,便于 ViewModel 注入与单元测试 mock。
|
/// 定位服务协议,便于 ViewModel 注入与单元测试 mock。
|
||||||
protocol LocationProviding: Sendable {
|
protocol LocationProviding: Sendable {
|
||||||
/// 按指定精度请求当前坐标与地址。
|
/// 按指定精度请求当前坐标与地址。
|
||||||
|
@MainActor
|
||||||
func requestSnapshot(desiredAccuracy: CLLocationAccuracy) async throws -> HomeLocationSnapshot
|
func requestSnapshot(desiredAccuracy: CLLocationAccuracy) async throws -> HomeLocationSnapshot
|
||||||
/// 按指定精度请求当前坐标。
|
/// 按指定精度请求当前坐标。
|
||||||
|
@MainActor
|
||||||
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D
|
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D
|
||||||
/// 逆地理编码。
|
/// 逆地理编码。
|
||||||
|
@MainActor
|
||||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String
|
func reverseGeocode(latitude: Double, longitude: Double) async -> String
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LocationProviding {
|
extension LocationProviding {
|
||||||
/// 请求当前坐标与地址,默认使用最高精度以保持既有业务行为。
|
/// 请求当前坐标与地址,默认使用最高精度以保持既有业务行为。
|
||||||
|
@MainActor
|
||||||
func requestSnapshot() async throws -> HomeLocationSnapshot {
|
func requestSnapshot() async throws -> HomeLocationSnapshot {
|
||||||
try await requestSnapshot(desiredAccuracy: kCLLocationAccuracyBest)
|
try await requestSnapshot(desiredAccuracy: kCLLocationAccuracyBest)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 请求当前坐标,默认使用最高精度以保持既有业务行为。
|
/// 请求当前坐标,默认使用最高精度以保持既有业务行为。
|
||||||
|
@MainActor
|
||||||
func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
||||||
try await requestCoordinate(desiredAccuracy: kCLLocationAccuracyBest)
|
try await requestCoordinate(desiredAccuracy: kCLLocationAccuracyBest)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
// suixinkan
|
// suixinkan
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CoreLocation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
/// 主 Tab 中间扫码按钮结果处理,对齐 Android `MainTabViewModel.signIn`。
|
/// 主 Tab 中间扫码按钮结果处理,对齐 Android `MainTabViewModel.signIn`。
|
||||||
|
|||||||
@ -7,6 +7,10 @@ import CoreLocation
|
|||||||
import SnapKit
|
import SnapKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
import MapKit
|
||||||
|
#endif
|
||||||
|
|
||||||
/// 地图缩放/定位控件。
|
/// 地图缩放/定位控件。
|
||||||
final class LocationReportMapControlStack: UIView {
|
final class LocationReportMapControlStack: UIView {
|
||||||
|
|
||||||
@ -295,7 +299,112 @@ final class LocationReportBottomActionView: UIControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 地图容器,封装高德 `MAMapView`。
|
/// 位置上报地图容器,模拟器使用 MapKit。
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
final class LocationReportMapView: UIView, MKMapViewDelegate {
|
||||||
|
|
||||||
|
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||||
|
|
||||||
|
let mapView: MKMapView
|
||||||
|
private var markerAnnotation: MKPointAnnotation?
|
||||||
|
private var shouldCenterOnNextUserLocation = false
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
mapView = MKMapView(frame: .zero)
|
||||||
|
super.init(frame: frame)
|
||||||
|
mapView.delegate = self
|
||||||
|
mapView.showsUserLocation = true
|
||||||
|
mapView.showsCompass = false
|
||||||
|
addSubview(mapView)
|
||||||
|
mapView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
|
||||||
|
tapGesture.cancelsTouchesInView = false
|
||||||
|
mapView.addGestureRecognizer(tapGesture)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 进入页面后,在首次拿到用户位置时将地图居中。
|
||||||
|
func enableInitialUserLocationCentering() {
|
||||||
|
shouldCenterOnNextUserLocation = true
|
||||||
|
centerOnUserLocationIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
func centerOnUserLocation() {
|
||||||
|
guard let coordinate = validUserCoordinate else { return }
|
||||||
|
setCenter(coordinate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCenter(_ coordinate: CLLocationCoordinate2D, animated: Bool = true) {
|
||||||
|
let region = MKCoordinateRegion(
|
||||||
|
center: coordinate,
|
||||||
|
span: MKCoordinateSpan(latitudeDelta: 0.012, longitudeDelta: 0.012)
|
||||||
|
)
|
||||||
|
mapView.setRegion(region, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func zoomIn() {
|
||||||
|
updateZoom(scale: 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func zoomOut() {
|
||||||
|
updateZoom(scale: 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMarker(latitude: Double?, longitude: Double?) {
|
||||||
|
if let markerAnnotation {
|
||||||
|
mapView.removeAnnotation(markerAnnotation)
|
||||||
|
self.markerAnnotation = nil
|
||||||
|
}
|
||||||
|
guard let latitude, let longitude else { return }
|
||||||
|
let annotation = MKPointAnnotation()
|
||||||
|
annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
||||||
|
markerAnnotation = annotation
|
||||||
|
mapView.addAnnotation(annotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
|
||||||
|
centerOnUserLocationIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func mapTapped(_ gesture: UITapGestureRecognizer) {
|
||||||
|
guard gesture.state == .ended else { return }
|
||||||
|
let point = gesture.location(in: mapView)
|
||||||
|
onMapTap?(mapView.convert(point, toCoordinateFrom: mapView))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var validUserCoordinate: CLLocationCoordinate2D? {
|
||||||
|
guard let coordinate = mapView.userLocation.location?.coordinate,
|
||||||
|
CLLocationCoordinate2DIsValid(coordinate),
|
||||||
|
coordinate.latitude != 0 || coordinate.longitude != 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return coordinate
|
||||||
|
}
|
||||||
|
|
||||||
|
private func centerOnUserLocationIfNeeded() {
|
||||||
|
guard shouldCenterOnNextUserLocation, let coordinate = validUserCoordinate else { return }
|
||||||
|
setCenter(coordinate, animated: false)
|
||||||
|
shouldCenterOnNextUserLocation = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateZoom(scale: Double) {
|
||||||
|
let current = mapView.region
|
||||||
|
let span = MKCoordinateSpan(
|
||||||
|
latitudeDelta: min(max(current.span.latitudeDelta * scale, 0.0005), 120),
|
||||||
|
longitudeDelta: min(max(current.span.longitudeDelta * scale, 0.0005), 120)
|
||||||
|
)
|
||||||
|
mapView.setRegion(MKCoordinateRegion(center: current.center, span: span), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
/// 位置上报地图容器,真机封装高德 `MAMapView`。
|
||||||
final class LocationReportMapView: UIView, MAMapViewDelegate {
|
final class LocationReportMapView: UIView, MAMapViewDelegate {
|
||||||
|
|
||||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||||
@ -381,3 +490,4 @@ final class LocationReportMapView: UIView, MAMapViewDelegate {
|
|||||||
shouldCenterOnNextUserLocation = false
|
shouldCenterOnNextUserLocation = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@ -3,11 +3,16 @@
|
|||||||
// suixinkan
|
// suixinkan
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CoreLocation
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
import Photos
|
import Photos
|
||||||
import SnapKit
|
import SnapKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
import MapKit
|
||||||
|
#endif
|
||||||
|
|
||||||
/// 打卡点详情页,对齐 Android `PunchPointDetailScreen`。
|
/// 打卡点详情页,对齐 Android `PunchPointDetailScreen`。
|
||||||
final class PunchPointDetailViewController: BaseViewController {
|
final class PunchPointDetailViewController: BaseViewController {
|
||||||
private let viewModel: PunchPointDetailViewModel
|
private let viewModel: PunchPointDetailViewModel
|
||||||
@ -447,7 +452,192 @@ final class PunchPointDetailViewController: BaseViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 高德地图容器,供打卡点详情与表单复用。
|
/// 高德地图容器,供打卡点详情与表单复用。
|
||||||
/// 打卡点地图视图,封装高德地图 marker、缩放、定位与点击选点行为。
|
/// 打卡点地图视图,模拟器使用 MapKit 提供 marker、缩放、定位与点击选点行为。
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
final class PunchPointMapView: UIView, MKMapViewDelegate {
|
||||||
|
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||||
|
|
||||||
|
private let mapView: MKMapView
|
||||||
|
private var punchPointAnnotation: MKPointAnnotation?
|
||||||
|
private var selectedAnnotation: MKPointAnnotation?
|
||||||
|
private var punchPointImageURL: URL?
|
||||||
|
private var fitWithUserLocation = false
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
mapView = MKMapView(frame: .zero)
|
||||||
|
super.init(frame: frame)
|
||||||
|
mapView.delegate = self
|
||||||
|
mapView.showsUserLocation = true
|
||||||
|
mapView.showsCompass = false
|
||||||
|
mapView.showsScale = false
|
||||||
|
mapView.pointOfInterestFilter = .includingAll
|
||||||
|
addSubview(mapView)
|
||||||
|
mapView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
|
||||||
|
tapGesture.cancelsTouchesInView = false
|
||||||
|
mapView.addGestureRecognizer(tapGesture)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新原打卡点标记,可使用首张图片作为圆角 Marker。
|
||||||
|
func updatePunchPointMarker(
|
||||||
|
coordinate: CLLocationCoordinate2D?,
|
||||||
|
imageURL: String? = nil,
|
||||||
|
fitWithUserLocation: Bool = false
|
||||||
|
) {
|
||||||
|
if let punchPointAnnotation {
|
||||||
|
mapView.removeAnnotation(punchPointAnnotation)
|
||||||
|
self.punchPointAnnotation = nil
|
||||||
|
}
|
||||||
|
punchPointImageURL = imageURL.flatMap(URL.init(string:))
|
||||||
|
self.fitWithUserLocation = fitWithUserLocation
|
||||||
|
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
|
||||||
|
let annotation = MKPointAnnotation()
|
||||||
|
annotation.coordinate = coordinate
|
||||||
|
annotation.title = "punch_point_original"
|
||||||
|
punchPointAnnotation = annotation
|
||||||
|
mapView.addAnnotation(annotation)
|
||||||
|
if fitWithUserLocation {
|
||||||
|
fitPunchPointAndUserLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 兼容仅需更新普通 Marker 的调用。
|
||||||
|
func updateMarker(coordinate: CLLocationCoordinate2D?) {
|
||||||
|
updatePunchPointMarker(coordinate: coordinate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新地图点击产生的新位置 Marker。
|
||||||
|
func updateSelectedMarker(coordinate: CLLocationCoordinate2D?) {
|
||||||
|
if let selectedAnnotation {
|
||||||
|
mapView.removeAnnotation(selectedAnnotation)
|
||||||
|
self.selectedAnnotation = nil
|
||||||
|
}
|
||||||
|
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
|
||||||
|
let annotation = MKPointAnnotation()
|
||||||
|
annotation.coordinate = coordinate
|
||||||
|
annotation.title = "punch_point_selected"
|
||||||
|
selectedAnnotation = annotation
|
||||||
|
mapView.addAnnotation(annotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置地图中心点。
|
||||||
|
func setCenter(_ coordinate: CLLocationCoordinate2D, zoomLevel: Double = 15, animated: Bool = true) {
|
||||||
|
let delta = min(max(0.012 * pow(2, 15 - zoomLevel), 0.0005), 120)
|
||||||
|
let region = MKCoordinateRegion(
|
||||||
|
center: coordinate,
|
||||||
|
span: MKCoordinateSpan(latitudeDelta: delta, longitudeDelta: delta)
|
||||||
|
)
|
||||||
|
mapView.setRegion(region, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 放大地图。
|
||||||
|
func zoomIn() {
|
||||||
|
updateZoom(scale: 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 缩小地图。
|
||||||
|
func zoomOut() {
|
||||||
|
updateZoom(scale: 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 居中到用户当前位置。
|
||||||
|
func centerOnUserLocation() {
|
||||||
|
guard let coordinate = mapView.userLocation.location?.coordinate,
|
||||||
|
CLLocationCoordinate2DIsValid(coordinate),
|
||||||
|
coordinate.latitude != 0 || coordinate.longitude != 0 else { return }
|
||||||
|
setCenter(coordinate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 同时展示原打卡点、用户位置和新选位置。
|
||||||
|
func fitAllMarkers(edgePadding: UIEdgeInsets = UIEdgeInsets(top: 80, left: 56, bottom: 80, right: 56)) {
|
||||||
|
var annotations: [any MKAnnotation] = []
|
||||||
|
if let punchPointAnnotation { annotations.append(punchPointAnnotation) }
|
||||||
|
if let selectedAnnotation { annotations.append(selectedAnnotation) }
|
||||||
|
if mapView.userLocation.location != nil { annotations.append(mapView.userLocation) }
|
||||||
|
guard !annotations.isEmpty else { return }
|
||||||
|
if annotations.count == 1, let annotation = annotations.first {
|
||||||
|
setCenter(annotation.coordinate, zoomLevel: 16)
|
||||||
|
} else {
|
||||||
|
var mapRect = MKMapRect.null
|
||||||
|
for annotation in annotations {
|
||||||
|
let point = MKMapPoint(annotation.coordinate)
|
||||||
|
mapRect = mapRect.union(MKMapRect(
|
||||||
|
origin: point,
|
||||||
|
size: MKMapSize(width: 1, height: 1)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
mapView.setVisibleMapRect(mapRect, edgePadding: edgePadding, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapView(_ mapView: MKMapView, viewFor annotation: any MKAnnotation) -> MKAnnotationView? {
|
||||||
|
if annotation is MKUserLocation { return nil }
|
||||||
|
let isOriginal = (annotation as AnyObject) === punchPointAnnotation
|
||||||
|
let reuseIdentifier = isOriginal ? "PunchPointImageMarker" : "PunchPointSelectedMarker"
|
||||||
|
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
|
||||||
|
?? MKAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
|
||||||
|
annotationView.annotation = annotation
|
||||||
|
annotationView.canShowCallout = false
|
||||||
|
annotationView.centerOffset = CGPoint(x: 0, y: -12)
|
||||||
|
|
||||||
|
if isOriginal, let punchPointImageURL {
|
||||||
|
annotationView.image = UIImage(named: "punch_point_marker")
|
||||||
|
KingfisherManager.shared.retrieveImage(with: punchPointImageURL) { result in
|
||||||
|
guard case let .success(value) = result else { return }
|
||||||
|
Task { @MainActor in
|
||||||
|
guard annotationView.annotation === annotation else { return }
|
||||||
|
annotationView.image = Self.roundedMarkerImage(value.image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
annotationView.image = UIImage(named: "punch_point_marker")
|
||||||
|
}
|
||||||
|
return annotationView
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
|
||||||
|
guard userLocation.location != nil else { return }
|
||||||
|
fitPunchPointAndUserLocation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fitPunchPointAndUserLocation() {
|
||||||
|
guard fitWithUserLocation else { return }
|
||||||
|
fitAllMarkers(edgePadding: UIEdgeInsets(top: 80, left: 50, bottom: 80, right: 50))
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func roundedMarkerImage(_ source: UIImage) -> UIImage {
|
||||||
|
let size = CGSize(width: 40, height: 40)
|
||||||
|
return UIGraphicsImageRenderer(size: size).image { _ in
|
||||||
|
UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 6).addClip()
|
||||||
|
source.draw(in: CGRect(origin: .zero, size: size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func mapTapped(_ gesture: UITapGestureRecognizer) {
|
||||||
|
guard gesture.state == .ended else { return }
|
||||||
|
let point = gesture.location(in: mapView)
|
||||||
|
onMapTap?(mapView.convert(point, toCoordinateFrom: mapView))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateZoom(scale: Double) {
|
||||||
|
let current = mapView.region
|
||||||
|
let span = MKCoordinateSpan(
|
||||||
|
latitudeDelta: min(max(current.span.latitudeDelta * scale, 0.0005), 120),
|
||||||
|
longitudeDelta: min(max(current.span.longitudeDelta * scale, 0.0005), 120)
|
||||||
|
)
|
||||||
|
mapView.setRegion(MKCoordinateRegion(center: current.center, span: span), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
/// 打卡点地图视图,真机封装高德地图 marker、缩放、定位与点击选点行为。
|
||||||
final class PunchPointMapView: UIView, MAMapViewDelegate {
|
final class PunchPointMapView: UIView, MAMapViewDelegate {
|
||||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||||
|
|
||||||
@ -614,6 +804,7 @@ final class PunchPointMapView: UIView, MAMapViewDelegate {
|
|||||||
onMapTap?(poi.coordinate)
|
onMapTap?(poi.coordinate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/// 打卡点地图控制按钮。
|
/// 打卡点地图控制按钮。
|
||||||
/// 地图悬浮控制按钮。
|
/// 地图悬浮控制按钮。
|
||||||
|
|||||||
@ -4,10 +4,15 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
import MAMapKit
|
|
||||||
import SnapKit
|
import SnapKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
import MapKit
|
||||||
|
#else
|
||||||
|
import MAMapKit
|
||||||
|
#endif
|
||||||
|
|
||||||
/// 我的举报列表页,支持按状态筛选并进入举报详情。
|
/// 我的举报列表页,支持按状态筛选并进入举报详情。
|
||||||
final class WildPhotographerReportListViewController: BaseViewController {
|
final class WildPhotographerReportListViewController: BaseViewController {
|
||||||
private let viewModel: WildPhotographerReportListViewModel
|
private let viewModel: WildPhotographerReportListViewModel
|
||||||
@ -1797,6 +1802,49 @@ private final class WildReportDetailMediaThumbnailView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 举报详情页地图预览,绘制简化道路、路径和定位点。
|
/// 举报详情页地图预览,绘制简化道路、路径和定位点。
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
private final class WildReportDetailMapPreviewView: UIView {
|
||||||
|
private let mapView: MKMapView
|
||||||
|
|
||||||
|
init(scenicName: String, detailAddress: String, coordinate: CLLocationCoordinate2D?) {
|
||||||
|
mapView = MKMapView(frame: .zero)
|
||||||
|
super.init(frame: .zero)
|
||||||
|
backgroundColor = AppColor.pageBackgroundSoft
|
||||||
|
layer.cornerRadius = 10
|
||||||
|
clipsToBounds = true
|
||||||
|
|
||||||
|
mapView.showsCompass = false
|
||||||
|
mapView.showsScale = false
|
||||||
|
mapView.isScrollEnabled = false
|
||||||
|
mapView.isZoomEnabled = false
|
||||||
|
mapView.isRotateEnabled = false
|
||||||
|
mapView.isPitchEnabled = false
|
||||||
|
addSubview(mapView)
|
||||||
|
mapView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let coordinate else { return }
|
||||||
|
let annotation = MKPointAnnotation()
|
||||||
|
annotation.coordinate = coordinate
|
||||||
|
annotation.title = scenicName
|
||||||
|
annotation.subtitle = detailAddress
|
||||||
|
mapView.addAnnotation(annotation)
|
||||||
|
mapView.setRegion(
|
||||||
|
MKCoordinateRegion(
|
||||||
|
center: coordinate,
|
||||||
|
span: MKCoordinateSpan(latitudeDelta: 0.006, longitudeDelta: 0.006)
|
||||||
|
),
|
||||||
|
animated: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
private final class WildReportDetailMapPreviewView: UIView {
|
private final class WildReportDetailMapPreviewView: UIView {
|
||||||
private let mapView: MAMapView
|
private let mapView: MAMapView
|
||||||
|
|
||||||
@ -1834,6 +1882,7 @@ private final class WildReportDetailMapPreviewView: UIView {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/// 举报详情页处理进度时间线单行。
|
/// 举报详情页处理进度时间线单行。
|
||||||
private final class WildReportDetailTimelineRowView: UIView {
|
private final class WildReportDetailTimelineRowView: UIView {
|
||||||
|
|||||||
@ -3,19 +3,19 @@
|
|||||||
// suixinkan
|
// suixinkan
|
||||||
//
|
//
|
||||||
|
|
||||||
#if __has_include(<AMapFoundationKit/AMapFoundationKit.h>)
|
#if !TARGET_OS_SIMULATOR && __has_include(<AMapFoundationKit/AMapFoundationKit.h>)
|
||||||
#import <AMapFoundationKit/AMapFoundationKit.h>
|
#import <AMapFoundationKit/AMapFoundationKit.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<AMapLocationKit/AMapLocationKit.h>)
|
#if !TARGET_OS_SIMULATOR && __has_include(<AMapLocationKit/AMapLocationKit.h>)
|
||||||
#import <AMapLocationKit/AMapLocationKit.h>
|
#import <AMapLocationKit/AMapLocationKit.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<MAMapKit/MAMapKit.h>)
|
#if !TARGET_OS_SIMULATOR && __has_include(<MAMapKit/MAMapKit.h>)
|
||||||
#import <MAMapKit/MAMapKit.h>
|
#import <MAMapKit/MAMapKit.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if __has_include(<AMapSearchKit/AMapSearchKit.h>)
|
#if !TARGET_OS_SIMULATOR && __has_include(<AMapSearchKit/AMapSearchKit.h>)
|
||||||
#import <AMapSearchKit/AMapSearchKit.h>
|
#import <AMapSearchKit/AMapSearchKit.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
204
suixinkanTests/CoreLocationProviderTests.swift
Normal file
204
suixinkanTests/CoreLocationProviderTests.swift
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
//
|
||||||
|
// CoreLocationProviderTests.swift
|
||||||
|
// suixinkanTests
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreLocation
|
||||||
|
import XCTest
|
||||||
|
@testable import suixinkan
|
||||||
|
|
||||||
|
/// Core Location fallback 的授权、定位与逆地理编码测试。
|
||||||
|
@MainActor
|
||||||
|
final class CoreLocationProviderTests: XCTestCase {
|
||||||
|
|
||||||
|
func testPrivacyAgreementIsRequiredBeforeLocation() async {
|
||||||
|
let context = makeContext(privacyAccepted: false)
|
||||||
|
|
||||||
|
await assertLocationError(.privacyNotAccepted) {
|
||||||
|
_ = try await context.provider.requestCoordinate()
|
||||||
|
}
|
||||||
|
XCTAssertEqual(context.manager.requestLocationCallCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeniedAuthorizationReturnsPermissionError() async {
|
||||||
|
let context = makeContext(authorizationStatus: .denied)
|
||||||
|
|
||||||
|
await assertLocationError(.permissionDenied) {
|
||||||
|
_ = try await context.provider.requestCoordinate()
|
||||||
|
}
|
||||||
|
XCTAssertEqual(context.manager.requestLocationCallCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNotDeterminedAuthorizationContinuesAfterGrant() async throws {
|
||||||
|
let coordinate = CLLocationCoordinate2D(latitude: 30.2741, longitude: 120.1551)
|
||||||
|
let context = makeContext(
|
||||||
|
authorizationStatus: .notDetermined,
|
||||||
|
requestedAuthorizationStatus: .authorizedWhenInUse,
|
||||||
|
locationResult: .success(CLLocation(
|
||||||
|
latitude: coordinate.latitude,
|
||||||
|
longitude: coordinate.longitude
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
let result = try await context.provider.requestCoordinate(
|
||||||
|
desiredAccuracy: kCLLocationAccuracyHundredMeters
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(context.manager.requestAuthorizationCallCount, 1)
|
||||||
|
XCTAssertEqual(context.manager.requestLocationCallCount, 1)
|
||||||
|
XCTAssertEqual(context.manager.desiredAccuracy, kCLLocationAccuracyHundredMeters)
|
||||||
|
XCTAssertEqual(result.latitude, coordinate.latitude, accuracy: 0.000_001)
|
||||||
|
XCTAssertEqual(result.longitude, coordinate.longitude, accuracy: 0.000_001)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLocationFailureUsesExistingBusinessError() async {
|
||||||
|
let context = makeContext(locationResult: .failure)
|
||||||
|
|
||||||
|
await assertLocationError(.locationFailed) {
|
||||||
|
_ = try await context.provider.requestCoordinate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSnapshotContainsReverseGeocodedAddress() async throws {
|
||||||
|
let context = makeContext(
|
||||||
|
locationResult: .success(CLLocation(latitude: 31.2304, longitude: 121.4737)),
|
||||||
|
geocodedAddress: "上海市黄浦区"
|
||||||
|
)
|
||||||
|
|
||||||
|
let snapshot = try await context.provider.requestSnapshot()
|
||||||
|
|
||||||
|
XCTAssertEqual(snapshot.latitude, 31.2304, accuracy: 0.000_001)
|
||||||
|
XCTAssertEqual(snapshot.longitude, 121.4737, accuracy: 0.000_001)
|
||||||
|
XCTAssertEqual(snapshot.address, "上海市黄浦区")
|
||||||
|
XCTAssertEqual(context.geocoder.requestedCoordinates.count, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSnapshotFallsBackToEmptyAddressWhenGeocoderHasNoResult() async throws {
|
||||||
|
let context = makeContext(
|
||||||
|
locationResult: .success(CLLocation(latitude: 39.9042, longitude: 116.4074)),
|
||||||
|
geocodedAddress: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
let snapshot = try await context.provider.requestSnapshot()
|
||||||
|
|
||||||
|
XCTAssertEqual(snapshot.address, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeContext(
|
||||||
|
privacyAccepted: Bool = true,
|
||||||
|
authorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse,
|
||||||
|
requestedAuthorizationStatus: CLAuthorizationStatus = .authorizedWhenInUse,
|
||||||
|
locationResult: FakeCoreLocationManager.LocationResult = .success(
|
||||||
|
CLLocation(latitude: 39.9042, longitude: 116.4074)
|
||||||
|
),
|
||||||
|
geocodedAddress: String = "北京市东城区"
|
||||||
|
) -> TestContext {
|
||||||
|
let suiteName = "CoreLocationProviderTests.\(UUID().uuidString)"
|
||||||
|
let defaults = UserDefaults(suiteName: suiteName)!
|
||||||
|
defaults.removePersistentDomain(forName: suiteName)
|
||||||
|
let appStore = AppStore(defaults: defaults)
|
||||||
|
appStore.session.privacyAgreementAccepted = privacyAccepted
|
||||||
|
|
||||||
|
let manager = FakeCoreLocationManager(
|
||||||
|
authorizationStatus: authorizationStatus,
|
||||||
|
requestedAuthorizationStatus: requestedAuthorizationStatus,
|
||||||
|
locationResult: locationResult
|
||||||
|
)
|
||||||
|
let geocoder = FakeLocationAddressGeocoder(address: geocodedAddress)
|
||||||
|
let provider = CoreLocationProvider(
|
||||||
|
appStore: appStore,
|
||||||
|
manager: manager,
|
||||||
|
geocoder: geocoder
|
||||||
|
)
|
||||||
|
return TestContext(provider: provider, manager: manager, geocoder: geocoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func assertLocationError(
|
||||||
|
_ expected: LocationProviderError,
|
||||||
|
operation: () async throws -> Void
|
||||||
|
) async {
|
||||||
|
do {
|
||||||
|
try await operation()
|
||||||
|
XCTFail("Expected \(expected)")
|
||||||
|
} catch {
|
||||||
|
XCTAssertEqual(error as? LocationProviderError, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Core Location fallback 测试依赖集合。
|
||||||
|
@MainActor
|
||||||
|
private struct TestContext {
|
||||||
|
let provider: CoreLocationProvider
|
||||||
|
let manager: FakeCoreLocationManager
|
||||||
|
let geocoder: FakeLocationAddressGeocoder
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 可控的 `CLLocationManager` 替身。
|
||||||
|
@MainActor
|
||||||
|
private final class FakeCoreLocationManager: CoreLocationManaging {
|
||||||
|
|
||||||
|
/// 单次定位请求的模拟结果。
|
||||||
|
enum LocationResult {
|
||||||
|
case success(CLLocation)
|
||||||
|
case failure
|
||||||
|
}
|
||||||
|
|
||||||
|
var authorizationStatus: CLAuthorizationStatus
|
||||||
|
var desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest
|
||||||
|
weak var delegate: CLLocationManagerDelegate?
|
||||||
|
private(set) var requestAuthorizationCallCount = 0
|
||||||
|
private(set) var requestLocationCallCount = 0
|
||||||
|
|
||||||
|
private let requestedAuthorizationStatus: CLAuthorizationStatus
|
||||||
|
private let locationResult: LocationResult
|
||||||
|
|
||||||
|
init(
|
||||||
|
authorizationStatus: CLAuthorizationStatus,
|
||||||
|
requestedAuthorizationStatus: CLAuthorizationStatus,
|
||||||
|
locationResult: LocationResult
|
||||||
|
) {
|
||||||
|
self.authorizationStatus = authorizationStatus
|
||||||
|
self.requestedAuthorizationStatus = requestedAuthorizationStatus
|
||||||
|
self.locationResult = locationResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestWhenInUseAuthorization() {
|
||||||
|
requestAuthorizationCallCount += 1
|
||||||
|
authorizationStatus = requestedAuthorizationStatus
|
||||||
|
delegate?.locationManagerDidChangeAuthorization?(CLLocationManager())
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestLocation() {
|
||||||
|
requestLocationCallCount += 1
|
||||||
|
let callbackManager = CLLocationManager()
|
||||||
|
switch locationResult {
|
||||||
|
case let .success(location):
|
||||||
|
delegate?.locationManager?(callbackManager, didUpdateLocations: [location])
|
||||||
|
case .failure:
|
||||||
|
delegate?.locationManager?(
|
||||||
|
callbackManager,
|
||||||
|
didFailWithError: CLError(.locationUnknown)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 可控的逆地理编码替身。
|
||||||
|
@MainActor
|
||||||
|
private final class FakeLocationAddressGeocoder: LocationAddressGeocoding {
|
||||||
|
private(set) var requestedCoordinates: [CLLocationCoordinate2D] = []
|
||||||
|
private let address: String
|
||||||
|
|
||||||
|
init(address: String) {
|
||||||
|
self.address = address
|
||||||
|
}
|
||||||
|
|
||||||
|
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||||
|
requestedCoordinates.append(CLLocationCoordinate2D(
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude
|
||||||
|
))
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@
|
|||||||
// suixinkanTests
|
// suixinkanTests
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CoreLocation
|
||||||
import XCTest
|
import XCTest
|
||||||
@testable import suixinkan
|
@testable import suixinkan
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user