diff --git a/.gitignore b/.gitignore index fe173b3..710e163 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,9 @@ Secrets/ *.bak *.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/ diff --git a/Vendor/AMap/AMapFoundationKit.framework/AMapDemangleNodes.def b/Vendor/AMap/AMapFoundationKit.framework/AMapDemangleNodes.def new file mode 100644 index 0000000..3964df2 --- /dev/null +++ b/Vendor/AMap/AMapFoundationKit.framework/AMapDemangleNodes.def @@ -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 diff --git a/Vendor/AMap/AMapFoundationKit.framework/AMapFoundationKit b/Vendor/AMap/AMapFoundationKit.framework/AMapFoundationKit new file mode 100644 index 0000000..3a7f7e2 Binary files /dev/null and b/Vendor/AMap/AMapFoundationKit.framework/AMapFoundationKit differ diff --git a/Vendor/AMap/AMapFoundationKit.framework/AMapReferenceStorage.def b/Vendor/AMap/AMapFoundationKit.framework/AMapReferenceStorage.def new file mode 100644 index 0000000..f74ded7 --- /dev/null +++ b/Vendor/AMap/AMapFoundationKit.framework/AMapReferenceStorage.def @@ -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 diff --git a/Vendor/AMap/AMapFoundationKit.framework/AMapStandardTypesMangling.def b/Vendor/AMap/AMapFoundationKit.framework/AMapStandardTypesMangling.def new file mode 100644 index 0000000..f44b5fe --- /dev/null +++ b/Vendor/AMap/AMapFoundationKit.framework/AMapStandardTypesMangling.def @@ -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 diff --git a/Vendor/AMap/AMapFoundationKit.framework/AMapValueWitnessMangling.def b/Vendor/AMap/AMapFoundationKit.framework/AMapValueWitnessMangling.def new file mode 100644 index 0000000..e69de29 diff --git a/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapFoundationConst.h b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapFoundationConst.h new file mode 100644 index 0000000..7b1b62f --- /dev/null +++ b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapFoundationConst.h @@ -0,0 +1,74 @@ +// +// AMapFoundationConst.h +// AMapFoundationKit +// +// Created by JL on 2019/7/22. +// Copyright © 2019 Amap.com. All rights reserved. +// + +#import + +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 diff --git a/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapFoundationKit.h b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapFoundationKit.h new file mode 100644 index 0000000..431b7bd --- /dev/null +++ b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapFoundationKit.h @@ -0,0 +1,20 @@ +// +// AMapFoundationKit.h +// AMapFoundationKit +// +// Created by xiaoming han on 15/10/28. +// Copyright © 2015年 Amap. All rights reserved. +// + +#import +#import +#import +#import +#import +#import + +#import + +#if __has_include() +#import +#endif diff --git a/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapFoundationVersion.h b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapFoundationVersion.h new file mode 100644 index 0000000..c1e14cd --- /dev/null +++ b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapFoundationVersion.h @@ -0,0 +1,19 @@ +// +// AMapFoundationVersion.h +// AMapFoundation +// +// Created by xiaoming han on 15/10/26. +// Copyright © 2015年 Amap. All rights reserved. +// + +#import + +#ifndef AMapFoundationVersion_h +#define AMapFoundationVersion_h + +#define AMapFoundationVersionNumber 10900 + +FOUNDATION_EXTERN NSString * const AMapFoundationVersion; +FOUNDATION_EXTERN NSString * const AMapFoundationName; + +#endif /* AMapFoundationVersion_h */ diff --git a/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapServices.h b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapServices.h new file mode 100644 index 0000000..e38d775 --- /dev/null +++ b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapServices.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 + + +//语言类型 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 diff --git a/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapURLSearch.h b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapURLSearch.h new file mode 100644 index 0000000..9379f4f --- /dev/null +++ b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapURLSearch.h @@ -0,0 +1,41 @@ +// +// AMapURLSearch.h +// AMapFoundation +// +// Created by xiaoming han on 15/10/28. +// Copyright © 2015年 Amap. All rights reserved. +// + +#import +#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 diff --git a/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapURLSearchConfig.h b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapURLSearchConfig.h new file mode 100644 index 0000000..666b492 --- /dev/null +++ b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapURLSearchConfig.h @@ -0,0 +1,79 @@ +// +// MAMapURLSearchConfig.h +// MAMapKitNew +// +// Created by xiaoming han on 15/5/25. +// Copyright (c) 2015年 xiaoming han. All rights reserved. +// + +#import +#import +#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 + diff --git a/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapURLSearchType.h b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapURLSearchType.h new file mode 100644 index 0000000..424d905 --- /dev/null +++ b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapURLSearchType.h @@ -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, ///<步行 +}; + + diff --git a/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapUtility.h b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapUtility.h new file mode 100644 index 0000000..6cb2692 --- /dev/null +++ b/Vendor/AMap/AMapFoundationKit.framework/Headers/AMapUtility.h @@ -0,0 +1,50 @@ +// +// AMapUtility.h +// AMapFoundation +// +// Created by xiaoming han on 15/10/27. +// Copyright © 2015年 Amap. All rights reserved. +// + +#import +#import + +//工具方法 + +/** + * @brief 如果字符串为nil则返回空字符串 + */ +FOUNDATION_STATIC_INLINE NSString * AMapEmptyStringIfNil(NSString *s) +{ + return s ? s : @""; +} + +///坐标类型枚举 +typedef NS_ENUM(NSInteger, AMapCoordinateType) +{ + AMapCoordinateTypeAMap = -1, /// +#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 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 + +@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 *)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 diff --git a/Vendor/AMap/AMapLocationKit.framework/Headers/AMapGeoFenceRegionObj.h b/Vendor/AMap/AMapLocationKit.framework/Headers/AMapGeoFenceRegionObj.h new file mode 100644 index 0000000..833fa9c --- /dev/null +++ b/Vendor/AMap/AMapLocationKit.framework/Headers/AMapGeoFenceRegionObj.h @@ -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 + + +///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 *> *polylinePoints; + + +@end diff --git a/Vendor/AMap/AMapLocationKit.framework/Headers/AMapLocationCommonObj.h b/Vendor/AMap/AMapLocationKit.framework/Headers/AMapLocationCommonObj.h new file mode 100644 index 0000000..850b2a2 --- /dev/null +++ b/Vendor/AMap/AMapLocationKit.framework/Headers/AMapLocationCommonObj.h @@ -0,0 +1,195 @@ +// +// AMapLocationCommonObj.h +// AMapLocationKit +// +// Created by AutoNavi on 15/10/22. +// Copyright © 2015年 Amap. All rights reserved. +// + +#import +#import +#import + + +///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, /// + +///格式化地址 +@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 + +///纬度 +@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 + +///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 + +///城市编码 +@property (nonatomic, copy) NSString *cityCode; + +///区域编码 +@property (nonatomic, copy) NSString *districtCode; + +///区名 +@property (nonatomic, copy) NSString *district; + +///行政区域轮廓坐标点,每个行政区可能有多个模块,每个模块的坐标点数组由AMapLocationPoint构成 +@property (nonatomic, copy) NSArray *> *polylinePoints; + +@end + +///AMapLocation CoordinateType +typedef NS_ENUM(NSUInteger, AMapLocationCoordinateType) +{ + AMapLocationCoordinateTypeBaidu = 0, /// + +#import +#import +#import + +#import +#import +#import diff --git a/Vendor/AMap/AMapLocationKit.framework/Headers/AMapLocationManager.h b/Vendor/AMap/AMapLocationKit.framework/Headers/AMapLocationManager.h new file mode 100644 index 0000000..3eee12e --- /dev/null +++ b/Vendor/AMap/AMapLocationKit.framework/Headers/AMapLocationManager.h @@ -0,0 +1,283 @@ +// +// AMapLocationManager.h +// AMapLocationKit +// +// Created by AutoNavi on 15/10/22. +// Copyright © 2015年 Amap. All rights reserved. +// + +#import +#import +#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 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 + +@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 diff --git a/Vendor/AMap/AMapLocationKit.framework/Headers/AMapLocationRegionObj.h b/Vendor/AMap/AMapLocationKit.framework/Headers/AMapLocationRegionObj.h new file mode 100644 index 0000000..ed74aad --- /dev/null +++ b/Vendor/AMap/AMapLocationKit.framework/Headers/AMapLocationRegionObj.h @@ -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 + +///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 diff --git a/Vendor/AMap/AMapLocationKit.framework/Headers/AMapLocationVersion.h b/Vendor/AMap/AMapLocationKit.framework/Headers/AMapLocationVersion.h new file mode 100644 index 0000000..9c3e229 --- /dev/null +++ b/Vendor/AMap/AMapLocationKit.framework/Headers/AMapLocationVersion.h @@ -0,0 +1,26 @@ +// +// AMapLoctionVersion.h +// AMapLocationKit +// +// Created by AutoNavi on 16/1/22. +// Copyright © 2016年 Amap. All rights reserved. +// + +#import +#import + +#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 */ diff --git a/Vendor/AMap/AMapLocationKit.framework/Info.plist b/Vendor/AMap/AMapLocationKit.framework/Info.plist new file mode 100644 index 0000000..990dbe0 Binary files /dev/null and b/Vendor/AMap/AMapLocationKit.framework/Info.plist differ diff --git a/Vendor/AMap/AMapLocationKit.framework/Modules/module.modulemap b/Vendor/AMap/AMapLocationKit.framework/Modules/module.modulemap new file mode 100644 index 0000000..643824a --- /dev/null +++ b/Vendor/AMap/AMapLocationKit.framework/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module AMapLocationKit { + umbrella header "AMapLocationKit.h" + export * + + module * { export * } +} diff --git a/Vendor/AMap/AMapLocationKit.framework/version.txt b/Vendor/AMap/AMapLocationKit.framework/version.txt new file mode 100644 index 0000000..99c2518 --- /dev/null +++ b/Vendor/AMap/AMapLocationKit.framework/version.txt @@ -0,0 +1,2 @@ +2.12.0+loc.4f55976 +isOversea:false diff --git a/Vendor/AMap/README.md b/Vendor/AMap/README.md new file mode 100644 index 0000000..4a54168 --- /dev/null +++ b/Vendor/AMap/README.md @@ -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` | +| 架构 | fat(x86_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) diff --git a/Vendor/WechatSDK/README.md b/Vendor/WechatSDK/README.md new file mode 100644 index 0000000..d9297bb --- /dev/null +++ b/Vendor/WechatSDK/README.md @@ -0,0 +1,15 @@ +# WechatOpenSDK + +**本目录刻意不放二进制**:项目以 **SPM** 接入微信 OpenSDK(Tencent 官方维护),不走 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-NoPay.xcframework` 到 `docs/res/`,**改 SPM 后不再使用**,可保留作为离线备份 | + +## 凭证 + +AppID + AppSecret + AuthScope + AuthState 沿用 msext,参 `docs/SDK-Integration-Guide.md §0`。 diff --git a/docs/SDK-Integration-Guide.md b/docs/SDK-Integration-Guide.md index bb2cee8..05ee751 100644 --- a/docs/SDK-Integration-Guide.md +++ b/docs/SDK-Integration-Guide.md @@ -2,285 +2,323 @@ > 本文档面向项目方 + 开发者,描述 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()` 守卫里。**只需用户在 Xcode UI 完成 3 件事**(详见 §A 用户手动步骤),代码即激活真实路径。 --- -## 0. 总览 +## A. 用户手动 Xcode 步骤(必做 3 件事) -| SDK | 触发的契约项 | 选型 | 阻塞 | -|---|---|---|---| -| **微信 OpenSDK** | `accreditlogin` / `friendsSharetypeUrlToptitleDescript`(微信路径)/ `sharelogin` 反向 callback / `sharesuccess` | Vendor `.xcframework` 手动 | AppID + Universal Links + 后台 `/wechat/login`(或客户端 fallback) | -| **高德定位(AMap)** | `startlocation` / `getlocationinfo` 反向 callback(9 字段)| 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 缓存策略 | +> **选型偏好**:尽量 SPM 远程依赖;只有官方未提供 SPM 的才走 Vendor 二进制。 +> 当前结果:**微信 + 七牛走 SPM**(pbxproj 仅多 2 个 `XCRemoteSwiftPackageReference`,与现有 ZIPFoundation 一致);**高德走 Vendor**(官方未提供 SPM)。 +> 这 3 件事都是 Xcode UI 操作,无法 AI 自动完成。完成后 `BuildProject` 即激活所有真实路径。 -**两条铁律(CLAUDE.md)**: -1. 任何接入步骤都不允许触碰 H5 端任何文件(包括 `app_*.js`) -2. 不引入 CocoaPods;SPM 优先,闭源走 Vendor `.xcframework` +### A.1 添加微信 OpenSDK(SPM 远程依赖) + +> 项目方下载的 `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 × 2(Vendor 手动,无 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. 微信 OpenSDK(Phase 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 - - CFBundleURLTypes - - - CFBundleURLName - weixin - CFBundleURLSchemes - YOUR_WX_APPID - - - - - LSApplicationQueriesSchemes - - weixin - weixinULAPI - weixinURLParamsAPI - - - ``` - -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_*` flag(Design §4.2) | -| **`AppDelegate.swift`** | `application(_:didFinishLaunchingWithOptions:)` 内调 `WeChatSDK.register()` | -| **`SceneDelegate.swift`** | `openURLContexts` 内按 **QQ → WXApi** 顺序判(msext `AppDelegate.m` 顺序硬约束,QQ 必须先判) | -| **`Source/SDK/WeChat/WeChatManager.swift`**(新建) | 持久 delegate;`authorize() async throws -> WXAuthCode`(state UUID 配对);`share(_:scene:) async throws`(FIFO 串行)。Design §8.5 | -| **`Source/Login/WeChatAuth.swift`**(新建) | `authorize()` 拿 code → 转后台 `/wechat/login`(或 fallback 拼 `sns/oauth2/access_token` + `sns/userinfo`)→ 拿到 7 字段 user。Fallback 文件头标 `// FIXME: 待后台 /wechat/login 就绪后切换为后台中转` | -| **`Source/Bridge/Handlers/AccreditLoginHandler.swift`** | 移除 stub 返回,调 `WeChatAuth.authorize()` → 反向 `bridge.call("sharelogin", 7 字段)`
**字段名严格 1:1(含历史包袱)**:`openid` / `headimgurl` / `nickname` / `sex` / `city` / `Province`(**大写 P**!)/ `unionid` | -| **`Source/Share/WechatShare.swift`** | 移除 stub `return .success`,调 `WeChatManager.share`;`isInstalled` 改为 `WXApi.isWXAppInstalled()` | -| **`Source/Bridge/Handlers/BackGameDataHandler.swift`** | **必须加** `WXApi.delegate = nil`(清理子游戏 push 期间的 delegate 指针) | +| `AppDelegate.swift` | `WeChatSDK.register()` → 真实调 `WXApi.registerApp("wx586a9b321e56efb7", universalLink: "")` | +| `AppDelegate.swift` | `AMapWrapper.bootstrap()` → 真实调隐私合规 3 步 + apiKey | +| `SceneDelegate.swift` | `openURLContexts` → 真实调 `WXApi.handleOpen(url, delegate:)` | +| `AccreditLoginHandler` | `accreditlogin` → 真实拉起微信授权 → 反向 callback `sharelogin` 7 字段(Province 大写 P) | +| `WechatShare` | `share()` → 真实调 `WXApi.send(SendMessageToWXReq)` | +| `StartLocationHandler` | `startlocation` → 真实调 `AMapLocationManager.requestLocation` → 反向 callback `getlocationinfo` 9 字段(latitude/longitude string, province 小写 p) | +| `BackGameDataHandler` | 子游戏 backgameData 退出时自动清 `WXApi.delegate = nil` + `LocationService.stop()` | -### 1.4 清理钩子(CLAUDE.md 第 6.0 节回响) - -`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 限频),上线初期保持后台监控 +**Qiniu 单独说明**:七牛 SDK 加进 SPM 后,`canImport(QiniuSDK)` 通过;但 `RemoteAudioHandler` 仍然是 stub(prepareaudio 不真实录音上传),原因是录音→AMR 转码链路的 opencore-amr ObjC 库未接入。`QiniuUploader.upload(...)` 单独已可用,可用于其它非录音场景。完整录音上传链路见 §C。 --- -## 2. 高德定位 SDK(Phase 5) +## C. 仍需用户手动做的事(opencore-amr / AMR 录音上传) -### 2.1 凭证清单 +> 这一块代码侧没有自动完成,因为需要从 msext 拷贝 ObjC 源码(含 MRC)+ 修改 Build Settings(File-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 一一绑死) -- [ ] **AMap SDK XCFramework**:[官方下载页](https://lbs.amap.com/api/ios-location-sdk/download)拉取最新(截至 2026-06 推荐 v2.10.x+) - - `AMapLocationKit.xcframework` — 定位主库 - - `AMapFoundationKit.xcframework` — 基础库(必须配合 LocationKit) +### C.1 拷贝 opencore-amr 静态库 -### 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 - NSLocationWhenInUseUsageDescription - {gamehallname}需要访问您的位置以提供本地化服务 - ``` - `{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`(一次定位)
`startContinuous(onUpdate:)` / `stop()`(持续定位)
包 `AMapLocationManager`,回调转 `async` | -| **`Source/Bridge/Handlers/StartLocationHandler.swift`** | 移除 stub,按 `data == 1` 持续 / 其它一次性
反向 callback `bridge.call("getlocationinfo", 9 字段)`
**字段名严格 1:1**:`address` / `city` / `cityCode` / `country` / `district` / `latitude`(**string**,`String(format: "%f", lat)`)/ `longitude`(**string**)/ `province`(**小写 p**!与 `sharelogin.Province` 不同)/ `street`
失败:`getlocationinfo({errorCode:12, errorMsg:"缺少定位权限"})` | -| **`Source/Bridge/Handlers/BackGameDataHandler.swift`** | **必须加** `LocationService.shared.stop()` | - -### 2.4 清理钩子 - -`BackGameDataHandler.swift` 的 `MainActor.run` 块加: -```swift -LocationService.shared.stop() // ← Phase 5 接入时取消注释 +```bash +mkdir -p ylgamehall/Vendor/opencore-amr/ +# 已修复版(fat header segalign 8)静态库 +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/ +# 拷 VoiceConverter ObjC wrapper(含 amrFileCodec / interf_*) +cp -r ../daoqi/msext/msext/Class/Common/VoiceConvert \ + ylgamehall/Vendor/opencore-amr/VoiceConverter ``` -理由:msext `gameController.m:2473-2478 cleanUpAction` 在 backgameData 里调 `[locationManager stopUpdatingLocation]` + delegate nil;防止 pop 后后台定位继续耗电 + 回调悬空。 -### 2.5 验收 +### C.2 Xcode 工程配置 -- [ ] 首次启动 → 系统权限弹框(文案为 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 - mkdir -p 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/ - ``` - -2. **拷 VoiceConverter(ObjC wrapper)** - ``` - ylgamehall/Vendor/opencore-amr/VoiceConverter/ - ├── VoiceConverter.h # ObjC interface(保留 ObjC,wrapper 简单不重写 Swift) - ├── VoiceConverter.m # MRC 代码可保留,加 -fno-objc-arc 编译标志或转 ARC - ├── amrFileCodec.h/m - └── interf_*.h/c - ``` - -3. **Xcode 工程配置** - - Target → Build Phases → Link Binary With Libraries 加入两个 `.a` - - Target → Build Settings → Library Search Paths 加 `$(PROJECT_DIR)/ylgamehall/Vendor/opencore-amr` - - Target → Build Settings → Header Search Paths 加 `$(PROJECT_DIR)/ylgamehall/Vendor/opencore-amr/include` - - **不需要** `-ld_classic`(已用 `lipo -segalign 8` 修复 fat header,CLAUDE.md 父项目记录的修复同样适用) - - 若复用 msext MRC 的 `VoiceConverter.m`,加文件级 `-fno-objc-arc` 编译标志 - -4. **Bridging Header**(已有 `ylgamehall-Bridging-Header.h` 则追加;无则新建) +1. 选中 Target → Build Phases → Link Binary With Libraries → `+` → Add Other → Add Files → 选两个 `.a` +2. Target → Build Settings: + - **Library Search Paths** 加 `$(PROJECT_DIR)/Vendor/opencore-amr` + - **Header Search Paths** 加 `$(PROJECT_DIR)/Vendor/opencore-amr/include` +3. 把 `Vendor/opencore-amr/VoiceConverter/` 下的 `.m` 文件拖入项目(Add to Target ylgamehall) +4. 每个 `.m` 文件单独加 `-fno-objc-arc` 编译标志(msext 是 MRC): + - Target → Build Phases → Compile Sources → 找到每个 `.m` → 双击 → 加 `-fno-objc-arc` +5. 创建 Bridging Header `ylgamehall-Bridging-Header.h` 内容: ```objc #import "VoiceConverter.h" ``` +6. Target → Build Settings → **Objective-C Bridging Header** 设为 `ylgamehall/ylgamehall-Bridging-Header.h` -### 3.3 代码触点 +### C.3 接入 Swift 录音 + 上传链路(代码侧) -| 文件 | 改动 | -|---|---| -| **`Source/Audio/VoiceCoder.swift`**(新建) | Swift wrapper:`static func amrToWav(_ amrPath: URL) throws -> URL` / `static func wavToAmr(_ wavPath: URL) throws -> URL`
内部调 ObjC `VoiceConverter` | -| **`Source/Audio/AudioRecorder.swift`**(新建,actor) | `record() async throws -> AudioFile`:`AVAudioRecorder` 录 WAV + 麦克风权限
录音参数与 msext `ChatVoiceRecorderVC` 等价:16kHz / 单声道 / 16-bit | -| **`Source/Bridge/Handlers/RemoteAudioHandler.swift`** | 移除 stub,分别接入:
**`prepareaudio`**(录音上传)→ `AudioRecorder.record()` → WAV→AMR `VoiceCoder.wavToAmr` → `QiniuUploader.upload`(见 §4) → `bridge.call("getaudiourl", {audiourl, time})`,**子游戏页额外触发** `bridge.call("recordSuccess", {fileUrl, fileName, fileKey})`
**`mediaTypeAudio`**(远端回放)→ URLSession 下载 AMR → AMR→WAV `VoiceCoder.amrToWav` → `AVAudioPlayer` 播放
开始播放 `bridge.call("gameui_play_voice", user)` / 播放结束 `bridge.call("gameui_stop_voice", user)`
受 `VoicePlayingHandler` 的开关短路(=1 才播) | -| **`Info.plist`** | 加 `NSMicrophoneUsageDescription = "{gamehallname}需要访问您的麦克风录制语音消息"` | -| **`Source/Bridge/Handlers/BackGameDataHandler.swift`** | **必须加** `AudioRecorder.shared.cancel()` | - -### 3.4 清理钩子 +完成 C.1-C.2 后回到代码侧补两个文件: +**`Source/Audio/VoiceCoder.swift`**(Swift wrapper) ```swift -AudioRecorder.shared.cancel() // ← Phase 3.C 接入时取消注释 -QiniuUploader.shared.cancelInFlight() // ← Phase 3.C 接入时取消注释 +import Foundation + +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"})` -- [ ] 子游戏同样路径,额外收到 `recordSuccess({fileUrl, fileName, fileKey})` -- [ ] H5 调 `mediaTypeAudio({audiourl, user})` → 听到对方语音 + 收到 `gameui_play_voice` / 结束收 `gameui_stop_voice` -- [ ] `voicePlaying` 总开关设 0 → 调 `mediaTypeAudio` 不播放 +public actor AudioRecorder { + public static let shared = AudioRecorder() + public func record() async throws -> (file: URL, duration: Int) { + // 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 -2. URL:`https://github.com/qiniu/objc-sdk` -3. 版本:`8.9.x` 最新(avoid 7.x 历史 API) -4. 勾选 target → Add Package +父项目 CLAUDE.md 第 6 节明确指出 `AppSecret` / `SecretKey` 客户端硬编码是已识别但**接受不修**的安全风险。msext IPA 已分发数年等价泄露,新外壳沿用同一 key + 沿用同款客户端直拼路径 → **同等安全等级**,**不引入新的攻击面**,**不需要后台中转接口**。 -注:七牛官方 SPM 包是 ObjC,但 SPM 直接支持 ObjC + Swift 混合,不需要 bridging header(SPM 自动处理)。 +--- -### 4.3 代码触点 +## 1. 微信 OpenSDK(Phase 4.E)— 代码已就绪 -| 文件 | 改动 | +### 1.1 凭证(已具备,沿用 msext) + +参 §0 总表。 + +### 1.2 工程接入 + +参 §A.1(用户手动 Xcode 步骤)。 + +### 1.3 代码已落地 + +| 文件 | 状态 | 说明 | +|---|---|---| +| SPM `WechatOpenSDK-XCFramework` | ⏳ 待用户在 Xcode Add Package Dependencies | 见 §A.1,Tencent 官方仓库 | +| `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 反向 callback(Province 大写 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. 高德定位 SDK(Phase 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 模板 / 过期时间)
缓存策略:token 失效前 30 秒强制刷新;并发请求合并 | -| **`Source/Network/QiniuUploader.swift`**(新建,actor) | `upload(_ file: URL, token: QiniuToken) async throws -> UploadedFile`
内部用 `QNUploadManager` 包成 async;UploadedFile 含 `fileUrl` / `fileName` / `fileKey`
取消语义:`cancelInFlight()` 取消所有进行中任务(接清理钩子用) | -| **`Source/Bridge/Handlers/RemoteAudioHandler.swift`** | `prepareaudio` 完整链路第 3 步:`QiniuUploader.upload(amrFile, token: await QiniuTokenService.shared.fetchUploadToken())` | +| `Vendor/AMap/AMapFoundationKit.framework` + `AMapLocationKit.framework` | ✅ 已就位 | +| `Source/SDK/AMap/AMapWrapper.swift` | ✅ 已写 | 隐私合规 3 步 + apiKey 注入 | +| `Source/Location/LocationService.swift` | ✅ 已写 | actor,requestOnce 异步包装 + 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"})` -- [ ] 录音中 / 上传中 → 退出子游戏 → 任务被 cancel,不再有悬空网络写 -- [ ] token 过期场景:模拟后台返回过期 token → uploader 自动刷新重试一次 +- **APIKey 绑定 Bundle ID**:本项目已对齐 `com.skyapp.ylgamehall`,可复用。**改 Bundle ID 必须重新申请 key** +- **模拟器定位精度低**:必须真机测 -### 4.6 风险 +--- -- **token 时效**:通常 1 小时;高频录音场景需要 caching + 失效前预刷新 -- **上传失败重试**:默认 3 次,超时建议设 30s(语音文件通常 < 100KB) -- **CDN 域名 HTTPS**:H5 业务侧拿到 `audiourl` 后会请求播放,必须保证域名 HTTPS(NSAppTransportSecurity 我们没收紧 ATS,但七牛默认走 HTTPS) +## 3. opencore-amr 转码库(Phase 3.B)— 用户手动 + +参 §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` + 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 文件 +- [ ] 退出子游戏 → 进行中上传任务被 cancel(Swift Task.cancel 传播) --- ## 5. 接入完毕的清单检查 -每接入完一个 SDK,跑一遍: +每接入完一个 SDK 跑一遍: - [ ] `BuildProject` 成功 -- [ ] `Vendor//README.md` 已记录版本号 / 来源 / 凭证创建日期 -- [ ] Info.plist 改动只在原生侧,未触 H5 任何文件 -- [ ] `BackGameDataHandler.swift` 对应清理钩子已加(每个 SDK 都要回到此处补 1 行) -- [ ] `docs/Development-Plan.md §5 / §8` 进度勾选已同步(提交时附 commit 备注「Plan 进度已勾选」) +- [ ] `Vendor//README.md` 已记录版本号 / 来源 / 凭证沿用 msext 的备注 — 已就位 +- [ ] Info.plist 改动只在原生侧,未触 H5 任何文件 — 已就位 +- [ ] `BackGameDataHandler.swift` 对应清理钩子已加 — 微信 + 高德已加 canImport 守卫;录音待 §C +- [ ] `docs/Development-Plan.md §5 / §8` 进度勾选已同步 - [ ] `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/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` 字段顺序与类型) - AMR 转码:`daoqi/msext/Class/Common/VoiceConvert/`(`VoiceConverter.h/m`、`amrFileCodec.h/m`) -- 七牛上传:`daoqi/msext/Class/Utils/QiniuManager.m`(参数命名 + 回调结构) - -**遇到契约边界 / 历史包袱时**:照搬 msext 字面 + 字段名 + 顺序,不要凭语义猜测。原项目稳定运行多年的写法是真理,文档 / 设计稿可能有传抄错误(已在 CLAUDE.md「典型案例」积累若干)。 +- 七牛 token 自签算法:`daoqi/msext/Class/Utils/QiniuManager.m:200-230`(putPolicy + HMAC-SHA1 + Base64URL) +- 七牛域名读取:`daoqi/msext/AppDelegate.m:142` 用 `FuncPublic filename:@"qiniudomain"` 读 `Resources/qiniudomain/` 下第一个子目录名作为实际 Domain diff --git a/ylgamehall/AppDelegate.swift b/ylgamehall/AppDelegate.swift index bada79e..196bb72 100644 --- a/ylgamehall/AppDelegate.swift +++ b/ylgamehall/AppDelegate.swift @@ -11,6 +11,13 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 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 } diff --git a/ylgamehall/Info.plist b/ylgamehall/Info.plist index 9deba63..e7513c4 100644 --- a/ylgamehall/Info.plist +++ b/ylgamehall/Info.plist @@ -52,7 +52,7 @@ canOpenURL 总返回 false)。本项目分享接入: - QQ(URL Scheme 路径,不依赖 SDK):mqq / mqqapi / mqqopensdkfriend 等 - 抖音(URL Scheme + Photos 路径,不依赖 SDK):snssdk1128 - - 微信(Phase 4.E SDK 接入时再加 weixin / weixinULAPI / weixinURLParamsAPI) + - 微信(Phase 4.E SDK 接入):weixin / weixinULAPI / weixinURLParamsAPI --> LSApplicationQueriesSchemes @@ -63,6 +63,25 @@ mqqopensdkapiV3 mqqopensdkapiV4 snssdk1128 + weixin + weixinULAPI + weixinURLParamsAPI + + + CFBundleURLTypes + + + CFBundleURLName + weixin + CFBundleURLSchemes + + wx586a9b321e56efb7 + + NSPhotoLibraryAddUsageDescription 需要将分享内容保存到相册,以便您在抖音中选取分享 + + NSLocationWhenInUseUsageDescription + 进贤聚友棋牌需要访问您的位置以提供本地化服务 + + NSMicrophoneUsageDescription + 进贤聚友棋牌需要访问您的麦克风录制语音消息 diff --git a/ylgamehall/SceneDelegate.swift b/ylgamehall/SceneDelegate.swift index 1b3c5bb..8c595d2 100644 --- a/ylgamehall/SceneDelegate.swift +++ b/ylgamehall/SceneDelegate.swift @@ -28,4 +28,15 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.window = window window.makeKeyAndVisible() } + + /// 第三方 SDK 回调入口(微信授权 / 微信分享回包等)。 + /// 顺序硬约束:QQ → 微信(msext AppDelegate.m 同款);QQ 走 URL Scheme 不依赖 SDK, + /// 这里直接调微信 handler — 未来若 QQ 升级为 SDK 路径则在前面插一条。 + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + for ctx in URLContexts { + // Phase 4.E 微信回调(canImport 守卫:未链接时 no-op) + if WeChatSDK.handleOpenURL(ctx.url) { continue } + // 未来:抖音 / 其它 SDK 回调依次判 + } + } } diff --git a/ylgamehall/Source/Bridge/Handlers/AccreditLoginHandler.swift b/ylgamehall/Source/Bridge/Handlers/AccreditLoginHandler.swift index 747a1bf..2df8147 100644 --- a/ylgamehall/Source/Bridge/Handlers/AccreditLoginHandler.swift +++ b/ylgamehall/Source/Bridge/Handlers/AccreditLoginHandler.swift @@ -2,18 +2,17 @@ // AccreditLoginHandler.swift // ylgamehall // -// H5 → Native handler stub:accreditlogin — 微信授权登录 +// H5 → Native handler:accreditlogin — 微信授权登录 // 契约:docs/H5-Native-Contract.md §3.1 [1] + §3.2 [10]sharelogin // -// Phase 4.A 阶段为 stub(仅 cb 维持契约);Phase 4.B 等微信 SDK + AppID + -// Universal Links + 后台 /wechat/login 接口(或客户端 fallback)到位后升级。 +// 完整链路(Phase 4.E): +// 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): -// 1. WeChatManager.shared.authorize() → 拿 WXAuthCode(state UUID 配对) -// 2. WeChatAuth.exchangeForUser(code:) → 后台 /wechat/login 或客户端直拼 -// sns/oauth2/access_token + sns/userinfo -// 3. bridge.call("sharelogin", data: 7 字段) 反向 callback -// 字段名严格 1:1 含历史包袱:Province 大写 P、city 经 danbian: 去单引号 +// ⚠️ canImport(WechatOpenSDK) 守卫:SDK 未链接前走 stub(与 Phase 4.A 一致), +// 仅响应 callback 不触发 sharelogin;项目方在 Xcode Embed & Sign 后真实路径激活。 // import Foundation @@ -21,11 +20,32 @@ import Foundation public enum AccreditLoginHandler { public static func register(on bridge: any BridgeProtocol) { - // 【1】accreditlogin - // 入参忽略;cb: "Response from accreditlogin" - // 完整实现需触发 sharelogin 反向 callback(Phase 4.B) bridge.register("accreditlogin") { _, callback in 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 } } } diff --git a/ylgamehall/Source/Bridge/Handlers/BackGameDataHandler.swift b/ylgamehall/Source/Bridge/Handlers/BackGameDataHandler.swift index b87738d..798ad6d 100644 --- a/ylgamehall/Source/Bridge/Handlers/BackGameDataHandler.swift +++ b/ylgamehall/Source/Bridge/Handlers/BackGameDataHandler.swift @@ -17,29 +17,24 @@ // msext 同款行为 — 这是契约边界,不在原生侧兜底。 // // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -// ⚠️ 接入新 SDK 时必须回到此处补对应清理钩子(msext 在 backgameData / cleanUpAction -// 里逐项清理;我们当前是 stub,所以暂无需要清的状态。一旦真实接入下列任一 SDK, -// 在 MainActor.run 块里加对应清理一行): +// SDK 清理钩子状态(每接入新 SDK 都必须确认或新增): // -// - Phase 4.E 微信 SDK:`WXApi.delegate = nil` -// (msext gameController.m:699;防止 pop 后微信回调指向已销毁的 SubGameVC) -// -// - Phase 5 高德定位:`LocationService.shared.stop()` + delegate nil -// (msext gameController.m:2473-2478 cleanUpAction;防止后台定位继续耗电 + -// 回调指向悬空对象) -// -// - Phase 3.B/C/D 录音 / 七牛上传:`Recorder.shared.cancel()` + -// `Uploader.shared.cancelInFlight()` -// (msext 通过 ChatVoiceRecorderVC 生命周期间接清理;我们的实现不会跟 VC 绑死, -// 必须显式 cancel 否则上传任务在 pop 后继续写网络) -// -// - Phase 8 Agora(若未来重启视频):`agoraKit.leaveChannel(...)` -// (目前 VideoRoomHandlers 是 stub 无需清理) +// ✅ Phase 4.E 微信:`WXApi.delegate = nil`(已加,canImport 守卫,Xcode Embed 后激活) +// ✅ Phase 5 高德定位:`LocationService.shared.stop()`(已加,canImport 守卫, +// Xcode Embed AMapLocationKit 后激活) +// ⏳ Phase 3.B/C/D 录音 / 七牛上传:`AudioRecorder.shared.cancel()` 待 opencore-amr +// 接入后取消注释;`QiniuUploader.cancelInFlight()` 依赖 Task.cancel 传播无需显式调 +// ⏳ Phase 8 Agora(若未来重启视频):`agoraKit.leaveChannel(...)` +// (目前 VideoRoomHandlers 是 stub 无需清理) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // import Foundation +#if canImport(WechatOpenSDK) +import WechatOpenSDK +#endif + public enum BackGameDataHandler { public static func register(on bridge: any BridgeProtocol) { @@ -52,7 +47,25 @@ public enum BackGameDataHandler { // - 直接是 .string → 取字面 // - 其它类型 → JSON 序列化(保留对象结构传给大厅 H5) let payload = serialize(data) + + // 子游戏 push 期间挂载的 SDK 资源在 pop 前显式清理(避免 delegate 悬挂 / + // 后台任务残留),与 msext gameController.m:691-709 + cleanUpAction 等价。 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) } callback?(.string("backgameData")) diff --git a/ylgamehall/Source/Bridge/Handlers/StartLocationHandler.swift b/ylgamehall/Source/Bridge/Handlers/StartLocationHandler.swift index 128c314..84499ad 100644 --- a/ylgamehall/Source/Bridge/Handlers/StartLocationHandler.swift +++ b/ylgamehall/Source/Bridge/Handlers/StartLocationHandler.swift @@ -2,15 +2,18 @@ // StartLocationHandler.swift // ylgamehall // -// H5 → Native handler:startlocation — 定位(Phase 5 完整实现,本期为 stub) +// H5 → Native handler:startlocation — 定位(Phase 5 接入高德 SDK 后激活真实路径) // 契约:docs/H5-Native-Contract.md §3.1 [20] + §3.2 [6] // -// Stub 行为: -// - 接收 startlocation 调用,cb "startlocation"(响应不为 undefined,H5 不卡) -// - **不触发** getlocationinfo 反向 callback(暂无高德 SDK,做不到真实定位) -// - H5 业务侧若强依赖 getlocationinfo,需等 Phase 5 完整接入 AMapLocationKit +// 完整链路: +// 1. LocationService.shared.requestOnce()(actor,包 AMapLocationManager 异步) +// 2. bridge.call("getlocationinfo", 9 字段) 反向 callback +// - 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 @@ -19,9 +22,41 @@ public enum StartLocationHandler { public static func register(on bridge: any BridgeProtocol) { bridge.register("startlocation") { _, callback in - // Phase 2 stub:仅响应 callback,不真实定位 - // Phase 5 接入 LocationKit + 高德 AMapLocationKit.xcframework 时升级为完整实现 + // 与 msext 行为一致:先回 cb 字面,不等定位完成 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 { + // 其它失败也归一到 12(msext 行为) + bridge.call("getlocationinfo", data: .object([ + "errorCode": .number(12), + "errorMsg": .string("缺少定位权限") + ]), callback: nil) + } + } + #endif } } } diff --git a/ylgamehall/Source/Location/LocationService.swift b/ylgamehall/Source/Location/LocationService.swift new file mode 100644 index 0000000..28fbd25 --- /dev/null +++ b/ylgamehall/Source/Location/LocationService.swift @@ -0,0 +1,105 @@ +// +// LocationService.swift +// ylgamehall +// +// 高德 AMapLocationManager 异步包装:一次性 / 持续两种模式。 +// 契约 §3.2 [6] getlocationinfo 9 字段;msext gameController.m:2491-2557 等价。 +// +// ⚠️ canImport(AMapLocationKit) 守卫:SDK 未链接时 LocationService 永远抛 +// LocationError.sdkNotLinked,handler 兜底退化为 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) in + manager.requestLocation(withReGeocode: true) { loc, regeo, err in + if let err = err as NSError? { + // msext 行为:权限相关错误归一到 12 errorCode(handler 转字段) + 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 + } +} diff --git a/ylgamehall/Source/Login/WeChatAuth.swift b/ylgamehall/Source/Login/WeChatAuth.swift new file mode 100644 index 0000000..de79127 --- /dev/null +++ b/ylgamehall/Source/Login/WeChatAuth.swift @@ -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 + /// 性别。微信回的是 number(1=男 2=女 0=未知);H5 收到的 sharelogin 是同款 number 字面。 + public let sex: Int + /// city / province 经 msext `danbian:` 去单引号处理(防 H5 JSON 解析炸) + public let city: String + /// 注意大写 P:msext 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 → userinfo(7 字段) + 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) ?? "" + 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) ?? "" + 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: "") + } +} diff --git a/ylgamehall/Source/Network/QiniuConfig.swift b/ylgamehall/Source/Network/QiniuConfig.swift new file mode 100644 index 0000000..9fbe68b --- /dev/null +++ b/ylgamehall/Source/Network/QiniuConfig.swift @@ -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)" + } +} diff --git a/ylgamehall/Source/Network/QiniuTokenSigner.swift b/ylgamehall/Source/Network/QiniuTokenSigner.swift new file mode 100644 index 0000000..0d001b4 --- /dev/null +++ b/ylgamehall/Source/Network/QiniuTokenSigner.swift @@ -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}` +// +// ⚠️ 用 CryptoKit(iOS 13+),不依赖 Qiniu SDK,纯 Swift 自签 token。 +// + +import Foundation +import CryptoKit + +public enum QiniuTokenSigner { + + /// 生成上传 token。 + /// - Parameters: + /// - key: 上传文件 key(可空:scope = bucket,allow override;非空:scope = 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.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: "") + } +} diff --git a/ylgamehall/Source/Network/QiniuUploader.swift b/ylgamehall/Source/Network/QiniuUploader.swift new file mode 100644 index 0000000..bb5c8e2 --- /dev/null +++ b/ylgamehall/Source/Network/QiniuUploader.swift @@ -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-op,upload 永远抛 +// QiniuUploadError.sdkNotLinked。后续 handler 可降级到 stub 路径。 +// +// 上传完成后返回 UploadedFile,字段映射给 H5 `getaudiourl` / `recordSuccess`。 +// + +import Foundation + +#if canImport(QiniuSDK) +import QiniuSDK +#endif + +public struct UploadedFile: Sendable { + public let fileUrl: String // 公网访问 URL(http://{domain}/{key}) + public let fileName: String // 原始本地文件名 + public let fileKey: String // 七牛 key(path-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: 本地文件 URL(amr 录音文件) + /// - 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) 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 + } +} diff --git a/ylgamehall/Source/SDK/AMap/AMapWrapper.swift b/ylgamehall/Source/SDK/AMap/AMapWrapper.swift new file mode 100644 index 0000000..cf46ce0 --- /dev/null +++ b/ylgamehall/Source/SDK/AMap/AMapWrapper.swift @@ -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 diff --git a/ylgamehall/Source/SDK/WeChat/WeChatManager.swift b/ylgamehall/Source/SDK/WeChat/WeChatManager.swift new file mode 100644 index 0000000..3a1cd6f --- /dev/null +++ b/ylgamehall/Source/SDK/WeChat/WeChatManager.swift @@ -0,0 +1,160 @@ +// +// WeChatManager.swift +// ylgamehall +// +// 微信 SDK 持久 delegate + authorize + share 异步包装。 +// 把 WXApiDelegate 的回调 → async/await,state UUID 配对多发起方。 +// +// Design §8.5;msext 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 continuation(state UUID 配对) + private var authPending: [String: CheckedContinuation] = [:] + /// 待 resolve 的 share continuation(FIFO 串行 — 微信回包不带配对 ID) + private var sharePending: [CheckedContinuation] = [] + + 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) 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) +} diff --git a/ylgamehall/Source/SDK/WeChat/WeChatSDK.swift b/ylgamehall/Source/SDK/WeChat/WeChatSDK.swift new file mode 100644 index 0000000..459cec7 --- /dev/null +++ b/ylgamehall/Source/SDK/WeChat/WeChatSDK.swift @@ -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-op,不影响主工程编译。Embed 后自动激活真实路径。 +// + +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 Links(msext 同款路径,企业签 / 超签分发场景 ULAPI 无意义) + WXApi.registerApp(appID, universalLink: "") + #else + // SDK 未链接:no-op。Xcode 加完 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 + } +} diff --git a/ylgamehall/Source/Share/WechatShare.swift b/ylgamehall/Source/Share/WechatShare.swift index 87f00b9..1df2706 100644 --- a/ylgamehall/Source/Share/WechatShare.swift +++ b/ylgamehall/Source/Share/WechatShare.swift @@ -2,14 +2,18 @@ // WechatShare.swift // ylgamehall // -// 微信分享 SharePlatform 实现 stub。 +// 微信分享 SharePlatform 实现。 // -// Phase 4.E 阶段为 stub(等项目方提供微信 SDK + AppID + Universal Links); -// 完整实现见 Design §8.2 ShareKit + §8.5 微信 / QQ 多发起方派发。 +// ⚠️ canImport(WechatOpenSDK) 守卫:SDK 未链接前 isInstalled = true / share = .success +// 让 SharePanel 按钮可点;项目方在 Xcode Embed & Sign 后真实路径激活。 // import Foundation +#if canImport(WechatOpenSDK) +import WechatOpenSDK +#endif + @MainActor public final class WechatShare: SharePlatform { @@ -17,15 +21,44 @@ public final class WechatShare: SharePlatform { public let name = "WeChat" - /// Stub: 微信 SDK 未接入前永远返回 true,让 SharePanel 按钮可点; - /// 真实接入 SDK 后改为 `WXApi.isWXAppInstalled()` - public var isInstalled: Bool { true } + public var isInstalled: Bool { + #if canImport(WechatOpenSDK) + return WXApi.isWXAppInstalled() + #else + return true // 未链接 SDK 时让按钮可点,share 也只是 stub success + #endif + } public init() {} public func share(_ content: ShareContent, scene: ShareScene) async -> ShareResult { - // Phase 4.E 阶段 stub:仅返回 success,不真实调起微信 - // 完整实现:WXApi.send + SendMessageToWXResp 回调 + 多发起方 state 配对 + #if canImport(WechatOpenSDK) + 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 + } 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 } }