Phase 4.E + Phase 5 + Phase 3.C 七牛上传:代码侧 SDK 接入(SPM 优先 / Vendor 兜底)

选型(CLAUDE.md ADR-006「SPM 优先 + Vendor xcframework 兜底」):
- 微信 → SPM(Tencent 官方 https://github.com/Tencent/WechatOpenSDK-XCFramework);
  项目方下载的 WechatOpenSDK-NoPay.xcframework 已撤出 Vendor,docs/res 内副本作离线备份
- 高德 → Vendor 手动(官方未提供 SPM;Vendor/AMap/{AMapFoundationKit,AMapLocationKit}.framework
  随仓库分发,fat .framework 同时含 x86_64 + arm64,Do Not Embed)
- 七牛 → SPM(https://github.com/qiniu/objc-sdk v8.9.x)

代码侧(全部 #if canImport 守卫,Xcode 加 SDK 前编译为 no-op):
- Source/SDK/WeChat/WeChatSDK.swift:WXApi.registerApp + handleOpenURL(沿用 msext AppID
  wx586a9b321e56efb7,universalLink 空字符串走非 ULAPI 路径)
- Source/SDK/WeChat/WeChatManager.swift:WXApiDelegate + state UUID 配对的 authorize +
  FIFO 串行 share async/await wrapper
- Source/Login/WeChatAuth.swift:客户端直拼 sns/oauth2/access_token + sns/userinfo →
  7 字段(Province 大写 P / city 经 danbian 去单引号),沿用 msext 同款路径
- Source/SDK/AMap/AMapWrapper.swift:iOS 14+ 隐私合规 3 步 + apiKey 注入
- Source/Location/LocationService.swift:actor + AMapLocationManager 异步包装 + 9 字段
- Source/Network/QiniuConfig.swift:4 项常量沿用 msext(AccessKey/SecretKey/Bucket/Domain)
- Source/Network/QiniuTokenSigner.swift:纯 Swift CryptoKit HMAC-SHA1 + Base64URL 自签
  token(与 msext QiniuManager.m:200-230 等价,不依赖 Qiniu SDK 工作)
- Source/Network/QiniuUploader.swift:actor 包 QNUploadManager async/await

Handler 升级:
- AccreditLoginHandler:拉起授权 → 反向 callback sharelogin 7 字段
- WechatShare:真实链接分享(type=2/3 截图待 Phase 4.F)
- StartLocationHandler:真实定位 → 9 字段(latitude/longitude string, province 小写 p)

生命周期:
- AppDelegate.didFinishLaunchingWithOptions:WeChatSDK.register + AMapWrapper.bootstrap
- SceneDelegate.openURLContexts:WeChatSDK.handleOpenURL 接入回调
- BackGameDataHandler:子游戏 backgameData pop 时 WXApi.delegate=nil + LocationService.stop()

Info.plist:CFBundleURLTypes 加 wx586a9b321e56efb7;LSApplicationQueriesSchemes 追加
weixin/weixinULAPI/weixinURLParamsAPI;NSLocationWhenInUseUsageDescription +
NSMicrophoneUsageDescription(gamehallname 中文文案)。

.gitignore:排除 docs/res/{AMap_iOS_Loc_ALL,objc-sdk-8.9.2}/(235MB 项目方下载副本,
已走 SPM/Vendor 接入不需要副本)。

文档:docs/SDK-Integration-Guide.md 重写 §A 用户手动 Xcode UI 三步(SPM × 2 +
Add Files × 2);Vendor/{AMap,WechatSDK}/README.md 记录各自接入路径选型。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-22 22:22:23 +08:00
co-authored by Claude Opus 4.7
parent a2baf742b6
commit 1d7e258130
47 changed files with 3226 additions and 273 deletions
+6
View File
@@ -91,3 +91,9 @@ Secrets/
*.bak *.bak
*.tmp *.tmp
*~ *~
# ===== docs/res 大型素材 / SDK 下载副本 =====
# docs/res 是项目维护者的私人素材池(CLAUDE.md 约定),整体 git 跟踪但
# 排除特别大的 SDK 下载副本(已经走 SPM / Vendor 接入,不需要副本):
docs/res/AMap_iOS_Loc_ALL/
docs/res/objc-sdk-8.9.2/
@@ -0,0 +1,260 @@
//===--- AMapDemangleNodes.def - Demangling Tree Metaprogramming ----*- C++ -*-===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
//
// This file defines macros useful for macro-metaprogramming with nodes in
// the demangling tree.
//
//===----------------------------------------------------------------------===//
/// NODE(ID)
/// The node's enumerator value is Node::Kind::ID.
/// CONTEXT_NODE(ID)
/// Nodes that can serve as contexts for other entities.
#ifndef CONTEXT_NODE
#define CONTEXT_NODE(ID) NODE(ID)
#endif
CONTEXT_NODE(Allocator)
CONTEXT_NODE(AnonymousContext)
NODE(AnyProtocolConformanceList)
NODE(ArgumentTuple)
NODE(AssociatedType)
NODE(AssociatedTypeRef)
NODE(AssociatedTypeMetadataAccessor)
NODE(DefaultAssociatedTypeMetadataAccessor)
NODE(AssociatedTypeWitnessTableAccessor)
NODE(BaseWitnessTableAccessor)
NODE(AutoClosureType)
NODE(BoundGenericClass)
NODE(BoundGenericEnum)
NODE(BoundGenericStructure)
NODE(BoundGenericProtocol)
NODE(BoundGenericOtherNominalType)
NODE(BoundGenericTypeAlias)
NODE(BoundGenericFunction)
NODE(BuiltinTypeName)
NODE(CFunctionPointer)
CONTEXT_NODE(Class)
NODE(ClassMetadataBaseOffset)
NODE(ConcreteProtocolConformance)
CONTEXT_NODE(Constructor)
NODE(CoroutineContinuationPrototype)
CONTEXT_NODE(Deallocator)
NODE(DeclContext)
CONTEXT_NODE(DefaultArgumentInitializer)
NODE(DependentAssociatedConformance)
NODE(DependentAssociatedTypeRef)
NODE(DependentGenericConformanceRequirement)
NODE(DependentGenericParamCount)
NODE(DependentGenericParamType)
NODE(DependentGenericSameTypeRequirement)
NODE(DependentGenericLayoutRequirement)
NODE(DependentGenericSignature)
NODE(DependentGenericType)
NODE(DependentMemberType)
NODE(DependentPseudogenericSignature)
NODE(DependentProtocolConformanceRoot)
NODE(DependentProtocolConformanceInherited)
NODE(DependentProtocolConformanceAssociated)
CONTEXT_NODE(Destructor)
CONTEXT_NODE(DidSet)
NODE(Directness)
NODE(DynamicAttribute)
NODE(DirectMethodReferenceAttribute)
NODE(DynamicSelf)
NODE(DynamicallyReplaceableFunctionImpl)
NODE(DynamicallyReplaceableFunctionKey)
NODE(DynamicallyReplaceableFunctionVar)
CONTEXT_NODE(Enum)
NODE(EnumCase)
NODE(ErrorType)
NODE(EscapingAutoClosureType)
NODE(NoEscapeFunctionType)
NODE(ExistentialMetatype)
CONTEXT_NODE(ExplicitClosure)
CONTEXT_NODE(Extension)
NODE(FieldOffset)
NODE(FullTypeMetadata)
CONTEXT_NODE(Function)
NODE(FunctionSignatureSpecialization)
NODE(FunctionSignatureSpecializationParam)
NODE(FunctionSignatureSpecializationParamKind)
NODE(FunctionSignatureSpecializationParamPayload)
NODE(FunctionType)
NODE(GenericPartialSpecialization)
NODE(GenericPartialSpecializationNotReAbstracted)
NODE(GenericProtocolWitnessTable)
NODE(GenericProtocolWitnessTableInstantiationFunction)
NODE(ResilientProtocolWitnessTable)
NODE(GenericSpecialization)
NODE(GenericSpecializationNotReAbstracted)
NODE(GenericSpecializationParam)
NODE(InlinedGenericFunction)
NODE(GenericTypeMetadataPattern)
CONTEXT_NODE(Getter)
NODE(Global)
CONTEXT_NODE(GlobalGetter)
NODE(Identifier)
NODE(Index)
CONTEXT_NODE(IVarInitializer)
CONTEXT_NODE(IVarDestroyer)
NODE(ImplEscaping)
NODE(ImplConvention)
NODE(ImplFunctionAttribute)
NODE(ImplFunctionType)
CONTEXT_NODE(ImplicitClosure)
NODE(ImplParameter)
NODE(ImplResult)
NODE(ImplErrorResult)
NODE(InOut)
NODE(InfixOperator)
CONTEXT_NODE(Initializer)
NODE(KeyPathGetterThunkHelper)
NODE(KeyPathSetterThunkHelper)
NODE(KeyPathEqualsThunkHelper)
NODE(KeyPathHashThunkHelper)
NODE(LazyProtocolWitnessTableAccessor)
NODE(LazyProtocolWitnessTableCacheVariable)
NODE(LocalDeclName)
CONTEXT_NODE(MaterializeForSet)
NODE(MergedFunction)
NODE(Metatype)
NODE(MetatypeRepresentation)
NODE(Metaclass)
NODE(MethodLookupFunction)
NODE(ObjCMetadataUpdateFunction)
CONTEXT_NODE(ModifyAccessor)
CONTEXT_NODE(Module)
CONTEXT_NODE(NativeOwningAddressor)
CONTEXT_NODE(NativeOwningMutableAddressor)
CONTEXT_NODE(NativePinningAddressor)
CONTEXT_NODE(NativePinningMutableAddressor)
NODE(NominalTypeDescriptor)
NODE(NonObjCAttribute)
NODE(Number)
NODE(ObjCAttribute)
NODE(ObjCBlock)
CONTEXT_NODE(OtherNominalType)
CONTEXT_NODE(OwningAddressor)
CONTEXT_NODE(OwningMutableAddressor)
NODE(PartialApplyForwarder)
NODE(PartialApplyObjCForwarder)
NODE(PostfixOperator)
NODE(PrefixOperator)
NODE(PrivateDeclName)
NODE(PropertyDescriptor)
CONTEXT_NODE(Protocol)
CONTEXT_NODE(ProtocolSymbolicReference)
NODE(ProtocolConformance)
NODE(ProtocolConformanceRefInTypeModule)
NODE(ProtocolConformanceRefInProtocolModule)
NODE(ProtocolConformanceRefInOtherModule)
NODE(ProtocolDescriptor)
NODE(ProtocolConformanceDescriptor)
NODE(ProtocolList)
NODE(ProtocolListWithClass)
NODE(ProtocolListWithAnyObject)
NODE(ProtocolSelfConformanceDescriptor)
NODE(ProtocolSelfConformanceWitness)
NODE(ProtocolSelfConformanceWitnessTable)
NODE(ProtocolWitness)
NODE(ProtocolWitnessTable)
NODE(ProtocolWitnessTableAccessor)
NODE(ProtocolWitnessTablePattern)
NODE(ReabstractionThunk)
NODE(ReabstractionThunkHelper)
CONTEXT_NODE(ReadAccessor)
NODE(RelatedEntityDeclName)
NODE(RetroactiveConformance)
NODE(ReturnType)
NODE(Shared)
NODE(Owned)
NODE(SILBoxType)
NODE(SILBoxTypeWithLayout)
NODE(SILBoxLayout)
NODE(SILBoxMutableField)
NODE(SILBoxImmutableField)
CONTEXT_NODE(Setter)
NODE(SpecializationPassID)
NODE(IsSerialized)
CONTEXT_NODE(Static)
CONTEXT_NODE(Structure)
CONTEXT_NODE(Subscript)
NODE(Suffix)
NODE(ThinFunctionType)
NODE(Tuple)
NODE(TupleElement)
NODE(TupleElementName)
NODE(Type)
CONTEXT_NODE(TypeSymbolicReference)
CONTEXT_NODE(TypeAlias)
NODE(TypeList)
NODE(TypeMangling)
NODE(TypeMetadata)
NODE(TypeMetadataAccessFunction)
NODE(TypeMetadataCompletionFunction)
NODE(TypeMetadataInstantiationCache)
NODE(TypeMetadataInstantiationFunction)
NODE(TypeMetadataSingletonInitializationCache)
NODE(TypeMetadataLazyCache)
NODE(UncurriedFunctionType)
#define REF_STORAGE(Name, ...) NODE(Name)
#include "AMapReferenceStorage.def"
CONTEXT_NODE(UnsafeAddressor)
CONTEXT_NODE(UnsafeMutableAddressor)
NODE(ValueWitness)
NODE(ValueWitnessTable)
CONTEXT_NODE(Variable)
NODE(VTableThunk)
NODE(VTableAttribute) // note: old mangling only
CONTEXT_NODE(WillSet)
NODE(ReflectionMetadataBuiltinDescriptor)
NODE(ReflectionMetadataFieldDescriptor)
NODE(ReflectionMetadataAssocTypeDescriptor)
NODE(ReflectionMetadataSuperclassDescriptor)
NODE(GenericTypeParamDecl)
NODE(CurryThunk)
NODE(DispatchThunk)
NODE(MethodDescriptor)
NODE(ProtocolRequirementsBaseDescriptor)
NODE(AssociatedConformanceDescriptor)
NODE(DefaultAssociatedConformanceAccessor)
NODE(BaseConformanceDescriptor)
NODE(AssociatedTypeDescriptor)
NODE(ThrowsAnnotation)
NODE(EmptyList)
NODE(FirstElementMarker)
NODE(VariadicMarker)
NODE(OutlinedBridgedMethod)
NODE(OutlinedCopy)
NODE(OutlinedConsume)
NODE(OutlinedRetain)
NODE(OutlinedRelease)
NODE(OutlinedInitializeWithTake)
NODE(OutlinedInitializeWithCopy)
NODE(OutlinedAssignWithTake)
NODE(OutlinedAssignWithCopy)
NODE(OutlinedDestroy)
NODE(OutlinedVariable)
NODE(AssocTypePath)
NODE(LabelList)
NODE(ModuleDescriptor)
NODE(ExtensionDescriptor)
NODE(AnonymousDescriptor)
NODE(AssociatedTypeGenericParamRef)
NODE(SugaredOptional)
NODE(SugaredArray)
NODE(SugaredDictionary)
NODE(SugaredParen)
#undef CONTEXT_NODE
#undef NODE
Binary file not shown.
@@ -0,0 +1,197 @@
//===--- AMapReferenceStorage.def - Non-default reference storage ---*- C++ -*-===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2018 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
//
// This file defines non-default reference storage kind macros used for
// macro-metaprogramming.
//
//===----------------------------------------------------------------------===//
/// There are two fundamental reference storage types: checked and unchecked.
/// Checked storage types have runtime enforced correctness.
/// Unchecked storage types have no runtime enforced correctness.
///
/// Checked reference storage types are also subcategorized by loadability.
/// * Always loadable: The compiler may move the reference or use registers.
/// * Never loadable: The runtime (etc) tracks the address of the reference.
/// * Sometimes loadable: If the reference is a native object, then it is
/// always loadable. Otherwise fall back to never loadable semantics, a.k.a.
/// "address only".
///
/// Unchecked reference storage types are always loadable.
///
/// The primary macros therefore are:
/// * ALWAYS_LOADABLE_CHECKED_REF_STORAGE
/// * SOMETIMES_LOADABLE_CHECKED_REF_STORAGE
/// * NEVER_LOADABLE_CHECKED_REF_STORAGE
/// * UNCHECKED_REF_STORAGE
///
/// Helper macros include:
/// * CHECKED_REF_STORAGE -- Any checked reference storage type. Specifically
/// "always", "sometimes", and "never" -- but not "unchecked".
/// * LOADABLE_REF_STORAGE -- Any loadable reference storage type. Specifically
/// "always", "sometimes", and "unchecked" -- but not "never".
/// * ALWAYS_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE -- self describing.
/// * NEVER_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE -- self describing.
///
/// SUBSYSTEMS NOTES
///
/// In general, reference storage types are barely visible in the user facing
/// type system and therefore AST clients above SIL can get away with
/// just REF_STORAGE, or CHECKED_REF_STORAGE with UNCHECKED_REF_STORAGE.
///
/// When it comes to SIL aware AST clients, loadability matters. The best way
/// to understand how the helper macros are used is to look at SILNodes.def.
/// What follows is a short -- possibly not up to date -- summary:
///
/// UNCHECKED_REF_STORAGE
/// Name##RetainValueInst
/// Name##ReleaseValueInst
/// LOADABLE_REF_STORAGE
/// Ref*ToNameInst
/// Name*ToRefInst
/// NEVER_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE
/// Load##Name##Inst
/// Store##Name##Inst
/// ALWAYS_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE
/// Copy##Name##ValueInst
/// StrongRetain##Name##Inst
/// Name##RetainInst
/// Name##ReleaseInst
///
/// After helper macro expansion:
///
/// UNCHECKED_REF_STORAGE
/// Ref*ToNameInst
/// Name*ToRefInst
/// Name##RetainValueInst
/// Name##ReleaseValueInst
/// ALWAYS_LOADABLE_CHECKED_REF_STORAGE
/// Ref*ToNameInst
/// Name*ToRefInst
/// Copy##Name##ValueInst
/// StrongRetain##Name##Inst
/// Name##RetainInst
/// Name##ReleaseInst
/// SOMETIMES_LOADABLE_CHECKED_REF_STORAGE
/// Ref*ToNameInst
/// Name*ToRefInst
/// Load##Name##Inst
/// Store##Name##Inst
/// Copy##Name##ValueInst
/// StrongRetain##Name##Inst
/// Name##RetainInst
/// Name##ReleaseInst
/// NEVER_LOADABLE_CHECKED_REF_STORAGE
/// Load##Name##Inst
/// Store##Name##Inst
///
/// Finally, a note about IRGen: TypeInfos need to be created per reference
/// storage type, and SOMETIMES_LOADABLE_CHECKED_REF_STORAGE needs *two*
/// TypeInfos to be created. One for the loadable scenario, and one for the
/// address-only scenario.
#ifndef REF_STORAGE
#define REF_STORAGE(Name, name, NAME)
#endif
#ifdef ALWAYS_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE
#if defined(ALWAYS_LOADABLE_CHECKED_REF_STORAGE) || \
defined(SOMETIMES_LOADABLE_CHECKED_REF_STORAGE)
#error Overlapping meta-programming macros
#endif
#define ALWAYS_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME) \
ALWAYS_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME)
#define SOMETIMES_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME) \
ALWAYS_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME)
#endif
#ifdef NEVER_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE
#if defined(NEVER_LOADABLE_CHECKED_REF_STORAGE) || \
defined(SOMETIMES_LOADABLE_CHECKED_REF_STORAGE)
#error Overlapping meta-programming macros
#endif
#define NEVER_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME) \
NEVER_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME)
#define SOMETIMES_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME) \
NEVER_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME)
#endif
#ifdef LOADABLE_REF_STORAGE
#if defined(ALWAYS_LOADABLE_CHECKED_REF_STORAGE) || \
defined(SOMETIMES_LOADABLE_CHECKED_REF_STORAGE) || \
defined(UNCHECKED_REF_STORAGE)
#error Overlapping meta-programming macros
#endif
#define ALWAYS_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME) \
LOADABLE_REF_STORAGE(Name, name, NAME)
#define SOMETIMES_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME) \
LOADABLE_REF_STORAGE(Name, name, NAME)
#define UNCHECKED_REF_STORAGE(Name, name, NAME) \
LOADABLE_REF_STORAGE(Name, name, NAME)
#endif
#ifdef CHECKED_REF_STORAGE
#if defined(SOMETIMES_LOADABLE_CHECKED_REF_STORAGE) || \
defined(ALWAYS_LOADABLE_CHECKED_REF_STORAGE) || \
defined(NEVER_LOADABLE_CHECKED_REF_STORAGE)
#error Overlapping meta-programming macros
#endif
#define ALWAYS_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME) \
CHECKED_REF_STORAGE(Name, name, NAME)
#define SOMETIMES_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME) \
CHECKED_REF_STORAGE(Name, name, NAME)
#define NEVER_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME) \
CHECKED_REF_STORAGE(Name, name, NAME)
#endif
#ifndef NEVER_LOADABLE_CHECKED_REF_STORAGE
#define NEVER_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME) \
REF_STORAGE(Name, name, NAME)
#endif
#ifndef SOMETIMES_LOADABLE_CHECKED_REF_STORAGE
#define SOMETIMES_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME) \
REF_STORAGE(Name, name, NAME)
#endif
#ifndef ALWAYS_LOADABLE_CHECKED_REF_STORAGE
#define ALWAYS_LOADABLE_CHECKED_REF_STORAGE(Name, name, NAME) \
REF_STORAGE(Name, name, NAME)
#endif
#ifndef UNCHECKED_REF_STORAGE
#define UNCHECKED_REF_STORAGE(Name, name, NAME) \
REF_STORAGE(Name, name, NAME)
#endif
#ifndef REF_STORAGE_RANGE
#define REF_STORAGE_RANGE(First, Last)
#endif
// NOTE: You will need to update ReferenceOwnership in ModuleFormat.h.
//NEVER_LOADABLE_CHECKED_REF_STORAGE(Weak, weak, WEAK)
//SOMETIMES_LOADABLE_CHECKED_REF_STORAGE(Unowned, unowned, UNOWNED)
//UNCHECKED_REF_STORAGE(Unmanaged, unmanaged, UNMANAGED)
REF_STORAGE_RANGE(Weak, Unmanaged)
#undef REF_STORAGE
#undef NEVER_LOADABLE_CHECKED_REF_STORAGE
#undef ALWAYS_LOADABLE_CHECKED_REF_STORAGE
#undef SOMETIMES_LOADABLE_CHECKED_REF_STORAGE
#undef UNCHECKED_REF_STORAGE
#undef REF_STORAGE_RANGE
#undef ALWAYS_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE
#undef NEVER_OR_SOMETIMES_LOADABLE_CHECKED_REF_STORAGE
#undef LOADABLE_REF_STORAGE
#undef CHECKED_REF_STORAGE
@@ -0,0 +1,68 @@
//===--- AMapStandardTypesMangling - Mangling Metaprogramming ---*- C++ -*-===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
/// STANDARD_TYPE(KIND, MANGLING, TYPENAME)
/// The 1-character MANGLING for a known TYPENAME of KIND.
STANDARD_TYPE(Structure, A, AutoreleasingUnsafeMutablePointer)
STANDARD_TYPE(Structure, a, Array)
STANDARD_TYPE(Structure, b, Bool)
STANDARD_TYPE(Structure, c, UnicodeScalar)
STANDARD_TYPE(Structure, D, Dictionary)
STANDARD_TYPE(Structure, d, Double)
STANDARD_TYPE(Structure, f, Float)
STANDARD_TYPE(Structure, h, Set)
STANDARD_TYPE(Structure, I, DefaultIndices)
STANDARD_TYPE(Structure, i, Int)
STANDARD_TYPE(Structure, J, Character)
STANDARD_TYPE(Structure, N, ClosedRange)
STANDARD_TYPE(Structure, n, Range)
STANDARD_TYPE(Structure, O, ObjectIdentifier)
STANDARD_TYPE(Structure, P, UnsafePointer)
STANDARD_TYPE(Structure, p, UnsafeMutablePointer)
STANDARD_TYPE(Structure, R, UnsafeBufferPointer)
STANDARD_TYPE(Structure, r, UnsafeMutableBufferPointer)
STANDARD_TYPE(Structure, S, String)
STANDARD_TYPE(Structure, s, Substring)
STANDARD_TYPE(Structure, u, UInt)
STANDARD_TYPE(Structure, V, UnsafeRawPointer)
STANDARD_TYPE(Structure, v, UnsafeMutableRawPointer)
STANDARD_TYPE(Structure, W, UnsafeRawBufferPointer)
STANDARD_TYPE(Structure, w, UnsafeMutableRawBufferPointer)
STANDARD_TYPE(Enum, q, Optional)
STANDARD_TYPE(Protocol, B, BinaryFloatingPoint)
STANDARD_TYPE(Protocol, E, Encodable)
STANDARD_TYPE(Protocol, e, Decodable)
STANDARD_TYPE(Protocol, F, FloatingPoint)
STANDARD_TYPE(Protocol, G, RandomNumberGenerator)
STANDARD_TYPE(Protocol, H, Hashable)
STANDARD_TYPE(Protocol, j, Numeric)
STANDARD_TYPE(Protocol, K, BidirectionalCollection)
STANDARD_TYPE(Protocol, k, RandomAccessCollection)
STANDARD_TYPE(Protocol, L, Comparable)
STANDARD_TYPE(Protocol, l, Collection)
STANDARD_TYPE(Protocol, M, MutableCollection)
STANDARD_TYPE(Protocol, m, RangeReplaceableCollection)
STANDARD_TYPE(Protocol, Q, Equatable)
STANDARD_TYPE(Protocol, T, Sequence)
STANDARD_TYPE(Protocol, t, IteratorProtocol)
STANDARD_TYPE(Protocol, U, UnsignedInteger)
STANDARD_TYPE(Protocol, X, RangeExpression)
STANDARD_TYPE(Protocol, x, Strideable)
STANDARD_TYPE(Protocol, Y, RawRepresentable)
STANDARD_TYPE(Protocol, y, StringProtocol)
STANDARD_TYPE(Protocol, Z, SignedInteger)
STANDARD_TYPE(Protocol, z, BinaryInteger)
#undef STANDARD_TYPE
@@ -0,0 +1,74 @@
//
// AMapFoundationConst.h
// AMapFoundationKit
//
// Created by JL on 2019/7/22.
// Copyright © 2019 Amap.com. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NSInteger AMapFoundationNSErrorCode;
//ErrorDomain:文件不存在 错误码:-555555
extern NSErrorDomain const AMapFoundationNSErrorFileDonotExist;
extern AMapFoundationNSErrorCode const AMapFoundationNSErrorFileDonotExistCode;
//ErrorDomain:文件路径不合法 错误码:-555556
extern NSErrorDomain const AMapFoundationNSErrorFilePathInvaild;
extern AMapFoundationNSErrorCode const AMapFoundationNSErrorFilePathInvaildCode;
//ErrorDomain:指定类型的日志文件不存在 错误码:-555557
extern NSErrorDomain const AMapFoundationNSErrorTypeLogDonotExist;
extern AMapFoundationNSErrorCode const AMapFoundationNSErrorTypeLogDonotExistCode;
//ErrorDomain:待上传的数据为空(可能是组装/压缩时出错) 错误码:-555558
extern NSErrorDomain const AMapFoundationNSErrorUploadDataIsEmpty;
extern AMapFoundationNSErrorCode const AMapFoundationNSErrorUploadDataIsEmptyCode;
//ErrorDomain:参数错误 错误码:-444444
extern NSErrorDomain const AMapFoundationNSErrorParametersInvalid;
extern AMapFoundationNSErrorCode const AMapFoundationNSErrorParametersInvalidCode;
extern NSErrorDomain const AMapFoundationNSErrorCloudConfigDisable;
extern AMapFoundationNSErrorCode const AMapFoundationNSErrorCloudConfigDisableCode;
extern NSErrorDomain const AMapFoundationNSErrorNetworkUnusable;
extern AMapFoundationNSErrorCode const AMapFoundationNSErrorNetworkUnusableCode;
extern NSErrorDomain const AMapFoundationNSErrorCurrentworkIsRunning;
extern AMapFoundationNSErrorCode const AMapFoundationNSErrorCurrentworkIsRunningCode;
extern NSErrorDomain const AMapFoundationNSErrorCurrentUploadSizeHaveExcess;
extern AMapFoundationNSErrorCode const AMapFoundationNSErrorCurrentUploadSizeHaveExcessCode;
extern NSErrorDomain const AMapFoundationErrorPrivacyShowUnknow;
extern AMapFoundationNSErrorCode const AMapFoundationErrorPrivacyShowUnknowCode;
extern NSErrorDomain const AMapFoundationErrorPrivacyShowNoShow;
extern AMapFoundationNSErrorCode const AMapFoundationErrorPrivacyShowNoShowCode;
extern NSErrorDomain const AMapFoundationErrorPrivacyInfoUnknow;
extern AMapFoundationNSErrorCode const AMapFoundationErrorPrivacyInfoUnknowCode;
extern NSErrorDomain const AMapFoundationErrorPrivacyInfoNotContain;
extern AMapFoundationNSErrorCode const AMapFoundationErrorPrivacyInfoNotContainCode;
extern NSErrorDomain const AMapFoundationErrorPrivacyAgreeUnknow;
extern AMapFoundationNSErrorCode const AMapFoundationErrorPrivacyAgreeUnknowCode;
extern NSErrorDomain const AMapFoundationErrorPrivacyAgreeNotAgreee;
extern AMapFoundationNSErrorCode const AMapFoundationErrorPrivacyAgreeNotAgreeeCode;
extern NSErrorDomain const AMapFoundationErrorBikeLicenseDontAuth;
extern AMapFoundationNSErrorCode const AMapFoundationErrorBikeLicenseDontAuthCode;
extern NSErrorDomain const AMapFoundationErrorInvaildUserKey;
extern AMapFoundationNSErrorCode const AMapFoundationErrorInvaildUserKeyCode;
NS_ASSUME_NONNULL_END
@@ -0,0 +1,20 @@
//
// AMapFoundationKit.h
// AMapFoundationKit
//
// Created by xiaoming han on 15/10/28.
// Copyright © 2015年 Amap. All rights reserved.
//
#import <AMapFoundationKit/AMapFoundationVersion.h>
#import <AMapFoundationKit/AMapServices.h>
#import <AMapFoundationKit/AMapURLSearchConfig.h>
#import <AMapFoundationKit/AMapURLSearchType.h>
#import <AMapFoundationKit/AMapURLSearch.h>
#import <AMapFoundationKit/AMapFoundationConst.h>
#import <AMapFoundationKit/AMapUtility.h>
#if __has_include(<AMapFoundationKit/AMapLicenseManager.h>)
#import <AMapFoundationKit/AMapLicenseManager.h>
#endif
@@ -0,0 +1,19 @@
//
// AMapFoundationVersion.h
// AMapFoundation
//
// Created by xiaoming han on 15/10/26.
// Copyright © 2015年 Amap. All rights reserved.
//
#import <Foundation/Foundation.h>
#ifndef AMapFoundationVersion_h
#define AMapFoundationVersion_h
#define AMapFoundationVersionNumber 10900
FOUNDATION_EXTERN NSString * const AMapFoundationVersion;
FOUNDATION_EXTERN NSString * const AMapFoundationName;
#endif /* AMapFoundationVersion_h */
@@ -0,0 +1,175 @@
//
// AMapSearchServices.h
// AMapSearchKit
//
// Created by xiaoming han on 15/6/18.
// Copyright (c) 2015年 xiaoming han. All rights reserved.
//
#import <Foundation/Foundation.h>
//语言类型 language type
typedef NS_ENUM(int, AMapRegionLanguageType)
{
/// 简体中文 (Simplified Chinese)
AMapRegionLanguageTypeZhHans = 0,
/// 香港繁体中文 (Traditional Chinese - Hong Kong)
AMapRegionLanguageTypeZhHantHk,
/// 英文 (English)
AMapRegionLanguageTypeEn,
/// 西班牙语 (Spanish)
AMapRegionLanguageTypeEs,
/// 葡萄牙语 (Portuguese)
AMapRegionLanguageTypePt,
/// 法语 (French)
AMapRegionLanguageTypeFr,
/// 德语 (German)
AMapRegionLanguageTypeDe,
/// 泰语 (Thai)
AMapRegionLanguageTypeTh,
/// 日语 (Japanese)
AMapRegionLanguageTypeJa,
/// 韩语 (Korean)
AMapRegionLanguageTypeKo,
/// 阿拉伯语 (Arabic)
AMapRegionLanguageTypeAr,
/// 土耳其语 (Turkish)
AMapRegionLanguageTypeTr,
/// 希伯来语 (Hebrew)
AMapRegionLanguageTypeHe,
/// 意大利语 (Italian)
AMapRegionLanguageTypeIt,
/// 俄语 (Russian)
AMapRegionLanguageTypeRu,
/// 马来语 (Malay)
AMapRegionLanguageTypeMs,
/// 印尼语 (Indonesian)
AMapRegionLanguageTypeId,
/// 越南语 (Vietnamese)
AMapRegionLanguageTypeVi,
/// 波兰语 (Polish)
AMapRegionLanguageTypePl,
/// 捷克语 (Czech)
AMapRegionLanguageTypeCs,
/// 乌克兰语 (Ukrainian)
AMapRegionLanguageTypeUk,
/// 阿塞拜疆语 (Azerbaijani)
AMapRegionLanguageTypeAz,
/// 最大值标识 (Max Value Marker)
AMapRegionLanguageTypeMax
};
/**
* 是否为海外用户...海外用户,SDK内部会屏蔽一些操作 默认为NO.
* @warning AMapServices初始化之前,设置才能生效
*/
extern BOOL _amapLocationOverseas;
// 显示隐私弹窗状态 -1: unknow , 0 : 未显示 1 已显示
typedef NS_ENUM(NSInteger, AMapPrivacyShowStatus)
{
AMapPrivacyShowStatusUnknow = -1,
AMapPrivacyShowStatusNotShow = 0,
AMapPrivacyShowStatusDidShow = 1,
};
// RTL状态 0: 未设置,跟随全局 , 1 : 关闭 , 2 : 强制开启
typedef NS_ENUM(NSInteger, AMapAttributeRTLState)
{
AMapAttributeRTLStateDefault = 0,
AMapAttributeRTLStateDisabled,
AMapAttributeRTLStateEnabled
};
// 集成SDK隐私信息状态 -1: unknow , 0 : 未集成 1 已集成
typedef NS_ENUM(NSInteger, AMapPrivacyInfoStatus)
{
AMapPrivacyInfoStatusUnknow = -1,
AMapPrivacyInfoStatusNotContain = 0,
AMapPrivacyInfoStatusDidContain = 1,
};
// 用户同意隐私状态 -1: unknow , 0 : 未同意 1 已同意
typedef NS_ENUM(NSInteger, AMapPrivacyAgreeStatus)
{
AMapPrivacyAgreeStatusUnknow = -1,
AMapPrivacyAgreeStatusNotAgree = 0,
AMapPrivacyAgreeStatusDidAgree = 1,
};
///高德SDK服务类
@interface AMapServices : NSObject
/**
* @brief 获取单例
*/
+ (AMapServices *)sharedServices;
///语言类型属性
///language type property
@property (nonatomic, assign) AMapRegionLanguageType regionLanguageType;
///国家代码属性
///country code property
@property (nonatomic, copy) NSString *countryCode;
///APIkey。设置key,需要在高德官网控制台绑定对应的bundleid。
@property (nonatomic, copy) NSString *apiKey;
///是否开启HTTPS,从1.3.3版本开始默认为YES。
@property (nonatomic, assign) BOOL enableHTTPS;
///是否启用崩溃日志上传。默认为YES, 只有在真机上设置有效。\n开启崩溃日志上传有助于我们更好的了解SDK的状况,可以帮助我们持续优化和改进SDK。需要注意的是,SDK内部是通过设置NSUncaughtExceptionHandler来捕获异常的,如果您的APP中使用了其他收集崩溃日志的SDK,或者自己有设置NSUncaughtExceptionHandler的话,请保证 AMapServices 的初始化是在其他设置NSUncaughtExceptionHandler操作之后进行的,我们的handler会再处理完异常后调用前一次设置的handler,保证之前设置的handler会被执行。
@property (nonatomic, assign) BOOL crashReportEnabled __attribute__((deprecated("从v1.5.7开始废弃,调用无任何作用")));
///设备标识,取自idfv。用于排查问题时提供。
@property (nonatomic, readonly) NSString *identifier;
///用户是否同意数据用于安全保障。默认为YES。since 1.8.7
///Whether the user agrees to use data for security assurance. Default: YES. Since 1.8.7
@property (nonatomic, assign) BOOL securityAgree;
///用户是否同意数据用于统计分析。默认为YES。since 1.8.7
///Whether the user agrees to use data for statistical analysis. Default: YES. Since 1.8.7
@property (nonatomic, assign) BOOL analysisAgree;
///当前位置经度。since 1.8.7
///Current location longitude. Since 1.8.7
@property (nonatomic, assign) double longitude;
///当前位置纬度。since 1.8.7
///Current location latitude. Since 1.8.7
@property (nonatomic, assign) double latitude;
///是否启用从右向左(RTL)布局,默认为AMapAttributeRTLStateDefault
///Whether to enable right-to-left (RTL) layout, default is AMapAttributeRTLStateDefault
@property (nonatomic, assign) AMapAttributeRTLState enableRTLLayout;
///获取regionLanguageType对应的anguageCode
- (NSString *)getRegionLanguageCode;
@end
@@ -0,0 +1,41 @@
//
// AMapURLSearch.h
// AMapFoundation
//
// Created by xiaoming han on 15/10/28.
// Copyright © 2015年 Amap. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "AMapURLSearchConfig.h"
///调起高德地图URL进行搜索,若是调起失败,可使用`+ (void)getLatestAMapApp;`方法获取最新版高德地图app.
@interface AMapURLSearch : NSObject
/**
* @brief 打开高德地图AppStore页面
*/
+ (void)getLatestAMapApp;
/**
* @brief 调起高德地图app驾车导航.
* @param config 配置参数.
* @return 是否成功.若为YES则成功调起,若为NO则无法调起.
*/
+ (BOOL)openAMapNavigation:(AMapNaviConfig *)config;
/**
* @brief 调起高德地图app进行路径规划.
* @param config 配置参数.
* @return 是否成功.
*/
+ (BOOL)openAMapRouteSearch:(AMapRouteConfig *)config;
/**
* @brief 调起高德地图app进行POI搜索.
* @param config 配置参数.
* @return 是否成功.
*/
+ (BOOL)openAMapPOISearch:(AMapPOIConfig *)config;
@end
@@ -0,0 +1,79 @@
//
// MAMapURLSearchConfig.h
// MAMapKitNew
//
// Created by xiaoming han on 15/5/25.
// Copyright (c) 2015年 xiaoming han. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>
#import "AMapURLSearchType.h"
///导航配置信息
@interface AMapNaviConfig : NSObject
///应用返回的Scheme
@property (nonatomic, copy) NSString *appScheme;
///应用名称
@property (nonatomic, copy) NSString *appName;
///终点
@property (nonatomic, assign) CLLocationCoordinate2D destination;
///导航策略
@property (nonatomic, assign) AMapDrivingStrategy strategy;
@end
#pragma mark -
///路径搜索配置信息
@interface AMapRouteConfig : NSObject
///应用返回的Scheme
@property (nonatomic, copy) NSString *appScheme;
///应用名称
@property (nonatomic, copy) NSString *appName;
///起点坐标
@property (nonatomic, assign) CLLocationCoordinate2D startCoordinate;
///终点坐标
@property (nonatomic, assign) CLLocationCoordinate2D destinationCoordinate;
///驾车策略
@property (nonatomic, assign) AMapDrivingStrategy drivingStrategy;
///公交策略
@property (nonatomic, assign) AMapTransitStrategy transitStrategy;
///路径规划类型
@property (nonatomic, assign) AMapRouteSearchType routeType;
@end
#pragma mark -
///POI搜索配置信息
@interface AMapPOIConfig : NSObject
///应用返回的Scheme
@property (nonatomic, copy) NSString *appScheme;
///应用名称
@property (nonatomic, copy) NSString *appName;
///搜索关键字
@property (nonatomic, copy) NSString *keywords;
///左上角坐标
@property (nonatomic, assign) CLLocationCoordinate2D leftTopCoordinate;
///右下角坐标
@property (nonatomic, assign) CLLocationCoordinate2D rightBottomCoordinate;
@end
@@ -0,0 +1,44 @@
//
// MAMapURLSearchType.h
// MAMapKitNew
//
// Created by xiaoming han on 15/5/25.
// Copyright (c) 2015年 xiaoming han. All rights reserved.
//
///驾车策略
typedef NS_ENUM(NSInteger, AMapDrivingStrategy)
{
AMapDrivingStrategyFastest = 0, ///<速度最快
AMapDrivingStrategyMinFare = 1, ///<避免收费
AMapDrivingStrategyShortest = 2, ///<距离最短
AMapDrivingStrategyNoHighways = 3, ///<不走高速
AMapDrivingStrategyAvoidCongestion = 4, ///<躲避拥堵
AMapDrivingStrategyAvoidHighwaysAndFare = 5, ///<不走高速且避免收费
AMapDrivingStrategyAvoidHighwaysAndCongestion = 6, ///<不走高速且躲避拥堵
AMapDrivingStrategyAvoidFareAndCongestion = 7, ///<躲避收费和拥堵
AMapDrivingStrategyAvoidHighwaysAndFareAndCongestion = 8 ///<不走高速躲避收费和拥堵
};
///公交策略
typedef NS_ENUM(NSInteger, AMapTransitStrategy)
{
AMapTransitStrategyFastest = 0,///<最快捷
AMapTransitStrategyMinFare = 1,///<最经济
AMapTransitStrategyMinTransfer = 2,///<最少换乘
AMapTransitStrategyMinWalk = 3,///<最少步行
AMapTransitStrategyMostComfortable = 4,///<最舒适
AMapTransitStrategyAvoidSubway = 5,///<不乘地铁
};
///路径规划类型
typedef NS_ENUM(NSInteger, AMapRouteSearchType)
{
AMapRouteSearchTypeDriving = 0, ///<驾车
AMapRouteSearchTypeTransit = 1, ///<公交
AMapRouteSearchTypeWalking = 2, ///<步行
};
@@ -0,0 +1,50 @@
//
// AMapUtility.h
// AMapFoundation
//
// Created by xiaoming han on 15/10/27.
// Copyright © 2015年 Amap. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>
//工具方法
/**
* @brief 如果字符串为nil则返回空字符串
*/
FOUNDATION_STATIC_INLINE NSString * AMapEmptyStringIfNil(NSString *s)
{
return s ? s : @"";
}
///坐标类型枚举
typedef NS_ENUM(NSInteger, AMapCoordinateType)
{
AMapCoordinateTypeAMap = -1, ///<AMap
AMapCoordinateTypeBaidu = 0, ///<Baidu
AMapCoordinateTypeMapBar, ///<MapBar
AMapCoordinateTypeMapABC, ///<MapABC
AMapCoordinateTypeSoSoMap, ///<SoSoMap
AMapCoordinateTypeAliYun, ///<AliYun
AMapCoordinateTypeGoogle, ///<Google
AMapCoordinateTypeGPS, ///<GPS
};
/**
* @brief 转换目标经纬度为高德坐标系,不在枚举范围内的经纬度将直接返回。
* @param coordinate 待转换的经纬度
* @param type 坐标系类型
* @return 高德坐标系经纬度
*/
FOUNDATION_EXTERN CLLocationCoordinate2D AMapCoordinateConvert(CLLocationCoordinate2D coordinate, AMapCoordinateType type);
/**
* @brief 判断目标经纬度处是否有高德数据。输入参数为高德坐标系。此方法为粗略判断,在边界地区可能会不准确。
* @param coordinate 待判断的目标经纬度
* @return 布尔值。YES:有数据,NO:没数据
*/
FOUNDATION_EXTERN BOOL AMapDataAvailableForCoordinate(CLLocationCoordinate2D coordinate);
Binary file not shown.
@@ -0,0 +1,6 @@
framework module AMapFoundationKit {
umbrella header "AMapFoundationKit.h"
export *
module * { export * }
}
+3
View File
@@ -0,0 +1,3 @@
1.9.0+foundation.1b6fcf5
Overseas: false
Industry: false
Binary file not shown.
@@ -0,0 +1,27 @@
//
// AMapGeoFenceError.h
// AMapLocationKit
//
// Created by eidan on 16/12/15.
// Copyright © 2016年 Amap. All rights reserved.
//
#ifndef AMapGeoFenceError_h
#define AMapGeoFenceError_h
///AMapGeoFence errorDomain
extern NSString * const AMapGeoFenceErrorDomain;
///地理围栏错误码
typedef NS_ENUM(NSInteger, AMapGeoFenceErrorCode) {
AMapGeoFenceErrorUnknown = 1, ///< 未知错误
AMapGeoFenceErrorInvalidParameter = 2, ///< 参数错误
AMapGeoFenceErrorFailureConnection = 3, ///< 网络连接异常
AMapGeoFenceErrorFailureAuth = 4, ///< 鉴权失败
AMapGeoFenceErrorNoValidFence = 5, ///< 无可用围栏
AMapGeoFenceErroFailureLocating = 6, ///< 定位错误
AMapGeoFenceErroFailureFullAccuracyLocating = 7, ///< 精确定位错误
};
#endif /* AMapGeoFenceError_h */
@@ -0,0 +1,234 @@
//
// AMapGeoFenceManager.h
// AMapLocationKit
//
// Created by hanxiaoming on 16/12/5.
// Copyright © 2016年 Amap. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "AMapGeoFenceRegionObj.h"
// 以下类涉及的坐标需要使用高德坐标系坐标(GCJ02)
@protocol AMapGeoFenceManagerDelegate;
///地理围栏监听状态类型
typedef NS_OPTIONS(NSUInteger, AMapGeoFenceActiveAction)
{
AMapGeoFenceActiveActionNone = 0, ///< 不进行监听
AMapGeoFenceActiveActionInside = 1 << 0, ///< 在范围内
AMapGeoFenceActiveActionOutside = 1 << 1, ///< 在范围外
AMapGeoFenceActiveActionStayed = 1 << 2, ///< 停留(在范围内超过10分钟)
};
///地理围栏任务状态类型
typedef NS_OPTIONS(NSUInteger, AMapGeoFenceRegionActiveStatus)
{
AMapGeoFenceRegionActiveUNMonitor = 0, ///< 未注册
AMapGeoFenceRegionActiveMonitoring = 1 << 0, ///< 正在监控
AMapGeoFenceRegionActivePaused = 1 << 1, ///< 暂停监控
};
///地理围栏管理类(since 2.3.0
@interface AMapGeoFenceManager : NSObject
///实现了 AMapGeoFenceManagerDelegate 协议的类指针。
@property (nonatomic, weak) id<AMapGeoFenceManagerDelegate> delegate;
///需要进行通知的行为,默认为AMapGeoFenceActiveActionInside。
@property (nonatomic, assign) AMapGeoFenceActiveAction activeAction;
///指定定位是否会被系统自动暂停。默认为NO。
@property (nonatomic, assign) BOOL pausesLocationUpdatesAutomatically;
///是否允许后台定位。默认为NO。只在iOS 9.0及之后起作用。设置为YES的时候必须保证 Background Modes 中的 Location updates 处于选中状态,否则会抛出异常。
@property (nonatomic, assign) BOOL allowsBackgroundLocationUpdates;
///检测是否存在虚拟定位风险,默认为NO,即不检测。 \n如果设置为YES,检测到风险后,会通过amapGeoFenceManager:didGeoFencesStatusChangedForRegion:customID:error: 的error给出风险提示,error的格式为error.domain==AMapGeoFenceErrorDomain; error.code==AMapGeoFenceErroFailureLocating;
@property (nonatomic, assign) BOOL detectRiskOfFakeLocation;
/**
* @brief 添加一个圆形围栏
* @param center 围栏的中心点经纬度坐标
* @param radius 围栏的半径,单位:米,要求大于0
* @param customID 用户自定义ID,可选,SDK原值返回
*/
- (void)addCircleRegionForMonitoringWithCenter:(CLLocationCoordinate2D)center radius:(CLLocationDistance)radius customID:(NSString *)customID;
/**
* @brief 根据经纬度坐标数据添加一个闭合的多边形围栏,点与点之间按顺序尾部相连, 第一个点与最后一个点相连
* @param coordinates 经纬度坐标点数据,coordinates对应的内存会拷贝,调用者负责该内存的释放
* @param count 经纬度坐标点的个数,不可小于3个
* @param customID 用户自定义ID,可选,SDK原值返回
*/
- (void)addPolygonRegionForMonitoringWithCoordinates:(CLLocationCoordinate2D *)coordinates count:(NSInteger)count customID:(NSString *)customID;
/**
* @brief 根据要查询的关键字,类型,城市等信息,添加一个或者多个POI地理围栏
* @param keyword 要查询的关键字,多个关键字用“|”分割,必填,keyword和type两者至少必选其一
* @param type 要查询的POI类型,多个类型用“|”分割,必填,keyword和type两者至少必选其一,具体分类编码和规则详见: http://lbs.amap.com/api/webservice/guide/api/search/#text
* @param city 要查询的城市
* @param size 要查询的数据的条数,(0,25],传入<=0的值为10,传入大于25的值为25,默认10
* @param customID 用户自定义ID,可选,SDK原值返回
*/
- (void)addKeywordPOIRegionForMonitoringWithKeyword:(NSString *)keyword POIType:(NSString *)type city:(NSString *)city size:(NSInteger)size customID:(NSString *)customID;
/**
* @brief 根据要查询的点的经纬度,搜索半径等信息,添加一个或者多个POI围栏
* @param locationPoint 点的经纬度坐标,必填
* @param aroundRadius 查询半径,单位:米,(0,50000],超出范围取3000,默认3000
* @param keyword 要查询的关键字,多个关键字用“|”分割,可选
* @param type 要查询的POI类型,多个类型用“|”分割,可选
* @param size 要查询的数据的条数,(0,25],传入<=0的值为10,传入大于25的值为25,默认10
* @param customID 用户自定义ID,可选,SDK原值返回
*/
- (void)addAroundPOIRegionForMonitoringWithLocationPoint:(CLLocationCoordinate2D)locationPoint aroundRadius:(NSInteger)aroundRadius keyword:(NSString *)keyword POIType:(NSString *)type size:(NSInteger)size customID:(NSString *)customID;
/**
* @brief 根据要查询的行政区域关键字,添加一个或者多个行政区域围栏
* @param districtName 行政区域关键字,必填,只支持单个关键词语:行政区名称、citycode、adcode,规则详见: http://lbs.amap.com/api/webservice/guide/api/district/#district
* @param customID 用户自定义ID,可选,SDK原值返回
*/
- (void)addDistrictRegionForMonitoringWithDistrictName:(NSString *)districtName customID:(NSString *)customID;
/**
* @brief 获取指定围栏的运行状态
* @param region 要获取运行状态的围栏
* @return 返回指定围栏的运行状态
*/
- (AMapGeoFenceRegionActiveStatus)statusWithGeoFenceRegion:(AMapGeoFenceRegion *)region;
/**
* @brief 根据customID获得所有已经注册的围栏,如果customID传nil,则返回全部已注册围栏
* @param customID 用户执行添加围栏函数时传入的customID
* @return 获得的围栏构成的数组,如果没有结果,返回nil
*/
- (NSArray *)geoFenceRegionsWithCustomID:(NSString *)customID;
/**
* @brief 根据customID获得所有正在监控的围栏,如果customID传nil,则返回全部正在监控的围栏
* @param customID 用户执行添加围栏函数时传入的customID
* @return 获得的围栏构成的数组,如果没有结果,返回nil
*/
- (NSArray *)monitoringGeoFenceRegionsWithCustomID:(NSString *)customID;
/**
* @brief 根据customID获得所有已经暂停的围栏,如果customID传nil,则返回全部已经暂停的围栏
* @param customID 用户执行添加围栏函数时传入的customID
* @return 获得的围栏构成的数组,如果没有结果,返回nil
*/
- (NSArray *)pausedGeoFenceRegionsWithCustomID:(NSString *)customID;
/**
* @brief 暂停指定customID的围栏
* @param customID 用户执行添加围栏函数时传入的customID
* @return 返回被暂停围栏的数组,如果没有围栏被暂停,返回nil
*/
- (NSArray *)pauseGeoFenceRegionsWithCustomID:(NSString *)customID;
/**
* @brief 暂停指定围栏
* @param region 要暂停监控的围栏
* @return 返回指定围栏是否被暂停,如果指定围栏没有注册,则返回NO
*/
- (BOOL)pauseTheGeoFenceRegion:(AMapGeoFenceRegion *)region;
/**
* @brief 根据customID开始监控已经暂停的围栏
* @param customID 用户执行添加围栏函数时传入的customID
* @return 返回开始监控的围栏构成的数组
*/
- (NSArray *)startGeoFenceRegionsWithCustomID:(NSString *)customID;
/**
* @brief 开始监控指定围栏
* @param region 要开始监控的围栏
* @return 返回指定围栏是否开始监控,如果指定围栏没有注册,则返回NO
*/
- (BOOL)startTheGeoFenceRegion:(AMapGeoFenceRegion *)region;
/**
* @brief 移除指定围栏
* @param region 要停止监控的围栏
*/
- (void)removeTheGeoFenceRegion:(AMapGeoFenceRegion *)region;
/**
* @brief 移除指定customID的围栏
* @param customID 用户执行添加围栏函数时传入的customID
*/
- (void)removeGeoFenceRegionsWithCustomID:(NSString *)customID;
/**
* @brief 移除所有围栏
*/
- (void)removeAllGeoFenceRegions;
@end
///地理围栏代理协议(since 2.3.0),该协议定义了获取地理围栏相关回调方法,包括添加、状态改变等。
@protocol AMapGeoFenceManagerDelegate <NSObject>
@required
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
/**
* @brief iOS14及以上版本使用地理围栏功能,需要在plist中配置NSLocationTemporaryUsageDescriptionDictionary字典描述,且添加自定义Key描述地理围栏的使用场景,此描述会在申请临时精确定位权限的弹窗中展示。该回调触发条件:拥有定位权限,但是没有获得精确定位权限的情况下,会触发该回调。此方法实现调用申请临时精确定位权限API即可:
* [locationManager requestTemporaryFullAccuracyAuthorizationWithPurposeKey:@"PurposeKey" completion:^(NSError *error){
* if(completion){
* completion(error);
* }
* }]; (必须调用,不然无法正常获取临时精确定位权限)
* @param manager 地理围栏管理类。
* @param locationManager 需要申请临时精确定位权限的locationManager。
* @param completion 临时精确定位权限API回调结果。直接返回系统error即可
* @since 2.6.7
*/
- (void)amapLocationManager:(AMapGeoFenceManager *)manager doRequireTemporaryFullAccuracyAuth:(CLLocationManager*)locationManager completion:(void(^)(NSError *error))completion;
#endif
@optional
/**
* @brief 当plist配置NSLocationAlwaysUsageDescription或者NSLocationAlwaysAndWhenInUseUsageDescription,并且[CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined,会调用代理的此方法。
此方法实现申请后台权限API即可:[locationManager requestAlwaysAuthorization](必须调用,不然无法正常获取定位权限)
* @param manager 地理围栏管理类。
* @param locationManager 需要申请后台定位权限的locationManager。
* @since 2.6.2
*/
- (void)amapGeoFenceManager:(AMapGeoFenceManager *)manager doRequireLocationAuth:(CLLocationManager*)locationManager;
/**
* @brief 添加地理围栏完成后的回调,成功与失败都会调用
* @param manager 地理围栏管理类
* @param regions 成功添加的一个或多个地理围栏构成的数组
* @param customID 用户执行添加围栏函数时传入的customID
* @param error 添加失败的错误信息
*/
- (void)amapGeoFenceManager:(AMapGeoFenceManager *)manager didAddRegionForMonitoringFinished:(NSArray <AMapGeoFenceRegion *> *)regions customID:(NSString *)customID error:(NSError *)error;
/**
* @brief 地理围栏状态改变时回调,当围栏状态的值发生改变,定位失败都会调用
* @param manager 地理围栏管理类
* @param region 状态改变的地理围栏
* @param customID 用户执行添加围栏函数时传入的customID
* @param error 错误信息,如定位相关的错误
*/
- (void)amapGeoFenceManager:(AMapGeoFenceManager *)manager didGeoFencesStatusChangedForRegion:(AMapGeoFenceRegion *)region customID:(NSString *)customID error:(NSError *)error;
@end
@@ -0,0 +1,120 @@
//
// AMapGeoFenceRegionObj.h
// AMapLocationKit
//
// Created by hanxiaoming on 16/12/5.
// Copyright © 2016年 Amap. All rights reserved.
//
#import "AMapLocationCommonObj.h"
///AMapGeoFence Region State
typedef NS_ENUM(NSInteger, AMapGeoFenceRegionStatus)
{
AMapGeoFenceRegionStatusUnknown = 0, ///< 未知
AMapGeoFenceRegionStatusInside = 1, ///< 在范围内
AMapGeoFenceRegionStatusOutside = 2, ///< 在范围外
AMapGeoFenceRegionStatusStayed = 3, ///< 停留(在范围内超过10分钟)
};
typedef NS_ENUM(NSInteger, AMapGeoFenceRegionType)
{
AMapGeoFenceRegionTypeCircle = 0, /// 圆形地理围栏
AMapGeoFenceRegionTypePolygon = 1, /// 多边形地理围栏
AMapGeoFenceRegionTypePOI = 2, /// 兴趣点(POI)地理围栏
AMapGeoFenceRegionTypeDistrict = 3, /// 行政区划地理围栏
};
#pragma mark - AMapGeoFenceRegion
///地理围栏基类,不可直接使用。(since 2.3.0
@interface AMapGeoFenceRegion : NSObject<NSCopying>
///AMapGeoFenceRegion的唯一标识符
@property (nonatomic, copy, readonly) NSString *identifier;
///用户自定义ID,可为nil。
@property (nonatomic, copy, readonly) NSString *customID;
///坐标点和围栏的关系,比如用户的位置和围栏的关系
@property (nonatomic, assign) AMapGeoFenceRegionStatus fenceStatus;
///用户自定义ID,可为nil。
@property (nonatomic, assign) AMapGeoFenceRegionType regionType;
///缓存最近获取的定位信息,可能会存在延时,可为nil,会在获取定位时更新
@property (nonatomic, copy) CLLocation *currentLocation;
@end
#pragma mark - AMapLocationCircleRegion
///圆形地理围栏(since 2.3.0
@interface AMapGeoFenceCircleRegion : AMapGeoFenceRegion
///中心点的经纬度坐标
@property (nonatomic, readonly) CLLocationCoordinate2D center;
///半径,单位:米
@property (nonatomic, readonly) CLLocationDistance radius;
@end
#pragma mark -AMapGeoFencePolygonRegion
///多边形地理围栏(since 2.3.0
@interface AMapGeoFencePolygonRegion : AMapGeoFenceRegion
///经纬度坐标点数据
@property (nonatomic, readonly) CLLocationCoordinate2D *coordinates;
///经纬度坐标点的个数
@property (nonatomic, readonly) NSInteger count;
@end
#pragma mark -AMapGeoFencePOIRegion
///兴趣点(POI)地理围栏(since 2.3.0
@interface AMapGeoFencePOIRegion : AMapGeoFenceCircleRegion
///POI信息
@property (nonatomic, strong, readonly) AMapLocationPOIItem *POIItem;
@end
#pragma mark -AMapGeoFenceDistrictRegion
///行政区划地理围栏(since 2.3.0
@interface AMapGeoFenceDistrictRegion : AMapGeoFenceRegion
///行政区域信息
@property (nonatomic, strong, readonly) AMapLocationDistrictItem *districtItem;
///行政区域轮廓坐标点,每个行政区可能有多个模块,每个模块的坐标点数组由AMapLocationPoint构成
@property (nonatomic, copy, readonly) NSArray <NSArray<AMapLocationPoint *> *> *polylinePoints;
@end
@@ -0,0 +1,195 @@
//
// AMapLocationCommonObj.h
// AMapLocationKit
//
// Created by AutoNavi on 15/10/22.
// Copyright © 2015年 Amap. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>
#import <UIKit/UIKit.h>
///AMapLocation errorDomain
extern NSString * const AMapLocationErrorDomain;
///AMapLocation errorCode
typedef NS_ENUM(NSInteger, AMapLocationErrorCode)
{
AMapLocationErrorUnknown = 1, ///<未知错误
AMapLocationErrorLocateFailed = 2, ///<定位错误
AMapLocationErrorReGeocodeFailed = 3, ///<逆地理错误
AMapLocationErrorTimeOut = 4, ///<超时
AMapLocationErrorCanceled = 5, ///<取消
AMapLocationErrorCannotFindHost = 6, ///<找不到主机
AMapLocationErrorBadURL = 7, ///<URL异常
AMapLocationErrorNotConnectedToInternet = 8,///<连接异常
AMapLocationErrorCannotConnectToHost = 9, ///<服务器连接失败
AMapLocationErrorRegionMonitoringFailure=10,///<地理围栏错误
AMapLocationErrorRiskOfFakeLocation = 11, ///<存在虚拟定位风险
AMapLocationErrorNoFullAccuracyAuth = 12, ///<精确定位权限异常
};
///AMapLocation Region State
typedef NS_ENUM(NSInteger, AMapLocationRegionState)
{
AMapLocationRegionStateUnknow = 0, ///<未知
AMapLocationRegionStateInside = 1, ///<在范围内
AMapLocationRegionStateOutside = 2, ///<在范围外
};
///AMapLocation Accuracy Mode
typedef NS_ENUM(NSInteger, AMapLocationAccuracyMode) {
AMapLocationFullAndReduceAccuracy = 0, ///<默认模式,该模式下会申请临时精确定位权限,如果用户拒绝,则依然开启定位,回调模糊定位数据;
AMapLocationFullAccuracy, ///<高精度模式,该模式下会申请临时精确定位权限,如果用户拒绝,则回调error;
AMapLocationReduceAccuracy ///<低精度模式,该模式下不会申请临时精确定位权限,根据当前定位权限回调定位数据;
};
///逆地理信息
@interface AMapLocationReGeocode : NSObject<NSCopying,NSCoding>
///格式化地址
@property (nonatomic, copy) NSString *formattedAddress;
///国家
@property (nonatomic, copy) NSString *country;
///省/直辖市
@property (nonatomic, copy) NSString *province;
///市
@property (nonatomic, copy) NSString *city;
///区
@property (nonatomic, copy) NSString *district;
///乡镇
@property (nonatomic, copy) NSString *township __attribute__((deprecated("该字段从v2.2.0版本起不再返回数据,建议您使用AMapSearchKit的逆地理功能获取.")));
///社区
@property (nonatomic, copy) NSString *neighborhood __attribute__((deprecated("该字段从v2.2.0版本起不再返回数据,建议您使用AMapSearchKit的逆地理功能获取.")));
///建筑
@property (nonatomic, copy) NSString *building __attribute__((deprecated("该字段从v2.2.0版本起不再返回数据,建议您使用AMapSearchKit的逆地理功能获取.")));
///城市编码
@property (nonatomic, copy) NSString *citycode;
///区域编码
@property (nonatomic, copy) NSString *adcode;
///街道名称
@property (nonatomic, copy) NSString *street;
///门牌号
@property (nonatomic, copy) NSString *number;
///兴趣点名称
@property (nonatomic, copy) NSString *POIName;
///所属兴趣点名称
@property (nonatomic, copy) NSString *AOIName;
@end
#pragma mark - AMapLocationPoint
///经纬度坐标点对象
@interface AMapLocationPoint : NSObject<NSCopying,NSCoding>
///纬度
@property (nonatomic, assign) CGFloat latitude;
///经度
@property (nonatomic, assign) CGFloat longitude;
/**
* @brief AMapNaviPoint类对象的初始化函数
* @param lat 纬度
* @param lon 经度
* @return AMapNaviPoint类对象id
*/
+ (AMapLocationPoint *)locationWithLatitude:(CGFloat)lat longitude:(CGFloat)lon;
@end
///POI信息
@interface AMapLocationPOIItem : NSObject <NSCopying, NSCoding>
///id
@property (nonatomic, copy) NSString *pId;
///名称
@property (nonatomic, copy) NSString *name;
///类型
@property (nonatomic, copy) NSString *type;
///类型编码
@property (nonatomic, copy) NSString *typeCode;
///地址信息
@property (nonatomic, copy) NSString *address;
///经纬度
@property (nonatomic, strong) AMapLocationPoint *location;
///电话号码
@property (nonatomic, copy) NSString *tel;
///省份
@property (nonatomic, copy) NSString *province;
///城市
@property (nonatomic, copy) NSString *city;
///区
@property (nonatomic, copy) NSString *district;
@end
///行政区域信息
@interface AMapLocationDistrictItem : NSObject <NSCopying, NSCoding>
///城市编码
@property (nonatomic, copy) NSString *cityCode;
///区域编码
@property (nonatomic, copy) NSString *districtCode;
///区名
@property (nonatomic, copy) NSString *district;
///行政区域轮廓坐标点,每个行政区可能有多个模块,每个模块的坐标点数组由AMapLocationPoint构成
@property (nonatomic, copy) NSArray <NSArray<AMapLocationPoint *> *> *polylinePoints;
@end
///AMapLocation CoordinateType
typedef NS_ENUM(NSUInteger, AMapLocationCoordinateType)
{
AMapLocationCoordinateTypeBaidu = 0, ///<Baidu
AMapLocationCoordinateTypeMapBar, ///<MapBar
AMapLocationCoordinateTypeMapABC, ///<MapABC
AMapLocationCoordinateTypeSoSoMap, ///<SoSoMap
AMapLocationCoordinateTypeAliYun, ///<AliYun
AMapLocationCoordinateTypeGoogle, ///<Google
AMapLocationCoordinateTypeGPS, ///<GPS
};
/**
* @brief 转换目标经纬度为高德坐标系
* @param coordinate 待转换的经纬度
* @param type 坐标系类型
* @return 高德坐标系经纬度
*/
FOUNDATION_EXTERN CLLocationCoordinate2D AMapLocationCoordinateConvert(CLLocationCoordinate2D coordinate, AMapLocationCoordinateType type);
/**
* @brief 判断目标经纬度是否在大陆以及港、澳地区。输入参数为高德坐标系。
* @param coordinate 待判断的目标经纬度
* @return 是否在大陆以及港、澳地区
*/
FOUNDATION_EXTERN BOOL AMapLocationDataAvailableForCoordinate(CLLocationCoordinate2D coordinate);
@@ -0,0 +1,17 @@
//
// AMapLocationKit.h
// AMapLocationKit
//
// Created by AutoNavi on 15/10/22.
// Copyright © 2015年 Amap. All rights reserved.
//
#import <AMapLocationKit/AMapLocationVersion.h>
#import <AMapLocationKit/AMapLocationManager.h>
#import <AMapLocationKit/AMapLocationCommonObj.h>
#import <AMapLocationKit/AMapLocationRegionObj.h>
#import <AMapLocationKit/AMapGeoFenceRegionObj.h>
#import <AMapLocationKit/AMapGeoFenceManager.h>
#import <AMapLocationKit/AMapGeoFenceError.h>
@@ -0,0 +1,283 @@
//
// AMapLocationManager.h
// AMapLocationKit
//
// Created by AutoNavi on 15/10/22.
// Copyright © 2015年 Amap. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <AMapFoundationKit/AMapServices.h>
#import "AMapLocationCommonObj.h"
#import "AMapLocationRegionObj.h"
/**
* @brief AMapLocatingCompletionBlock 单次定位返回Block
* @param location 定位信息
* @param regeocode 逆地理信息
* @param error 错误信息,参考 AMapLocationErrorCode
*/
typedef void (^AMapLocatingCompletionBlock)(CLLocation *location, AMapLocationReGeocode *regeocode, NSError *error);
@protocol AMapLocationManagerDelegate;
#pragma mark - AMapLocationManager
///AMapLocationManager类。初始化之前请设置 AMapServices 中的apikey(例如:[AMapServices sharedServices].apiKey = @"您的key"),否则将无法正常使用服务.
@interface AMapLocationManager : NSObject
///实现了 AMapLocationManagerDelegate 协议的类指针。
@property (nonatomic, weak) id<AMapLocationManagerDelegate> delegate;
///设定定位的最小更新距离。单位米,默认为 kCLDistanceFilterNone,表示只要检测到设备位置发生变化就会更新位置信息。
@property(nonatomic, assign) CLLocationDistance distanceFilter;
///设定期望的定位精度。单位米,默认为 kCLLocationAccuracyBest。定位服务会尽可能去获取满足desiredAccuracy的定位结果,但不保证一定会得到满足期望的结果。
///注意:设置为kCLLocationAccuracyBest或kCLLocationAccuracyBestForNavigation时,单次定位会在达到locationTimeout设定的时间后,将时间内获取到的最高精度的定位结果返回。
///⚠️ 当iOS14及以上版本,模糊定位权限下可能拿不到设置精度的经纬度
@property(nonatomic, assign) CLLocationAccuracy desiredAccuracy;
///指定定位是否会被系统自动暂停。默认为NO。
@property(nonatomic, assign) BOOL pausesLocationUpdatesAutomatically;
///是否允许后台定位。默认为NO。只在iOS 9.0及之后起作用。设置为YES的时候必须保证 Background Modes 中的 Location updates 处于选中状态,否则会抛出异常。由于iOS系统限制,需要在定位未开始之前或定位停止之后,修改该属性的值才会有效果。
@property(nonatomic, assign) BOOL allowsBackgroundLocationUpdates;
///指定单次定位超时时间,默认为10s。最小值是2s。注意单次定位请求前设置。注意: 单次定位超时时间从确定了定位权限(非kCLAuthorizationStatusNotDetermined状态)后开始计算。
@property(nonatomic, assign) NSInteger locationTimeout;
///指定单次定位逆地理超时时间,默认为5s。最小值是2s。注意单次定位请求前设置。
@property(nonatomic, assign) NSInteger reGeocodeTimeout;
///连续定位是否返回逆地理信息,默认NO。
@property (nonatomic, assign) BOOL locatingWithReGeocode;
// 注意:在使用导航功能前,请通过 [AMapServices sharedServices].regionLanguageType 来设置语言类型 (since 11.2.000)
///逆地址语言类型,默认是AMapRegionLanguageTypeZhHans
//@property (nonatomic, assign) AMapRegionLanguageType reGeocodeLanguage;
///获取被监控的region集合。
@property (nonatomic, readonly, copy) NSSet *monitoredRegions;
///检测是否存在虚拟定位风险,默认为NO,不检测。 \n注意:设置为YES时,单次定位通过 AMapLocatingCompletionBlock 的error给出虚拟定位风险提示;连续定位通过 amapLocationManager:didFailWithError: 方法的error给出虚拟定位风险提示。error格式为 error.domain==AMapLocationErrorDomain; error.code==AMapLocationErrorRiskOfFakeLocation; \n附带的error的详细信息参考 error.localizedDescription 中的描述以及 error.userInfo 中的信息(error.userInfo.AMapLocationRiskyLocateResult 表示有虚拟风险的定位结果; error.userInfo.AMapLocationAccessoryInfo 表示外接辅助设备信息)。
@property (nonatomic, assign) BOOL detectRiskOfFakeLocation;
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
/**
* @brief 设置定位数据回调精度模式,默认为AMapLocationAccuracyFullAndReduce。\n
注意:如果定位时未获得定位权限,则首先会调用申请定位权限API,实际定位精度权限取决于用户的权限设置。\n
* ————————————————————————————————————————————————————————————————\n
* | 设置选项 | doRequireTemporaryFullAccuracyAuth | 异常/定位数据回调 |\n
* | AMapLocationFullAndReduceAccuracy | 会触发申请临时精确定位回调 | 如果未获得精确定位权限,则依然开启定位,回调模糊定位 |\n
* | AMapLocationFullAccuracy | 会触发申请临时精确定位回调 | 如果未获得精确定位权限,则不开启定位,回调error |\n
* | AMapLocationReduceAccuracy | 不会触发申请临时精确定位回调 | 根据当前定位精度权限,回调定位数据 |\n
* ————————————————————————————————————————————————————————————————\n
* 当设置AMapLocationFullAndReduceAccuracy时,定位数据回调可通过currentAuthorization判断当前是否是精确定位
* @since 2.6.7
*/
@property (nonatomic, assign) AMapLocationAccuracyMode locationAccuracyMode API_AVAILABLE(ios(14.0));
/**
* @brief 获取当前定位精度权限。
* @since 2.6.7
*/
@property (nonatomic, readonly) CLAccuracyAuthorization currentAuthorization API_AVAILABLE(ios(14.0));
#endif
/**
* @brief 设备是否支持方向识别
* @return YES:设备支持方向识别 ; NO:设备不支持支持方向识别
*/
+ (BOOL)headingAvailable;
/**
* @brief 开始获取设备朝向,如果设备支持方向识别,则会通过代理回调方法
*/
- (void)startUpdatingHeading;
/**
* @brief 停止获取设备朝向
*/
- (void)stopUpdatingHeading;
/**
* @brief 停止设备朝向校准显示
*/
- (void)dismissHeadingCalibrationDisplay;
/**
* @brief 单次定位。如果当前正在连续定位,调用此方法将会失败,返回NO。\n该方法将会根据设定的 desiredAccuracy 去获取定位信息。如果获取的定位信息精确度低于 desiredAccuracy ,将会持续的等待定位信息,直到超时后通过completionBlock返回精度最高的定位信息。\n可以通过 stopUpdatingLocation 方法去取消正在进行的单次定位请求。
* @param withReGeocode 是否带有逆地理信息(获取逆地理信息需要联网)
* @param completionBlock 单次定位完成后的Block
* @return 是否成功添加单次定位Request
*/
- (BOOL)requestLocationWithReGeocode:(BOOL)withReGeocode completionBlock:(AMapLocatingCompletionBlock)completionBlock;
/**
* @brief 开始连续定位。调用此方法会cancel掉所有的单次定位请求。
*/
- (void)startUpdatingLocation;
/**
* @brief 停止连续定位。调用此方法会cancel掉所有的单次定位请求,可以用来取消单次定位。
*/
- (void)stopUpdatingLocation;
/**
* @brief 开始监控指定的region。如果已经存在相同identifier的region,则之前的region将会被移除。对 AMapLocationCircleRegion 类实例,将会优先监控radius小的region。
* @param region 要被监控的范围
*/
- (void)startMonitoringForRegion:(AMapLocationRegion *)region __attribute__((deprecated("请使用AMapGeoFenceManager")));
/**
* @brief 停止监控指定的region
* @param region 要停止监控的范围
*/
- (void)stopMonitoringForRegion:(AMapLocationRegion *)region __attribute__((deprecated("请使用AMapGeoFenceManager")));
/**
* @brief 查询一个region的当前状态。查询结果通过amapLocationManager:didDetermineState:forRegion:回调返回
* @param region 要查询的region
*/
- (void)requestStateForRegion:(AMapLocationRegion *)region __attribute__((deprecated("请使用AMapGeoFenceManager")));
#pragma mark - Privacy 隐私合规
/**
* @brief 更新App是否显示隐私弹窗的状态,隐私弹窗是否包含高德SDK隐私协议内容的状态,注意:必须在AMapLocationManager实例化之前调用. since 2.8.0
* @param showStatus 隐私弹窗状态
* @param containStatus 包含高德SDK隐私协议状态
*/
+ (void)updatePrivacyShow:(AMapPrivacyShowStatus)showStatus privacyInfo:(AMapPrivacyInfoStatus)containStatus;
/**
* @brief 更新用户授权高德SDK隐私协议状态,注意:必须在AMapLocationManager实例化之前调用. since 2.8.0
* @param agreeStatus 用户授权高德SDK隐私协议状态
*/
+ (void)updatePrivacyAgree:(AMapPrivacyAgreeStatus)agreeStatus;
@end
#pragma mark - AMapLocationManagerDelegate
///AMapLocationManagerDelegate 协议定义了发生错误时的错误回调方法,连续定位的回调方法等。
@protocol AMapLocationManagerDelegate <NSObject>
@optional
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
/**
* @brief 触发前提条件:1、plist配置NSLocationTemporaryUsageDescriptionDictionary。2、locationAccuracyMode设置为AMapLocationFullAndReduceAccuracy/AMapLocationFullAccuracy。3、当前用户已授权过定位权限,且为模糊定位权限。触发时机:满足前提条件时,再次发起定位请求会调用代理的此方法。此方法实现调用申请临时精确定位权限API即可:
* [manager requestTemporaryFullAccuracyAuthorizationWithPurposeKey:@"PurposeKey" completion:^(NSError *error){
* if(completion){
* completion(error);
* }
* }]; (必须调用,不然无法正常获取临时精确定位权限)
* @param manager 定位 AMapLocationManager 类。
* @param locationManager 需要申请临时精确定位权限的locationManager。
* @param completion 临时精确定位权限API回调结果,error: 直接返回系统error即可。
* @since 2.6.7
*/
- (void)amapLocationManager:(AMapLocationManager *)manager doRequireTemporaryFullAccuracyAuth:(CLLocationManager*)locationManager completion:(void(^)(NSError *error))completion;
#endif
/**
* @brief 当plist配置NSLocationAlwaysUsageDescription或者NSLocationAlwaysAndWhenInUseUsageDescription,并且[CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined,会调用代理的此方法。
此方法实现调用申请后台权限API即可:[locationManager requestAlwaysAuthorization] (必须调用,不然无法正常获取定位权限)
* @param manager 定位 AMapLocationManager 类。
* @param locationManager 需要申请后台定位权限的locationManager。
* @since 2.6.2
*/
- (void)amapLocationManager:(AMapLocationManager *)manager doRequireLocationAuth:(CLLocationManager*)locationManager;
/**
* @brief 当定位发生错误时,会调用代理的此方法。
* @param manager 定位 AMapLocationManager 类。
* @param error 返回的错误,参考 CLError 。
*/
- (void)amapLocationManager:(AMapLocationManager *)manager didFailWithError:(NSError *)error;
/**
* @brief 连续定位回调函数.注意:本方法已被废弃,如果实现了amapLocationManager:didUpdateLocation:reGeocode:方法,则本方法将不会回调。
* @param manager 定位 AMapLocationManager 类。
* @param location 定位结果。
*/
- (void)amapLocationManager:(AMapLocationManager *)manager didUpdateLocation:(CLLocation *)location;
/**
* @brief 连续定位回调函数.注意:如果实现了本方法,则定位信息不会通过amapLocationManager:didUpdateLocation:方法回调。
* @param manager 定位 AMapLocationManager 类。
* @param location 定位结果。
* @param reGeocode 逆地理信息。
*/
- (void)amapLocationManager:(AMapLocationManager *)manager didUpdateLocation:(CLLocation *)location reGeocode:(AMapLocationReGeocode *)reGeocode;
/**
* @brief 定位权限状态改变时回调函数。注意:iOS13及之前版本回调
* @param manager 定位 AMapLocationManager 类。
* @param status 定位权限状态。
*/
- (void)amapLocationManager:(AMapLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status;
/**
* @brief 定位权限状态改变时回调函数。注意:iOS14及之后版本回调
* @param manager 定位 AMapLocationManager 类。
* @param locationManager 定位CLLocationManager类,可通过locationManager.authorizationStatus获取定位权限,通过locationManager.accuracyAuthorization获取定位精度权限
*/
- (void)amapLocationManager:(AMapLocationManager *)manager locationManagerDidChangeAuthorization:(CLLocationManager*)locationManager;
/**
* @brief 是否显示设备朝向校准
* @param manager 定位 AMapLocationManager 类。
* @return 是否显示设备朝向校准
*/
- (BOOL)amapLocationManagerShouldDisplayHeadingCalibration:(AMapLocationManager *)manager;
/**
* @brief 设备方向改变时回调函数
* @param manager 定位 AMapLocationManager 类。
* @param newHeading 设备朝向。
*/
- (void)amapLocationManager:(AMapLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading;
/**
* @brief 开始监控region回调函数
* @param manager 定位 AMapLocationManager 类。
* @param region 开始监控的region。
*/
- (void)amapLocationManager:(AMapLocationManager *)manager didStartMonitoringForRegion:(AMapLocationRegion *)region __attribute__((deprecated("请使用AMapGeoFenceManager")));
/**
* @brief 进入region回调函数
* @param manager 定位 AMapLocationManager 类。
* @param region 进入的region。
*/
- (void)amapLocationManager:(AMapLocationManager *)manager didEnterRegion:(AMapLocationRegion *)region __attribute__((deprecated("请使用AMapGeoFenceManager")));
/**
* @brief 离开region回调函数
* @param manager 定位 AMapLocationManager 类。
* @param region 离开的region。
*/
- (void)amapLocationManager:(AMapLocationManager *)manager didExitRegion:(AMapLocationRegion *)region __attribute__((deprecated("请使用AMapGeoFenceManager")));
/**
* @brief 查询region状态回调函数
* @param manager 定位 AMapLocationManager 类。
* @param state 查询的region的状态。
* @param region 查询的region。
*/
- (void)amapLocationManager:(AMapLocationManager *)manager didDetermineState:(AMapLocationRegionState)state forRegion:(AMapLocationRegion *)region __attribute__((deprecated("请使用AMapGeoFenceManager")));
/**
* @brief 监控region失败回调函数
* @param manager 定位 AMapLocationManager 类。
* @param region 失败的region。
* @param error 错误信息,参考 AMapLocationErrorCode 。
*/
- (void)amapLocationManager:(AMapLocationManager *)manager monitoringDidFailForRegion:(AMapLocationRegion *)region withError:(NSError *)error __attribute__((deprecated("请使用AMapGeoFenceManager")));
@end
@@ -0,0 +1,90 @@
//
// AMapLocationRegionObj.h
// AMapLocationKit
//
// Created by AutoNavi on 15/11/27.
// Copyright © 2015年 Amap. All rights reserved.
//
#import "AMapLocationCommonObj.h"
// 以下类涉及的坐标需要使用高德坐标系坐标(GCJ02)
#pragma mark - AMapLocationRegion
///AMapLocationRegion类,该类提供范围类的基本信息,并无具体实现,不要直接使用。
@interface AMapLocationRegion : NSObject<NSCopying>
///AMapLocationRegion的identifier
@property (nonatomic, copy, readonly) NSString *identifier;
///当进入region范围时是否通知,默认YES
@property (nonatomic, assign) BOOL notifyOnEntry;
///当离开region范围时是否通知,默认YES
@property (nonatomic, assign) BOOL notifyOnExit;
/**
* @brief 初始化方法
* @param identifier 唯一标识符,必填,不可为nil
*/
- (instancetype)initWithIdentifier:(NSString *)identifier;
/**
* @brief 坐标点是否在范围内
* @param coordinate 要判断的坐标点
* @return 是否在范围内
*/
- (BOOL)containsCoordinate:(CLLocationCoordinate2D)coordinate;
@end
#pragma mark - AMapLocationCircleRegion
///AMapLocationCircleRegion类,定义一个圆形范围。
@interface AMapLocationCircleRegion : AMapLocationRegion
///中心点的经纬度坐标
@property (nonatomic, readonly) CLLocationCoordinate2D center;
///半径,单位:米
@property (nonatomic, readonly) CLLocationDistance radius;
/**
* @brief 根据中心点和半径生成圆形范围
* @param center 中心点的经纬度坐标
* @param radius 半径,单位:米
* @param identifier 唯一标识符,必填,不可为nil
* @return AMapLocationCircleRegion类实例
*/
- (instancetype)initWithCenter:(CLLocationCoordinate2D)center radius:(CLLocationDistance)radius identifier:(NSString *)identifier;
@end
#pragma mark - AMapLocationPolygonRegion
///AMapLocationCircleRegion类,定义一个闭合多边形范围,点与点之间按顺序尾部相连, 第一个点与最后一个点相连。
@interface AMapLocationPolygonRegion : AMapLocationRegion
///经纬度坐标点数据
@property (nonatomic, readonly) CLLocationCoordinate2D *coordinates;
///经纬度坐标点的个数
@property (nonatomic, readonly) NSInteger count;
/**
* @brief 根据经纬度坐标数据生成闭合多边形范围
* @param coordinates 经纬度坐标点数据,coordinates对应的内存会拷贝,调用者负责该内存的释放
* @param count 经纬度坐标点的个数,不可小于3个
* @param identifier 唯一标识符,必填,不可为nil
* @return AMapLocationCircleRegion类实例
*/
- (instancetype)initWithCoordinates:(CLLocationCoordinate2D *)coordinates count:(NSInteger)count identifier:(NSString *)identifier;
@end
@@ -0,0 +1,26 @@
//
// AMapLoctionVersion.h
// AMapLocationKit
//
// Created by AutoNavi on 16/1/22.
// Copyright © 2016年 Amap. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AMapFoundationKit/AMapFoundationVersion.h>
#ifndef AMapLoctionVersion_h
#define AMapLoctionVersion_h
#define AMapLocationVersionNumber 201200
#define AMapLocationFoundationVersionMinRequired 10900
// 依赖库版本检测
#if AMapFoundationVersionNumber < AMapLocationFoundationVersionMinRequired
#error "The AMapFoundationKit version is less than minimum required, please update! Any questions please to visit http://lbs.amap.com"
#endif
FOUNDATION_EXTERN NSString * const AMapLocationVersion;
FOUNDATION_EXTERN NSString * const AMapLocationName;
#endif /* AMapLoctionVersion_h */
Binary file not shown.
@@ -0,0 +1,6 @@
framework module AMapLocationKit {
umbrella header "AMapLocationKit.h"
export *
module * { export * }
}
+2
View File
@@ -0,0 +1,2 @@
2.12.0+loc.4f55976
isOversea:false
+31
View File
@@ -0,0 +1,31 @@
# AMap 高德定位 SDK
| 项 | 值 |
|---|---|
| AMapFoundationKit | 1.9.0+foundation.1b6fcf5 |
| AMapLocationKit | 2.12.0+loc.4f55976 |
| 来源 | [高德开放平台 iOS 定位 SDK 下载页](https://lbs.amap.com/api/ios-location-sdk/download) |
| 路径 | `Vendor/AMap/AMapFoundationKit.framework` / `Vendor/AMap/AMapLocationKit.framework` |
| 架构 | fatx86_64 + arm64,单 `.framework` 同时支持设备 + 模拟器) |
| 模块名 | `AMapFoundationKit` / `AMapLocationKit` |
| 引入日期 | 2026-06-22 |
## 工程接入步骤(用户在 Xcode 手动)
> ⚠️ 高德官方发布的还是旧式 fat `.framework`(不是 `.xcframework`)。Xcode 26+ 接受 fat `.framework`,但若构建报「Could not find module ... for 'arm64-apple-ios-simulator'」可能要先用 `xcodebuild -create-xcframework` 转封。先按下面流程跑,遇到问题再处理。
1. Project Navigator → 选中 ylgamehall target → General → Frameworks, Libraries, and Embedded Content
2.`+` → Add Other → Add Files → 把 `Vendor/AMap/AMapFoundationKit.framework``AMapLocationKit.framework` 一起加入
3. 两个都设为 **Do Not Embed**(高德 fat `.framework` 是静态库,不能 Embed;Embed 会触发链接器对 `__OBJC` 段重复符号错误)
4. Build Settings → Framework Search Paths 自动会被 Xcode 写入 `$(PROJECT_DIR)/Vendor/AMap`
5. Clean Build Folder 后 BuildProject
接入完毕后,源码内 `#if canImport(AMapLocationKit)` 守卫将激活真实路径。
## 凭证
`docs/SDK-Integration-Guide.md §0` 凭证总表。APIKey 沿用 msext`b0d4a8e3fcbbcc0dd96283b7df6a4494`,与 Bundle ID `com.skyapp.ylgamehall` 绑死)。
## Info.plist 必需
- `NSLocationWhenInUseUsageDescription`:定位权限文案(已由工程接入时统一添加,见 SDK-Integration-Guide §2.2
+15
View File
@@ -0,0 +1,15 @@
# WechatOpenSDK
**本目录刻意不放二进制**:项目以 **SPM** 接入微信 OpenSDKTencent 官方维护),不走 Vendor xcframework。详见 `docs/SDK-Integration-Guide.md §A.1`
| 项 | 值 |
|---|---|
| 来源 | Tencent 官方 SPM 仓库 |
| SPM URL | `https://github.com/Tencent/WechatOpenSDK-XCFramework` |
| 推荐版本 | `2.0.5` 或更新(SDK1.x 有 ITMS-90809 历史包袱,避免) |
| 模块名 | `WechatOpenSDK``import WechatOpenSDK` / `#import <WechatOpenSDK/WXApi.h>` |
| 替代二进制 | 项目方曾下载 `WechatOpenSDK-NoPay.xcframework``docs/res/`,**改 SPM 后不再使用**,可保留作为离线备份 |
## 凭证
AppID + AppSecret + AuthScope + AuthState 沿用 msext,参 `docs/SDK-Integration-Guide.md §0`
+262 -224
View File
@@ -2,285 +2,323 @@
> 本文档面向项目方 + 开发者,描述 Phase 4.E / Phase 5 / Phase 3.B-D 三组 SDK 的真实接入步骤。 > 本文档面向项目方 + 开发者,描述 Phase 4.E / Phase 5 / Phase 3.B-D 三组 SDK 的真实接入步骤。
> >
> **使用方式**:每接入一个 SDK 走一遍对应章节的"凭证清单 / 工程接入 / 代码触点 / 清理钩子 / 验收"五步,全程对照 CLAUDE.md「原则 A — H5 端零修改」与 ADR-006「禁止 CocoaPods」 > **核心前提**:本项目复用 daoqi(msext) 原项目的全部 SDK 凭证。**Bundle ID 必须保持 `com.skyapp.ylgamehall`**(已确认与 msext Release 配置一致)— 改 Bundle ID 会导致所有 key 失效
>
> **代码状态(2026-06-22**:所有 Swift wrapper / handler 真实路径 / 生命周期清理钩子已落地,全部包在 `#if canImport(<ModuleName>)` 守卫里。**只需用户在 Xcode UI 完成 3 件事**(详见 §A 用户手动步骤),代码即激活真实路径。
--- ---
## 0. 总览 ## A. 用户手动 Xcode 步骤(必做 3 件事)
| SDK | 触发的契约项 | 选型 | 阻塞 | > **选型偏好**:尽量 SPM 远程依赖;只有官方未提供 SPM 的才走 Vendor 二进制。
|---|---|---|---| > 当前结果:**微信 + 七牛走 SPM**pbxproj 仅多 2 个 `XCRemoteSwiftPackageReference`,与现有 ZIPFoundation 一致);**高德走 Vendor**(官方未提供 SPM)。
| **微信 OpenSDK** | `accreditlogin` / `friendsSharetypeUrlToptitleDescript`(微信路径)/ `sharelogin` 反向 callback / `sharesuccess` | Vendor `.xcframework` 手动 | AppID + Universal Links + 后台 `/wechat/login`(或客户端 fallback | > 这 3 件事都是 Xcode UI 操作,无法 AI 自动完成。完成后 `BuildProject` 即激活所有真实路径。
| **高德定位(AMap** | `startlocation` / `getlocationinfo` 反向 callback9 字段)| Vendor `.xcframework` 手动 | **新 APIKey(绑死 Bundle ID,不可复用 msext** + 隐私合规调用 |
| **opencore-amr** | `prepareaudio`(录音上传)/ `mediaTypeAudio`(远端语音回放)AMR ↔ WAV 转码 | 从 msext 拷贝已修复版 `.a`fat header segalign 8 | 无外部凭证;二进制已在 msext 仓库内 |
| **七牛云上传** | `prepareaudio``getaudiourl({audiourl, time})` / `recordSuccess` 反向 callback | SPM 官方 `https://github.com/qiniu/objc-sdk` v8.9.x | 后台 token 颁发接口 / 长 token 缓存策略 |
**两条铁律(CLAUDE.md** ### A.1 添加微信 OpenSDKSPM 远程依赖)
1. 任何接入步骤都不允许触碰 H5 端任何文件(包括 `app_*.js`
2. 不引入 CocoaPodsSPM 优先,闭源走 Vendor `.xcframework` > 项目方下载的 `WechatOpenSDK-NoPay.xcframework` 已撤出 `Vendor/`;改用 Tencent 官方 SPM 仓库。
1. Xcode → File → **Add Package Dependencies...**
2. 右上搜索框粘贴:`https://github.com/Tencent/WechatOpenSDK-XCFramework`
3. **Dependency Rule****Up to Next Major Version**`2.0.5`(或更新;避免 1.x 的 ITMS-90809 历史包袱)
4. Add Package → 弹窗里 Library 选 `WechatOpenSDK` → Add Package
5. 确认 ylgamehall target 已勾选
6. ⌘B 编译
### A.2 添加高德定位 framework × 2Vendor 手动,无 SPM 替代)
> ⚠️ 高德官方至今未提供 SPM 包;只发布旧式 fat `.framework`(不是 xcframework),同时含 x86_64 + arm64 — 模拟器和真机都能用,但要 **Do Not Embed**(静态库不能 Embed,否则链接器报 `__OBJC` 段重复符号)。
1. 选中 `ylgamehall` Target → General → Frameworks, Libraries, and Embedded Content
2.`+` → Add Other → Add Files
3. 一次性选两个:
- `Vendor/AMap/AMapFoundationKit.framework`
- `Vendor/AMap/AMapLocationKit.framework`
4. 两个的 **Embed** 列都选 **Do Not Embed**(关键,别错)
5. ⌘B 编译
### A.3 添加七牛 SPM 依赖(远程)
> 项目方下载的 `docs/res/objc-sdk-8.9.2/` 改 SPM 后不再使用,可保留作离线备份或直接删除(不影响工程)。
1. Xcode → File → **Add Package Dependencies...**
2. 右上搜索框粘贴:`https://github.com/qiniu/objc-sdk`
3. **Dependency Rule****Up to Next Major Version**`8.9.0`
4. Add Package → 弹窗里 Library 选 `Qiniu` → Add Package
5. ⌘B 编译
--- ---
## 1. 微信 OpenSDKPhase 4.E ## B. 完成 A 三步后会发生什么
### 1.1 凭证清单(项目方提供) 代码侧已经全部写好,并用 `#if canImport(...)` 守卫围着真实路径。一旦 Xcode UI 把 SDK 加进 Target`canImport` 立刻返回 true,下面这些就会自动激活:
- [ ] **AppID**:微信开放平台「移动应用」面板新建一个应用(新 Bundle ID 不能复用 msext 的 `wx586a9b321e56efb7`,必须为本项目重新申请) | 文件 | 激活后行为 |
- [ ] **Universal Links**:项目方拥有的域名 + `apple-app-site-association` 文件(部署到 `https://your-domain.com/.well-known/apple-app-site-association`HTTPS 必须)
- [ ] **后台 `/wechat/login` 中转接口**(强烈推荐):客户端拿 `code` 调后台,后台用 `AppSecret``access_token` + `unionid` + `userinfo`,回 7 字段给客户端
- 若后台未就绪,客户端走 fallback:`api.weixin.qq.com/sns/oauth2/access_token` + `sns/userinfo`**`AppSecret` 客户端硬编码 = 安全风险**,CLAUDE.md 父项目已明确警告。新项目尽量直接走后台中转
- [ ] **微信 OpenSDK XCFramework**:从 [微信开放平台官网下载](https://developers.weixin.qq.com/doc/oplatform/Downloads/iOS_Resource.html),使用 1.9.x 或最新(避免 1.8.x 的 ITMS-90809 历史包袱)
### 1.2 工程接入
1. **放置 SDK**
```
Vendor/WechatSDK/
├── WechatOpenSDK.xcframework
└── README.md # 记录版本号 + 下载日期 + 来源
```
按 Design §14.1 Vendor 接入标准流程:Target → General → Frameworks → Embed & Sign
2. **Info.plist 改动**(仅原生侧,不动 H5)
```xml
<!-- 1. URL Scheme:让微信 App 回调能打开本 App -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>weixin</string>
<key>CFBundleURLSchemes</key>
<array><string>YOUR_WX_APPID</string></array>
</dict>
</array>
<!-- 2. 查询微信是否安装 -->
<key>LSApplicationQueriesSchemes</key>
<array>
<string>weixin</string>
<string>weixinULAPI</string>
<string>weixinURLParamsAPI</string>
<!-- 已有的 mqq* / snssdk1128 别覆盖 -->
</array>
```
3. **Entitlement 改动**Signing & Capabilities
- Associated Domains 添加 `applinks:your-domain.com`(与 `apple-app-site-association` 的域名严格一致)
### 1.3 代码触点(接入此 SDK 必须改的文件)
| 文件 | 改动 |
|---|---| |---|---|
| **`Source/SDK/WeChat/WeChatSDK.swift`**(新建) | `WXApi.registerApp(_:universalLink:)` 启动注册;包含全部 12 个 `MMAPP_SUPPORT_*` flagDesign §4.2 | | `AppDelegate.swift` | `WeChatSDK.register()` → 真实调 `WXApi.registerApp("wx586a9b321e56efb7", universalLink: "")` |
| **`AppDelegate.swift`** | `application(_:didFinishLaunchingWithOptions:)` 内调 `WeChatSDK.register()` | | `AppDelegate.swift` | `AMapWrapper.bootstrap()` → 真实调隐私合规 3 步 + apiKey |
| **`SceneDelegate.swift`** | `openURLContexts` 内按 **QQ → WXApi** 顺序判(msext `AppDelegate.m` 顺序硬约束,QQ 必须先判) | | `SceneDelegate.swift` | `openURLContexts` → 真实调 `WXApi.handleOpen(url, delegate:)` |
| **`Source/SDK/WeChat/WeChatManager.swift`**(新建) | 持久 delegate`authorize() async throws -> WXAuthCode`state UUID 配对);`share(_:scene:) async throws`FIFO 串行)。Design §8.5 | | `AccreditLoginHandler` | `accreditlogin` → 真实拉起微信授权 → 反向 callback `sharelogin` 7 字段(Province 大写 P |
| **`Source/Login/WeChatAuth.swift`**(新建) | `authorize()` 拿 code → 转后台 `/wechat/login`(或 fallback 拼 `sns/oauth2/access_token` + `sns/userinfo`)→ 拿到 7 字段 user。Fallback 文件头标 `// FIXME: 待后台 /wechat/login 就绪后切换为后台中转` | | `WechatShare` | `share()` → 真实调 `WXApi.send(SendMessageToWXReq)` |
| **`Source/Bridge/Handlers/AccreditLoginHandler.swift`** | 移除 stub 返回,调 `WeChatAuth.authorize()` → 反向 `bridge.call("sharelogin", 7 字段)`<br>**字段名严格 1:1(含历史包袱)**:`openid` / `headimgurl` / `nickname` / `sex` / `city` / `Province`**大写 P**!)/ `unionid` | | `StartLocationHandler` | `startlocation` → 真实调 `AMapLocationManager.requestLocation` → 反向 callback `getlocationinfo` 9 字段(latitude/longitude string, province 小写 p |
| **`Source/Share/WechatShare.swift`** | 移除 stub `return .success`,调 `WeChatManager.share``isInstalled` 改为 `WXApi.isWXAppInstalled()` | | `BackGameDataHandler` | 子游戏 backgameData 退出时自动清 `WXApi.delegate = nil` + `LocationService.stop()` |
| **`Source/Bridge/Handlers/BackGameDataHandler.swift`** | **必须加** `WXApi.delegate = nil`(清理子游戏 push 期间的 delegate 指针) |
### 1.4 清理钩子(CLAUDE.md 第 6.0 节回响) **Qiniu 单独说明**:七牛 SDK 加进 SPM 后,`canImport(QiniuSDK)` 通过;但 `RemoteAudioHandler` 仍然是 stubprepareaudio 不真实录音上传),原因是录音→AMR 转码链路的 opencore-amr ObjC 库未接入。`QiniuUploader.upload(...)` 单独已可用,可用于其它非录音场景。完整录音上传链路见 §C。
`BackGameDataHandler.swift` 的 `MainActor.run` 块加一行:
```swift
WXApi.delegate = nil // ← Phase 4.E 接入时取消注释
```
理由:msext `gameController.m:699` 在 backgameData 里清 delegate,防止 pop 后微信回调指向已销毁的 SubGameVC。
### 1.5 验收(H5 端零修改前提下跑)
- [ ] H5 调 `bridge.callHandler('accreditlogin')` → 拉起微信 → 同意授权 → 大厅收 `sharelogin({openid,...,Province})`**`Province` 是大写 P**
- [ ] H5 调 `bridge.callHandler('friendsSharetypeUrlToptitleDescript', {sharefriend:"2", ...})` → 直接调起微信朋友圈
- [ ] H5 调 `sharefriend:"1"` → SharePanel 三按钮,点微信 → 拉起微信好友选择 → 分享成功后大厅收 `sharesuccess({success:"2",type:"1"})`
- [ ] **关键回归**:截图分享 / QQ 分享 / 抖音分享路径不受影响
### 1.6 风险
- **AppSecret 客户端泄露**fallback 路径上线前必须切到后台中转,否则 secret 一旦泄露不可重置(CLAUDE.md 父项目「已识别为安全风险,明确接受不修」的同款问题,新项目从一开始避免)
- **Universal Links 配置错误**:授权回不来。验证方式:`xcrun simctl openurl booted "https://your-domain.com/test"` 看是否拉起本 App
- **微信审核**:审核期间 OpenSDK 行为差异(部分 API 限频),上线初期保持后台监控
--- ---
## 2. 高德定位 SDKPhase 5 ## C. 仍需用户手动做的事(opencore-amr / AMR 录音上传
### 2.1 凭证清单 > 这一块代码侧没有自动完成,因为需要从 msext 拷贝 ObjC 源码(含 MRC+ 修改 Build SettingsFile-level `-fno-objc-arc` flag+ Bridging Header。这些 pbxproj 改动对 Xcode 26 同步项目风险较高,留用户手动。
- [ ] **新 APIKey**[高德控制台](https://lbs.amap.com/dev/key/app) 用项目方账号新建应用,绑定本项目的 Bundle ID**msext 的 `b0d4a8e3fcbbcc0dd96283b7df6a4494` 不能复用**APIKey 与 Bundle ID 一一绑死) ### C.1 拷贝 opencore-amr 静态库
- [ ] **AMap SDK XCFramework**[官方下载页](https://lbs.amap.com/api/ios-location-sdk/download)拉取最新(截至 2026-06 推荐 v2.10.x+
- `AMapLocationKit.xcframework` — 定位主库
- `AMapFoundationKit.xcframework` — 基础库(必须配合 LocationKit
### 2.2 工程接入
1. **放置 SDK**
```
Vendor/AMap/
├── AMapLocationKit.xcframework
├── AMapFoundationKit.xcframework
└── README.md # 记录 SDK 版本 + APIKey 创建日期 + 绑定 Bundle ID
```
Target → General → Frameworks → Embed & Sign(两个都要)
2. **Info.plist**(仅原生侧)
```xml
<key>NSLocationWhenInUseUsageDescription</key>
<string>{gamehallname}需要访问您的位置以提供本地化服务</string>
```
`{gamehallname}` 占位由 ChannelConfig.plist 注入或硬编为渠道名。
### 2.3 代码触点
| 文件 | 改动 |
|---|---|
| **`Source/SDK/AMap/AMapWrapper.swift`**(新建) | 启动期调用顺序:`updatePrivacyShow(.didShow, privacyInfo: .didContain)` → `updatePrivacyAgree(.didAgree)` → `AMapServices.shared().apiKey = "..."`**iOS 14+ 隐私合规必走的 3 步**,少一步定位拿不到结果) |
| **`AppDelegate.swift`** | `didFinishLaunchingWithOptions` 内调 `AMapWrapper.bootstrap()` |
| **`Source/Location/LocationService.swift`**(新建,actor | `requestOnce() async throws -> LocationPayload`(一次定位)<br>`startContinuous(onUpdate:)` / `stop()`(持续定位)<br>包 `AMapLocationManager`,回调转 `async` |
| **`Source/Bridge/Handlers/StartLocationHandler.swift`** | 移除 stub,按 `data == 1` 持续 / 其它一次性<br>反向 callback `bridge.call("getlocationinfo", 9 字段)`<br>**字段名严格 1:1**`address` / `city` / `cityCode` / `country` / `district` / `latitude`**string**`String(format: "%f", lat)`/ `longitude`**string**/ `province`**小写 p**!与 `sharelogin.Province` 不同)/ `street`<br>失败:`getlocationinfo({errorCode:12, errorMsg:"缺少定位权限"})` |
| **`Source/Bridge/Handlers/BackGameDataHandler.swift`** | **必须加** `LocationService.shared.stop()` |
### 2.4 清理钩子
`BackGameDataHandler.swift` 的 `MainActor.run` 块加:
```swift
LocationService.shared.stop() // ← Phase 5 接入时取消注释
```
理由:msext `gameController.m:2473-2478 cleanUpAction` 在 backgameData 里调 `[locationManager stopUpdatingLocation]` + delegate nil;防止 pop 后后台定位继续耗电 + 回调悬空。
### 2.5 验收
- [ ] 首次启动 → 系统权限弹框(文案为 Info.plist 设置的中文)
- [ ] H5 调 `bridge.callHandler('startlocation', "1")` → 收到 `getlocationinfo({...9 字段...})``latitude` / `longitude` 是 string 类型,`province` 是小写 p
- [ ] 拒绝权限 → H5 收 `getlocationinfo({errorCode:12,errorMsg:"缺少定位权限"})`
- [ ] 子游戏定位中 → 调 `backgameData` 退出 → 大厅不再收到悬挂定位回调(验证 cleanUp 钩子)
### 2.6 风险
- **APIKey 绑定**Bundle ID 改了 / 多 Target 用不同 ID 都要重新申请 key,否则定位失败但无明显报错(高德回 errorCode 7「KEY 校验失败」)
- **隐私合规调用顺序**:iOS 14+ 必须先 `updatePrivacyShow` → `updatePrivacyAgree` 再 `apiKey`,否则定位接口直接拒绝
- **模拟器**:高德定位在模拟器走 Apple 的 CoreLocation 模拟坐标,定位精度低,必须真机测
---
## 3. opencore-amr 转码库(Phase 3.B
### 3.1 凭证清单
- 无凭证。二进制已在 msext 仓库内,**且已经过 fat header `segalign 8` 修复**(参 CLAUDE.md 父项目「已处理的定时炸弹」一节,2026-06-20 摘掉 `-ld_classic` 的同一组 `.a`
### 3.2 工程接入
1. **拷贝二进制**(已修复版)
```bash ```bash
mkdir -p ylgamehall/Vendor/opencore-amr/ mkdir -p ylgamehall/Vendor/opencore-amr/
cp ../daoqi/msext/msext/Class/Common/VoiceConvert/lib/libopencore-amrnb.a ylgamehall/Vendor/opencore-amr/ # 已修复版(fat header segalign 8)静态库
cp ../daoqi/msext/msext/Class/Common/VoiceConvert/lib/libopencore-amrwb.a ylgamehall/Vendor/opencore-amr/ cp ../daoqi/msext/msext/Class/Common/VoiceConvert/lib/libopencore-amrnb.a \
ylgamehall/Vendor/opencore-amr/
cp ../daoqi/msext/msext/Class/Common/VoiceConvert/lib/libopencore-amrwb.a \
ylgamehall/Vendor/opencore-amr/
# 拷头文件 # 拷头文件
cp -r ../daoqi/msext/msext/Class/Common/VoiceConvert/include ylgamehall/Vendor/opencore-amr/ cp -r ../daoqi/msext/msext/Class/Common/VoiceConvert/include \
ylgamehall/Vendor/opencore-amr/
# 拷 VoiceConverter ObjC wrapper(含 amrFileCodec / interf_*
cp -r ../daoqi/msext/msext/Class/Common/VoiceConvert \
ylgamehall/Vendor/opencore-amr/VoiceConverter
``` ```
2. **拷 VoiceConverterObjC wrapper** ### C.2 Xcode 工程配置
```
ylgamehall/Vendor/opencore-amr/VoiceConverter/
├── VoiceConverter.h # ObjC interface(保留 ObjCwrapper 简单不重写 Swift
├── VoiceConverter.m # MRC 代码可保留,加 -fno-objc-arc 编译标志或转 ARC
├── amrFileCodec.h/m
└── interf_*.h/c
```
3. **Xcode 工程配置** 1. 选中 Target → Build Phases → Link Binary With Libraries → `+` → Add Other → Add Files → 选两个 `.a`
- Target → Build Phases → Link Binary With Libraries 加入两个 `.a` 2. Target → Build Settings
- Target → Build Settings → Library Search Paths 加 `$(PROJECT_DIR)/ylgamehall/Vendor/opencore-amr` - **Library Search Paths**`$(PROJECT_DIR)/Vendor/opencore-amr`
- Target → Build Settings → Header Search Paths 加 `$(PROJECT_DIR)/ylgamehall/Vendor/opencore-amr/include` - **Header Search Paths**`$(PROJECT_DIR)/Vendor/opencore-amr/include`
- **不需要** `-ld_classic`(已用 `lipo -segalign 8` 修复 fat headerCLAUDE.md 父项目记录的修复同样适用 3.`Vendor/opencore-amr/VoiceConverter/` 下的 `.m` 文件拖入项目(Add to Target ylgamehall
- 若复用 msext MRC 的 `VoiceConverter.m`,加文件级 `-fno-objc-arc` 编译标志 4. 每个 `.m` 文件单独加 `-fno-objc-arc` 编译标志msext 是 MRC):
- Target → Build Phases → Compile Sources → 找到每个 `.m` → 双击 → 加 `-fno-objc-arc`
4. **Bridging Header**(已有 `ylgamehall-Bridging-Header.h` 则追加;无则新建) 5. 创建 Bridging Header `ylgamehall-Bridging-Header.h` 内容:
```objc ```objc
#import "VoiceConverter.h" #import "VoiceConverter.h"
``` ```
6. Target → Build Settings → **Objective-C Bridging Header** 设为 `ylgamehall/ylgamehall-Bridging-Header.h`
### 3.3 代码触点 ### C.3 接入 Swift 录音 + 上传链路(代码侧)
| 文件 | 改动 | 完成 C.1-C.2 后回到代码侧补两个文件:
|---|---|
| **`Source/Audio/VoiceCoder.swift`**(新建) | Swift wrapper`static func amrToWav(_ amrPath: URL) throws -> URL` / `static func wavToAmr(_ wavPath: URL) throws -> URL`<br>内部调 ObjC `VoiceConverter` |
| **`Source/Audio/AudioRecorder.swift`**(新建,actor | `record() async throws -> AudioFile``AVAudioRecorder` 录 WAV + 麦克风权限<br>录音参数与 msext `ChatVoiceRecorderVC` 等价:16kHz / 单声道 / 16-bit |
| **`Source/Bridge/Handlers/RemoteAudioHandler.swift`** | 移除 stub,分别接入:<br>**`prepareaudio`**(录音上传)→ `AudioRecorder.record()` → WAV→AMR `VoiceCoder.wavToAmr` → `QiniuUploader.upload`(见 §4 → `bridge.call("getaudiourl", {audiourl, time})`**子游戏页额外触发** `bridge.call("recordSuccess", {fileUrl, fileName, fileKey})`<br>**`mediaTypeAudio`**(远端回放)→ URLSession 下载 AMR → AMR→WAV `VoiceCoder.amrToWav` → `AVAudioPlayer` 播放<br>开始播放 `bridge.call("gameui_play_voice", user)` / 播放结束 `bridge.call("gameui_stop_voice", user)`<br>受 `VoicePlayingHandler` 的开关短路(=1 才播) |
| **`Info.plist`** | 加 `NSMicrophoneUsageDescription = "{gamehallname}需要访问您的麦克风录制语音消息"` |
| **`Source/Bridge/Handlers/BackGameDataHandler.swift`** | **必须加** `AudioRecorder.shared.cancel()` |
### 3.4 清理钩子
**`Source/Audio/VoiceCoder.swift`**Swift wrapper
```swift ```swift
AudioRecorder.shared.cancel() // ← Phase 3.C 接入时取消注释 import Foundation
QiniuUploader.shared.cancelInFlight() // ← Phase 3.C 接入时取消注释
public enum VoiceCoder {
public static func wavToAmr(_ wav: URL) throws -> URL {
let amr = wav.deletingPathExtension().appendingPathExtension("amr")
// VoiceConverter 是 msext ObjC 类(MRC),桥到 Swift 后直接调
let ok = VoiceConverter.convertWav(toAmr: wav.path, amrSavePath: amr.path)
guard ok else { throw VoiceCodeError.transcodeFailed }
return amr
}
public static func amrToWav(_ amr: URL) throws -> URL {
let wav = amr.deletingPathExtension().appendingPathExtension("wav")
let ok = VoiceConverter.convertAmr(toWav: amr.path, wavSavePath: wav.path)
guard ok else { throw VoiceCodeError.transcodeFailed }
return wav
}
public enum VoiceCodeError: Error { case transcodeFailed }
}
``` ```
理由:录音 / 上传任务跨 VC 生命周期容易遗留;msext 通过 `ChatVoiceRecorderVC` 生命周期间接清理,我们的 actor 实现不绑 VC 必须显式 cancel。
### 3.5 验收 **`Source/Audio/AudioRecorder.swift`**actor
```swift
import AVFoundation
- [ ] H5 调 `prepareaudio` → 弹麦克风权限 → 同意后开始录音 → 录完上传七牛 → 大厅收 `getaudiourl({audiourl:"https://...", time:"3"})` public actor AudioRecorder {
- [ ] 子游戏同样路径,额外收到 `recordSuccess({fileUrl, fileName, fileKey})` public static let shared = AudioRecorder()
- [ ] H5 调 `mediaTypeAudio({audiourl, user})` → 听到对方语音 + 收到 `gameui_play_voice` / 结束收 `gameui_stop_voice` public func record() async throws -> (file: URL, duration: Int) {
- [ ] `voicePlaying` 总开关设 0 → 调 `mediaTypeAudio` 不播放 // AVAudioRecorder 16kHz / mono / 16-bit WAV
// 等价 msext ChatVoiceRecorderVC 配置
// ... 实现略,约 60 行
}
public func cancel() { /* 停录音 */ }
}
```
**升级 `RemoteAudioHandler.swift` 的 prepareaudio 分支**
```swift
bridge.register("prepareaudio") { _, callback in
callback?(.string("Response from prepareaudio"))
Task { @MainActor in
do {
let rec = try await AudioRecorder.shared.record()
let amr = try VoiceCoder.wavToAmr(rec.file)
let uploaded = try await QiniuUploader.shared.upload(amr, timeSec: rec.duration)
bridge.call("getaudiourl", data: .object([
"audiourl": .string(uploaded.fileUrl),
"time": .string("\(uploaded.timeSec)")
]), callback: nil)
// 子游戏页额外触发(仅 SubGameViewController 注册的 handler 时才发,
// 需要在 SubGameViewController.registerBridgeHandlers 里包一层判别):
// bridge.call("recordSuccess", data: ...)
} catch {
// 失败兜底(msext 同款乐观策略:不反向通知 H5)
}
}
}
```
**升级 BackGameDataHandler 清理钩子**:取消那行 `AudioRecorder.shared.cancel()` 注释。
--- ---
## 4. 七牛云上传(Phase 3.C / 3.D ## 0. 凭证总表(沿用 msext
### 4.1 凭证清单 > 全部来自 daoqi/msext 原项目,**直接复用,无需重新申请**。
- [ ] **后台 token 颁发接口**:每次录音前向后台 GET `/api/qiniu/upload-token?scene=voice` 拿到上传凭证 + 域名 | 项 | 值 | 来源 |
- [ ] **七牛 Bucket 名 / CDN 域名**:项目方提供(可硬编到 BundleConfig,或随 ChannelConfig 注入) |---|---|---|
| **Bundle ID** | `com.skyapp.ylgamehall` | `daoqi/msext.xcodeproj` Release |
| **App 显示名** | `进贤聚友棋牌` | `SGDefineInfo.h:103 gamehallname` |
| 微信 AppID | `wx586a9b321e56efb7` | `SGDefineInfo.h:105 kAuthOpenID` |
| 微信 AppSecret | `b2792724b9565be23e8f5ba548f117cf` | `SGDefineInfo.h:107 Appsecret` |
| 微信 AuthScope | `snsapi_message,snsapi_userinfo,snsapi_friend,snsapi_contact` | `SGDefineInfo.h:104` |
| 微信 AuthState | `wechat_sdk` | `SGDefineInfo.h:106 kAuthState` |
| 高德定位 APIKey | `b0d4a8e3fcbbcc0dd96283b7df6a4494` | `daoqi/msext/Class/Common/APIKey.h:14` |
| 七牛 AccessKey | `dQbQLUm1jIuL9PEq4jd6VKB-6pPxPEdg7le9KeBm` | `QiniuConfig.m:12` |
| 七牛 SecretKey | `RCZpwLhAPoQ2sQQyWXzMJc7Po2MyZWfUJeW4Jmfq` | `QiniuConfig.m:13` |
| 七牛 BucketName | `iosaudio` | `QiniuConfig.m:16` |
| 七牛 CDN Domain | `iosaudio.daoqi88.cn` | `daoqi/msext/msext/qiniudomain/` 目录名 |
### 4.2 工程接入(SPM,优于二进制 ### 0.1 安全权衡(与父项目 CLAUDE.md 同步
1. Xcode → File → Add Package Dependencies 父项目 CLAUDE.md 第 6 节明确指出 `AppSecret` / `SecretKey` 客户端硬编码是已识别但**接受不修**的安全风险。msext IPA 已分发数年等价泄露,新外壳沿用同一 key + 沿用同款客户端直拼路径 → **同等安全等级**,**不引入新的攻击面**,**不需要后台中转接口**。
2. URL`https://github.com/qiniu/objc-sdk`
3. 版本:`8.9.x` 最新(avoid 7.x 历史 API
4. 勾选 target → Add Package
注:七牛官方 SPM 包是 ObjC,但 SPM 直接支持 ObjC + Swift 混合,不需要 bridging headerSPM 自动处理)。 ---
### 4.3 代码触点 ## 1. 微信 OpenSDKPhase 4.E)— 代码已就绪
| 文件 | 改动 | ### 1.1 凭证(已具备,沿用 msext)
参 §0 总表。
### 1.2 工程接入
参 §A.1(用户手动 Xcode 步骤)。
### 1.3 代码已落地
| 文件 | 状态 | 说明 |
|---|---|---|
| SPM `WechatOpenSDK-XCFramework` | ⏳ 待用户在 Xcode Add Package Dependencies | 见 §A.1Tencent 官方仓库 |
| `Source/SDK/WeChat/WeChatSDK.swift` | ✅ 已写 | `register()` + `handleOpenURL` 包 canImport |
| `Source/SDK/WeChat/WeChatManager.swift` | ✅ 已写 | authorize / shareLink async wrapper + WXApiDelegate |
| `Source/Login/WeChatAuth.swift` | ✅ 已写 | OAuth2 客户端直拼 sns/oauth2 + userinfo + 7 字段 |
| `Source/Bridge/Handlers/AccreditLoginHandler.swift` | ✅ 已升级 | 真实拉起授权 + sharelogin 反向 callbackProvince 大写 P |
| `Source/Share/WechatShare.swift` | ✅ 已升级 | 真实链接分享(type=2/3 截图待 Phase 4.F |
| `Source/Bridge/Handlers/BackGameDataHandler.swift` | ✅ 已加钩子 | `WXApi.delegate = nil` 子游戏 pop 时清理 |
| `AppDelegate.swift` | ✅ 已加注册 | `WeChatSDK.register()` |
| `SceneDelegate.swift` | ✅ 已加 openURL | `WeChatSDK.handleOpenURL(ctx.url)` |
| `Info.plist` | ✅ 已加 | CFBundleURLTypes + LSApplicationQueriesSchemes 微信 3 项 |
### 1.4 验收(用户 Xcode 加完 framework 后跑)
- [ ] H5 调 `bridge.callHandler('accreditlogin')` → 拉起微信 → 同意授权 → 大厅 H5 收 `sharelogin({openid,...,Province})`**Province 大写 P**
- [ ] H5 调 `bridge.callHandler('friendsSharetypeUrlToptitleDescript', {sharefriend:"2", ...})` → 拉起微信朋友圈
- [ ] H5 调 `sharefriend:"1"` → SharePanel 三按钮,点微信 → 拉起微信好友选择
- [ ] **关键回归**:截图分享 / QQ 分享 / 抖音分享路径不受影响
### 1.5 风险
- AppSecret / SecretKey 已永久泄露(msext IPA 已分发数年)。复用同一 secret 安全等级不变,不引入新风险。
- 企业签 / TF 分发,不上 App Store,不需要 Universal Links / 审核合规。
---
## 2. 高德定位 SDKPhase 5)— 代码已就绪
### 2.1 凭证(已具备)
参 §0 总表。APIKey 与 Bundle ID `com.skyapp.ylgamehall` 绑死,已复用。
### 2.2 工程接入
参 §A.2(用户手动 Xcode 步骤)。
### 2.3 代码已落地
| 文件 | 状态 |
|---|---| |---|---|
| **`Source/Network/QiniuTokenService.swift`**(新建,actor | `fetchUploadToken() async throws -> QiniuToken`(含 token / 上传域名 / Key 模板 / 过期时间)<br>缓存策略:token 失效前 30 秒强制刷新;并发请求合并 | | `Vendor/AMap/AMapFoundationKit.framework` + `AMapLocationKit.framework` | ✅ 已就位 |
| **`Source/Network/QiniuUploader.swift`**(新建,actor | `upload(_ file: URL, token: QiniuToken) async throws -> UploadedFile`<br>内部用 `QNUploadManager` 包成 asyncUploadedFile 含 `fileUrl` / `fileName` / `fileKey`<br>取消语义:`cancelInFlight()` 取消所有进行中任务(接清理钩子用) | | `Source/SDK/AMap/AMapWrapper.swift` | ✅ 已写 | 隐私合规 3 步 + apiKey 注入 |
| **`Source/Bridge/Handlers/RemoteAudioHandler.swift`** | `prepareaudio` 完整链路第 3 步:`QiniuUploader.upload(amrFile, token: await QiniuTokenService.shared.fetchUploadToken())` | | `Source/Location/LocationService.swift` | ✅ 已写 | actorrequestOnce 异步包装 + stop |
| `Source/Bridge/Handlers/StartLocationHandler.swift` | ✅ 已升级 | 真实定位 + getlocationinfo 9 字段(latitude/longitude string, province 小写 p/ 失败回 errorCode 12 |
| `Source/Bridge/Handlers/BackGameDataHandler.swift` | ✅ 已加钩子 | `LocationService.shared.stop()` |
| `AppDelegate.swift` | ✅ 已加注册 | `AMapWrapper.bootstrap()` |
| `Info.plist` | ✅ 已加 | NSLocationWhenInUseUsageDescription 中文文案 |
### 4.4 清理钩子 ### 2.4 验收
见 §3.4(与 opencore-amr 同一处加 `QiniuUploader.shared.cancelInFlight()`)。 - [ ] 首次启动 → 系统权限弹框(中文文案)
- [ ] H5 调 `startlocation` → 收 `getlocationinfo({...9 字段...})`**latitude/longitude 是 string、province 小写 p**
- [ ] 拒绝权限 → H5 收 `getlocationinfo({errorCode:12, errorMsg:"缺少定位权限"})`
- [ ] 子游戏定位中 → 退出子游戏 → 大厅不再收悬挂定位回调
### 4.5 验收 ### 2.5 风险
- [ ] H5 调 `prepareaudio` → 录音 3 秒 → 上传 → 大厅收 `getaudiourl({audiourl:"https://your-cdn.com/voice/xxx.amr", time:"3"})` - **APIKey 绑定 Bundle ID**:本项目已对齐 `com.skyapp.ylgamehall`,可复用。**改 Bundle ID 必须重新申请 key**
- [ ] 录音中 / 上传中 → 退出子游戏 → 任务被 cancel,不再有悬空网络写 - **模拟器定位精度低**:必须真机测
- [ ] token 过期场景:模拟后台返回过期 token → uploader 自动刷新重试一次
### 4.6 风险 ---
- **token 时效**:通常 1 小时;高频录音场景需要 caching + 失效前预刷新 ## 3. opencore-amr 转码库(Phase 3.B)— 用户手动
- **上传失败重试**:默认 3 次,超时建议设 30s(语音文件通常 < 100KB
- **CDN 域名 HTTPS**H5 业务侧拿到 `audiourl` 后会请求播放,必须保证域名 HTTPSNSAppTransportSecurity 我们没收紧 ATS,但七牛默认走 HTTPS) 参 §C(用户手动步骤)。代码侧的 wrapper 文件待用户完成 C.1-C.2 后自行新增(§C.3 已给出代码骨架)。
---
## 4. 七牛云上传(Phase 3.C / 3.D)— 代码已就绪(除录音链路)
### 4.1 凭证(已具备)
参 §0 总表。客户端用 AccessKey + SecretKey 本地 HMAC-SHA1 拼 token(与 msext 等价路径),**无后台依赖**。
### 4.2 工程接入
参 §A.3(用户手动添加 SPM 依赖)。
### 4.3 代码已落地
| 文件 | 状态 |
|---|---|
| `Source/Network/QiniuConfig.swift` | ✅ 已写 | 4 项常量(AccessKey / SecretKey / BucketName / cdnDomain+ publicURL 拼接 |
| `Source/Network/QiniuTokenSigner.swift` | ✅ 已写 | 纯 Swift 用 `CryptoKit.HMAC<Insecure.SHA1>` + Base64URL 自签 token(与 msext QiniuManager.m:200-230 等价) |
| `Source/Network/QiniuUploader.swift` | ✅ 已写 | actor`upload(_ file: URL, timeSec: Int) async throws -> UploadedFile`,包 `QNUploadManager` 异步 |
### 4.4 上层调用待 §C 完成
`Source/Bridge/Handlers/RemoteAudioHandler.swift` 的 `prepareaudio` 真实链路依赖 §C 的 AMR 转码 + AudioRecorder。完成后取消 §C.3 的 TODO 注释即可激活全链路。
### 4.5 验收(依赖 §C 完成)
- [ ] H5 调 `prepareaudio` → 录音 3 秒 → 上传 → 大厅 H5 收 `getaudiourl({audiourl:"http://iosaudio.daoqi88.cn/xxx.amr", time:"3"})`
- [ ] 浏览器粘贴 `audiourl` 能直接下载 amr 文件
- [ ] 退出子游戏 → 进行中上传任务被 cancelSwift Task.cancel 传播)
--- ---
## 5. 接入完毕的清单检查 ## 5. 接入完毕的清单检查
每接入完一个 SDK跑一遍: 每接入完一个 SDK 跑一遍:
- [ ] `BuildProject` 成功 - [ ] `BuildProject` 成功
- [ ] `Vendor/<SDK>/README.md` 已记录版本号 / 来源 / 凭证创建日期 - [ ] `Vendor/<SDK>/README.md` 已记录版本号 / 来源 / 凭证沿用 msext 的备注 — 已就位
- [ ] Info.plist 改动只在原生侧,未触 H5 任何文件 - [ ] Info.plist 改动只在原生侧,未触 H5 任何文件 — 已就位
- [ ] `BackGameDataHandler.swift` 对应清理钩子已加(每个 SDK 都要回到此处补 1 行) - [ ] `BackGameDataHandler.swift` 对应清理钩子已加 — 微信 + 高德已加 canImport 守卫;录音待 §C
- [ ] `docs/Development-Plan.md §5 / §8` 进度勾选已同步(提交时附 commit 备注「Plan 进度已勾选」) - [ ] `docs/Development-Plan.md §5 / §8` 进度勾选已同步
- [ ] `docs/Verification-Checklist.md` 对应 Phase 验证项已补充 - [ ] `docs/Verification-Checklist.md` 对应 Phase 验证项已补充
- [ ] 真机跑过该 SDK 的验收项(模拟器对 SDK 行为不可靠:高德、微信、AVAudioRecorder 都受影响 - [ ] 真机跑过验收项(模拟器对微信 / 高德 / AVAudioRecorder 都不可靠
- [ ] **Bundle ID 没改**`com.skyapp.ylgamehall`),改了所有 key 失效
--- ---
@@ -288,9 +326,9 @@ QiniuUploader.shared.cancelInFlight() // ← Phase 3.C 接入时取消注释
接入过程中若行为与文档描述不一致,**优先参考原 daoqi 工程实现**(参 `CLAUDE.md`「原项目 daoqi:遇到问题时的参考来源」一节): 接入过程中若行为与文档描述不一致,**优先参考原 daoqi 工程实现**(参 `CLAUDE.md`「原项目 daoqi:遇到问题时的参考来源」一节):
- 微信回调字段:`daoqi/msext/Class/RootVC/NewRootVC.m:2428``sharelogin` 7 字段,含 `Province` 大写) - 微信 OAuth + 7 字段:`daoqi/msext/Class/RootVC/NewRootVC.m:2428``sharelogin` 7 字段,含 `Province` 大写)
- 微信 SDK 12 个 MMAPP flag`daoqi/msext/AppDelegate.m` 搜 `registerApp`
- 高德 9 字段:`daoqi/msext/Class/RootVC/gameController.m:2528 / 2557``getlocationinfo` 字段顺序与类型) - 高德 9 字段:`daoqi/msext/Class/RootVC/gameController.m:2528 / 2557``getlocationinfo` 字段顺序与类型)
- AMR 转码:`daoqi/msext/Class/Common/VoiceConvert/``VoiceConverter.h/m`、`amrFileCodec.h/m` - AMR 转码:`daoqi/msext/Class/Common/VoiceConvert/``VoiceConverter.h/m`、`amrFileCodec.h/m`
- 七牛上传`daoqi/msext/Class/Utils/QiniuManager.m`(参数命名 + 回调结构 - 七牛 token 自签算法`daoqi/msext/Class/Utils/QiniuManager.m:200-230`putPolicy + HMAC-SHA1 + Base64URL
- 七牛域名读取:`daoqi/msext/AppDelegate.m:142` 用 `FuncPublic filename:@"qiniudomain"` 读 `Resources/qiniudomain/` 下第一个子目录名作为实际 Domain
**遇到契约边界 / 历史包袱时**:照搬 msext 字面 + 字段名 + 顺序,不要凭语义猜测。原项目稳定运行多年的写法是真理,文档 / 设计稿可能有传抄错误(已在 CLAUDE.md「典型案例」积累若干)。
+7
View File
@@ -11,6 +11,13 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
application.applicationSupportsShakeToEdit = true application.applicationSupportsShakeToEdit = true
// Phase 4.E SDK canImport no-op
WeChatSDK.register()
// Phase 5 SDK iOS 14+ 3 canImport no-op
AMapWrapper.bootstrap()
return true return true
} }
+31 -1
View File
@@ -52,7 +52,7 @@
canOpenURL 总返回 false)。本项目分享接入: canOpenURL 总返回 false)。本项目分享接入:
- QQURL Scheme 路径,不依赖 SDK):mqq / mqqapi / mqqopensdkfriend 等 - QQURL Scheme 路径,不依赖 SDK):mqq / mqqapi / mqqopensdkfriend 等
- 抖音(URL Scheme + Photos 路径,不依赖 SDK):snssdk1128 - 抖音(URL Scheme + Photos 路径,不依赖 SDK):snssdk1128
- 微信(Phase 4.E SDK 接入时再加 weixin / weixinULAPI / weixinURLParamsAPI - 微信(Phase 4.E SDK 接入):weixin / weixinULAPI / weixinURLParamsAPI
--> -->
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
@@ -63,6 +63,25 @@
<string>mqqopensdkapiV3</string> <string>mqqopensdkapiV3</string>
<string>mqqopensdkapiV4</string> <string>mqqopensdkapiV4</string>
<string>snssdk1128</string> <string>snssdk1128</string>
<string>weixin</string>
<string>weixinULAPI</string>
<string>weixinURLParamsAPI</string>
</array>
<!--
CFBundleURLTypes — 第三方 SDK 回调跳回本 App 用的 URL Scheme。
- 微信(Phase 4.E SDK 接入):wx586a9b321e56efb7(沿用 msext AppID
docs/SDK-Integration-Guide.md §0
-->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>weixin</string>
<key>CFBundleURLSchemes</key>
<array>
<string>wx586a9b321e56efb7</string>
</array>
</dict>
</array> </array>
<!-- <!--
NSPhotoLibraryAddUsageDescription — Phase 4.D 抖音分享需要保存截图 / 远端图 NSPhotoLibraryAddUsageDescription — Phase 4.D 抖音分享需要保存截图 / 远端图
@@ -71,5 +90,16 @@
--> -->
<key>NSPhotoLibraryAddUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>
<string>需要将分享内容保存到相册,以便您在抖音中选取分享</string> <string>需要将分享内容保存到相册,以便您在抖音中选取分享</string>
<!--
NSLocationWhenInUseUsageDescription — Phase 5 高德定位 startlocation handler
使用前提(iOS 14+ 必须显式申请)。文案用 SGDefineInfo.h gamehallname 字面。
-->
<key>NSLocationWhenInUseUsageDescription</key>
<string>进贤聚友棋牌需要访问您的位置以提供本地化服务</string>
<!--
NSMicrophoneUsageDescription — Phase 3.C 录音 (prepareaudio handler) 使用前提。
-->
<key>NSMicrophoneUsageDescription</key>
<string>进贤聚友棋牌需要访问您的麦克风录制语音消息</string>
</dict> </dict>
</plist> </plist>
+11
View File
@@ -28,4 +28,15 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
self.window = window self.window = window
window.makeKeyAndVisible() window.makeKeyAndVisible()
} }
/// SDK /
/// QQ msext AppDelegate.m QQ URL Scheme SDK
/// handler QQ SDK
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
for ctx in URLContexts {
// Phase 4.E canImport no-op
if WeChatSDK.handleOpenURL(ctx.url) { continue }
// / SDK
}
}
} }
@@ -2,18 +2,17 @@
// AccreditLoginHandler.swift // AccreditLoginHandler.swift
// ylgamehall // ylgamehall
// //
// H5 Native handler stubaccreditlogin // H5 Native handleraccreditlogin
// docs/H5-Native-Contract.md §3.1 1 + §3.2 10sharelogin // docs/H5-Native-Contract.md §3.1 1 + §3.2 10sharelogin
// //
// Phase 4.A stub cb Phase 4.B SDK + AppID + // Phase 4.E
// Universal Links + /wechat/login fallback // 1. WeChatAuth.authorize() + oauth2 + userinfo
// (msext NewRootVC.m:2415-2428 沿 AppID/AppSecret/scope)
// 2. bridge.call("sharelogin", data: 7 ) callback
// 1:1 `Province` P / `city` danbian
// //
// Phase 4.B // canImport(WechatOpenSDK) SDK stub Phase 4.A
// 1. WeChatManager.shared.authorize() WXAuthCodestate UUID // callback sharelogin Xcode Embed & Sign
// 2. WeChatAuth.exchangeForUser(code:) /wechat/login
// sns/oauth2/access_token + sns/userinfo
// 3. bridge.call("sharelogin", data: 7 ) callback
// 1:1 Province Pcity danbian:
// //
import Foundation import Foundation
@@ -21,11 +20,32 @@ import Foundation
public enum AccreditLoginHandler { public enum AccreditLoginHandler {
public static func register(on bridge: any BridgeProtocol) { public static func register(on bridge: any BridgeProtocol) {
// 1accreditlogin
// cb: "Response from accreditlogin"
// sharelogin callbackPhase 4.B
bridge.register("accreditlogin") { _, callback in bridge.register("accreditlogin") { _, callback in
callback?(.string("Response from accreditlogin")) callback?(.string("Response from accreditlogin"))
#if canImport(WechatOpenSDK)
// SDK 7 callback sharelogin
Task { @MainActor in
do {
let user = try await WeChatAuth.authorize()
bridge.call("sharelogin", data: .object([
// 1:1 contract §3.2 [10] / msext NewRootVC.m:2428
"openid": .string(user.openid),
"headimgurl": .string(user.headimgurl),
"nickname": .string(user.nickname),
"sex": .number(Double(user.sex)),
"city": .string(user.city),
"Province": .string(user.Province), // P
"unionid": .string(user.unionid)
]), callback: nil)
} catch WeChatAuthError.userCancelled {
// msext alert sharelogin
} catch {
// msext H5
// Phase 9 Sentry breadcrumb
}
}
#endif
} }
} }
} }
@@ -17,29 +17,24 @@
// msext // msext
// //
// //
// SDK msext backgameData / cleanUpAction // SDK SDK
// stub SDK
// MainActor.run
// //
// - Phase 4.E SDK`WXApi.delegate = nil` // Phase 4.E `WXApi.delegate = nil`canImport Xcode Embed
// msext gameController.m:699 pop SubGameVC // Phase 5 `LocationService.shared.stop()`canImport
// // Xcode Embed AMapLocationKit
// - Phase 5 `LocationService.shared.stop()` + delegate nil // Phase 3.B/C/D / `AudioRecorder.shared.cancel()` opencore-amr
// msext gameController.m:2473-2478 cleanUpAction + // `QiniuUploader.cancelInFlight()` Task.cancel
// // Phase 8 Agora`agoraKit.leaveChannel(...)`
//
// - Phase 3.B/C/D / `Recorder.shared.cancel()` +
// `Uploader.shared.cancelInFlight()`
// msext ChatVoiceRecorderVC VC
// cancel pop
//
// - Phase 8 Agora`agoraKit.leaveChannel(...)`
// VideoRoomHandlers stub // VideoRoomHandlers stub
// //
// //
import Foundation import Foundation
#if canImport(WechatOpenSDK)
import WechatOpenSDK
#endif
public enum BackGameDataHandler { public enum BackGameDataHandler {
public static func register(on bridge: any BridgeProtocol) { public static func register(on bridge: any BridgeProtocol) {
@@ -52,7 +47,25 @@ public enum BackGameDataHandler {
// - .string // - .string
// - JSON H5 // - JSON H5
let payload = serialize(data) let payload = serialize(data)
// push SDK pop delegate /
// msext gameController.m:691-709 + cleanUpAction
AudioPlayer.shared.stopAllBackground() AudioPlayer.shared.stopAllBackground()
#if canImport(WechatOpenSDK)
// msext gameController.m:699 WXApi delegate VC
WXApi.delegate = nil
#endif
#if canImport(AMapLocationKit)
// msext gameController.m:2473-2478 cleanUpAction
LocationService.shared.stop()
#endif
// Phase 3.C / Phase 3.B QiniuUploader.cancelInFlight
// Task.cancel AudioRecorder opencore-amr
// AudioRecorder.shared.cancel() // Phase 3.C
AppCoordinator.shared.popSubGame(returningData: payload) AppCoordinator.shared.popSubGame(returningData: payload)
} }
callback?(.string("backgameData")) callback?(.string("backgameData"))
@@ -2,15 +2,18 @@
// StartLocationHandler.swift // StartLocationHandler.swift
// ylgamehall // ylgamehall
// //
// H5 Native handlerstartlocation Phase 5 stub // H5 Native handlerstartlocation Phase 5 SDK
// docs/H5-Native-Contract.md §3.1 20 + §3.2 6 // docs/H5-Native-Contract.md §3.1 20 + §3.2 6
// //
// Stub //
// - startlocation cb "startlocation" undefinedH5 // 1. LocationService.shared.requestOnce()actor AMapLocationManager
// - **** getlocationinfo callback SDK // 2. bridge.call("getlocationinfo", 9 ) callback
// - H5 getlocationinfo Phase 5 AMapLocationKit // - latitude / longitude **string**`String(format: "%f")`
// - province p sharelogin.Province
// 3. getlocationinfo({errorCode:12, errorMsg:""})
// //
// CLAUDE.md A H5 使 // canImport(AMapLocationKit) SDK LocationService sdkNotLinked
// stub Phase 2 callback getlocationinfo
// //
import Foundation import Foundation
@@ -19,9 +22,41 @@ public enum StartLocationHandler {
public static func register(on bridge: any BridgeProtocol) { public static func register(on bridge: any BridgeProtocol) {
bridge.register("startlocation") { _, callback in bridge.register("startlocation") { _, callback in
// Phase 2 stub callback // msext cb
// Phase 5 LocationKit + AMapLocationKit.xcframework
callback?(.string("startlocation")) callback?(.string("startlocation"))
#if canImport(AMapLocationKit)
// SDK callback 9
// data == 1 Phase 5.4
Task { @MainActor in
do {
let payload = try await LocationService.shared.requestOnce()
bridge.call("getlocationinfo", data: .object([
"address": .string(payload.address),
"city": .string(payload.city),
"cityCode": .string(payload.cityCode),
"country": .string(payload.country),
"district": .string(payload.district),
"latitude": .string(payload.latitude), // string
"longitude": .string(payload.longitude), // string
"province": .string(payload.province), // p
"street": .string(payload.street)
]), callback: nil)
} catch LocationError.authorizationDenied {
// msext gameController.m:2507
bridge.call("getlocationinfo", data: .object([
"errorCode": .number(12),
"errorMsg": .string("缺少定位权限")
]), callback: nil)
} catch {
// 12msext
bridge.call("getlocationinfo", data: .object([
"errorCode": .number(12),
"errorMsg": .string("缺少定位权限")
]), callback: nil)
}
}
#endif
} }
} }
} }
@@ -0,0 +1,105 @@
//
// LocationService.swift
// ylgamehall
//
// AMapLocationManager /
// §3.2 6 getlocationinfo 9 msext gameController.m:2491-2557
//
// canImport(AMapLocationKit) SDK LocationService
// LocationError.sdkNotLinkedhandler 退 stub
//
import Foundation
import CoreLocation
#if canImport(AMapLocationKit)
import AMapLocationKit
#endif
#if canImport(AMapFoundationKit)
import AMapFoundationKit
#endif
/// 9 1:1 H5 contract §3.2 [6]
/// - latitude / longitude ** string** (`String(format: "%f", ...)`)
/// - province ** p** sharelogin.Province
public struct LocationPayload: Sendable {
public let address: String
public let city: String
public let cityCode: String
public let country: String
public let district: String
public let latitude: String
public let longitude: String
public let province: String
public let street: String
}
public enum LocationError: Error, Sendable {
case sdkNotLinked
case authorizationDenied
case timeout
case underlying(any Error)
}
@MainActor
public final class LocationService {
public static let shared = LocationService()
public init() {}
#if canImport(AMapLocationKit)
private let manager: AMapLocationManager = {
let m = AMapLocationManager()
m.desiredAccuracy = kCLLocationAccuracyHundredMeters
m.locationTimeout = 6
m.reGeocodeTimeout = 3
m.locatingWithReGeocode = true
return m
}()
#endif
/// + msext locAction + completionBlock
public func requestOnce() async throws -> LocationPayload {
#if canImport(AMapLocationKit)
return try await withCheckedThrowingContinuation { (cont: CheckedContinuation<LocationPayload, Error>) in
manager.requestLocation(withReGeocode: true) { loc, regeo, err in
if let err = err as NSError? {
// msext 12 errorCodehandler
if err.code == AMapLocationErrorCode.locateFailed.rawValue {
cont.resume(throwing: LocationError.authorizationDenied)
} else {
cont.resume(throwing: LocationError.underlying(err))
}
return
}
guard let loc, let regeo else {
cont.resume(throwing: LocationError.timeout)
return
}
cont.resume(returning: LocationPayload(
address: regeo.formattedAddress ?? "",
city: regeo.city ?? "",
cityCode: regeo.citycode ?? "",
country: regeo.country ?? "",
district: regeo.district ?? "",
latitude: String(format: "%f", loc.coordinate.latitude),
longitude: String(format: "%f", loc.coordinate.longitude),
province: regeo.province ?? "",
street: regeo.street ?? ""
))
}
}
#else
throw LocationError.sdkNotLinked
#endif
}
/// msext gameController.m:2473-2478 cleanUpAction
public func stop() {
#if canImport(AMapLocationKit)
manager.stopUpdatingLocation()
manager.delegate = nil
#endif
}
}
+128
View File
@@ -0,0 +1,128 @@
//
// WeChatAuth.swift
// ylgamehall
//
// OAuth2 msext 4 ViewController
// Code access_token userinfo 7 user
//
// AppSecret = CLAUDE.md
// msext IPA Appsecret 沿 secret
// /wechat/login docs/SDK-Integration-Guide.md §0.1
//
// 1:1 msext NewRootVC.m:2428 sharelogin payload
// openid / headimgurl / nickname / sex / city / Province P/ unionid
//
import Foundation
public struct WeChatUser: Sendable {
public let openid: String
public let headimgurl: String
public let nickname: String
/// number1= 2= 0=H5 sharelogin number
public let sex: Int
/// city / province msext `danbian:` H5 JSON
public let city: String
/// Pmsext NewRootVC.m:2428 payload key "Province"
public let Province: String
public let unionid: String
}
public enum WeChatAuthError: Error, Sendable {
case userCancelled
case authStepFailed(Int)
case tokenRequestFailed(any Error)
case tokenResponseInvalid(String)
case userInfoRequestFailed(any Error)
case userInfoResponseInvalid(String)
}
@MainActor
public enum WeChatAuth {
/// AppSecret沿 msext SGDefineInfo.h:107 sns/oauth2
static let appSecret = "b2792724b9565be23e8f5ba548f117cf"
/// OAuth2 code access_token userinfo 7 user
public static func authorize() async throws -> WeChatUser {
// Step 1: code
let payload: WXAuthCodePayload
do {
payload = try await WeChatManager.shared.authorize()
} catch let WeChatError.authFailed(errCode) {
// msext errCode == -2
if errCode == -2 { throw WeChatAuthError.userCancelled }
throw WeChatAuthError.authStepFailed(errCode)
}
// Step 2: code access_token + openid
let token = try await exchangeAccessToken(code: payload.code)
// Step 3: access_token + openid userinfo7
return try await fetchUserInfo(accessToken: token.accessToken, openid: token.openid)
}
// MARK: - Private OAuth2 step 2/3 directly hitting api.weixin.qq.com
private struct TokenResponse {
let accessToken: String
let openid: String
}
private static func exchangeAccessToken(code: String) async throws -> TokenResponse {
let urlString = "https://api.weixin.qq.com/sns/oauth2/access_token"
+ "?appid=\(WeChatSDK.appID)"
+ "&secret=\(appSecret)"
+ "&code=\(code)"
+ "&grant_type=authorization_code"
guard let url = URL(string: urlString) else {
throw WeChatAuthError.tokenResponseInvalid("bad url")
}
let data: Data
do {
(data, _) = try await URLSession.shared.data(from: url)
} catch {
throw WeChatAuthError.tokenRequestFailed(error)
}
guard let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
let accessToken = obj["access_token"] as? String,
let openid = obj["openid"] as? String
else {
let raw = String(data: data, encoding: .utf8) ?? "<binary>"
throw WeChatAuthError.tokenResponseInvalid(raw)
}
return TokenResponse(accessToken: accessToken, openid: openid)
}
private static func fetchUserInfo(accessToken: String, openid: String) async throws -> WeChatUser {
let urlString = "https://api.weixin.qq.com/sns/userinfo"
+ "?access_token=\(accessToken)"
+ "&openid=\(openid)"
guard let url = URL(string: urlString) else {
throw WeChatAuthError.userInfoResponseInvalid("bad url")
}
let data: Data
do {
(data, _) = try await URLSession.shared.data(from: url)
} catch {
throw WeChatAuthError.userInfoRequestFailed(error)
}
guard let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
let raw = String(data: data, encoding: .utf8) ?? "<binary>"
throw WeChatAuthError.userInfoResponseInvalid(raw)
}
return WeChatUser(
openid: (obj["openid"] as? String) ?? "",
headimgurl: (obj["headimgurl"] as? String) ?? "",
nickname: (obj["nickname"] as? String) ?? "",
sex: (obj["sex"] as? Int) ?? 0,
// msext danbian: JSON
city: danbian((obj["city"] as? String) ?? ""),
Province: danbian((obj["province"] as? String) ?? ""),
unionid: (obj["unionid"] as? String) ?? ""
)
}
/// msext FuncPublic.danbian:
private static func danbian(_ s: String) -> String {
s.replacingOccurrences(of: "'", with: "")
}
}
@@ -0,0 +1,35 @@
//
// QiniuConfig.swift
// ylgamehall
//
// 沿 msext QiniuConfig.m
//
// SecretKey = CLAUDE.md
// AppSecret msext 沿
//
import Foundation
public enum QiniuConfig {
/// AccessKey沿 msext QiniuConfig.m:12
public static let accessKey = "dQbQLUm1jIuL9PEq4jd6VKB-6pPxPEdg7le9KeBm"
/// SecretKey沿 msext QiniuConfig.m:13 HMAC-SHA1 putPolicy
public static let secretKey = "RCZpwLhAPoQ2sQQyWXzMJc7Po2MyZWfUJeW4Jmfq"
/// 沿 msext QiniuConfig.m:16 putPolicy scope
public static let bucketName = "iosaudio"
/// CDN 沿 msext qiniudomain/
/// msext fallback `iosaudio.daoqi8888.cn` `daoqi88`
/// 沿
public static let cdnDomain = "iosaudio.daoqi88.cn"
/// key msext QiniuConfig.m:22
public static let recordingDirectory = ""
/// 访 URL`http://{cdnDomain}/{key}`msext QiniuManager.m:197
public static func publicURL(forKey key: String) -> String {
"http://\(cdnDomain)/\(key)"
}
}
@@ -0,0 +1,68 @@
//
// QiniuTokenSigner.swift
// ylgamehall
//
// token HMAC-SHA1 + Base64URL
// msext QiniuManager.m:200-230 generateUploadToken
//
// putPolicy
// { "scope": "iosaudio[:key]", "deadline": now + expiresIn }
//
// putPolicy JSON Base64URL HMAC-SHA1(secretKey) Base64URL
// `${AccessKey}:${sign}:${encodedPolicy}`
//
// CryptoKitiOS 13+ Qiniu SDK Swift token
//
import Foundation
import CryptoKit
public enum QiniuTokenSigner {
/// token
/// - Parameters:
/// - key: keyscope = bucketallow overridescope = bucket:key
/// - expiresIn: token 3600
public static func uploadToken(key: String? = nil, expiresIn: TimeInterval = 3600) -> String {
// 1. scope
let scope: String
if let key, !key.isEmpty {
scope = "\(QiniuConfig.bucketName):\(key)"
} else {
scope = QiniuConfig.bucketName
}
let deadline = Int(Date().timeIntervalSince1970 + expiresIn)
// 2. putPolicy JSON key msext scope / deadline
// msext 便
let putPolicy: [String: Any] = [
"scope": scope,
"deadline": deadline
]
let policyData = (try? JSONSerialization.data(withJSONObject: putPolicy, options: [.sortedKeys])) ?? Data()
// 3. Base64URL(putPolicy)
let encodedPolicy = base64URLEncode(policyData)
// 4. HMAC-SHA1(secretKey, encodedPolicy) Base64URL
let secretKeyData = Data(QiniuConfig.secretKey.utf8)
let key = SymmetricKey(data: secretKeyData)
let signature = HMAC<Insecure.SHA1>.authenticationCode(
for: Data(encodedPolicy.utf8),
using: key
)
let encodedSign = base64URLEncode(Data(signature))
// 5. token = AccessKey:encodedSign:encodedPolicy
return "\(QiniuConfig.accessKey):\(encodedSign):\(encodedPolicy)"
}
/// Base64URL: Base64 + -/ _ =
/// SDK `QNUrlSafeBase64.encodeData:`
private static func base64URLEncode(_ data: Data) -> String {
data.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
@@ -0,0 +1,97 @@
//
// QiniuUploader.swift
// ylgamehall
//
// actor wrapper `QNUploadManager` async/await
//
// canImport(QiniuSDK) Xcode https://github.com/qiniu/objc-sdk
// Swift Package Dependencies no-opupload
// QiniuUploadError.sdkNotLinked handler stub
//
// UploadedFile H5 `getaudiourl` / `recordSuccess`
//
import Foundation
#if canImport(QiniuSDK)
import QiniuSDK
#endif
public struct UploadedFile: Sendable {
public let fileUrl: String // 访 URLhttp://{domain}/{key}
public let fileName: String //
public let fileKey: String // keypath-on-cdn
public let timeSec: Int //
}
public enum QiniuUploadError: Error, Sendable {
case sdkNotLinked
case fileNotFound(String)
case uploadFailed(any Error)
case responseInvalid
}
public actor QiniuUploader {
public static let shared = QiniuUploader()
public init() {}
///
/// - Parameters:
/// - localFile: URLamr
/// - timeSec: H5 `getaudiourl`
/// - Returns: UploadedFile URL
public func upload(_ localFile: URL, timeSec: Int) async throws -> UploadedFile {
#if canImport(QiniuSDK)
let fm = FileManager.default
guard fm.fileExists(atPath: localFile.path) else {
throw QiniuUploadError.fileNotFound(localFile.path)
}
// key`{recordingDirectory}{uuid}.{ext}` msext QiniuManager.m:55-60
let uuid = UUID().uuidString.lowercased().replacingOccurrences(of: "-", with: "")
let ext = localFile.pathExtension
let key = QiniuConfig.recordingDirectory + uuid + (ext.isEmpty ? "" : ".\(ext)")
// token QiniuTokenSigner / msext QiniuManager
let token = QiniuTokenSigner.uploadToken(key: key)
return try await withCheckedThrowingContinuation { (cont: CheckedContinuation<UploadedFile, Error>) in
let mgr = QNUploadManager()
let fileName = localFile.lastPathComponent
mgr.putFile(
localFile.path,
key: key,
token: token,
complete: { info, savedKey, _ in
if let info, info.isOK, let savedKey {
let url = QiniuConfig.publicURL(forKey: savedKey)
cont.resume(returning: UploadedFile(
fileUrl: url,
fileName: fileName,
fileKey: savedKey,
timeSec: timeSec
))
} else if let error = info?.error {
cont.resume(throwing: QiniuUploadError.uploadFailed(error))
} else {
cont.resume(throwing: QiniuUploadError.responseInvalid)
}
},
option: nil
)
}
#else
throw QiniuUploadError.sdkNotLinked
#endif
}
/// BackGameDataHandler
/// QNUploadManager API QNUploadOption.cancellationSignal
/// task Swift Task.cancel withCheckedThrowingContinuation
/// cancellation throw CancellationError
public func cancelInFlight() {
// no-op Task.cancel cancel QNUploadOption cancellationSignal
}
}
@@ -0,0 +1,42 @@
//
// AMapWrapper.swift
// ylgamehall
//
// SDK iOS 14+ 3
// updatePrivacyShow updatePrivacyAgree apiKey
// errorCode 7 KEY
//
// APIKey 沿 msext APIKey.h:14 Bundle ID com.skyapp.ylgamehall
//
// canImport(AMapFoundationKit) Vendor/AMap/*.framework Xcode
// Target Frameworks no-op
//
import Foundation
#if canImport(AMapFoundationKit)
import AMapFoundationKit
#endif
@MainActor
public enum AMapWrapper {
/// APIKey沿 msext APIKey.h:14 Bundle ID com.skyapp.ylgamehall
public static let apiKey = "b0d4a8e3fcbbcc0dd96283b7df6a4494"
/// AppDelegate.didFinishLaunchingWithOptions
/// iOS 14+ updatePrivacyShow updatePrivacyAgree apiKey
public static func bootstrap() {
#if canImport(AMapFoundationKit)
AMapLocationServices.updatePrivacyShow(.didShow, privacyInfo: .didContain)
AMapLocationServices.updatePrivacyAgree(.didAgree)
AMapServices.shared().apiKey = apiKey
#else
// SDK no-op
#endif
}
}
#if canImport(AMapFoundationKit)
private typealias AMapLocationServices = AMapServices
#endif
@@ -0,0 +1,160 @@
//
// WeChatManager.swift
// ylgamehall
//
// SDK delegate + authorize + share
// WXApiDelegate async/awaitstate UUID
//
// Design §8.5msext WXApiManager.m
//
import Foundation
import UIKit
#if canImport(WechatOpenSDK)
import WechatOpenSDK
#endif
@MainActor
public final class WeChatManager: NSObject {
public static let shared = WeChatManager()
/// scope沿 msext SGDefineInfo.h:104
public static let authScope = "snsapi_message,snsapi_userinfo,snsapi_friend,snsapi_contact"
/// resolve authorize continuationstate UUID
private var authPending: [String: CheckedContinuation<WXAuthCodePayload, Error>] = [:]
/// resolve share continuationFIFO ID
private var sharePending: [CheckedContinuation<Void, Error>] = []
public override init() {
super.init()
}
// MARK: - Authorize
/// msext state = "wechat_sdk" + scope = 4
/// code WeChatAuth.exchangeForUser 7
public func authorize() async throws -> WXAuthCodePayload {
#if canImport(WechatOpenSDK)
let state = UUID().uuidString
return try await withCheckedThrowingContinuation { cont in
authPending[state] = cont
let req = SendAuthReq()
req.scope = Self.authScope
req.state = state
WXApi.send(req)
}
#else
throw WeChatError.sdkNotLinked
#endif
}
// MARK: - Share
public enum ShareScene {
case session //
case timeline //
}
public struct ShareLink: Sendable {
public let url: String
public let title: String
public let desc: String
public let thumbnailURL: String?
public init(url: String, title: String, desc: String, thumbnailURL: String?) {
self.url = url; self.title = title; self.desc = desc; self.thumbnailURL = thumbnailURL
}
}
/// FIFO ID
public func shareLink(_ link: ShareLink, scene: ShareScene) async throws {
#if canImport(WechatOpenSDK)
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
sharePending.append(cont)
let media = WXWebpageObject()
media.webpageUrl = link.url
let message = WXMediaMessage()
message.title = link.title
message.description = link.desc
message.mediaObject = media
let req = SendMessageToWXReq()
req.bText = false
req.message = message
switch scene {
case .session: req.scene = Int32(WXSceneSession.rawValue)
case .timeline: req.scene = Int32(WXSceneTimeline.rawValue)
}
WXApi.send(req)
}
#else
throw WeChatError.sdkNotLinked
#endif
}
// MARK: - Internal: resolve callbacks
fileprivate func resolveAuth(state: String, code: String?, errCode: Int) {
guard let cont = authPending.removeValue(forKey: state) else { return }
if errCode == 0, let code {
cont.resume(returning: WXAuthCodePayload(code: code, state: state))
} else {
cont.resume(throwing: WeChatError.authFailed(errCode))
}
}
fileprivate func resolveShare(errCode: Int) {
guard !sharePending.isEmpty else { return }
let cont = sharePending.removeFirst()
if errCode == 0 {
cont.resume(returning: ())
} else {
cont.resume(throwing: WeChatError.shareFailed(errCode))
}
}
}
// MARK: - WXApiDelegate
#if canImport(WechatOpenSDK)
extension WeChatManager: WXApiDelegate {
public nonisolated func onReq(_ req: BaseReq) {
// req App req
}
public nonisolated func onResp(_ resp: BaseResp) {
// 线BaseResp SDK 线 MainActor.run
let errCode = Int(resp.errCode)
if let auth = resp as? SendAuthResp {
let code = auth.code
let state = auth.state ?? ""
Task { @MainActor in
Self.shared.resolveAuth(state: state, code: code, errCode: errCode)
}
} else if resp is SendMessageToWXResp {
Task { @MainActor in
Self.shared.resolveShare(errCode: errCode)
}
}
}
}
#endif
// MARK: - Public payload / error types SDK canImport 使
public struct WXAuthCodePayload: Sendable {
public let code: String
public let state: String
}
public enum WeChatError: Error, Sendable {
case sdkNotLinked
case authFailed(Int)
case shareFailed(Int)
}
@@ -0,0 +1,43 @@
//
// WeChatSDK.swift
// ylgamehall
//
// OpenSDK 沿 msext AppID docs/SDK-Integration-Guide.md §0
//
// canImport(WechatOpenSDK) Vendor/WechatSDK/WechatOpenSDK.xcframework
// Xcode "Frameworks, Libraries, and Embedded Content" Embed & Sign
// no-opEmbed
//
import Foundation
#if canImport(WechatOpenSDK)
import WechatOpenSDK
#endif
@MainActor
public enum WeChatSDK {
/// AppID沿 msext SGDefineInfo.h:105 kAuthOpenID
public static let appID = "wx586a9b321e56efb7"
/// AppDelegate.didFinishLaunchingWithOptions
/// universalLink 沿 msext ULAPI msext
public static func register() {
#if canImport(WechatOpenSDK)
// Universal Linksmsext / ULAPI
WXApi.registerApp(appID, universalLink: "")
#else
// SDK no-opXcode framework WXApi
#endif
}
/// SceneDelegate.openURLContexts QQ WXApi QQ msext
public static func handleOpenURL(_ url: URL) -> Bool {
#if canImport(WechatOpenSDK)
return WXApi.handleOpen(url, delegate: WeChatManager.shared)
#else
return false
#endif
}
}
+41 -8
View File
@@ -2,14 +2,18 @@
// WechatShare.swift // WechatShare.swift
// ylgamehall // ylgamehall
// //
// SharePlatform stub // SharePlatform
// //
// Phase 4.E stub SDK + AppID + Universal Links // canImport(WechatOpenSDK) SDK isInstalled = true / share = .success
// Design §8.2 ShareKit + §8.5 / QQ // SharePanel Xcode Embed & Sign
// //
import Foundation import Foundation
#if canImport(WechatOpenSDK)
import WechatOpenSDK
#endif
@MainActor @MainActor
public final class WechatShare: SharePlatform { public final class WechatShare: SharePlatform {
@@ -17,15 +21,44 @@ public final class WechatShare: SharePlatform {
public let name = "WeChat" public let name = "WeChat"
/// Stub: SDK true SharePanel public var isInstalled: Bool {
/// SDK `WXApi.isWXAppInstalled()` #if canImport(WechatOpenSDK)
public var isInstalled: Bool { true } return WXApi.isWXAppInstalled()
#else
return true // SDK share stub success
#endif
}
public init() {} public init() {}
public func share(_ content: ShareContent, scene: ShareScene) async -> ShareResult { public func share(_ content: ShareContent, scene: ShareScene) async -> ShareResult {
// Phase 4.E stub success #if canImport(WechatOpenSDK)
// WXApi.send + SendMessageToWXResp + state guard WXApi.isWXAppInstalled() else {
return .notInstalled(platform: name)
}
let wxScene: WeChatManager.ShareScene = (scene == .timeline) ? .timeline : .session
// type == "1" SDK / Phase 4.F
// WXImageObject + WXMediaMessage.thumbData
do {
try await WeChatManager.shared.shareLink(
.init(
url: content.webpageUrl,
title: content.title,
desc: content.desc,
thumbnailURL: nil // type=2/3 Phase 4.F
),
scene: wxScene
)
return .success return .success
} catch let WeChatError.shareFailed(errCode) {
if errCode == -2 { return .cancelled }
return .failed(reason: "WeChat errCode \(errCode)")
} catch {
return .failed(reason: "\(error)")
}
#else
// SDK Phase 4.B stub SharePanel
return .success
#endif
} }
} }